diff --git a/tools/blender/asset_report.json b/tools/blender/asset_report.json index 900cc5e..14157d5 100644 --- a/tools/blender/asset_report.json +++ b/tools/blender/asset_report.json @@ -1,5 +1,5 @@ { - "blender": "5.1.2", + "blender": "5.0.1", "assets": [ { "name": "ref_capsule", @@ -817,6 +817,73 @@ "status": "PASS", "problems": [] }, + { + "name": "hail_net_frame_01", + "dims": [ + 3.42, + 3.02, + 2.766 + ], + "tris": 328, + "nodes": [ + "footings", + "hail_net_frame_01", + "legs", + "net_anchor_01", + "net_anchor_02", + "net_anchor_03", + "net_anchor_04", + "net_roll", + "top_frame" + ], + "anchors": [ + { + "node": "net_anchor_01", + "anchor_type": "hail_net_frame", + "rating_hint": 0.8, + "collateral": "glasshouse" + }, + { + "node": "net_anchor_02", + "anchor_type": "hail_net_frame", + "rating_hint": 0.8, + "collateral": "glasshouse" + }, + { + "node": "net_anchor_03", + "anchor_type": "hail_net_frame", + "rating_hint": 0.8, + "collateral": "glasshouse" + }, + { + "node": "net_anchor_04", + "anchor_type": "hail_net_frame", + "rating_hint": 0.8, + "collateral": "glasshouse" + } + ], + "status": "PASS", + "problems": [] + }, + { + "name": "hail_net_frame_01_wrecked", + "dims": [ + 4.3007, + 3.02, + 2.7832 + ], + "tris": 480, + "nodes": [ + "footings", + "hail_net_frame_01_wrecked", + "legs", + "net_down", + "top_frame" + ], + "anchors": [], + "status": "PASS", + "problems": [] + }, { "name": "pool_kit_01", "dims": [ @@ -955,5 +1022,42 @@ "problems": [] } ], - "debris": [] + "debris": [ + { + "file": "BlueCrate_v2.glb", + "dims": [ + 0.36, + 0.36, + 0.29 + ], + "sane": true + }, + { + "file": "BlackTub_v2.glb", + "dims": [ + 0.36, + 0.54, + 0.2 + ], + "sane": true + }, + { + "file": "WhiteTub_v2.glb", + "dims": [ + 0.36, + 0.54, + 0.2 + ], + "sane": true + }, + { + "file": "WoodenBin_v2.glb", + "dims": [ + 0.35, + 0.36, + 0.31 + ], + "sane": true + } + ] } \ No newline at end of file diff --git a/tools/blender/build_yard_assets.py b/tools/blender/build_yard_assets.py index 394c399..75d7c30 100644 --- a/tools/blender/build_yard_assets.py +++ b/tools/blender/build_yard_assets.py @@ -20,6 +20,25 @@ Outputs (resolved from this file, so no absolute home paths): tools/blender/contact_sheet.png verification render vs the 1.7 m capsule tools/blender/asset_report.json measured dims / tris / node names +⚠️ BLENDER VERSION IS PART OF THE OUTPUT (SPRINT18, E — measured, not guessed). + The shipped GLBs were exported by Blender **5.1.2** (glTF I/O v5.1.20), which + `asset_report.json` records in its `blender` field — check it before you + build. On Blender 5.0.1 (I/O v5.0.21) this script is still perfectly + deterministic — three consecutive full runs gave 56/56 byte-identical + artifacts — but its output DIFFERS from the shipped files in two ways, and + both are cosmetic: + · GLB accessor min/max drift in the 7th decimal (~6e-8 m, i.e. 60 nm). + Node graph, mesh topology, materials and every custom prop are + bit-identical; only float formatting of the bounds moves. + · The generated PNGs lose their sRGB/gAMA/cHRM chunks — 73 bytes per file. + IDAT pixel data decompresses bit-identically; the newer exporter simply + writes colour-space chunks that 5.0.x omits. + So a full run on the wrong Blender rewrites all 45 binaries with no change + anybody asked for, and quietly DROPS the colour-space chunks. If your `git + status` shows every GLB and texture modified after adding one asset, that is + this and not your work: `git checkout --` the ones you did not author, and + commit only the new files. Better: build on 5.1.2 and the churn is zero. + Idiom follows ~/Documents/Destroyulater/3D-STORE/racks_to_glb.py: reset_to_empty() per asset -> build under a root empty AT THE ORIGIN -> join by group -> stamp custom props -> export_scene.gltf(export_yup=True, @@ -150,6 +169,23 @@ PAL = { # a pool is the one water in the game that is # supposed to look inviting "deck_board": "#A8845C", # decking oil, a shade lighter than fence timber + "net_frame_white": "#E6E7E1", # SPRINT18 gate 4 — the hail-net frame's + # powder-coat, and the BRIGHTEST entry in this + # palette on purpose. The reason is metalness, + # not lightness — S17's measured lesson applied + # deliberately this time. Galvanised ships at + # metallic 0.90, and with no environment map a + # near-mirror has nothing to mirror, so every + # steel you are ALLOWED to trust renders + # near-black at yard distance (measured: rgb + # 49,51,49 at 20 m). Powder-coat is paint — + # metallic 0.05, diffuse — so this frame reads + # as a white cage standing over the glass from + # the far fence, and can never be misread as + # the galvanised steel of an honest sail post. + "net_mesh": "#3A4340", # knitted HDPE hail mesh: dark, matte, and the + # only large dark mass any yard carries up at + # 2.7 m. Rolled it is a bar; down it is a heap. } # Rainwater caught in a sail is not swimming-pool blue. It's shallow, murky, it @@ -1916,6 +1952,283 @@ def build_glasshouse_01_wrecked(name): return root +# --------------------------------------------------------------------------- +# THE HAIL-NET FRAME (SPRINT18 gate 4 — the glasshouse tier's LIVE half) +# --------------------------------------------------------------------------- +# SPRINT16 shipped the glasshouse and priced it at 320. SPRINT18 read the gate +# and found the thing the price was waiting on, written in e.test's own words: +# "nothing in the sim wrecks a glasshouse yet — the value is inert data". The +# apex could not be reached. main.js bills collateral off a BROKEN RIG CORNER +# (`world.anchor(c.anchorId).collateral`), and a glasshouse deliberately offers +# no anchors — correctly, it is the one rule pinned from every direction — so +# the most expensive object in the game could not cost anybody a cent. That is +# the free-failure bug inverted: not a hazard billing nothing by accident, but +# a hazard that cannot bill at all by construction. +# +# DESIGN.md line 179 already named the missing asset: "Jobs: hail nets and mesh +# sails rigged OVER glass, where your own failure is the worst debris of all." +# You do not tie to the glasshouse. You tie to the frame that STRADDLES it — +# and every corner of that frame is a corner whose failure drops steel, cloth +# and hardware through fourteen panes. +# +# So this is the tier, and its teaching is new. Every other trap in this game +# punishes you with a bad number: the carport beam at 0.22, the fascia at 0.35, +# the corroded post at 0.55 whose real capacity you cannot read. This one is +# GOOD hardware — purpose-built, plumb, on its own footings, factory eyes — +# and it is the most dangerous anchor in the game because of WHERE IT STANDS. +# The rating is not the hazard. The position is. +NET_FRAME = dict( + CLEAR_X=0.30, # portal stands this far clear of each gable end + CLEAR_Y=0.45, # legs stand this far outside the long walls, on soil + HEAD_H=2.75, # head-beam height. MUST clear the ridge (2.28) — a frame + # that fouled the glass it protects is a joke, and + # e.test measures the clearance off the two shipped GLBs. + TUBE=0.032, # 32 mm net-frame section + PAD=0.22, # footing pad +) + +# The rung. PROPOSAL — Lane A rules the number, same as every rung before it. +# +# Ladder as shipped: 0.22 carport beam < 0.30 carport post < 0.35 house fascia +# < 0.45 swing frame < 0.55 corroded post < 0.65 pergola < 1.00 sail post. +# This goes at 0.80, and the argument has two halves: +# +# · WHY IT OUT-RATES THE PERGOLA (0.65) BY A DISTANCE. The pergola is a deck +# structure screwed to a house and it flexes; the corroded post's capacity +# is unknowable; the swing frame stands on grass. This is an engineered +# portal frame on four real footings with welded eyes, and it is the only +# structure in the palette that was BUILT to be tied to. Rating it near the +# bad steel would be the lie the corroded post exists to punish, inverted: +# honest steel de-rated for looking dangerous. +# · WHY IT IS NOT THE SAIL POST (1.00). The section is 32 mm sized for hail +# mesh — a couple of hundred grams per square metre, and near-zero drag +# because mesh SPILLS wind rather than catching it. A 25 m² impermeable +# shade sail in a southerly is a load one order of magnitude larger. 0.80 +# is a 20% de-rate for "engineered for a load you are about to exceed". +# +# And 0.80 is chosen so that IT USUALLY HOLDS. That is the point of the tier, +# not a softness in it: a corner that fails one night in five costs you nothing +# on a sail post and 320 dollars here. The player who learns "0.80 is fine" on +# every other yard is exactly the player this yard is for. The lesson is not +# "don't trust this steel" — the steel is fine. It is "the same failure has a +# different price depending on what is underneath it", which is the sentence +# DESIGN.md line 179 has been waiting for an asset to say. +NET_FRAME_RATING = 0.80 + + +def _net_frame(name, root, steel, conc, mesh_m, *, wrecked=False): + """The portal frame, shared by intact and wreck so the two cannot drift. + + The wreck fails at the -x portal ONLY: its feet kick out of their pads and + its head beam comes down onto the glasshouse ridge. +x stands, holding the + eave rails up, which is why the wreck keeps the intact height — a hail-net + frame does not fall over, it folds at one end, and the end it folds is the + end that empties the glass.""" + G, N = GLASSHOUSE, NET_FRAME + hx = G["W"] / 2 + N["CLEAR_X"] # 1.60 — clear of the gable ends + hy = G["D"] / 2 + N["CLEAR_Y"] # 1.40 — clear of the long walls + H, r, pad_h = N["HEAD_H"], N["TUBE"] / 2, 0.06 + + def foot(sx, sy): + """Where the leg meets the ground. Wrecked -x feet are OUT of the pad — + a foot beside its own footing is the most legible steel failure there + is, and it costs one offset to say.""" + if wrecked and sx < 0: + return (sx * hx - 0.45, sy * hy, pad_h + (0.14 if sy > 0 else 0.0)) + return (sx * hx, sy * hy, pad_h) + + def head(sx, sy): + """Where the leg meets the top frame. The wrecked -x portal has come + down and is RESTING ON THE RIDGE (2.28) rather than passing through it: + the glasshouse's own wreck keeps its aluminium frame standing at full + height, so a beam that ended up below the ridge line would be inside + it. Lying ACROSS the ridge is both true and non-interpenetrating. + + The head comes IN (+x, dragged by the sail) while the feet kick OUT + (-x) — the portal RACKS into a parallelogram, the carport's failure + mode on a lighter section. The offsets are deliberately large (0.45 + each way, ~22° of lean over 2.3 m) because I looked at 0.30/0.16 in + the render and the top frame still read as a clean level rectangle: + a 0.43 m drop on a 2.77 m frame of 32 mm tube is invisible in + perspective, and a wreck you cannot see is not a wreck.""" + if wrecked and sx < 0: + return (sx * hx + 0.45, sy * hy * 0.90, G["RIDGE_H"] + 0.05) + return (sx * hx, sy * hy, H) + + def mid(sx): + a, b = head(sx, -1), head(sx, 1) + return ((a[0] + b[0]) / 2, 0.0, (a[2] + b[2]) / 2) + + pads, legs = [], [] + for sx in (-1, 1): + for sy in (-1, 1): + pads.append(add_box(f"{name}_pad_{sx}_{sy}", (N["PAD"], N["PAD"], pad_h), + (sx * hx, sy * hy, pad_h / 2), conc)) + legs.append(add_tube_between(f"{name}_leg_{sx}_{sy}", + foot(sx, sy), head(sx, sy), r, steel, verts=8)) + join_group(pads, "footings", root) + join_group(legs, "legs", root) + + top = [] + for sx in (-1, 1): # portal head beams, spanning y + top.append(add_tube_between(f"{name}_head_{sx}", head(sx, -1), head(sx, 1), + r, steel, verts=8)) + for sy in (-1, 1): # eave rails, spanning x + top.append(add_tube_between(f"{name}_eave_{sy}", head(-1, sy), head(1, sy), + r, steel, verts=8)) + # The tensioning wire down the middle, deliberately thinner than the frame: + # at 20 m the gauge difference is the "made for a net" read, and it is the + # honest reason the whole thing rates 0.80 instead of 1.00. + top.append(add_tube_between(f"{name}_ridge_line", mid(-1), mid(1), + r * 0.35, steel, verts=6)) + join_group(top, "top_frame", root) + + if not wrecked: + # Rolled along the +y eave, because the JOB is to rig it. A deployed + # net would be a finished yard, and this asset exists to be worked on. + join_group([add_tube_between(f"{name}_roll", (-hx, hy, H - 0.11), + (hx, hy, H - 0.11), 0.085, mesh_m, verts=10)], + "net_roll", root) + else: + # YOUR debris, not the storm's. The net has come off its lashings at the + # collapsed end and is draped down over the glass and heaped out past + # the footing — deterministic scatter, so the same wreck twice. + rng = rng_for(name) + drape = [] + pts = [(hx * 0.85, hy, H - 0.10), + (hx * 0.10, hy * 0.92, H - 0.78), + (-hx * 0.55, hy * 0.80, G["RIDGE_H"] * 0.55), + (-hx - 0.28, hy * 0.55, 0.24), + (-hx - 0.80, hy * 0.20, 0.06)] + for i in range(len(pts) - 1): + drape.append(add_tube_between(f"{name}_net_sag_{i}", pts[i], pts[i + 1], + 0.055, mesh_m, verts=6)) + for i in range(9): + a = rng.uniform(0, math.tau) + rr = rng.uniform(0.05, 0.45) + drape.append(add_box( + f"{name}_net_heap_{i}", + (rng.uniform(0.22, 0.50), rng.uniform(0.16, 0.40), rng.uniform(0.03, 0.09)), + (-hx - 0.58 + math.cos(a) * rr, hy * 0.20 + math.sin(a) * rr * 0.8, + rng.uniform(0.02, 0.07)), + mesh_m, rot=(0, 0, rng.uniform(0, math.pi)))) + join_group(drape, "net_down", root) + + return hx, hy + + +def build_hail_net_frame_01(name): + """The hail-net frame — four good anchors in the worst place in the game. + + The glasshouse tier's live half (SPRINT18 gate 4). This is what DESIGN.md + line 179 means by "hail nets and mesh sails rigged OVER glass": a portal + frame that straddles the glasshouse, clear of it on all four sides, with a + head height that clears the ridge and a rolled mesh net waiting to go up. + + THE TELL, and it is a position rather than a defect. Every reading of every + other trap in this game is a bad number on the object. This object's numbers + are GOOD — 0.80, plumb, four footings, welded eyes — and what the player has + to read instead is the plan view: the frame BRACKETS the glass. e.test pins + that geometrically off the two shipped GLBs (frame footprint strictly + contains the glasshouse footprint; head height strictly clears the ridge), + because "it stands over the glass" has to be a measurement, not a vibe. + The 20 m read is the powder-coat: white, matte, metallic 0.05, against a + palette where every trustworthy steel is metallic 0.90 and therefore renders + near-black. A white cage over a glass box, visible from the far fence. + + WHAT THE ANCHORS SAY. All four bake collateral="glasshouse". They are the + only nodes in the palette whose collateral names a DIFFERENT structure than + the one they sit on (the house's fascia→"gutter" is the precedent, and it is + the same shape: the anchor names what its failure TAKES, not what it is). + Break a corner here and the sail, the shackle and the turnbuckle go through + fourteen panes: your own failure is the worst debris of all, wired as data. + + AND WHAT THIS ROOT DELIBERATELY DOES NOT CARRY: collateral_key. See below — + it is the whole of gate 5 in one omission. + """ + root = add_empty(name) + # Powder-coat is PAINT, so metallic stays near zero. This is not a style + # choice, it is the S17 measurement: high metalness with no environment map + # renders near-black, and this asset's entire long-range tell is that it is + # the brightest thing in the yard. + steel = get_material("Mat_NetFrame", PAL["net_frame_white"], 0.50, metallic=0.05) + mesh_m = get_material("Mat_HailNet", PAL["net_mesh"], 0.85, metallic=0.0) + conc = get_material("Mat_Concrete", PAL["concrete"], 0.95) + + hx, hy = _net_frame(name, root, steel, conc, mesh_m) + + # The four eyes, at the four corners of the top frame — where a net corner + # ties, and therefore where a sail corner will be tied instead. + for i, (sx, sy) in enumerate(((-1, -1), (1, -1), (1, 1), (-1, 1))): + e = add_empty(f"net_anchor_{i + 1:02d}", + (sx * hx, sy * hy, NET_FRAME["HEAD_H"] - 0.06), root, size=0.18) + e["anchor_type"] = "hail_net_frame" + e["rating_hint"] = NET_FRAME_RATING + e["collateral"] = "glasshouse" + e["why"] = ("sound steel, and the most expensive corner on the board: it is " + "rated for hail mesh, and everything it drops lands on glass") + + stamp(root, name, "structure") + root["site_hint"] = "glasshouse" + root["is_anchor_trap"] = True + root["rated_for"] = ("knitted hail mesh, ~0.2 kg/m2, near-zero drag — mesh spills " + "wind. A 25 m2 impermeable sail is an order of magnitude more.") + # ⚠️ NO collateral_key ON THIS ROOT, AND THAT IS THE POINT (gate 5). + # + # world.js resolves a collateral key to a structure with structFor(), which + # returns the FIRST structure in site order whose key matches. If this frame + # claimed collateral_key="glasshouse" — which looks like the helpful thing + # to do, since its anchors bill that key — then in a yard where the frame is + # authored before the glasshouse, every failure would bill "the glasshouse" + # for 320 dollars and then WRECK THE FRAME: the player watches their own + # frame fold while the invoice charges them for panes that are still in + # their rebates. The bill and the wreck would agree with each other and + # both be wrong, which is the only kind of wrong this repo cannot see. + # + # So exactly ONE structure owns a key, and it is the one that owns the + # price. This frame POINTS at the key and never claims it. The pointer + # lives in a field nothing resolves, so it can document intent without + # arming anything. + root["collateral_target"] = "glasshouse" + root["rigs_over"] = "glasshouse_01" + return root + + +def build_hail_net_frame_01_wrecked(name): + """The frame after the corner you trusted let go. + + The -x portal has folded: both its feet are out of their pads, and its head + beam is lying across the glasshouse ridge with the net draped down over the + glass and heaped out past the footing. The +x portal stands, holding the + eave rails at full height — so this wreck is the same height as the intact + frame, which is the honest read: a portal frame folds at one end, it does + not lie down. + + It carries NO GLASS. The glasshouse has its own wreck variant and swaps + independently; a net-frame wreck that shipped shards would draw them twice + the moment both swapped on the same night, and would also quietly claim + the glass's failure as its own. e.test pins the absence. + + UNPRICED, BY RULING — the bike/jacaranda pattern. A folded portal frame is + obviously a repair bill, but the player is being taught about the glass, and + one broken corner can only name one collateral key. Inventing a second bill + with no mechanism to split it between them is exactly the guess the invoice + exists to refuse. Price it when a break can bill two things.""" + root = add_empty(name) + steel = get_material("Mat_NetFrame", PAL["net_frame_white"], 0.50, metallic=0.05) + mesh_m = get_material("Mat_HailNet", PAL["net_mesh"], 0.85, metallic=0.0) + conc = get_material("Mat_Concrete", PAL["concrete"], 0.95) + + _net_frame(name, root, steel, conc, mesh_m, wrecked=True) + + stamp(root, name, "structure") + root["broken_variant_of"] = "hail_net_frame_01" + # No anchor survives the wreck — the fascia rule, sixth application. An + # anchor that outlives its structure is the free-failure bug in a costume. + root["collateral_target"] = "glasshouse" + return root + + # --------------------------------------------------------------------------- # THE POOL KIT (SPRINT16 gate 5 — the compliant ring whose steel all says no) # --------------------------------------------------------------------------- @@ -3824,6 +4137,25 @@ ASSETS = [ dict(name="glasshouse_01_wrecked", fn=build_glasshouse_01_wrecked, dims=((3.4, 5.4), (2.6, 3.8), (2.15, 2.40)), nodes=["frame", "door", "panes_left", "shards"]), + # SPRINT18 gate 4 — the glasshouse tier's live half. Every band here is a + # design claim about the glass underneath it, which is why they are tight: + # x and y floors must both EXCEED the glasshouse's ceilings (2.8 / 2.1), or + # the frame has stopped straddling the thing it bills; z must clear the + # ridge (2.40 ceiling) or a frame built to protect glass is resting on it. + dict(name="hail_net_frame_01", fn=build_hail_net_frame_01, + dims=((3.30, 3.55), (2.90, 3.15), (2.70, 2.85)), + nodes=["footings", "legs", "top_frame", "net_roll", + "net_anchor_01", "net_anchor_02", "net_anchor_03", "net_anchor_04"]), + # Folded at one end, not felled: the SAME z band as the intact frame, + # because the +x portal stands and holds the eave rails up. If z ever + # collapses, someone has laid a portal frame down flat and it never does + # that. x runs WIDE on the -x side — the net heaped out past the footing, + # and the wreck apron a yard author has to keep clear. + # 4.10 floor, not 4.30: the measured span is 4.30 and it is set by the NET + # HEAP, not by the racked feet, so it must not sit on the band boundary. + dict(name="hail_net_frame_01_wrecked", fn=build_hail_net_frame_01_wrecked, + dims=((4.10, 5.30), (2.90, 3.15), (2.70, 2.85)), + nodes=["footings", "legs", "top_frame", "net_down"]), # The compliance ring. z is pinned tight around 1.2 m because the height # IS the flavour (AS 1926.1); y reaches past the ring for the ajar gate. dict(name="pool_kit_01", fn=build_pool_kit_01, diff --git a/tools/blender/contact_sheet.png b/tools/blender/contact_sheet.png index 963346c..8ff9c91 100644 Binary files a/tools/blender/contact_sheet.png and b/tools/blender/contact_sheet.png differ diff --git a/web/world/js/anchor_ratings.gen.js b/web/world/js/anchor_ratings.gen.js index a669582..06712eb 100644 --- a/web/world/js/anchor_ratings.gen.js +++ b/web/world/js/anchor_ratings.gen.js @@ -80,6 +80,16 @@ export const FACTORY_ANCHOR_RATINGS = Object.freeze({ "sail_post_corroded/top_anchor" ] }, + "hail_net_frame": { + "collateral": "glasshouse", + "rating_hint": 0.8, + "sources": [ + "hail_net_frame_01/net_anchor_01", + "hail_net_frame_01/net_anchor_02", + "hail_net_frame_01/net_anchor_03", + "hail_net_frame_01/net_anchor_04" + ] + }, "house": { "collateral": "gutter", "rating_hint": 0.35, diff --git a/web/world/js/contracts.js b/web/world/js/contracts.js index 772cd43..07ca54d 100644 --- a/web/world/js/contracts.js +++ b/web/world/js/contracts.js @@ -100,10 +100,26 @@ export const PHASES = ['forecast', 'prep', 'storm', 'aftermath']; * can't inspect the inside of). One rating for the whole type, per the * manifest rule. The GLB wears the tell (rust, a sagging pad eye); the type * string is the player's pre-rig read of the same fact. + * + * `hail_net_frame` (SPRINT18 gate 4, E): the frame that straddles a glasshouse + * so a hail net can be rigged OVER the glass (DESIGN.md line 179). It is the + * first anchor word in this enum whose danger is not in its own number. + * rating_hint 0.80 — the highest rung below the honest post, and it EARNS it: + * an engineered portal frame on four real footings with welded eyes, the only + * structure in the palette that was built to be tied to. The 20% de-rate is + * for section, not condition (32 mm sized for hail mesh, which spills wind; + * a 25 m² impermeable sail is an order of magnitude more load). + * What makes it the most dangerous anchor in the game is that its baked + * `collateral` names the GLASSHOUSE, not itself — the `house`→"gutter" + * precedent, and the same shape: an anchor names what its failure TAKES, not + * what it is. Break a corner here and the sail, the shackle and the turnbuckle + * go through fourteen panes at $320. The type string is the player's pre-rig + * read of a hazard that is a POSITION, and the reason it must not be typed + * `post`: "post" promises a corner whose failure costs a shackle. */ export const ANCHOR_TYPE = Object.freeze([ 'house', 'tree', 'post', 'carport', 'carport_post', 'swing_frame', 'pergola', - 'corroded_post', + 'corroded_post', 'hail_net_frame', ]); // --------------------------------------------------------------------------- diff --git a/web/world/models/hail_net_frame_01_v1.glb b/web/world/models/hail_net_frame_01_v1.glb new file mode 100644 index 0000000..5d73731 Binary files /dev/null and b/web/world/models/hail_net_frame_01_v1.glb differ diff --git a/web/world/models/hail_net_frame_01_wrecked_v1.glb b/web/world/models/hail_net_frame_01_wrecked_v1.glb new file mode 100644 index 0000000..cc52433 Binary files /dev/null and b/web/world/models/hail_net_frame_01_wrecked_v1.glb differ