extends RefCounted class_name Tables ## The tables, as data. Each is a Table spec (see Table.gd for the part vocabulary). ## ## These are ORIGINAL layouts, not recreations of the Microsoft table vendored next door — ## that decomp is here as a reference and a build target, not something to trace. ## ## A table is a Dictionary: name, palette, size, and a flat list of parts. That means a new ## table is authorable in minutes, tables can be diffed, and dev/probe_tables.gd can build ## every one of them headless and assert the playfield is sane. const ORDER := ["neon", "grotto", "foundry", "orbital", "thedig"] static func get_table(id: String) -> Dictionary: match id: "grotto": return grotto() "foundry": return foundry() "orbital": return orbital() "thedig": return thedig() return neon() static func next_id(id: String) -> String: var i := ORDER.find(id) return ORDER[(i + 1) % ORDER.size()] if i >= 0 else ORDER[0] # ================================================================ TABLE 1 ## NEON ARCADE — the friendly one. Wide open lower field, a three-bank of drop targets, ## two pop bumpers up top and a spinner lane on the left. Learn the flippers here. static func neon() -> Dictionary: var parts: Array = [] # --- the lower field: flippers, slings, outlane guides --- parts.append({"kind": "flipper", "id": "flip_l", "at": Vector3(-0.062, 0.022, 0.400), "side": -1, "length": 0.078, "limit_lo": -32.0, "limit_hi": 30.0, "torque": 7.0}) parts.append({"kind": "flipper", "id": "flip_r", "at": Vector3(0.062, 0.022, 0.400), "side": 1, "length": 0.078, "limit_lo": -30.0, "limit_hi": 32.0, "torque": 7.0}) parts.append({"kind": "sling", "id": "sling_l", "at": Vector3(-0.125, 0.0, 0.320), "dir": Vector3(1, 0, -0.55), "yaw": deg_to_rad(-32.0), "kick": 0.135, "points": 120}) parts.append({"kind": "sling", "id": "sling_r", "at": Vector3(0.125, 0.0, 0.320), "dir": Vector3(-1, 0, -0.55), "yaw": deg_to_rad(32.0), "kick": 0.135, "points": 120}) # outlane / inlane dividers — the guides that decide whether a drain was fair parts.append({"kind": "wall", "id": "guide_l", "at": Vector3(-0.175, 0.0, 0.380), "size": Vector3(0.012, 0.05, 0.150), "yaw": deg_to_rad(9.0)}) parts.append({"kind": "wall", "id": "guide_r", "at": Vector3(0.175, 0.0, 0.380), "size": Vector3(0.012, 0.05, 0.150), "yaw": deg_to_rad(-9.0)}) parts.append({"kind": "post", "id": "post_l", "at": Vector3(-0.100, 0.0, 0.366)}) parts.append({"kind": "post", "id": "post_r", "at": Vector3(0.100, 0.0, 0.366)}) # --- mid field: the drop-target bank and a pair of standing targets --- for i in 3: parts.append({"kind": "drop", "id": "drop_%d" % i, "bank": "neon", "at": Vector3(-0.130 + i * 0.034, 0.0, 0.055), "points": 500}) parts.append({"kind": "target", "id": "targ_l", "at": Vector3(-0.196, 0.0, -0.070), "yaw": deg_to_rad(24.0), "points": 300}) parts.append({"kind": "target", "id": "targ_r", "at": Vector3(0.196, 0.0, -0.070), "yaw": deg_to_rad(-24.0), "points": 300}) # --- the spinner lane, left side, fed by a hard left flipper shot --- parts.append({"kind": "spinner", "id": "spin_main", "at": Vector3(-0.155, 0.0, 0.170), "yaw": deg_to_rad(12.0), "points": 90}) parts.append({"kind": "wall", "id": "spin_guide", "at": Vector3(-0.205, 0.0, 0.170), "size": Vector3(0.012, 0.05, 0.180), "yaw": deg_to_rad(6.0)}) # --- top: two pops and a saucer between them --- parts.append({"kind": "bumper", "id": "pop_l", "at": Vector3(-0.075, 0.0, -0.250), "kick": 0.165, "points": 250}) parts.append({"kind": "bumper", "id": "pop_r", "at": Vector3(0.075, 0.0, -0.250), "kick": 0.165, "points": 250}) parts.append({"kind": "bumper", "id": "pop_t", "at": Vector3(0.0, 0.0, -0.335), "kick": 0.165, "points": 250}) parts.append({"kind": "saucer", "id": "saucer_top", "at": Vector3(0.0, 0.0, -0.150), "eject": Vector3(0.1, 0, 1), "hold": 0.85, "power": 0.20, "points": 2500}) # --- the N-E-O-N rollover lanes across the top arch --- var glyphs := ["N", "E", "O", "N"] for i in glyphs.size(): parts.append({"kind": "rollover", "id": "lane_%d" % i, "glyph": glyphs[i], "at": Vector3(-0.105 + i * 0.070, 0.0, -0.430), "points": 150}) # --- the right ramp: the money shot --- parts.append({"kind": "ramp", "id": "ramp_r", "at": Vector3(0.120, 0.0, 0.230), "yaw": deg_to_rad(-14.0), "length": 0.30, "width": 0.052, "rise": 0.06, "points": 1500}) return { "name": "NEON ARCADE", "subtitle": "wide open. learn the flippers here.", "width": 0.52, "length": 1.10, "palette": { "field": Color(0.10, 0.09, 0.20), "rail": Color(0.62, 0.64, 0.72), "flipper": Color(0.98, 0.30, 0.55), "bumper": Color(0.35, 0.95, 0.90), "sling": Color(0.55, 0.95, 0.45), "target": Color(0.98, 0.55, 0.20), "drop": Color(0.98, 0.88, 0.30), "spinner": Color(0.80, 0.90, 1.00), "lane": Color(0.45, 0.60, 1.00), "ramp": Color(0.30, 0.75, 0.95), "wall": Color(0.30, 0.32, 0.45), "post": Color(0.90, 0.92, 1.00), "saucer": Color(0.08, 0.10, 0.18), }, "parts": parts, } # ================================================================ TABLE 2 ## THE GROTTO — tight and mean. Narrower field, a five-bank guarding the only ramp, and ## the outlanes are wide open, so a bad shot is punished immediately. static func grotto() -> Dictionary: var parts: Array = [] parts.append({"kind": "flipper", "id": "flip_l", "at": Vector3(-0.058, 0.022, 0.410), "side": -1, "length": 0.072, "limit_lo": -34.0, "limit_hi": 28.0, "torque": 6.5}) parts.append({"kind": "flipper", "id": "flip_r", "at": Vector3(0.058, 0.022, 0.410), "side": 1, "length": 0.072, "limit_lo": -28.0, "limit_hi": 34.0, "torque": 6.5}) parts.append({"kind": "sling", "id": "sling_l", "at": Vector3(-0.118, 0.0, 0.330), "dir": Vector3(1, 0, -0.7), "yaw": deg_to_rad(-38.0), "kick": 0.155, "points": 150}) parts.append({"kind": "sling", "id": "sling_r", "at": Vector3(0.118, 0.0, 0.330), "dir": Vector3(-1, 0, -0.7), "yaw": deg_to_rad(38.0), "kick": 0.155, "points": 150}) # no inlane posts — the outlanes are open, which is the whole personality of this table # a five-bank straight across the middle: it BLOCKS the ramp until you clear it for i in 5: parts.append({"kind": "drop", "id": "gd_%d" % i, "bank": "gate", "at": Vector3(-0.096 + i * 0.048, 0.0, 0.010), "points": 400}) parts.append({"kind": "ramp", "id": "ramp_c", "at": Vector3(0.0, 0.0, -0.040), "yaw": 0.0, "length": 0.26, "width": 0.048, "rise": 0.07, "points": 3000}) parts.append({"kind": "spinner", "id": "spin_l", "at": Vector3(-0.170, 0.0, 0.130), "yaw": deg_to_rad(16.0), "points": 120}) parts.append({"kind": "spinner", "id": "spin_r", "at": Vector3(0.170, 0.0, 0.130), "yaw": deg_to_rad(-16.0), "points": 120}) parts.append({"kind": "bumper", "id": "pop_a", "at": Vector3(-0.060, 0.0, -0.290), "kick": 0.180, "points": 300}) parts.append({"kind": "bumper", "id": "pop_b", "at": Vector3(0.060, 0.0, -0.290), "kick": 0.180, "points": 300}) parts.append({"kind": "saucer", "id": "saucer_l", "at": Vector3(-0.150, 0.0, -0.220), "eject": Vector3(0.4, 0, 1), "hold": 1.1, "power": 0.22, "points": 4000}) parts.append({"kind": "saucer", "id": "saucer_r", "at": Vector3(0.150, 0.0, -0.220), "eject": Vector3(-0.4, 0, 1), "hold": 1.1, "power": 0.22, "points": 4000}) for i in 3: parts.append({"kind": "rollover", "id": "glane_%d" % i, "glyph": "DIG".substr(i, 1), "at": Vector3(-0.085 + i * 0.085, 0.0, -0.420), "points": 200}) parts.append({"kind": "post", "id": "gp_l", "at": Vector3(-0.100, 0.0, 0.190), "radius": 0.010}) parts.append({"kind": "post", "id": "gp_r", "at": Vector3(0.100, 0.0, 0.190), "radius": 0.010}) return { "name": "THE GROTTO", "subtitle": "open outlanes. the bank guards the ramp.", "width": 0.46, "length": 1.10, "palette": { "field": Color(0.06, 0.14, 0.13), "rail": Color(0.45, 0.55, 0.52), "flipper": Color(0.20, 0.85, 0.75), "bumper": Color(0.95, 0.60, 0.25), "sling": Color(0.30, 0.70, 0.95), "target": Color(0.90, 0.35, 0.45), "drop": Color(0.85, 0.95, 0.55), "spinner": Color(0.70, 0.95, 0.90), "lane": Color(0.30, 0.85, 0.70), "ramp": Color(0.95, 0.75, 0.35), "wall": Color(0.14, 0.26, 0.24), "post": Color(0.75, 0.90, 0.86), "saucer": Color(0.04, 0.10, 0.09), }, "parts": parts, } # ================================================================ TABLE 3 ## THE FOUNDRY — the long one. A tall upper field stacked with pops, twin ramps, and a ## target array you have to pick apart. Slower, more deliberate, higher ceiling. static func foundry() -> Dictionary: var parts: Array = [] parts.append({"kind": "flipper", "id": "flip_l", "at": Vector3(-0.064, 0.022, 0.430), "side": -1, "length": 0.082, "limit_lo": -30.0, "limit_hi": 32.0, "torque": 8.0}) parts.append({"kind": "flipper", "id": "flip_r", "at": Vector3(0.064, 0.022, 0.430), "side": 1, "length": 0.082, "limit_lo": -32.0, "limit_hi": 30.0, "torque": 8.0}) # an upper-left third flipper, fed by the left ramp — the skill shot of this table parts.append({"kind": "flipper", "id": "flip_u", "at": Vector3(-0.090, 0.022, -0.120), "side": -1, "length": 0.062, "limit_lo": -28.0, "limit_hi": 26.0, "torque": 6.0}) parts.append({"kind": "sling", "id": "sling_l", "at": Vector3(-0.132, 0.0, 0.350), "dir": Vector3(1, 0, -0.5), "yaw": deg_to_rad(-30.0), "kick": 0.130, "points": 100}) parts.append({"kind": "sling", "id": "sling_r", "at": Vector3(0.132, 0.0, 0.350), "dir": Vector3(-1, 0, -0.5), "yaw": deg_to_rad(30.0), "kick": 0.130, "points": 100}) parts.append({"kind": "post", "id": "fp_l", "at": Vector3(-0.112, 0.0, 0.345)}) parts.append({"kind": "post", "id": "fp_r", "at": Vector3(0.112, 0.0, 0.345)}) # twin ramps flanking the centre parts.append({"kind": "ramp", "id": "ramp_l", "at": Vector3(-0.135, 0.0, 0.190), "yaw": deg_to_rad(12.0), "length": 0.28, "width": 0.050, "rise": 0.065, "points": 2000}) parts.append({"kind": "ramp", "id": "ramp_r", "at": Vector3(0.135, 0.0, 0.190), "yaw": deg_to_rad(-12.0), "length": 0.28, "width": 0.050, "rise": 0.065, "points": 2000}) # a 2x3 target array in the centre you pick apart shot by shot for row in 2: for col in 3: parts.append({"kind": "target", "id": "arr_%d_%d" % [row, col], "at": Vector3(-0.058 + col * 0.058, 0.0, 0.020 - row * 0.075), "width": 0.030, "points": 350}) # a four-bank up top for i in 4: parts.append({"kind": "drop", "id": "fd_%d" % i, "bank": "smelt", "at": Vector3(-0.075 + i * 0.050, 0.0, -0.230), "points": 600}) parts.append({"kind": "spinner", "id": "spin_c", "at": Vector3(0.0, 0.0, 0.150), "points": 150}) parts.append({"kind": "bumper", "id": "pop_1", "at": Vector3(-0.100, 0.0, -0.330), "kick": 0.170, "points": 300}) parts.append({"kind": "bumper", "id": "pop_2", "at": Vector3(0.0, 0.0, -0.390), "kick": 0.170, "points": 300}) parts.append({"kind": "bumper", "id": "pop_3", "at": Vector3(0.100, 0.0, -0.330), "kick": 0.170, "points": 300}) parts.append({"kind": "saucer", "id": "saucer_deep", "at": Vector3(0.170, 0.0, -0.130), "eject": Vector3(-0.6, 0, 1), "hold": 1.2, "power": 0.24, "points": 5000}) for i in 4: parts.append({"kind": "rollover", "id": "flane_%d" % i, "glyph": "CAST".substr(i, 1), "at": Vector3(-0.120 + i * 0.080, 0.0, -0.455), "points": 175}) return { "name": "THE FOUNDRY", "subtitle": "twin ramps, an upper flipper, and a long way up.", "width": 0.54, "length": 1.20, "palette": { "field": Color(0.16, 0.10, 0.08), "rail": Color(0.58, 0.52, 0.46), "flipper": Color(0.95, 0.55, 0.15), "bumper": Color(0.98, 0.85, 0.35), "sling": Color(0.85, 0.40, 0.20), "target": Color(0.75, 0.80, 0.90), "drop": Color(0.95, 0.45, 0.25), "spinner": Color(0.90, 0.85, 0.75), "lane": Color(0.98, 0.70, 0.30), "ramp": Color(0.65, 0.68, 0.75), "wall": Color(0.26, 0.18, 0.14), "post": Color(0.85, 0.80, 0.72), "saucer": Color(0.10, 0.06, 0.05), }, "parts": parts, } # ================================================================ TABLE 4 ## ORBITAL — everything funnels to the centre. Two saucers flank a long central ramp, and ## the pops sit in a tight cluster at the top so a ball that gets up there RATTLES. static func orbital() -> Dictionary: var parts: Array = [] parts.append({"kind": "flipper", "id": "flip_l", "at": Vector3(-0.060, 0.022, 0.420), "side": -1, "length": 0.076, "limit_lo": -32.0, "limit_hi": 30.0, "torque": 7.5}) parts.append({"kind": "flipper", "id": "flip_r", "at": Vector3(0.060, 0.022, 0.420), "side": 1, "length": 0.076, "limit_lo": -30.0, "limit_hi": 32.0, "torque": 7.5}) parts.append({"kind": "sling", "id": "sling_l", "at": Vector3(-0.124, 0.0, 0.340), "dir": Vector3(1, 0, -0.6), "yaw": deg_to_rad(-34.0), "kick": 0.140, "points": 130}) parts.append({"kind": "sling", "id": "sling_r", "at": Vector3(0.124, 0.0, 0.340), "dir": Vector3(-1, 0, -0.6), "yaw": deg_to_rad(34.0), "kick": 0.140, "points": 130}) parts.append({"kind": "post", "id": "op_l", "at": Vector3(-0.098, 0.0, 0.384)}) parts.append({"kind": "post", "id": "op_r", "at": Vector3(0.098, 0.0, 0.384)}) # the long central ramp — the spine of the table parts.append({"kind": "ramp", "id": "ramp_c", "at": Vector3(0.0, 0.0, 0.180), "length": 0.34, "width": 0.054, "rise": 0.075, "points": 3500}) # orbit lanes hugging the ramp, each with a spinner in it parts.append({"kind": "spinner", "id": "orb_l", "at": Vector3(-0.148, 0.0, 0.120), "yaw": deg_to_rad(14.0), "points": 130}) parts.append({"kind": "spinner", "id": "orb_r", "at": Vector3(0.148, 0.0, 0.120), "yaw": deg_to_rad(-14.0), "points": 130}) parts.append({"kind": "wall", "id": "orb_gl", "at": Vector3(-0.196, 0.0, 0.120), "size": Vector3(0.012, 0.05, 0.220), "yaw": deg_to_rad(5.0)}) parts.append({"kind": "wall", "id": "orb_gr", "at": Vector3(0.196, 0.0, 0.120), "size": Vector3(0.012, 0.05, 0.220), "yaw": deg_to_rad(-5.0)}) # twin saucers = the multiball locks, deliberately easy to SEE and hard to HIT parts.append({"kind": "saucer", "id": "lock_l", "at": Vector3(-0.165, 0.0, -0.100), "eject": Vector3(0.5, 0, 1), "hold": 1.0, "power": 0.21, "points": 3000}) parts.append({"kind": "saucer", "id": "lock_r", "at": Vector3(0.165, 0.0, -0.100), "eject": Vector3(-0.5, 0, 1), "hold": 1.0, "power": 0.21, "points": 3000}) # a tight four-pop cluster at the top: get up here and the ball goes mad parts.append({"kind": "bumper", "id": "pop_n", "at": Vector3(0.0, 0.0, -0.400), "kick": 0.185, "points": 300}) parts.append({"kind": "bumper", "id": "pop_s", "at": Vector3(0.0, 0.0, -0.280), "kick": 0.185, "points": 300}) parts.append({"kind": "bumper", "id": "pop_w", "at": Vector3(-0.068, 0.0, -0.340), "kick": 0.185, "points": 300}) parts.append({"kind": "bumper", "id": "pop_e", "at": Vector3(0.068, 0.0, -0.340), "kick": 0.185, "points": 300}) for i in 3: parts.append({"kind": "drop", "id": "od_%d" % i, "bank": "airlock", "at": Vector3(-0.058 + i * 0.058, 0.0, -0.185), "points": 550}) var g := ["O", "R", "B"] for i in g.size(): parts.append({"kind": "rollover", "id": "olane_%d" % i, "glyph": g[i], "at": Vector3(-0.090 + i * 0.090, 0.0, -0.470), "points": 175}) return { "name": "ORBITAL", "subtitle": "everything funnels to the centre.", "width": 0.50, "length": 1.16, "palette": { "field": Color(0.04, 0.05, 0.12), "rail": Color(0.60, 0.66, 0.78), "flipper": Color(0.30, 0.70, 1.00), "bumper": Color(0.95, 0.95, 0.55), "sling": Color(0.60, 0.45, 0.95), "target": Color(0.90, 0.90, 0.95), "drop": Color(0.40, 0.95, 0.85), "spinner": Color(0.85, 0.90, 1.00), "lane": Color(0.55, 0.70, 1.00), "ramp": Color(0.75, 0.78, 0.88), "wall": Color(0.10, 0.14, 0.26), "post": Color(0.88, 0.92, 1.00), "saucer": Color(0.02, 0.03, 0.08), }, "parts": parts, } # ================================================================ TABLE 5 ## THE DIG — the record shop table, and the one that belongs to this house. The bank is a ## row of crates you flip through, the ramp is the counter, and the spinner is a turntable. ## Same universe as the shop in Destroyulator; the theme is the only thing borrowed. static func thedig() -> Dictionary: var parts: Array = [] parts.append({"kind": "flipper", "id": "flip_l", "at": Vector3(-0.063, 0.022, 0.405), "side": -1, "length": 0.080, "limit_lo": -31.0, "limit_hi": 31.0, "torque": 7.2}) parts.append({"kind": "flipper", "id": "flip_r", "at": Vector3(0.063, 0.022, 0.405), "side": 1, "length": 0.080, "limit_lo": -31.0, "limit_hi": 31.0, "torque": 7.2}) parts.append({"kind": "sling", "id": "sling_l", "at": Vector3(-0.128, 0.0, 0.325), "dir": Vector3(1, 0, -0.55), "yaw": deg_to_rad(-33.0), "kick": 0.138, "points": 125}) parts.append({"kind": "sling", "id": "sling_r", "at": Vector3(0.128, 0.0, 0.325), "dir": Vector3(-1, 0, -0.55), "yaw": deg_to_rad(33.0), "kick": 0.138, "points": 125}) parts.append({"kind": "post", "id": "dp_l", "at": Vector3(-0.102, 0.0, 0.371)}) parts.append({"kind": "post", "id": "dp_r", "at": Vector3(0.102, 0.0, 0.371)}) # THE CRATES: a six-bank you flip through one sleeve at a time. Clearing it is # "finding the record", which is the entire fantasy of a record shop. for i in 6: parts.append({"kind": "drop", "id": "crate_%d" % i, "bank": "crates", "at": Vector3(-0.112 + i * 0.050, 0.0, 0.070), "points": 450}) # the turntable: a spinner you can whip, dead centre parts.append({"kind": "spinner", "id": "deck", "at": Vector3(0.0, 0.0, -0.020), "points": 160}) # the counter — a ramp up to the register parts.append({"kind": "ramp", "id": "counter", "at": Vector3(-0.163, 0.0, 0.210), "yaw": deg_to_rad(7.0), "length": 0.28, "width": 0.050, "rise": 0.062, "points": 2200}) # listening booth: capture the ball, hold it a good long while parts.append({"kind": "saucer", "id": "booth", "at": Vector3(0.160, 0.0, -0.150), "eject": Vector3(-0.5, 0, 1), "hold": 1.4, "power": 0.22, "points": 4500}) # the wall of rare sleeves — a target array that pays for i in 4: parts.append({"kind": "target", "id": "rare_%d" % i, "at": Vector3(-0.175 + i * 0.030, 0.0, -0.250), "yaw": deg_to_rad(18.0), "width": 0.026, "points": 400}) parts.append({"kind": "bumper", "id": "pop_a", "at": Vector3(0.080, 0.0, -0.330), "kick": 0.170, "points": 275}) parts.append({"kind": "bumper", "id": "pop_b", "at": Vector3(0.150, 0.0, -0.395), "kick": 0.170, "points": 275}) parts.append({"kind": "bumper", "id": "pop_c", "at": Vector3(0.010, 0.0, -0.400), "kick": 0.170, "points": 275}) var g := ["D", "I", "G"] for i in g.size(): parts.append({"kind": "rollover", "id": "dlane_%d" % i, "glyph": g[i], "at": Vector3(-0.100 + i * 0.090, 0.0, -0.455), "points": 180}) return { "name": "THE DIG", "subtitle": "flip the crates. find the record.", "width": 0.52, "length": 1.12, "palette": { "field": Color(0.14, 0.07, 0.13), "rail": Color(0.58, 0.55, 0.60), "flipper": Color(1.00, 0.36, 0.62), "bumper": Color(0.98, 0.80, 0.30), "sling": Color(0.50, 0.85, 0.70), "target": Color(0.85, 0.60, 0.95), "drop": Color(0.35, 0.65, 0.95), "spinner": Color(0.20, 0.20, 0.24), "lane": Color(1.00, 0.50, 0.70), "ramp": Color(0.55, 0.38, 0.24), "wall": Color(0.24, 0.12, 0.22), "post": Color(0.92, 0.88, 0.95), "saucer": Color(0.08, 0.04, 0.08), }, "parts": parts, }