From b49c48731af0e8048c6a2f4b22d2110472e53549 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 00:06:16 +0000 Subject: [PATCH 01/10] platformer: add the BLOCK SLIME - a hopping cube (asset Phase B, slice 2) The unused slime_block_* foes art becomes a new slime-family kind "block": a cube that arcs back and forth across its band in hops (the _jump frame in the air, a walk_a/b squish idle when settled), reversing at the band edges and sleeping between hops. Stompable on the classic path (a stomp squashes it to slime_block_rest, a side touch knocks back) - no new b2kContact case, like the frog. - new pfMakeBlockSlime maker (hopper, modeled on pfMakeFrog) + "blockidle" anim - new case "block" in pfTickSlimes (hop on a timer when grounded; jump frame airborne; reuses gFrogAir as the per-index airborne flag) - debuts in L6 "Cavern Depths" (replaces a plain slime, 2440..2680 band) - audit-platformer.py gains the pfMakeBlockSlime parse branch Example-side only (no Kit change, no harness bump). check-livecodescript passes; audit clears all 6 levels (L6 still 0 findings, block slime parsed as a walker on solid ground). Statically verified; needs an OXT pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01X92APxUJUMue3bAzXiX9V2 --- CHANGELOG.md | 11 +++ examples/box2dxt-platformer.livecodescript | 84 ++++++++++++++++++++-- tools/audit-platformer.py | 3 + 3 files changed, 92 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c46299e..b98044a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,17 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `4`). ### Added +- **Platformer: the BLOCK SLIME - a hopping cube (asset-expansion Phase B, + slice 2).** The previously-unused `slime_block_*` foes art becomes a new + slime-family kind `block`: a cube that arcs back and forth across its band in + hops (the `_jump` frame in the air, a `walk_a/b` squish idle when settled), + reversing at the band edges and sleeping between hops. Stompable on the + classic path (a stomp squashes it to `slime_block_rest`, a side touch knocks + back) - no new contact case. Debuts in L6 "Cavern Depths" (it replaces a plain + slime, so the cavern's signature foe is the hopper). New `pfMakeBlockSlime` + maker + a `case "block"` in `pfTickSlimes`; `tools/audit-platformer.py` gains + its parse branch and clears L6 (still 0 findings). Example-side only (no Kit + change, no harness bump). Statically verified; needs an OXT pass. - **Platformer: LEVEL 6 "CAVERN DEPTHS" - the DIRT biome (asset-expansion Phase B, slice 1).** A sixth level built on the previously-unused `terrain_dirt_*` tile set (block tops, `block_center` mass under the mound, diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index 2e4f786..679a2cd 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -90,7 +90,8 @@ -- LEVEL 6 CAVERN DEPTHS (3040px) - the asset-expansion DIRT biome: a -- WALL-JUMP SHAFT of floating dirt columns over the entry (slot -- coin), a spike GAP to leap (checkpoint past it), a DIRT-RAMP --- mound, a one-way DIRT CLOUD high route, two slimes + a snail, +-- mound, a one-way DIRT CLOUD high route, a slime, a hopping BLOCK +-- SLIME + a snail, -- and carved dirt steps to the flag (the win). A bonus GEM rides -- high above the wall-jump shaft. (Block slime + conveyor in -- slices 2-3.) @@ -288,9 +289,11 @@ -- by wall-jump AND a straight double-jump (never trick-only), and the bonus -- GEM above the shaft is reachable (lower it if not); (f) the dirt MOUND -- walks up/across/down with no fall-through; the DIRT CLOUD is solid to its --- ends and DOWN+JUMP drops through it; the spike GAP is a clean leap; both --- slimes + the snail patrol on solid ground; the win screen says ALL SIX --- LEVELS CLEAR. (Block slime + conveyor arrive in slices 2-3.) +-- ends and DOWN+JUMP drops through it; the spike GAP is a clean leap; the +-- slime, the snail, and the BLOCK SLIME (slice 2) all patrol solid ground - +-- the block slime HOPS its band (jump frame airborne, squish idle settled, +-- no apex-flicker) and stomps to slime_block_rest; the win screen says ALL +-- SIX LEVELS CLEAR. (The conveyor belt arrives in slice 3.) -- ===================================================================== local gStarted, gHero, gHeroSpr, gHudLast, gHurtLock @@ -573,6 +576,7 @@ function pfLoadSheets b2kAnimDef "foes", "sawspin", "saw_a,saw_b", 10, true b2kAnimDef "foes", "slimewalk", "slime_normal_walk_a,slime_normal_walk_b", 4, true b2kAnimDef "foes", "spikewalk", "slime_spike_walk_a,slime_spike_walk_b", 4, true + b2kAnimDef "foes", "blockidle", "slime_block_walk_a,slime_block_walk_b", 4, true -- the block slime's settled squish between hops (Phase B) b2kAnimDef "foes", "snailwalk", "snail_walk_a,snail_walk_b", 4, true -- the showcase round's new walker species (all native 64px foes art, -- so no sheet/scale juggling; they ride the slime FAMILY as new kinds) @@ -1633,6 +1637,51 @@ command pfMakeFrog pIdx, pX, pMinX, pMaxX, pTopY put false into gFrogAir[pIdx] end pfMakeFrog +-- A BLOCK SLIME (asset-expansion Phase B): a hopping CUBE that arcs back and +-- forth across its band (slime_block_* foes art, previously unused). A +-- slime-family kind "block" - stompable via the CLASSIC path (a stomp squashes +-- it to slime_block_rest, a side touch knocks back), so it needs no b2kContact +-- case. The _jump frame shows on the hop, the squish idle (walk_a/b) when +-- settled. Reuses gFrogAir as its airborne flag (per-index, like the frog). +command pfMakeBlockSlime pIdx, pX, pMinX, pMaxX, pTopY + local tName, tBody, tSpr + put "pf_slimeBody" & pIdx into tName + create graphic tName + set the style of it to "rectangle" + set the rect of it to pX - 24, pTopY - 44, pX + 24, pTopY + set the filled of it to true + set the backgroundColor of it to "150,120,200" -- a purple cube (no-art fallback) + set the visible of it to false + put the long id of graphic tName into tBody + b2kAddBox tBody + b2kSetFixedRotation tBody, true + b2kSetFriction tBody, 0.4 + put empty into tSpr + if gAssetsOK is true and b2kSheetHasFrame("foes", "slime_block_rest") then + b2kSpriteNew "foes", "slime_block_rest", pX, pTopY - 28 + put the result into tSpr + if tSpr is not empty then + b2kSpriteBind tSpr, tBody, 0, -8 + b2kSpritePlay tSpr, "blockidle" + end if + end if + if tSpr is empty then set the visible of graphic tName to true + if the visible of graphic tName is true then b2kCamAdopt tBody + if pIdx > gSlimeN then put pIdx into gSlimeN + put tBody into gSlimeB[pIdx] + put tSpr into gSlimeSpr[pIdx] + put "block" into gSlimeKind[pIdx] + put 100 into gSlimeSpeed[pIdx] + put 100 into gSlimeDir[pIdx] + put "slime_block_rest" into gSlimeFlat[pIdx] -- the defeat pose (the only static cube frame) + put false into gSlimeFlip[pIdx] + put pMinX into gSlimeMin[pIdx] + put pMaxX into gSlimeMax[pIdx] + put empty into gSlimeGoneAt[pIdx] + put the milliseconds + 500 + random(400) into gSlimeT[pIdx] + put false into gFrogAir[pIdx] +end pfMakeBlockSlime + -- A piranha burrow at pX: the plant is a bodiless sprite pfTickPlants -- raises and sinks; its dark mouth-hole is drawn by the SCENE (so it -- layers under every later sprite). Unkillable, like the saws. @@ -3558,11 +3607,11 @@ command pfL6Cast pfMakeCoin 1568, 468 -- atop the dirt mound crest pfMakeCoin 2048, 408 -- on the one-way dirt cloud (the high route) pfMakeCoin 2200, 500 -- past the snail, on the lower run - pfMakeCoin 2560, 500 -- the second slime's beat + pfMakeCoin 2560, 500 -- the block slime's beat (grab it between hops) pfMakeCoin 2820, 476 -- on the first goal step pfMakeGem 428, 204, "green" -- BONUS gem ABOVE the wall-jump shaft: master the wall-jump to reach it (OXT: confirm it is reachable; lower it a touch if not) pfMakeSlime 1, "normal", 760, 640, 900, 576 - pfMakeSlime 2, "normal", 2560, 2440, 2680, 576 + pfMakeBlockSlime 2, 2560, 2440, 2680, 576 -- the cavern's hopping CUBE (Phase B; stomp it or time the hops) pfMakeSnail 3, 2050, 1980, 2120 pfMakeCheckpoint 1900 -- past the cavern gap, before the depths pfMakeGoal 3000, 416 @@ -3934,6 +3983,29 @@ command pfTickSlimes end if end if break + case "block" + -- Phase B: a HOPPING cube that arcs back and forth across its band + -- (jump frame in the air, the squish idle when settled), reversing + -- at the band edges. Always active; one velocity read per live + -- block-slime. Stomp/knockback live on the classic path (b2kContact). + put item 2 of b2kVelocity(tBody) into tVX -- vy, reusing tVX like the frog + if gFrogAir[i] is true then + if abs(tVX) < 8 then + put false into gFrogAir[i] -- landed: settle into the squish idle + if gSlimeSpr[i] is not empty then b2kSpritePlay gSlimeSpr[i], "blockidle" + end if + else if tMS > gSlimeT[i] and abs(tVX) < 8 then + if tX <= gSlimeMin[i] then put gSlimeSpeed[i] into gSlimeDir[i] + if tX >= gSlimeMax[i] then put -gSlimeSpeed[i] into gSlimeDir[i] + b2kSetVelocity tBody, gSlimeDir[i], -320 + put tMS + 650 + random(350) into gSlimeT[i] + put true into gFrogAir[i] + if gSlimeSpr[i] is not empty then + b2kSpriteFlipH gSlimeSpr[i], ((gSlimeDir[i] < 0) is not gSlimeFlip[i]) + b2kSpriteSetFrame gSlimeSpr[i], "slime_block_jump" + end if + end if + break default -- the classic patroller (normal/spike slimes, walking snails, -- and the showcase mice/worms/ladybugs - speed per row now, diff --git a/tools/audit-platformer.py b/tools/audit-platformer.py index 36c2638..bea2bba 100644 --- a/tools/audit-platformer.py +++ b/tools/audit-platformer.py @@ -122,6 +122,9 @@ def parse(): if s.startswith("pfMakeFrog"): v = nums(s) # idx,x,minx,maxx,topy L.enemies.append(("frog", v[0], v[1], v[2], v[3], v[4], 22)); continue + if s.startswith("pfMakeBlockSlime"): + v = nums(s) # idx,x,minx,maxx,topy (a hopping cube; patrols its band) + L.enemies.append(("block", v[0], v[1], v[2], v[3], v[4], 24)); continue if s.startswith("pfMakeThwomp"): body = s.split("--", 1)[0] # chained = the weight+chain look: no tile-face string, not a faced block From 6c64b2407dfb1c217e9ee20d3ec1f072aca23854 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 00:43:36 +0000 Subject: [PATCH 02/10] platformer: add the CONVEYOR BELT - completes asset Phase B (slice 3) The unused `conveyor` tile becomes a polled surface zone (pfMakeConveyor pL,pR, pDir) that adds a steady vx to the GROUNDED hero on top of his own walking, so you can power against it; jumping over it is unaffected and a hero on a higher platform at the same x is excluded. No body - the level's ground slab still owns the collision. Built example-side per the plan (no Kit "surface velocity" feature). The conveyor art is a single frame (no scroll animation), flipped to face the push direction. - new pfMakeConveyor maker + pfTickConveyor (added to the frame fan-out; one compare when no belts exist; reads the per-frame hero snapshot) - gConv* zone arrays (declared + reset in pfStartGame) - debuts in L6 as a leftward treadmill (2176..2368) before the block slime - audit-platformer.py: conveyor parse branch + an over-solid-ground check (a belt may not run over a pit); also corrected the now-stale spike-tiling comment Completes asset-expansion Phase B (dirt biome L6 + block slime + conveyor; torches landed in slice 1's polish). Example-side only (no Kit change, no harness bump). check-livecodescript passes; audit clears all 6 levels. Statically verified. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01X92APxUJUMue3bAzXiX9V2 --- CHANGELOG.md | 14 +++++ docs/asset-expansion-plan.md | 7 ++- examples/box2dxt-platformer.livecodescript | 62 ++++++++++++++++++++-- tools/audit-platformer.py | 25 +++++++-- 4 files changed, 98 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b98044a..9498ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `4`). ### Added +- **Platformer: the CONVEYOR BELT - a carried surface (asset-expansion Phase B, + slice 3).** The previously-unused `conveyor` tile becomes a polled surface + zone (`pfMakeConveyor pL, pR, pDir`) that adds a steady vx to the GROUNDED hero + on top of his own walking, so you can power against it; jumping over it is + unaffected, and a hero on a higher platform at the same x is excluded. No body + - the level's ground slab still owns the collision. Built example-side per the + plan (no Kit "surface velocity" feature). The `conveyor` art is a single frame + (no scroll animation), flipped to face the push direction. Debuts in L6 as a + leftward treadmill before the block slime. New `pfTickConveyor` in the frame + fan-out (one compare when no belts exist); `tools/audit-platformer.py` gains a + conveyor parse branch + an over-solid-ground check (a belt may not run over a + pit). This completes asset-expansion **Phase B**. Example-side only (no Kit + change, no harness bump); audit clears all 6 levels. Statically verified; + needs an OXT pass. - **Platformer: the BLOCK SLIME - a hopping cube (asset-expansion Phase B, slice 2).** The previously-unused `slime_block_*` foes art becomes a new slime-family kind `block`: a cube that arcs back and forth across its band in diff --git a/docs/asset-expansion-plan.md b/docs/asset-expansion-plan.md index a19526c..707d99e 100644 --- a/docs/asset-expansion-plan.md +++ b/docs/asset-expansion-plan.md @@ -148,8 +148,11 @@ needs an OXT eye. floating dirt columns, a spike gap, a dirt-ramp mound, a one-way-cloud bonus route, reused slimes + a snail, a bonus gem, dirt goal steps. Win moved to `gLevel >= 6`. Example-side; `audit-platformer.py` auto-discovers + clears L6. -- **Slices 2–3 — TODO:** the block slime (slice 2), then the conveyor + torches - (slice 3). +- **Slice 2 — DONE (statically verified; needs OXT):** the **block slime**, a + hopping cube (`slime_block_*`), a new slime-family kind `block` debuting in L6. +- **Slice 3 — DONE (statically verified; needs OXT):** the **conveyor belt** + (`pfMakeConveyor`, a polled vx zone) in L6. Torches were pulled forward into + slice 1's polish. **Phase B is complete** (pending the OXT pass). - **Assets:** `terrain_dirt_*` (whole biome, slice 1), `background_solid_dirt` (slice 1), `torch_off/on_a/on_b` (slice 3), `conveyor` (slice 3), `slime_block_*` (block slime, slice 2). diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index 679a2cd..873e98e 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -91,10 +91,9 @@ -- WALL-JUMP SHAFT of floating dirt columns over the entry (slot -- coin), a spike GAP to leap (checkpoint past it), a DIRT-RAMP -- mound, a one-way DIRT CLOUD high route, a slime, a hopping BLOCK --- SLIME + a snail, +-- SLIME + a snail, a CONVEYOR BELT (a treadmill you power across), -- and carved dirt steps to the flag (the win). A bonus GEM rides --- high above the wall-jump shaft. (Block slime + conveyor in --- slices 2-3.) +-- high above the wall-jump shaft. -- -- WHAT TO VERIFY (the Phase 3 + level-rebuild OXT pass) -- 1. Feel: a TAPPED jump is clearly shorter than a HELD one; jumping @@ -292,8 +291,11 @@ -- ends and DOWN+JUMP drops through it; the spike GAP is a clean leap; the -- slime, the snail, and the BLOCK SLIME (slice 2) all patrol solid ground - -- the block slime HOPS its band (jump frame airborne, squish idle settled, --- no apex-flicker) and stomps to slime_block_rest; the win screen says ALL --- SIX LEVELS CLEAR. (The conveyor belt arrives in slice 3.) +-- no apex-flicker) and stomps to slime_block_rest; (g) the CONVEYOR BELT +-- (slice 3) carries you LEFT when grounded on it - you walk slower toward +-- the goal and faster back, you can still power across, and jumping over it +-- is unaffected; the tread art faces the push direction (flip - OXT-verify +-- it reads). The win screen says ALL SIX LEVELS CLEAR. -- ===================================================================== local gStarted, gHero, gHeroSpr, gHudLast, gHurtLock @@ -321,6 +323,7 @@ local gSlimeT -- Wave 3: per-row timer (mimic hop cooldowns) local gBlockN, gBlockB, gBlockSpr, gBlockState, gBlockT, gBlockX local gMovN, gMovSpr, gMovX, gMovY, gMovAX, gMovPX, gMovAY, gMovPY local gMovHurtW, gMovHurtH, gMovFlip +local gConvN, gConvL, gConvR, gConvY, gConvDir -- Phase B: conveyor-belt zones -- Wave 3 (bestiary I): the piranha burrows, the ghost, the spooks sheet local gPlantN, gPlantSpr, gPlantX, gPlantState, gPlantT local gGhostSpr, gGhostX, gGhostY, gGhostShy @@ -847,6 +850,11 @@ command pfStartGame put empty into gMovHurtW put empty into gMovHurtH put empty into gMovFlip + put 0 into gConvN + put empty into gConvL + put empty into gConvR + put empty into gConvY + put empty into gConvDir put empty into gPrevState -- Wave 1 state starts the run empty/idle put false into gToysOK @@ -1682,6 +1690,45 @@ command pfMakeBlockSlime pIdx, pX, pMinX, pMaxX, pTopY put false into gFrogAir[pIdx] end pfMakeBlockSlime +-- A CONVEYOR BELT (asset-expansion Phase B): a polled surface ZONE that carries +-- the GROUNDED hero along pDir (+1 right, -1 left) ON TOP of his own walking, so +-- you can power against it. No body - the level's ground slab still owns the +-- collision; this only adds vx in pfTickConveyor. The belt art is the (single- +-- frame, unanimated) conveyor tile, flipped to face pDir. Per the plan this is +-- the example-side polled version (no Kit "surface velocity" feature). +command pfMakeConveyor pL, pR, pDir + local tX + add 1 to gConvN + put pL into gConvL[gConvN] + put pR into gConvR[gConvN] + put 576 into gConvY[gConvN] + put pDir into gConvDir[gConvN] + if gAssetsOK is true and b2kSheetHasFrame("tiles", "conveyor") then + repeat with tX = pL to pR - 64 step 64 + pfTile "conveyor", tX, 576, (pDir < 0) -- flip the tread to face pDir; the single frame has no scroll animation, so the flip is the only direction cue (OXT-verify it reads) + end repeat + end if +end pfMakeConveyor + +-- Carry the grounded hero along any belt he stands on (his input vx PLUS the +-- belt). One compare when no belts exist; uses the per-frame hero snapshot (no +-- extra position FFI). b2kSetVelocity wakes the hero - already awake under the +-- controller, so it is free. The y test (hero at/below the belt line) keeps a +-- hero on a HIGHER platform over the same x off the belt. +command pfTickConveyor + local i, tV + if gConvN is 0 or gConvN is empty then exit pfTickConveyor + if not b2kPlayerOnGround() then exit pfTickConveyor + set the itemDelimiter to comma + repeat with i = 1 to gConvN + if gHeroPX >= gConvL[i] and gHeroPX <= gConvR[i] and gHeroPY > gConvY[i] - 80 then + put b2kVelocity(gHero) into tV -- the controller's input-driven vx,vy this frame + b2kSetVelocity gHero, (item 1 of tV) + gConvDir[i] * 130, item 2 of tV + exit repeat -- one belt at a time + end if + end repeat +end pfTickConveyor + -- A piranha burrow at pX: the plant is a bodiless sprite pfTickPlants -- raises and sinks; its dark mouth-hole is drawn by the SCENE (so it -- layers under every later sprite). Unkillable, like the saws. @@ -3570,6 +3617,10 @@ command pfL6Scene set the foregroundColor of it to "150,120,90" b2kCamAdopt the long id of graphic "pf_cloudledgeP" end if + -- a CONVEYOR BELT on the lower run (pushes LEFT, back toward the cloud): a + -- treadmill you must power across to reach the block slime + the goal - the + -- cavern's mechanic (Phase B, slice 3). Grid-aligned, on solid ground2. + pfMakeConveyor 2176, 2368, -1 -- CAVE DRESSING: flickering wall TORCHES light the gloom; CHAINS and -- STALACTITES hang from the unseen roof; ground decor (a rock, cave -- mushrooms, a bush) breaks up the floor. Torches/chains/dirt-pieces are @@ -3752,6 +3803,7 @@ on b2kFrame pfTickSpiders -- Wave 6 (bestiary II) pfTickBoulder pfTickLift + pfTickConveyor -- Phase B: belts carry the grounded hero pfTickCollapse pfTickBarrel pfTickParallax -- Wave 8: drift the biome backdrop for depth diff --git a/tools/audit-platformer.py b/tools/audit-platformer.py index bea2bba..a8c1549 100644 --- a/tools/audit-platformer.py +++ b/tools/audit-platformer.py @@ -58,6 +58,7 @@ def __init__(self, n): self.goal = None self.bridge = None self.collapse = None + self.conveyors = [] # (pL,pR,dir) self.edgeL = 64 self.edgeR = None @@ -125,6 +126,9 @@ def parse(): if s.startswith("pfMakeBlockSlime"): v = nums(s) # idx,x,minx,maxx,topy (a hopping cube; patrols its band) L.enemies.append(("block", v[0], v[1], v[2], v[3], v[4], 24)); continue + if s.startswith("pfMakeConveyor"): + v = nums(s) # pL,pR,dir + L.conveyors.append((v[0], v[1], v[2])); continue if s.startswith("pfMakeThwomp"): body = s.split("--", 1)[0] # chained = the weight+chain look: no tile-face string, not a faced block @@ -282,12 +286,27 @@ def flag(sev, msg): mid = (hl + hr) / 2 if ground_top_at(L, mid) is not None: flag("WARN", f"spikes {hl:.0f}..{hr:.0f} overlap solid ground (mid x{mid:.0f}) -- expected an open pit") - # pfMakeSpikes centres tiles at pL+32..pR-32 step 64, so the row only - # fills the pit FLUSH when the width is a 64px multiple; otherwise a bare - # strip is left at the right edge (the pit "doesn't fit its spikes"). + # pfMakeSpikes tiles the row pL..pR-64 (top-lefts), so it fills the pit + # FLUSH only when the width is a 64px multiple; otherwise a partial tile + # is left (the pit "doesn't fit its spikes"). if (hr - hl) % 64 != 0: flag("WARN", f"spike pit {hl:.0f}..{hr:.0f} width {hr-hl:.0f} is not a 64px multiple -- the spike row won't fill it flush") + # CONVEYOR: a belt carries the GROUNDED hero, so every column must be solid + # ground (a belt over a pit would convey him into thin air), and its width + # must be a 64px multiple to tile flush. + for (cl, cr, cdir) in L.conveyors: + gap = None + x = cl + while x <= cr: + if ground_top_at(L, x) is None: + gap = x; break + x += 32 + if gap is not None: + flag("ERR", f"conveyor {cl:.0f}..{cr:.0f} runs over a pit (no ground at x{gap:.0f})") + if (cr - cl) % 64 != 0: + flag("WARN", f"conveyor {cl:.0f}..{cr:.0f} width {cr-cl:.0f} is not a 64px multiple -- the belt tiles won't fill it flush") + # WALKER vs THWOMP: a walker asserts its velocity every frame; if its swept # range gets within a few px of a crusher's body (x +/- 30) the two fight the # solver and the walker STICKS to the block (gotcha 17/18). Keep a margin. From f0115c583f5a828cb76bbbd911612345d436714e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 15:54:16 +0000 Subject: [PATCH 03/10] platformer: add LEVEL 7 "STONE KEEP" - the stone/castle biome (asset Phase C, slice 1) A seventh level on the previously-unused terrain_stone_* tile set, over a built dark stone backdrop (pfBuildStoneBackdrop, gray-blue): a watchtower (wall-jump, slot coin), a wide spiked MOAT (256px, four spikes) to leap, a rubble RAMP, a one-way stone BATTLEMENT high route, two slimes + a snail, a bonus GEM atop the tower, torchlit halls, and carved stone steps to the flag. - new pfL7Scene/pfL7Cast (proven L6 geometry in stone; 256px moat) + a new pfBuildStoneBackdrop; both pfStartGame switches wire case 7 - win moves to gLevel >= 7 (L6's flag now ADVANCES); help/splash/win strings + header walkthrough/verify updated to seven levels Built on the proven L6 geometry; the SPINNER hazard and the multi-key/switch PUZZLES (the keep's real identity) land in slices 2-3. Example-side only (no Kit change, no harness bump); audit-platformer auto-discovers and clears L7 (9 coins, 3 walkers, 0 findings across 7 levels). Stone frame names verified against the atlas. Statically verified; needs an OXT pass (header verify item 21). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01X92APxUJUMue3bAzXiX9V2 --- CHANGELOG.md | 13 ++ docs/asset-expansion-plan.md | 8 + examples/box2dxt-platformer.livecodescript | 201 +++++++++++++++++++-- 3 files changed, 211 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9498ba7..e683d6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `4`). ### Added +- **Platformer: LEVEL 7 "STONE KEEP" - the STONE/CASTLE biome (asset-expansion + Phase C, slice 1).** A seventh level on the previously-unused `terrain_stone_*` + tile set (block tops, `block_center`, carved corners, `ramp_long_a/b`, + `cloud_*`) over a built dark stone backdrop (`pfBuildStoneBackdrop`, gray-blue): + a WATCHTOWER (wall-jump, slot coin), a wide spiked MOAT (256px, four spikes) to + leap, a rubble RAMP, a one-way stone BATTLEMENT high route, two slimes + a + snail, a bonus GEM atop the tower, torchlit halls, and carved stone steps to + the flag. The win moves to L7 (`gLevel >= 7`); L6's flag now ADVANCES. Built on + the proven L6 geometry in stone; the SPINNER hazard and the multi-key/switch + PUZZLES (the keep's real identity) land in slices 2-3. Example-side only (no Kit + change, no harness bump); `tools/audit-platformer.py` auto-discovers and clears + L7 (9 coins, 3 walkers, 0 findings). Statically verified; needs an OXT pass + (header verify item 21). - **Platformer: the CONVEYOR BELT - a carried surface (asset-expansion Phase B, slice 3).** The previously-unused `conveyor` tile becomes a polled surface zone (`pfMakeConveyor pL, pR, pDir`) that adds a steady vx to the GROUNDED hero diff --git a/docs/asset-expansion-plan.md b/docs/asset-expansion-plan.md index 707d99e..38b5586 100644 --- a/docs/asset-expansion-plan.md +++ b/docs/asset-expansion-plan.md @@ -172,6 +172,14 @@ needs an OXT eye. a polled example-side version first. ### Phase C — Castle/dungeon biome → **Level 7 "STONE KEEP"** (M–L) +- **Slice 1 — DONE (statically verified; needs OXT):** the **stone biome + Level + 7 skeleton** that completes start-to-finish. `terrain_stone_*` (block tops, + `block_center`, carved corners, `ramp_long_a/b`, `cloud_*`) over a built dark + stone backdrop (`pfBuildStoneBackdrop`): a watchtower (wall-jump), a wide spiked + moat, a rubble ramp, a stone battlement, reused slimes + a snail, a bonus gem, + torchlit halls. Win moved to `gLevel >= 7`. Example-side; audit clears L7. +- **Slices 2–3 — TODO:** the **spinner** hazard (slice 2), then the **multi-key / + switch puzzles** (slice 3) — the keep's real identity. - **Assets:** `terrain_stone_*` (full), `switch_{blue,green,red}(_pressed)`, `key_{blue,green}`, `lock_{blue,green}`, `spinner*`/`spinnerHalf*`, `block_strong_*`. diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index 873e98e..ec66975 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -1,7 +1,7 @@ -- ===================================================================== -- box2dxt-platformer.livecodescript · Game Kit Phases 1+2+3+4 showcase -- --- A SIX-LEVEL collect-them-all platformer (each level its own +-- A SEVEN-LEVEL collect-them-all platformer (each level its own -- scrolling world in a 1024px viewport) that exercises everything -- built so far: the Kit's PLAYER CONTROLLER (b2kPlayerAttach drives -- movement, jumping, grounding, state and animations - this file holds @@ -14,8 +14,8 @@ -- barrel), and variety walkers (mouse, worm, ladybug, fire slime). -- Collect EVERY coin on a level (the goal -- flag turns GOLD), touch the flag, and the next level builds; level --- 6's flag is the win. Anything without sheet art falls back to plain --- graphics, so all six levels run even with no asset folder (the coin +-- 7's flag is the win. Anything without sheet art falls back to plain +-- graphics, so all seven levels run even with no asset folder (the coin -- totals count themselves as each level builds). -- -- HOW TO RUN @@ -34,7 +34,7 @@ -- are NOT draggable) · R restarts the CURRENT level · ESC pauses -- · M mutes the synthesized sound -- --- THE SIX LEVELS (every beat holds a coin; the flag advances): +-- THE SEVEN LEVELS (every beat holds a coin; the flag advances): -- LEVEL 1 GREEN HILLS (8640px) - movement + the toys: the -- SPRINGBOARD mid-meadow (sky coin above; a 42px hop for -- non-bouncers), the BONK ROW (headbutt ?-boxes, SMASH bricks @@ -92,8 +92,14 @@ -- coin), a spike GAP to leap (checkpoint past it), a DIRT-RAMP -- mound, a one-way DIRT CLOUD high route, a slime, a hopping BLOCK -- SLIME + a snail, a CONVEYOR BELT (a treadmill you power across), --- and carved dirt steps to the flag (the win). A bonus GEM rides --- high above the wall-jump shaft. +-- and carved dirt steps to the flag. A bonus GEM rides high above +-- the wall-jump shaft. +-- LEVEL 7 STONE KEEP (3040px) - the asset-expansion STONE/CASTLE biome: a +-- WATCHTOWER (wall-jump, slot coin), a wide spiked MOAT to leap +-- (checkpoint past it), a rubble RAMP, a one-way stone BATTLEMENT +-- high route, two slimes + a snail, torchlit halls, and carved stone +-- steps to the flag (the win). A bonus GEM tops the watchtower. (The +-- SPINNER hazard + the multi-key/switch PUZZLES land in slices 2-3.) -- -- WHAT TO VERIFY (the Phase 3 + level-rebuild OXT pass) -- 1. Feel: a TAPPED jump is clearly shorter than a HELD one; jumping @@ -295,7 +301,21 @@ -- (slice 3) carries you LEFT when grounded on it - you walk slower toward -- the goal and faster back, you can still power across, and jumping over it -- is unaffected; the tread art faces the push direction (flip - OXT-verify --- it reads). The win screen says ALL SIX LEVELS CLEAR. +-- it reads). L6's flag now ADVANCES to L7 (the win moved to L7). +-- 21. ASSET-EXPANSION PHASE C, SLICE 1 - LEVEL 7 "STONE KEEP" (the STONE/CASTLE +-- biome: terrain_stone_*, previously unused; the win now needs gLevel>=7). +-- VERIFY on a full asset folder: (a) the BACKDROP is the dark stone keep +-- (pfBuildStoneBackdrop), no border line on the two panels; (b) the stone +-- ground/ramp/battlement/cliff-corners read with NO missing-frame +-- fallbacks; (c) the WATCHTOWER slot coin is reachable by wall-jump AND a +-- straight double-jump, and the bonus GEM atop it is reachable; (d) the +-- wide MOAT (256px, four spikes) is a clean run/double-jump leap with the +-- spikes FLUSH between the carved lips; (e) the rubble RAMP walks up/across/ +-- down with no fall-through; the stone BATTLEMENT is solid to its ends and +-- DOWN+JUMP drops through it; (f) both slimes + the snail patrol solid +-- ground; torches/chains light the halls (orientation OXT-verify); the win +-- screen says ALL SEVEN LEVELS CLEAR. (Spinner + key/switch puzzles: slices +-- 2-3.) -- ===================================================================== local gStarted, gHero, gHeroSpr, gHudLast, gHurtLock @@ -669,6 +689,10 @@ command pfBuildBackdrop pfBuildCaveBackdrop -- the cavern gets a built dark backdrop, not a flat "solid_" frame exit pfBuildBackdrop end if + if gLevel is 7 then + pfBuildStoneBackdrop -- the stone keep, like the cavern, has no outdoor scene + exit pfBuildBackdrop + end if switch gLevel case 3 put "background_fade_hills" into tFrame -- pale + cold for the frozen citadel @@ -726,6 +750,28 @@ command pfBuildCaveBackdrop set the layer of graphic "pf_bgcave2" to 2 end pfBuildCaveBackdrop +-- The STONE KEEP (L7) interior, like the cavern, has no outdoor scene in the bg +-- atlas, so build a dark stone backdrop (cooler gray-blue than the cave's +-- brown) from two card graphics. Not parallax-drifted - the depth comes from +-- the scrolling stonework + torches in pfL7Scene. Reuses the pf_bgcave* names +-- (one level's backdrop exists at a time; pfWipeStage clears them each build). +command pfBuildStoneBackdrop + create graphic "pf_bgcave1" + set the style of graphic "pf_bgcave1" to "rectangle" + set the showBorder of graphic "pf_bgcave1" to false + set the filled of graphic "pf_bgcave1" to true + set the backgroundColor of graphic "pf_bgcave1" to "40,42,52" + set the rect of graphic "pf_bgcave1" to 0, 0, 1064, 640 + set the layer of graphic "pf_bgcave1" to 1 + create graphic "pf_bgcave2" + set the style of graphic "pf_bgcave2" to "rectangle" + set the showBorder of graphic "pf_bgcave2" to false + set the filled of graphic "pf_bgcave2" to true + set the backgroundColor of graphic "pf_bgcave2" to "58,61,74" + set the rect of graphic "pf_bgcave2" to 0, 388, 1064, 584 + set the layer of graphic "pf_bgcave2" to 2 +end pfBuildStoneBackdrop + -- Drift the backdrop slower than the 1:1 foreground (parallax depth) and wrap -- it seamlessly (the bg scenes tile at the 640px panel width). Gated on the -- scroll changing, so a still camera costs one compare. 0.3 = the drift rate. @@ -984,6 +1030,9 @@ command pfStartGame case 6 pfL6Scene break + case 7 + pfL7Scene + break default put 1 into gLevel pfL1Scene @@ -1079,6 +1128,9 @@ command pfStartGame case 6 pfL6Cast break + case 7 + pfL7Cast + break default pfL1Cast end switch @@ -1090,7 +1142,7 @@ command pfStartGame -- help + the level banner on the splash (visible through the intro -- beat); gCoinsTotal counted itself during the build above if gAssetsOK is true and gLoadNote is empty then - set the text of field "pfHelp" to "Arrows/A-D run, SPACE jumps - press it again in mid-air to DOUBLE JUMP, or off a wall to WALL-JUMP. SHIFT or X = DASH." & cr & "DOWN ducks to a stop (DOWN+JUMP drops through bridges/clouds); UP/DOWN climbs ladders. R restarts, ESC pauses, M mutes, MOUSE drags the crate." & cr & "LEVEL " & gLevel & " of 6: " & gLevelName & ". Collect ALL " & gCoinsTotal & " coins - the flag turns GOLD - then touch the flag. Six levels to win." + set the text of field "pfHelp" to "Arrows/A-D run, SPACE jumps - press it again in mid-air to DOUBLE JUMP, or off a wall to WALL-JUMP. SHIFT or X = DASH." & cr & "DOWN ducks to a stop (DOWN+JUMP drops through bridges/clouds); UP/DOWN climbs ladders. R restarts, ESC pauses, M mutes, MOUSE drags the crate." & cr & "LEVEL " & gLevel & " of 7: " & gLevelName & ". Collect ALL " & gCoinsTotal & " coins - the flag turns GOLD - then touch the flag. Seven levels to win." else if gAssetsOK is true then set the text of field "pfHelp" to "KENNEY CHARACTERS LOADED, but: " & gLoadNote & cr & "Missing atlases fall back to plain shapes." & cr & "Shift+Reset re-asks for the Spritesheets folder." @@ -1099,7 +1151,7 @@ command pfStartGame end if end if if there is a field "pfSplash" then - set the text of field "pfSplash" to "LEVEL " & gLevel & " / 6" & cr & gLevelName + set the text of field "pfSplash" to "LEVEL " & gLevel & " / 7" & cr & gLevelName end if -- ===== camera-dead fallback: clamp play to the visible screen ==== if gCamOK is not true then @@ -3669,6 +3721,133 @@ command pfL6Cast end pfL6Cast +-- ===================================================================== +-- LEVEL 7 - "STONE KEEP" (asset-expansion Phase C, slice 1): the CASTLE/ +-- DUNGEON biome. The whole terrain_stone_* set was unused before this +-- (only door-walls). A fortress interior on a dark stone backdrop: a +-- watchtower (wall-jump), a wide spiked MOAT, a rubble ramp, a stone +-- BATTLEMENT high route, torchlit halls. Built on the proven L6 geometry +-- in stone. Slice 1 reuses the slime/snail cast; the SPINNER (slice 2) +-- and the multi-key / switch PUZZLES (slice 3) are the keep's real +-- identity, landing next. +-- ===================================================================== +command pfL7Scene + local tX + put "STONE KEEP (fortified!)" into gLevelName + pfBounds kPfEdgeL, 3000 + kPfEndCap -- right edge hugs the goal flag (3000) + pfSlab "pf_ground1", 0, 576, 1024, 640 + pfSlab "pf_ground2", 1280, 576, 3072, 640 + pfSlab "pf_plat1", 2752, 512, 2912, 576 + pfSlab "pf_plat2", 2912, 448, 3072, 576 + -- the STONE biome: block tops across the keep floor (block_center under the + -- rubble ramp, one mass), carved CORNERS at the moat lips + the dais steps. + if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_stone_block_top") then + repeat with tX = 0 to 896 step 64 + pfTile "terrain_stone_block_top", tX, 576 + end repeat + pfTile "terrain_stone_block_top_right", 960, 576 -- the moat's LEFT lip (carved cliff edge) + pfTile "terrain_stone_block_top_left", 1280, 576 -- the moat's RIGHT lip + repeat with tX = 1344 to 3008 step 64 + if tX >= 1408 and tX <= 1792 then + pfTile "terrain_stone_block_center", tX, 576 -- under the rubble ramp: one mass + else + pfTile "terrain_stone_block_top", tX, 576 + end if + end repeat + pfTile "terrain_stone_block_top_left", 2752, 512 + pfTile "terrain_stone_block_top", 2816, 512 + pfTile "terrain_stone_block_top_right", 2880, 512 + pfTile "terrain_stone_block_top", 2912, 448 + pfTile "terrain_stone_block_top", 2976, 448 + pfTile "terrain_stone_block_center", 2912, 512 + pfTile "terrain_stone_block_center", 2976, 512 + pfShowSlabs false + else + pfShowSlabs true + end if + -- the WATCHTOWER (wall-jump): two floating stone columns frame a slot over + -- the courtyard - wall-jump (or double-jump) up it for the slot coin. The + -- L3/L6 shaft recipe in stone; the columns float so the main path runs under. + pfSlab "pf_wjShaftL", 320, 256, 384, 448 + pfSlab "pf_wjShaftR", 472, 256, 536, 448 + if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_stone_block_top") then + pfTile "terrain_stone_block_top", 320, 256 + pfTile "terrain_stone_block_center", 320, 320 + pfTile "terrain_stone_block_center", 320, 384 + pfTile "terrain_stone_block_top", 472, 256 + pfTile "terrain_stone_block_center", 472, 320 + pfTile "terrain_stone_block_center", 472, 384 + else + set the backgroundColor of graphic "pf_wjShaftL" to "120,124,140" + set the backgroundColor of graphic "pf_wjShaftR" to "120,124,140" + set the visible of graphic "pf_wjShaftL" to true + set the visible of graphic "pf_wjShaftR" to true + b2kCamAdopt the long id of graphic "pf_wjShaftL" + b2kCamAdopt the long id of graphic "pf_wjShaftR" + end if + pfMakeCoin 428, 340 -- the slot coin (slot 384..472), built in the SCENE to align with the columns + -- the MOAT: a wide spiked chasm (256px = four spikes), cleared with a run/ + -- double-jump; the checkpoint sits just past it. Carved stone lips finish it. + pfMakeSpikes 1024, 1280 + -- a RUBBLE RAMP (stone slope): up the left ramp, across the crest, down the + -- right. Six-point chain, outer two ghost anchors; ramps mirrored. + b2kSmoothGround "1856,576" & cr & "1792,576" & cr & "1664,512" & cr & "1536,512" & cr & "1408,576" & cr & "1344,576" + if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_stone_ramp_long_a") then + pfTile "terrain_stone_ramp_long_b", 1408, 512, true + pfTile "terrain_stone_ramp_long_a", 1472, 512, true + pfTile "terrain_stone_block_top", 1536, 512 + pfTile "terrain_stone_block_top", 1600, 512 + pfTile "terrain_stone_ramp_long_a", 1664, 512 + pfTile "terrain_stone_ramp_long_b", 1728, 512 + end if + -- a one-way STONE BATTLEMENT (drop through with DOWN+JUMP) for the high bonus + -- route; ghost-padded a tile past the art each side (solid span 1984..2176). + b2kSmoothGround "2240,448" & cr & "2176,448" & cr & "1984,448" & cr & "1920,448" + if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_stone_cloud_left") then + pfTile "terrain_stone_cloud_left", 1984, 448 + pfTile "terrain_stone_cloud_middle", 2048, 448 + pfTile "terrain_stone_cloud_right", 2112, 448 + else + create graphic "pf_cloudledgeS" + set the style of it to "line" + set the points of it to "1984,448" & cr & "2176,448" + set the lineSize of it to 4 + set the foregroundColor of it to "150,154,170" + b2kCamAdopt the long id of graphic "pf_cloudledgeS" + end if + -- KEEP DRESSING: wall torches + hanging chains light the dungeon (torches/ + -- chains are not audited decor). Orientation: OXT-verify (gotcha 26). + pfMakeTorch 352, 300 -- on the watchtower's left column + pfMakeTorch 504, 300 -- ...and its right column + pfMakeTorch 840, 452 -- the courtyard + pfMakeTorch 1360, 452 -- past the moat + pfMakeTorch 2360, 452 -- the keep run + pfMakeTorch 2860, 432 -- the climb to the dais + if gAssetsOK is true and b2kSheetHasFrame("tiles", "chain") then + pfTile "chain", 700, 96 + pfTile "chain", 1640, 96 + pfTile "chain", 2520, 96 + end if +end pfL7Scene + +command pfL7Cast + pfMakeCoin 240, 500 -- the courtyard + pfMakeCoin 700, 500 -- the first slime's beat + pfMakeCoin 1152, 452 -- mid-air over the MOAT: leap it + pfMakeCoin 1568, 468 -- atop the rubble ramp + pfMakeCoin 2048, 408 -- on the stone battlement (the high route) + pfMakeCoin 2300, 500 -- the keep run + pfMakeCoin 2560, 500 -- the second slime's beat + pfMakeCoin 2820, 476 -- on the first dais step + pfMakeGem 428, 204, "blue" -- BONUS gem above the watchtower (master the wall-jump) + pfMakeSlime 1, "normal", 700, 600, 860, 576 + pfMakeSlime 2, "normal", 2560, 2440, 2680, 576 + pfMakeSnail 3, 2050, 1980, 2120 + pfMakeCheckpoint 1360 -- just past the moat, before the keep + pfMakeGoal 3000, 416 +end pfL7Cast + + -- Every coin (sensor pickup or ?-box payout) lands here. The moment the -- LAST one arrives, the goal flag turns GOLD and a banner + chime send -- you to it - the win gate is a state you can SEE, never a surprise @@ -4831,7 +5010,7 @@ on b2kSensorEnter pSensorCtrl, pVisitorCtrl -- camera-dead fallback clamps play to one screen, so most coins are -- out of reach there: the near flag advances unconditionally instead if gCoins >= gCoinsTotal or gCamOK is not true then - if gLevel >= 6 then + if gLevel >= 7 then pfWin else pfLevelClear @@ -5131,7 +5310,7 @@ command pfWin put the result into tC if tC is not empty then b2kPush tC, random(361) - 180, 0 - (180 + random(220)) end repeat - put "ALL SIX LEVELS CLEAR!" & cr & cr into tMsg + put "ALL SEVEN LEVELS CLEAR!" & cr & cr into tMsg put "Every coin on every level collected" & cr after tMsg put "Total time " & format("%d:%02d", tSecs div 60, tSecs mod 60) & " Falls " & gTotalFalls & cr & cr after tMsg if gGemsAllTotal > 0 then put "Gems " & gGemsBank & " / " & gGemsAllTotal & " collected" & cr & cr after tMsg From 430ff7bfc4ace39de37e3b762c6f04a9c957d25b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 16:22:45 +0000 Subject: [PATCH 04/10] platformer: add a LEVEL PICKER (dev/test convenience) A top-right option-menu dropdown jumps straight to any level so the levels already approved need not be replayed after every update. - new pfbtn_level option menu in buildPfUI (top-right; the title field shrinks to make room), listing all 7 levels - on menuPick -> pfJumpToLevel: a fresh run-state rebuild (banks zeroed) to the chosen level; pfStartGame already hides the win dialogue + clears the win latches, so it works from play, pause, or the win screen - pfStartGame syncs the menu to the current level; pfChromeFront keeps it on top - kPfUIVersion "7" -> "8" (chrome changed; older pasted stacks rebuild the UI) Not part of the normal coin-gated progression. Example-side only (no Kit change, no harness bump). check-livecodescript passes. Update the menu list when a level is added. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01X92APxUJUMue3bAzXiX9V2 --- CHANGELOG.md | 7 ++++ examples/box2dxt-platformer.livecodescript | 44 ++++++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e683d6a..df8f708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `4`). ### Added +- **Platformer: a LEVEL PICKER (dev/test convenience).** A top-right option-menu + dropdown jumps straight to any level (`menuPick` -> `pfJumpToLevel`, a fresh + run-state rebuild), so levels already approved need not be replayed after every + update. Works from play, pause, or the win screen; the menu tracks the current + level. Chrome change, so `kPfUIVersion` bumps to "8" (older pasted stacks + rebuild their UI once). Not part of the normal coin-gated progression; the + title field shrinks to make room. Example-side only. - **Platformer: LEVEL 7 "STONE KEEP" - the STONE/CASTLE biome (asset-expansion Phase C, slice 1).** A seventh level on the previously-unused `terrain_stone_*` tile set (block tops, `block_center`, carved corners, `ramp_long_a/b`, diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index ec66975..01276b8 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -403,7 +403,7 @@ local gSpiderMinX, gSpiderMaxX, gSpiderDir, gSpiderState constant kMoveSpeed = 280 constant kJumpSpeed = 430 -constant kPfUIVersion = "7" +constant kPfUIVersion = "8" -- The playable box hugs the level CONTENT, not the raw world width. The hero -- spawns at x 120 and each goal flag sits near the right end, so the side -- walls sit just past the spawn (kPfEdgeL) and just past the flag @@ -466,6 +466,7 @@ command buildPfUI if there is a field "pfHud" then delete field "pfHud" if there is a button "pfbtn_pause" then delete button "pfbtn_pause" if there is a button "pfbtn_reset" then delete button "pfbtn_reset" + if there is a button "pfbtn_level" then delete button "pfbtn_level" set the width of this stack to 1024 set the height of this stack to 640 set the loc of this stack to the screenLoc @@ -474,11 +475,21 @@ command buildPfUI catch tErr end try create field "pfTitle" - set the rect of it to 20, 8, 1004, 30 + set the rect of it to 20, 8, 838, 30 set the lockText of it to true set the traversalOn of it to false set the textSize of it to 14 set the text of it to "Box2Dxt platformer - a scrolling Game Kit showcase (input + sprites + player + camera + joints)" + -- LEVEL PICKER (top-right): a DEV/TEST convenience - jump straight to any + -- level instead of replaying the ones already approved (menuPick -> + -- pfJumpToLevel). UPDATE this menu list when a level is added. + create button "pfbtn_level" + set the style of it to "menu" + set the menuMode of it to "option" + set the rect of it to 846, 6, 1004, 32 + set the traversalOn of it to false + set the text of it to "1 Green Hills" & cr & "2 The Works" & cr & "3 Frozen Citadel" & cr & "4 Haunted Hollow" & cr & "5 Scorched Dunes" & cr & "6 Cavern Depths" & cr & "7 Stone Keep" + set the menuHistory of it to 1 create field "pfHelp" set the rect of it to 20, 34, 1004, 92 set the lockText of it to true @@ -1197,6 +1208,7 @@ command pfStartGame put empty into gHudLast put true into gStarted put false into gBuilding + if there is a button "pfbtn_level" then set the menuHistory of button "pfbtn_level" to gLevel b2kStart end pfStartGame @@ -3881,7 +3893,7 @@ command pfChromeFront if there is a field tC then set the layer of field tC to tN end repeat if there is a graphic "pfWinShade" then set the layer of graphic "pfWinShade" to tN - 1 - repeat for each item tC in "pfbtn_pause,pfbtn_reset,pfbtn_again" + repeat for each item tC in "pfbtn_pause,pfbtn_reset,pfbtn_again,pfbtn_level" if there is a button tC then set the layer of button tC to tN end repeat end pfChromeFront @@ -5388,6 +5400,32 @@ on mouseUp end switch end mouseUp +-- The LEVEL PICKER (the top-right option menu): jump straight to the chosen +-- level. A DEV/TEST convenience so the levels already approved need not be +-- replayed after every update; NOT part of the normal coin-gated progression. +on menuPick pWhich + local tLv + if the short name of the target is "pfbtn_level" then + put word 1 of pWhich into tLv + if tLv is an integer and tLv >= 1 then pfJumpToLevel tLv + end if + pass menuPick +end menuPick + +-- Rebuild straight to pLevel with a FRESH run state (banks zeroed, like starting +-- there). pfStartGame hides the win dialogue + clears the win latches itself, so +-- jumping works from play, pause, or the win screen. +command pfJumpToLevel pLevel + if gBuilding is true then exit pfJumpToLevel + put pLevel into gLevel + put 0 into gTotalFalls + put 0 into gSecsBank + put 0 into gGemsBank + put 0 into gGemsAllTotal + put true into gStarted + pfStartGame +end pfJumpToLevel + on mouseRelease b2kRelease end mouseRelease From c3f361b21cabba1055454b675492fe7512180771 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 16:40:16 +0000 Subject: [PATCH 05/10] platformer: redesign LEVEL 7 into a vertical FORTRESS (was a stone-skinned L6) OXT pass flagged the Stone Keep as "a shorter version of the previous level" - it had reused L6's exact geometry (wall-jump shaft, rubble mound, one-way cloud, goal steps in the same spots) just reskinned to stone. Rebuilt as its own level: a CASTLE climbed not crossed. - a spiked MOAT (192px) at the gate, leapt into a BAILEY (a slime + a snail) - a RAMPART STAIRCASE: three solid stone tiers (pf_plat1/2/3, block_top tops + block_center bodies down to the floor - a massive keep wall, not floating ledges) climbing to a high PARAPET - a BLOCK SLIME guards the battlements; windows + torches set into the keep wall - the bonus gem hangs above the parapet's edge; the flag waits up top - no wall-jump shaft / mound / cloud (L6's signature beats) - structurally distinct - pfShowSlabs learned the new pf_plat3 tier (no-art fallback stays correct) Example-side only (no Kit change, no harness bump). check-livecodescript passes; audit-platformer clears L7 (8 coins, 3 walkers, 0 findings across 7 levels). Frame names verified. Statically verified; needs an OXT pass (verify item 21). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01X92APxUJUMue3bAzXiX9V2 --- CHANGELOG.md | 25 +-- examples/box2dxt-platformer.livecodescript | 201 +++++++++------------ 2 files changed, 101 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df8f708..0e9404d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,17 +19,20 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `4`). title field shrinks to make room. Example-side only. - **Platformer: LEVEL 7 "STONE KEEP" - the STONE/CASTLE biome (asset-expansion Phase C, slice 1).** A seventh level on the previously-unused `terrain_stone_*` - tile set (block tops, `block_center`, carved corners, `ramp_long_a/b`, - `cloud_*`) over a built dark stone backdrop (`pfBuildStoneBackdrop`, gray-blue): - a WATCHTOWER (wall-jump, slot coin), a wide spiked MOAT (256px, four spikes) to - leap, a rubble RAMP, a one-way stone BATTLEMENT high route, two slimes + a - snail, a bonus GEM atop the tower, torchlit halls, and carved stone steps to - the flag. The win moves to L7 (`gLevel >= 7`); L6's flag now ADVANCES. Built on - the proven L6 geometry in stone; the SPINNER hazard and the multi-key/switch - PUZZLES (the keep's real identity) land in slices 2-3. Example-side only (no Kit - change, no harness bump); `tools/audit-platformer.py` auto-discovers and clears - L7 (9 coins, 3 walkers, 0 findings). Statically verified; needs an OXT pass - (header verify item 21). + tile set over a built dark stone backdrop (`pfBuildStoneBackdrop`, gray-blue). + **A vertical FORTRESS, climbed not crossed** - deliberately distinct from L6's + flat cavern (an early build that stone-skinned L6's geometry was redesigned + after an OXT pass flagged it as a palette swap): an approach with a spiked + **moat** at the gate, a **bailey** (a slime + a snail), then a **rampart + staircase** up three solid stone tiers (`block_top` + `block_center` bodies) to + a high **parapet** where a block slime guards the battlements (windows + torches + set into the keep wall) and the flag waits. A bonus gem hangs above the + parapet's edge. The win moves to L7 (`gLevel >= 7`); L6's flag now ADVANCES. + The SPINNER hazard and the multi-key/switch PUZZLES (the keep's mechanics) land + in slices 2-3. Example-side only (no Kit change, no harness bump); + `tools/audit-platformer.py` auto-discovers and clears L7 (8 coins, 3 walkers, + 0 findings; `pfShowSlabs` learned the new `pf_plat3` tier). Statically verified; + needs an OXT pass (header verify item 21). - **Platformer: the CONVEYOR BELT - a carried surface (asset-expansion Phase B, slice 3).** The previously-unused `conveyor` tile becomes a polled surface zone (`pfMakeConveyor pL, pR, pDir`) that adds a steady vx to the GROUNDED hero diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index 01276b8..ae3d047 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -94,12 +94,14 @@ -- SLIME + a snail, a CONVEYOR BELT (a treadmill you power across), -- and carved dirt steps to the flag. A bonus GEM rides high above -- the wall-jump shaft. --- LEVEL 7 STONE KEEP (3040px) - the asset-expansion STONE/CASTLE biome: a --- WATCHTOWER (wall-jump, slot coin), a wide spiked MOAT to leap --- (checkpoint past it), a rubble RAMP, a one-way stone BATTLEMENT --- high route, two slimes + a snail, torchlit halls, and carved stone --- steps to the flag (the win). A bonus GEM tops the watchtower. (The --- SPINNER hazard + the multi-key/switch PUZZLES land in slices 2-3.) +-- LEVEL 7 STONE KEEP (2940px) - the asset-expansion STONE/CASTLE biome, +-- CLIMBED not crossed (distinct from the cavern's flat run): an +-- approach with a spiked MOAT at the gate (leap it), a bailey with a +-- slime + a snail, then a RAMPART STAIRCASE up three solid stone +-- tiers to a high PARAPET where a BLOCK SLIME guards the battlements +-- (windows + torches in the keep wall) and the flag waits (the win). +-- A bonus GEM hangs above the parapet's edge. (The SPINNER hazard + +-- the multi-key/switch PUZZLES land in slices 2-3.) -- -- WHAT TO VERIFY (the Phase 3 + level-rebuild OXT pass) -- 1. Feel: a TAPPED jump is clearly shorter than a HELD one; jumping @@ -306,16 +308,19 @@ -- biome: terrain_stone_*, previously unused; the win now needs gLevel>=7). -- VERIFY on a full asset folder: (a) the BACKDROP is the dark stone keep -- (pfBuildStoneBackdrop), no border line on the two panels; (b) the stone --- ground/ramp/battlement/cliff-corners read with NO missing-frame --- fallbacks; (c) the WATCHTOWER slot coin is reachable by wall-jump AND a --- straight double-jump, and the bonus GEM atop it is reachable; (d) the --- wide MOAT (256px, four spikes) is a clean run/double-jump leap with the --- spikes FLUSH between the carved lips; (e) the rubble RAMP walks up/across/ --- down with no fall-through; the stone BATTLEMENT is solid to its ends and --- DOWN+JUMP drops through it; (f) both slimes + the snail patrol solid --- ground; torches/chains light the halls (orientation OXT-verify); the win --- screen says ALL SEVEN LEVELS CLEAR. (Spinner + key/switch puzzles: slices --- 2-3.) +-- ground/stair-tiers/parapet/cliff-lips read with NO missing-frame +-- fallbacks and the keep wall looks SOLID (no transparent gaps in the +-- tiered bodies); (c) the MOAT (192px, three spikes) is a clean leap with +-- the spikes FLUSH between the carved lips; (d) the RAMPART STAIRCASE +-- climbs cleanly - the hero hops bailey->step1->step2->parapet with no +-- getting stuck against a tier face; (e) the bonus GEM above the parapet's +-- edge is reachable by a double-jump (lower it if not); (f) the slime + +-- snail patrol the bailey and the BLOCK SLIME hops the parapet, all on +-- solid ground; windows/torches/chains read in the keep wall (orientation +-- OXT-verify); the win screen says ALL SEVEN LEVELS CLEAR. (Spinner + key/ +-- switch puzzles: slices 2-3.) NOTE: this LEVEL WAS REDESIGNED from a +-- stone-skinned L6 clone into a vertical fortress - confirm it now reads +-- as its own level. -- ===================================================================== local gStarted, gHero, gHeroSpr, gHudLast, gHurtLock @@ -1259,7 +1264,7 @@ end pfTextureSlab command pfShowSlabs pFlag local tC set the itemDelimiter to comma - repeat for each item tC in "pf_ground1,pf_ground2,pf_ground3,pf_plat1,pf_plat2" + repeat for each item tC in "pf_ground1,pf_ground2,pf_ground3,pf_plat1,pf_plat2,pf_plat3" if there is a graphic tC then set the visible of graphic tC to pFlag if pFlag is true then b2kCamAdopt the long id of graphic tC @@ -3746,117 +3751,85 @@ end pfL6Cast command pfL7Scene local tX put "STONE KEEP (fortified!)" into gLevelName - pfBounds kPfEdgeL, 3000 + kPfEndCap -- right edge hugs the goal flag (3000) - pfSlab "pf_ground1", 0, 576, 1024, 640 - pfSlab "pf_ground2", 1280, 576, 3072, 640 - pfSlab "pf_plat1", 2752, 512, 2912, 576 - pfSlab "pf_plat2", 2912, 448, 3072, 576 - -- the STONE biome: block tops across the keep floor (block_center under the - -- rubble ramp, one mass), carved CORNERS at the moat lips + the dais steps. + pfBounds kPfEdgeL, 2900 + kPfEndCap -- right edge hugs the goal on the parapet + -- a CASTLE, CLIMBED not crossed (distinct from the cavern's flat run): an + -- approach with a MOAT at the gate, a bailey, then a RAMPART STAIRCASE up + -- three solid stone tiers to a high PARAPET where the flag waits. The whole + -- terrain_stone_* set was unused before this level. + pfSlab "pf_ground1", 0, 576, 576, 640 + pfSlab "pf_ground2", 768, 576, 1856, 640 + pfSlab "pf_plat1", 1856, 512, 2048, 640 -- rampart step 1 (top y512) + pfSlab "pf_plat2", 2048, 448, 2240, 640 -- rampart step 2 (top y448) + pfSlab "pf_plat3", 2240, 384, 3008, 640 -- the upper PARAPET, the keep top (y384) if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_stone_block_top") then - repeat with tX = 0 to 896 step 64 + -- the approach + the moat's carved lips + repeat with tX = 0 to 448 step 64 pfTile "terrain_stone_block_top", tX, 576 end repeat - pfTile "terrain_stone_block_top_right", 960, 576 -- the moat's LEFT lip (carved cliff edge) - pfTile "terrain_stone_block_top_left", 1280, 576 -- the moat's RIGHT lip - repeat with tX = 1344 to 3008 step 64 - if tX >= 1408 and tX <= 1792 then - pfTile "terrain_stone_block_center", tX, 576 -- under the rubble ramp: one mass - else - pfTile "terrain_stone_block_top", tX, 576 - end if + pfTile "terrain_stone_block_top_right", 512, 576 -- the moat's LEFT lip + pfTile "terrain_stone_block_top_left", 768, 576 -- the moat's RIGHT lip + -- the bailey floor + repeat with tX = 832 to 1792 step 64 + pfTile "terrain_stone_block_top", tX, 576 + end repeat + -- the RAMPART STAIRCASE: each tier's walkable TOP + its solid stone body + -- down to the floor line (a massive keep wall, not floating ledges) + repeat with tX = 1856 to 1984 step 64 + pfTile "terrain_stone_block_top", tX, 512 + pfTile "terrain_stone_block_center", tX, 576 + end repeat + repeat with tX = 2048 to 2176 step 64 + pfTile "terrain_stone_block_top", tX, 448 + pfTile "terrain_stone_block_center", tX, 512 + pfTile "terrain_stone_block_center", tX, 576 + end repeat + repeat with tX = 2240 to 2944 step 64 + pfTile "terrain_stone_block_top", tX, 384 + pfTile "terrain_stone_block_center", tX, 448 + pfTile "terrain_stone_block_center", tX, 512 + pfTile "terrain_stone_block_center", tX, 576 end repeat - pfTile "terrain_stone_block_top_left", 2752, 512 - pfTile "terrain_stone_block_top", 2816, 512 - pfTile "terrain_stone_block_top_right", 2880, 512 - pfTile "terrain_stone_block_top", 2912, 448 - pfTile "terrain_stone_block_top", 2976, 448 - pfTile "terrain_stone_block_center", 2912, 512 - pfTile "terrain_stone_block_center", 2976, 512 pfShowSlabs false else pfShowSlabs true end if - -- the WATCHTOWER (wall-jump): two floating stone columns frame a slot over - -- the courtyard - wall-jump (or double-jump) up it for the slot coin. The - -- L3/L6 shaft recipe in stone; the columns float so the main path runs under. - pfSlab "pf_wjShaftL", 320, 256, 384, 448 - pfSlab "pf_wjShaftR", 472, 256, 536, 448 - if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_stone_block_top") then - pfTile "terrain_stone_block_top", 320, 256 - pfTile "terrain_stone_block_center", 320, 320 - pfTile "terrain_stone_block_center", 320, 384 - pfTile "terrain_stone_block_top", 472, 256 - pfTile "terrain_stone_block_center", 472, 320 - pfTile "terrain_stone_block_center", 472, 384 - else - set the backgroundColor of graphic "pf_wjShaftL" to "120,124,140" - set the backgroundColor of graphic "pf_wjShaftR" to "120,124,140" - set the visible of graphic "pf_wjShaftL" to true - set the visible of graphic "pf_wjShaftR" to true - b2kCamAdopt the long id of graphic "pf_wjShaftL" - b2kCamAdopt the long id of graphic "pf_wjShaftR" - end if - pfMakeCoin 428, 340 -- the slot coin (slot 384..472), built in the SCENE to align with the columns - -- the MOAT: a wide spiked chasm (256px = four spikes), cleared with a run/ - -- double-jump; the checkpoint sits just past it. Carved stone lips finish it. - pfMakeSpikes 1024, 1280 - -- a RUBBLE RAMP (stone slope): up the left ramp, across the crest, down the - -- right. Six-point chain, outer two ghost anchors; ramps mirrored. - b2kSmoothGround "1856,576" & cr & "1792,576" & cr & "1664,512" & cr & "1536,512" & cr & "1408,576" & cr & "1344,576" - if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_stone_ramp_long_a") then - pfTile "terrain_stone_ramp_long_b", 1408, 512, true - pfTile "terrain_stone_ramp_long_a", 1472, 512, true - pfTile "terrain_stone_block_top", 1536, 512 - pfTile "terrain_stone_block_top", 1600, 512 - pfTile "terrain_stone_ramp_long_a", 1664, 512 - pfTile "terrain_stone_ramp_long_b", 1728, 512 - end if - -- a one-way STONE BATTLEMENT (drop through with DOWN+JUMP) for the high bonus - -- route; ghost-padded a tile past the art each side (solid span 1984..2176). - b2kSmoothGround "2240,448" & cr & "2176,448" & cr & "1984,448" & cr & "1920,448" - if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_stone_cloud_left") then - pfTile "terrain_stone_cloud_left", 1984, 448 - pfTile "terrain_stone_cloud_middle", 2048, 448 - pfTile "terrain_stone_cloud_right", 2112, 448 - else - create graphic "pf_cloudledgeS" - set the style of it to "line" - set the points of it to "1984,448" & cr & "2176,448" - set the lineSize of it to 4 - set the foregroundColor of it to "150,154,170" - b2kCamAdopt the long id of graphic "pf_cloudledgeS" - end if - -- KEEP DRESSING: wall torches + hanging chains light the dungeon (torches/ - -- chains are not audited decor). Orientation: OXT-verify (gotcha 26). - pfMakeTorch 352, 300 -- on the watchtower's left column - pfMakeTorch 504, 300 -- ...and its right column - pfMakeTorch 840, 452 -- the courtyard - pfMakeTorch 1360, 452 -- past the moat - pfMakeTorch 2360, 452 -- the keep run - pfMakeTorch 2860, 432 -- the climb to the dais + -- the MOAT: a spiked chasm at the gate (192px = three spikes), leapt into the + -- bailey; carved stone lips finish it. + pfMakeSpikes 576, 768 + -- CASTLE DRESSING: wall torches, hanging chains, and windows set into the + -- keep's stonework (torches/chains/windows are not audited decor; window/ + -- torch facing: OXT-verify, gotcha 26). + pfMakeTorch 200, 452 -- the approach + pfMakeTorch 1000, 452 -- the bailey + pfMakeTorch 1520, 452 -- the bailey + pfMakeTorch 2400, 320 -- the parapet + pfMakeTorch 2780, 320 -- the parapet, near the flag if gAssetsOK is true and b2kSheetHasFrame("tiles", "chain") then - pfTile "chain", 700, 96 + pfTile "chain", 1100, 96 pfTile "chain", 1640, 96 - pfTile "chain", 2520, 96 + end if + if gAssetsOK is true and b2kSheetHasFrame("tiles", "window") then + pfTile "window", 2400, 448 -- windows in the keep wall + pfTile "window", 2592, 448 + pfTile "window", 2784, 448 end if end pfL7Scene command pfL7Cast - pfMakeCoin 240, 500 -- the courtyard - pfMakeCoin 700, 500 -- the first slime's beat - pfMakeCoin 1152, 452 -- mid-air over the MOAT: leap it - pfMakeCoin 1568, 468 -- atop the rubble ramp - pfMakeCoin 2048, 408 -- on the stone battlement (the high route) - pfMakeCoin 2300, 500 -- the keep run - pfMakeCoin 2560, 500 -- the second slime's beat - pfMakeCoin 2820, 476 -- on the first dais step - pfMakeGem 428, 204, "blue" -- BONUS gem above the watchtower (master the wall-jump) - pfMakeSlime 1, "normal", 700, 600, 860, 576 - pfMakeSlime 2, "normal", 2560, 2440, 2680, 576 - pfMakeSnail 3, 2050, 1980, 2120 - pfMakeCheckpoint 1360 -- just past the moat, before the keep - pfMakeGoal 3000, 416 + pfMakeCoin 240, 500 -- the approach + pfMakeCoin 672, 452 -- mid-air over the MOAT: leap it + pfMakeCoin 1050, 500 -- the bailey slime's beat + pfMakeCoin 1550, 500 -- the bailey snail's beat + pfMakeCoin 1952, 480 -- on rampart step 1 + pfMakeCoin 2144, 416 -- on rampart step 2 + pfMakeCoin 2500, 352 -- up on the parapet + pfMakeCoin 2700, 352 -- the parapet, the block slime's beat + pfMakeGem 2300, 304, "blue" -- BONUS gem: a double-jump above the parapet's edge (OXT: confirm reachable; lower it if not) + pfMakeSlime 1, "normal", 1050, 900, 1250, 576 + pfMakeSnail 2, 1550, 1420, 1680 + pfMakeBlockSlime 3, 2620, 2400, 2840, 384 -- a CUBE hops the battlements (the parapet's guard) + pfMakeCheckpoint 1780 -- in the bailey, before the rampart climb + pfMakeGoal 2900, 352 end pfL7Cast From c894104a3334fbcc6597a3f8701ed708d0f4bf67 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 17:09:50 +0000 Subject: [PATCH 06/10] platformer: rebuild LEVEL 7 as a VERTICAL climbing tower (camera scrolls up) The user asked for an actual vertical level (jump UP to progress, not left-to- right). The Stone Keep is now a tall tower you CLIMB, with the camera scrolling UP as the hero jumps one-way stone ledges to the flag at the top - the first level in the game that scrolls vertically. New gated VERTICAL-CAMERA mode, parameterized so L1-L6 are byte-for-byte unchanged: - gCamTopY / gCamBotY / gKillPlaneY globals; pfBounds pins them (320/320/780 = no vertical scroll, as before), pfBoundsV opens them for a tall world - the per-frame camera follows the hero's Y clamped to gCamTopY..gCamBotY, and centres X when the world is no wider than the viewport (the tower) - the hero spawns at gRespawnX/gRespawnY (L1-L6 still 120/480; L7 = the tower bottom); the kill plane sits far below (a fall drops you to a lower ledge) - pfBoundsV: a tall positive-coord world (walls + ceiling), camera bounds, no respawn-on-fall (contained) The level: pfMakeLedge builds one-way stone climb platforms (b2kSmoothGround + stone_cloud); a zig-zag of 8 ledges, 8 coins, a summit gem, the flag atop the keep. tools/audit-platformer.py learned to skip a vertical level (the y=576 ground model doesn't apply); L1-L6 still 0 findings. Example-side only (no Kit change, no harness bump). check-livecodescript passes. THE VERTICAL CAMERA SCROLL is the one thing untested on the OXT engine (every other level only scrolls horizontally) - flagged in verify item 21. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01X92APxUJUMue3bAzXiX9V2 --- CHANGELOG.md | 35 +-- docs/asset-expansion-plan.md | 15 +- examples/box2dxt-platformer.livecodescript | 252 ++++++++++++--------- tools/audit-platformer.py | 20 +- 4 files changed, 187 insertions(+), 135 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9404d..33292fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,22 +17,27 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `4`). level. Chrome change, so `kPfUIVersion` bumps to "8" (older pasted stacks rebuild their UI once). Not part of the normal coin-gated progression; the title field shrinks to make room. Example-side only. -- **Platformer: LEVEL 7 "STONE KEEP" - the STONE/CASTLE biome (asset-expansion +- **Platformer: LEVEL 7 "STONE KEEP" - a VERTICAL climbing tower (asset-expansion Phase C, slice 1).** A seventh level on the previously-unused `terrain_stone_*` - tile set over a built dark stone backdrop (`pfBuildStoneBackdrop`, gray-blue). - **A vertical FORTRESS, climbed not crossed** - deliberately distinct from L6's - flat cavern (an early build that stone-skinned L6's geometry was redesigned - after an OXT pass flagged it as a palette swap): an approach with a spiked - **moat** at the gate, a **bailey** (a slime + a snail), then a **rampart - staircase** up three solid stone tiers (`block_top` + `block_center` bodies) to - a high **parapet** where a block slime guards the battlements (windows + torches - set into the keep wall) and the flag waits. A bonus gem hangs above the - parapet's edge. The win moves to L7 (`gLevel >= 7`); L6's flag now ADVANCES. - The SPINNER hazard and the multi-key/switch PUZZLES (the keep's mechanics) land - in slices 2-3. Example-side only (no Kit change, no harness bump); - `tools/audit-platformer.py` auto-discovers and clears L7 (8 coins, 3 walkers, - 0 findings; `pfShowSlabs` learned the new `pf_plat3` tier). Statically verified; - needs an OXT pass (header verify item 21). + set, built as **a tall tower you CLIMB, not a level you cross** - the camera + **scrolls UP** as the hero jumps from one one-way stone ledge to the next, to + the flag atop the keep. The first level in the game that scrolls vertically. + (Two earlier passes - a stone-skinned L6 clone, then a single-screen fortress - + were redesigned after OXT feedback; the user asked for a true vertical level.) + - **New gated vertical-camera mode**, all parameterized so L1-L6 are byte-for- + byte unchanged: `pfBoundsV` (a tall world, walls + ceiling, a kill plane far + below) sets `gCamTopY`/`gCamBotY`/`gKillPlaneY`; the per-frame camera follows + the hero's Y clamped to those (horizontal levels keep `gCamTopY=gCamBotY=320`, + pinning Y as before) and centres X when the world is no wider than the + viewport; the hero spawns at `gRespawnX`/`gRespawnY` (L1-L6 still 120/480). + - `pfMakeLedge` builds the one-way stone climb platforms (`b2kSmoothGround` + + `stone_cloud`); a zig-zag of 8 ledges, 8 coins, a summit gem, contained walls + (a fall drops you to a lower ledge, no respawn). + The win moves to L7 (`gLevel >= 7`); L6's flag now ADVANCES. Example-side only + (no Kit change, no harness bump). `tools/audit-platformer.py` learned to skip a + vertical level (the y=576 model doesn't apply); L1-L6 still 0 findings. + Statically verified; **the vertical camera scroll needs the OXT pass** (verify + item 21) - it is the one thing untested on the engine. - **Platformer: the CONVEYOR BELT - a carried surface (asset-expansion Phase B, slice 3).** The previously-unused `conveyor` tile becomes a polled surface zone (`pfMakeConveyor pL, pR, pDir`) that adds a steady vx to the GROUNDED hero diff --git a/docs/asset-expansion-plan.md b/docs/asset-expansion-plan.md index 38b5586..e5ae204 100644 --- a/docs/asset-expansion-plan.md +++ b/docs/asset-expansion-plan.md @@ -172,12 +172,15 @@ needs an OXT eye. a polled example-side version first. ### Phase C — Castle/dungeon biome → **Level 7 "STONE KEEP"** (M–L) -- **Slice 1 — DONE (statically verified; needs OXT):** the **stone biome + Level - 7 skeleton** that completes start-to-finish. `terrain_stone_*` (block tops, - `block_center`, carved corners, `ramp_long_a/b`, `cloud_*`) over a built dark - stone backdrop (`pfBuildStoneBackdrop`): a watchtower (wall-jump), a wide spiked - moat, a rubble ramp, a stone battlement, reused slimes + a snail, a bonus gem, - torchlit halls. Win moved to `gLevel >= 7`. Example-side; audit clears L7. +- **Slice 1 — DONE (statically verified; needs OXT):** Level 7 as a **VERTICAL + climbing tower** on the `terrain_stone_*` set over the dark stone backdrop + (`pfBuildStoneBackdrop`). A new **gated vertical-camera mode** (`pfBoundsV` + + `gCamTopY/gCamBotY/gKillPlaneY`, spawn at `gRespawnX/Y`) scrolls the camera UP + as the hero jumps one-way `pfMakeLedge` stone ledges to the flag atop the keep; + 8 coins + a summit gem. L1-L6 byte-for-byte unchanged. Win moved to + `gLevel >= 7`. The audit skips the vertical level. **The vertical camera scroll + is the OXT unknown.** (Earlier horizontal passes were redesigned after the user + asked for a true vertical level.) - **Slices 2–3 — TODO:** the **spinner** hazard (slice 2), then the **multi-key / switch puzzles** (slice 3) — the keep's real identity. - **Assets:** `terrain_stone_*` (full), `switch_{blue,green,red}(_pressed)`, diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index ae3d047..1d92b46 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -94,14 +94,13 @@ -- SLIME + a snail, a CONVEYOR BELT (a treadmill you power across), -- and carved dirt steps to the flag. A bonus GEM rides high above -- the wall-jump shaft. --- LEVEL 7 STONE KEEP (2940px) - the asset-expansion STONE/CASTLE biome, --- CLIMBED not crossed (distinct from the cavern's flat run): an --- approach with a spiked MOAT at the gate (leap it), a bailey with a --- slime + a snail, then a RAMPART STAIRCASE up three solid stone --- tiers to a high PARAPET where a BLOCK SLIME guards the battlements --- (windows + torches in the keep wall) and the flag waits (the win). --- A bonus GEM hangs above the parapet's edge. (The SPINNER hazard + --- the multi-key/switch PUZZLES land in slices 2-3.) +-- LEVEL 7 STONE KEEP (a VERTICAL TOWER, 1280px tall) - the asset-expansion +-- STONE/CASTLE biome, CLIMBED not crossed: you JUMP UP one-way stone +-- ledges that zig-zag to the top of the keep, and the camera SCROLLS +-- UP with you (the only level that scrolls vertically). Eight coins up +-- the climb, a bonus GEM at the very summit, the flag atop the keep +-- (the win); torchlit walls. A pure climb for slice 1 - the SPINNER +-- hazard + the multi-key/switch PUZZLES land in slices 2-3. -- -- WHAT TO VERIFY (the Phase 3 + level-rebuild OXT pass) -- 1. Feel: a TAPPED jump is clearly shorter than a HELD one; jumping @@ -304,23 +303,22 @@ -- the goal and faster back, you can still power across, and jumping over it -- is unaffected; the tread art faces the push direction (flip - OXT-verify -- it reads). L6's flag now ADVANCES to L7 (the win moved to L7). --- 21. ASSET-EXPANSION PHASE C, SLICE 1 - LEVEL 7 "STONE KEEP" (the STONE/CASTLE --- biome: terrain_stone_*, previously unused; the win now needs gLevel>=7). --- VERIFY on a full asset folder: (a) the BACKDROP is the dark stone keep --- (pfBuildStoneBackdrop), no border line on the two panels; (b) the stone --- ground/stair-tiers/parapet/cliff-lips read with NO missing-frame --- fallbacks and the keep wall looks SOLID (no transparent gaps in the --- tiered bodies); (c) the MOAT (192px, three spikes) is a clean leap with --- the spikes FLUSH between the carved lips; (d) the RAMPART STAIRCASE --- climbs cleanly - the hero hops bailey->step1->step2->parapet with no --- getting stuck against a tier face; (e) the bonus GEM above the parapet's --- edge is reachable by a double-jump (lower it if not); (f) the slime + --- snail patrol the bailey and the BLOCK SLIME hops the parapet, all on --- solid ground; windows/torches/chains read in the keep wall (orientation --- OXT-verify); the win screen says ALL SEVEN LEVELS CLEAR. (Spinner + key/ --- switch puzzles: slices 2-3.) NOTE: this LEVEL WAS REDESIGNED from a --- stone-skinned L6 clone into a vertical fortress - confirm it now reads --- as its own level. +-- 21. ASSET-EXPANSION PHASE C, SLICE 1 - LEVEL 7 "STONE KEEP" - a VERTICAL +-- TOWER you CLIMB (the win now needs gLevel>=7). *** THE BIG OXT ITEM: the +-- camera must SCROLL UP smoothly as the hero climbs *** - this is the only +-- level that scrolls vertically (pfBoundsV opens gCamTopY/gCamBotY; every +-- other level pins y=320). If the vertical scroll is wrong, that is where +-- to look (the per-frame camera in b2kFrame + pfBoundsV). ALSO VERIFY: (a) +-- the hero spawns at the BOTTOM and the view starts there; (b) the one-way +-- stone LEDGES are solid from above and you JUMP UP THROUGH them from below +-- (drop-through still works); (c) the zig-zag is climbable jump-to-jump (or +-- with a double-jump) and the side WALLS keep you in the tower; (d) falling +-- drops you to a lower ledge / the floor, never off the world or to a +-- respawn (the kill plane is off); (e) all 8 coins are grabbable up the +-- climb, the summit GEM needs a double-jump off the top, the flag is atop +-- the keep; (f) the backdrop is the dark stone keep and the chrome (HUD/ +-- help/buttons/level-picker) stays put while the world scrolls. The win +-- screen says ALL SEVEN LEVELS CLEAR. (Spinner + puzzles: slices 2-3.) -- ===================================================================== local gStarted, gHero, gHeroSpr, gHudLast, gHurtLock @@ -340,6 +338,7 @@ local gBuilding, gLands, gPrevState, gHudNextMS, gGateVel, gAirMS -- totals carried across level clears for the final win screen local gLevel, gLevelW, gLevelName, gTotalFalls, gSecsBank local gEdgeL, gEdgeR -- the playable L/R edges (content-hugging); set by pfBounds +local gCamTopY, gCamBotY, gKillPlaneY -- camera Y clamp + kill plane; pfBounds pins gCamTopY=gCamBotY=320 (no vertical scroll), pfBoundsV opens them for the climbing levels local gPlateX, gGateUpY, gGateDownY, gDoorX, gDoorWord, gCheckX -- enemy/trap tables, indexed 1..N (see pfMakeSlime/pfMakeThwomp/pfAddMover) local gSlimeN, gSlimeB, gSlimeSpr, gSlimeKind, gSlimeDir @@ -857,6 +856,9 @@ command pfStartGame put 2880 into gLevelW put 64 into gEdgeL -- pfBounds overrides both per level (belt-and-braces defaults) put 2880 into gEdgeR + put 320 into gCamTopY + put 320 into gCamBotY + put 780 into gKillPlaneY -- the indexed enemy/trap tables start the run empty put 0 into gSlimeN put empty into gSlimeB @@ -1075,13 +1077,13 @@ command pfStartGame put tH / 2 into gHeroHalfH -- bonks reach the brick at the real head create graphic "pf_heroBody" set the style of it to "rectangle" - set the rect of it to 120 - tW div 2, 480 - tH div 2, 120 + tW div 2, 480 + tH div 2 + set the rect of it to gRespawnX - tW div 2, gRespawnY - tH div 2, gRespawnX + tW div 2, gRespawnY + tH div 2 set the visible of it to false put the long id of graphic "pf_heroBody" into gHero if gAssetsOK is true then - b2kSpriteNew "chars", "character_beige_idle", 120, 480 + b2kSpriteNew "chars", "character_beige_idle", gRespawnX, gRespawnY else - b2kSpriteNew "chars", 1, 120, 480 + b2kSpriteNew "chars", 1, gRespawnX, gRespawnY end if put the result into gHeroSpr b2kSpriteBind gHeroSpr, gHero, 0, tDY @@ -1154,7 +1156,7 @@ command pfStartGame -- Deferred to HERE so the whole world - scenery, hero AND cast - was built -- at scroll 0; no create-once pickup is scroll-shifted. Per-frame tracking -- takes over in b2kFrame once the intro beat ends. - b2kCamGoto gEdgeL + 512, 320 + b2kCamGoto gEdgeL + 512, gCamBotY -- gCamBotY=320 (horizontal) or the tower's bottom (vertical) -- help + the level banner on the splash (visible through the intro -- beat); gCoinsTotal counted itself during the build above if gAssetsOK is true and gLoadNote is empty then @@ -2072,8 +2074,34 @@ command pfBounds pLeft, pRight -- stop you right at the content and the view never scrolls out onto bare -- floor past either end (no visible wall needed - the tight stop + clamp do it) b2kCamBounds pLeft, 0, pRight, 640 + put 320 into gCamTopY -- horizontal: the camera Y is pinned (the world is exactly one viewport tall) + put 320 into gCamBotY + put 780 into gKillPlaneY end pfBounds +-- VERTICAL bounds (the CLIMBING levels): a TALL world (pTop..pBottom), walls +-- around the full box + a ceiling, and the camera scrolls VERTICALLY between +-- gCamTopY..gCamBotY as the hero climbs. The kill plane sits well BELOW the +-- bottom floor: the climb is contained (a missed jump drops you onto a lower +-- platform or the floor, never off the world), so there is no respawn-on-fall. +-- Coords stay POSITIVE (the off-card collision worry was NEGATIVE coords; a +-- tall positive world collides the same way the horizontal walls do). +command pfBoundsV pLeft, pRight, pTop, pBottom + put pLeft into gEdgeL + put pRight into gEdgeR + put pRight into gLevelW + pfSlab "pf_wallL", 0, pTop, pLeft, pBottom -- POSITIVE coords only (x 0..pLeft), like pfBounds + pfSlab "pf_wallR", pRight, pTop, pRight + 256, pBottom + b2kWall pLeft, pTop, pLeft, pBottom + b2kWall pRight, pTop, pRight, pBottom + b2kWall pLeft, pTop, pRight, pTop -- the ceiling + b2kKillFloor pBottom + 300 -- below the floor slab; the climb never reaches it + b2kCamBounds pLeft, pTop, pRight, pBottom + put pTop + 320 into gCamTopY -- a 640-tall viewport sliding inside the tall world + put pBottom - 320 into gCamBotY + put pBottom + 300 into gKillPlaneY -- the explicit kill check is effectively off (contained) +end pfBoundsV + -- The button-and-gate machine: a polled pressure plate at pPlateX -- (pfTickGate reads gPlateX) drives a kinematic gate whose left edge -- is pGateL. The gateway coin is made FIRST so the closed gate hides @@ -3739,97 +3767,90 @@ end pfL6Cast -- ===================================================================== --- LEVEL 7 - "STONE KEEP" (asset-expansion Phase C, slice 1): the CASTLE/ --- DUNGEON biome. The whole terrain_stone_* set was unused before this --- (only door-walls). A fortress interior on a dark stone backdrop: a --- watchtower (wall-jump), a wide spiked MOAT, a rubble ramp, a stone --- BATTLEMENT high route, torchlit halls. Built on the proven L6 geometry --- in stone. Slice 1 reuses the slime/snail cast; the SPINNER (slice 2) --- and the multi-key / switch PUZZLES (slice 3) are the keep's real --- identity, landing next. +-- A one-way stone LEDGE for the vertical climb: a b2kSmoothGround chain (solid +-- from ABOVE, pass up THROUGH from below), ghost-padded a tile each side, then +-- skinned with stone_cloud tiles (a line in the no-art fallback). pL..pR is the +-- SOLID span at height pY. +command pfMakeLedge pL, pR, pY + local tX, tPts + put (pR + 64) & "," & pY & cr & pR & "," & pY & cr & pL & "," & pY & cr & (pL - 64) & "," & pY into tPts + b2kSmoothGround tPts + if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_stone_cloud_left") then + pfTile "terrain_stone_cloud_left", pL, pY + repeat with tX = pL + 64 to pR - 128 step 64 + pfTile "terrain_stone_cloud_middle", tX, pY + end repeat + pfTile "terrain_stone_cloud_right", pR - 64, pY + else + create graphic ("pf_ledge" & pL & "_" & pY) + set the style of it to "line" + set the points of it to pL & "," & pY & cr & pR & "," & pY + set the lineSize of it to 4 + set the foregroundColor of it to "150,154,170" + b2kCamAdopt the long id of graphic ("pf_ledge" & pL & "_" & pY) + end if +end pfMakeLedge + +-- ===================================================================== +-- LEVEL 7 - "STONE KEEP" (asset-expansion Phase C, slice 1): a VERTICAL +-- TOWER you CLIMB, not a level you cross. The terrain_stone_* set (unused +-- before this) builds a tall keep; the camera SCROLLS UP as the hero +-- jumps from one one-way stone ledge to the next (pfBoundsV opens the +-- vertical camera; L1-L6 are untouched). A pure climb for slice 1; the +-- SPINNER (slice 2) and the multi-key / switch PUZZLES (slice 3) are the +-- keep's hazards, landing next. NOTE: the vertical camera scroll is the +-- one thing that needs an OXT eye (every other level only scrolls +-- horizontally) - if it is janky, that is where to look. -- ===================================================================== command pfL7Scene local tX - put "STONE KEEP (fortified!)" into gLevelName - pfBounds kPfEdgeL, 2900 + kPfEndCap -- right edge hugs the goal on the parapet - -- a CASTLE, CLIMBED not crossed (distinct from the cavern's flat run): an - -- approach with a MOAT at the gate, a bailey, then a RAMPART STAIRCASE up - -- three solid stone tiers to a high PARAPET where the flag waits. The whole - -- terrain_stone_* set was unused before this level. - pfSlab "pf_ground1", 0, 576, 576, 640 - pfSlab "pf_ground2", 768, 576, 1856, 640 - pfSlab "pf_plat1", 1856, 512, 2048, 640 -- rampart step 1 (top y512) - pfSlab "pf_plat2", 2048, 448, 2240, 640 -- rampart step 2 (top y448) - pfSlab "pf_plat3", 2240, 384, 3008, 640 -- the upper PARAPET, the keep top (y384) + put "STONE KEEP (the climb!)" into gLevelName + put 160 into gRespawnX -- the hero starts at the BOTTOM of the tower + put 1140 into gRespawnY + pfBoundsV 64, 960, 0, 1280 -- a TALL world (climb it; camera scrolls UP); 64..960 wide = one viewport, no horizontal scroll + pfSlab "pf_ground1", 64, 1216, 960, 1280 -- the bottom floor (solid) if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_stone_block_top") then - -- the approach + the moat's carved lips - repeat with tX = 0 to 448 step 64 - pfTile "terrain_stone_block_top", tX, 576 - end repeat - pfTile "terrain_stone_block_top_right", 512, 576 -- the moat's LEFT lip - pfTile "terrain_stone_block_top_left", 768, 576 -- the moat's RIGHT lip - -- the bailey floor - repeat with tX = 832 to 1792 step 64 - pfTile "terrain_stone_block_top", tX, 576 - end repeat - -- the RAMPART STAIRCASE: each tier's walkable TOP + its solid stone body - -- down to the floor line (a massive keep wall, not floating ledges) - repeat with tX = 1856 to 1984 step 64 - pfTile "terrain_stone_block_top", tX, 512 - pfTile "terrain_stone_block_center", tX, 576 - end repeat - repeat with tX = 2048 to 2176 step 64 - pfTile "terrain_stone_block_top", tX, 448 - pfTile "terrain_stone_block_center", tX, 512 - pfTile "terrain_stone_block_center", tX, 576 - end repeat - repeat with tX = 2240 to 2944 step 64 - pfTile "terrain_stone_block_top", tX, 384 - pfTile "terrain_stone_block_center", tX, 448 - pfTile "terrain_stone_block_center", tX, 512 - pfTile "terrain_stone_block_center", tX, 576 + repeat with tX = 64 to 896 step 64 + pfTile "terrain_stone_block_top", tX, 1216 end repeat pfShowSlabs false else pfShowSlabs true end if - -- the MOAT: a spiked chasm at the gate (192px = three spikes), leapt into the - -- bailey; carved stone lips finish it. - pfMakeSpikes 576, 768 - -- CASTLE DRESSING: wall torches, hanging chains, and windows set into the - -- keep's stonework (torches/chains/windows are not audited decor; window/ - -- torch facing: OXT-verify, gotcha 26). - pfMakeTorch 200, 452 -- the approach - pfMakeTorch 1000, 452 -- the bailey - pfMakeTorch 1520, 452 -- the bailey - pfMakeTorch 2400, 320 -- the parapet - pfMakeTorch 2780, 320 -- the parapet, near the flag - if gAssetsOK is true and b2kSheetHasFrame("tiles", "chain") then - pfTile "chain", 1100, 96 - pfTile "chain", 1640, 96 - end if + -- THE CLIMB: one-way stone ledges zig-zag up to the keep's top, each ~128px + -- above the last and staggered side to side - jump (or double-jump) up + -- through each and land on top. The walls (pfBoundsV) keep you in the tower. + pfMakeLedge 256, 448, 1088 + pfMakeLedge 576, 768, 960 + pfMakeLedge 256, 448, 832 + pfMakeLedge 576, 768, 704 + pfMakeLedge 256, 448, 576 + pfMakeLedge 576, 768, 448 + pfMakeLedge 384, 640, 320 + pfMakeLedge 320, 704, 192 -- the KEEP TOP (the goal ledge; wide, to land + reach the flag) + -- TOWER DRESSING: torches climb the walls; a window or two in the stonework. + pfMakeTorch 96, 1000 + pfMakeTorch 928, 824 + pfMakeTorch 96, 632 + pfMakeTorch 928, 440 + pfMakeTorch 96, 264 if gAssetsOK is true and b2kSheetHasFrame("tiles", "window") then - pfTile "window", 2400, 448 -- windows in the keep wall - pfTile "window", 2592, 448 - pfTile "window", 2784, 448 + pfTile "window", 16, 900 -- set into the tower walls + pfTile "window", 912, 500 end if end pfL7Scene command pfL7Cast - pfMakeCoin 240, 500 -- the approach - pfMakeCoin 672, 452 -- mid-air over the MOAT: leap it - pfMakeCoin 1050, 500 -- the bailey slime's beat - pfMakeCoin 1550, 500 -- the bailey snail's beat - pfMakeCoin 1952, 480 -- on rampart step 1 - pfMakeCoin 2144, 416 -- on rampart step 2 - pfMakeCoin 2500, 352 -- up on the parapet - pfMakeCoin 2700, 352 -- the parapet, the block slime's beat - pfMakeGem 2300, 304, "blue" -- BONUS gem: a double-jump above the parapet's edge (OXT: confirm reachable; lower it if not) - pfMakeSlime 1, "normal", 1050, 900, 1250, 576 - pfMakeSnail 2, 1550, 1420, 1680 - pfMakeBlockSlime 3, 2620, 2400, 2840, 384 -- a CUBE hops the battlements (the parapet's guard) - pfMakeCheckpoint 1780 -- in the bailey, before the rampart climb - pfMakeGoal 2900, 352 + pfMakeCoin 352, 1040 -- climbing: above ledge 1 + pfMakeCoin 672, 912 -- ledge 2 + pfMakeCoin 352, 784 -- ledge 3 + pfMakeCoin 672, 656 -- ledge 4 + pfMakeCoin 352, 528 -- ledge 5 + pfMakeCoin 672, 400 -- ledge 6 + pfMakeCoin 512, 272 -- ledge 7 + pfMakeCoin 448, 144 -- on the keep top + pfMakeGem 544, 80, "blue" -- BONUS gem: a double-jump above the keep top (the summit) + pfMakeGoal 544, 160 -- the flag, atop the keep end pfL7Cast @@ -3891,7 +3912,7 @@ end pfWipeStage -- The game tick -- ===================================================================== on b2kFrame - local tHud, tPos, tHS, tCamX + local tHud, tPos, tHS, tCamX, tCamY if gHero is empty then exit b2kFrame -- THE HERO SNAPSHOT, once per frame: position and controller state -- feed the kill plane, edge failsafe, sound cues and every pf tick @@ -3915,7 +3936,7 @@ on b2kFrame -- Plus the EDGE FAILSAFE: a solver squeeze must never leave the hero -- beyond a wall (with the thick slabs it should never fire; when it -- does not, it costs two compares) - if gHurtLock is not true and gHeroPY > 780 then pfHurt -- the kill plane = respawn + if gHurtLock is not true and gHeroPY > gKillPlaneY then pfHurt -- the kill plane = respawn (off, far below, on a contained vertical climb) -- the EDGE CLAMP runs ALWAYS now (containment is non-negotiable): a hard -- per-frame snap so a knockback, a dash, or a wall that failed to collide -- can never leave the box - b2kMoveTo teleports the body back inside. @@ -3928,10 +3949,17 @@ on b2kFrame -- flags drew offset at the edges. b2kCamGoto keeps them in sync; we just -- pre-clamp the point (b2kCamBounds did not hold on the user's engine). if gIntroPan is 0 and gCamOK is true then - put gHeroPX into tCamX - if tCamX < gEdgeL + 512 then put gEdgeL + 512 into tCamX - if tCamX > gEdgeR - 512 then put gEdgeR - 512 into tCamX - b2kCamGoto tCamX, 320 + if gEdgeR - gEdgeL <= 1024 then + put (gEdgeL + gEdgeR) / 2 into tCamX -- a world no wider than the viewport (the tower): centre it, no horizontal scroll + else + put gHeroPX into tCamX + if tCamX < gEdgeL + 512 then put gEdgeL + 512 into tCamX + if tCamX > gEdgeR - 512 then put gEdgeR - 512 into tCamX + end if + put gHeroPY into tCamY -- follow vertically too; gCamTopY=gCamBotY=320 pins it on horizontal levels + if tCamY < gCamTopY then put gCamTopY into tCamY + if tCamY > gCamBotY then put gCamBotY into tCamY + b2kCamGoto tCamX, tCamY end if -- centre-screen banners (pfNotice) expire here if gNoticeUntil > 0 and the milliseconds > gNoticeUntil then diff --git a/tools/audit-platformer.py b/tools/audit-platformer.py index a8c1549..37c78f8 100644 --- a/tools/audit-platformer.py +++ b/tools/audit-platformer.py @@ -61,6 +61,7 @@ def __init__(self, n): self.conveyors = [] # (pL,pR,dir) self.edgeL = 64 self.edgeR = None + self.vertical = False # a VERTICAL climbing level (pfBoundsV): the y=576 ground model doesn't apply def parse(): levels = {} @@ -73,6 +74,9 @@ def parse(): mk = re.match(r"(pf\w+|b2kSmoothGround|b2kPlayerAddLadder)\b", s) if not s or not (mk or "pfBounds" in s): continue + if s.startswith("pfBoundsV"): # MUST precede the pfBounds test (startswith overlap) + L.vertical = True + continue if s.startswith("pfBounds"): v = nums(s) # kPfEdgeL=64, kPfEndCap=40 -> right arg literal + 40 @@ -188,6 +192,14 @@ def audit(L): def flag(sev, msg): out.append((sev, msg)) + # A VERTICAL climbing level (pfBoundsV) uses the full height as play space and + # the camera scrolls; the horizontal y=576 ground model (coins-near-a-surface, + # walkers-on-ground, pit widths) does not apply, so skip the geometry checks + # here and lean on the OXT pass + check-livecodescript for it. + if L.vertical: + flag("INFO", "vertical climbing level - geometry audit skipped (the y=576 ground model does not apply; verify in OXT)") + return out + # bounds lo, hi = L.edgeL, L.edgeR @@ -377,13 +389,17 @@ def main(): for n in sorted(levels): L = levels[n] res = audit(L) - head = f"===== LEVEL {n} (bounds {L.edgeL:.0f}..{L.edgeR:.0f}, {len(L.coins)} coins, {len(L.enemies)} walkers) =====" + if L.vertical: + head = f"===== LEVEL {n} (VERTICAL climb, {len(L.coins)} coins) =====" + else: + head = f"===== LEVEL {n} (bounds {L.edgeL:.0f}..{L.edgeR:.0f}, {len(L.coins)} coins, {len(L.enemies)} walkers) =====" print(head) if not res: print(" (clean)") for sev, msg in sorted(res, key=lambda r: (r[0] != "ERR", r[1])): print(f" {sev:4} {msg}") - total += 1 + if sev != "INFO": + total += 1 print() print(f"{total} finding(s) across {len(levels)} levels.") From 9da4bc279049cc983e45ad0e3b60311f5dcf5e18 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 17:19:26 +0000 Subject: [PATCH 07/10] platformer: centre the vertical tower (fix the left-shift) The vertical Stone Keep's camera was left-aligned: the play column (x64..960, 896px) is narrower than the 1024px viewport, and pfBoundsV set the camera's X bounds to pLeft..pRight, so the camera clamped its LEFT edge to 64 and showed dead space out to x1088 on the right (HUD: "view 64-1088") - the whole tower sat shoved left. Fix: pfBoundsV now sets the camera's X bounds to a viewport-WIDE range centred on the play column ((cx-512)..(cx+512), e.g. 0..1024), so the camera centres the tower instead of left-aligning. The Y bounds stay the full world height (the vertical scroll is unchanged). Horizontal levels are untouched (they use pfBounds). check-livecodescript passes; audit unaffected (L7 still skipped). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01X92APxUJUMue3bAzXiX9V2 --- examples/box2dxt-platformer.livecodescript | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index 1d92b46..ae9c120 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -2087,6 +2087,7 @@ end pfBounds -- Coords stay POSITIVE (the off-card collision worry was NEGATIVE coords; a -- tall positive world collides the same way the horizontal walls do). command pfBoundsV pLeft, pRight, pTop, pBottom + local tCx put pLeft into gEdgeL put pRight into gEdgeR put pRight into gLevelW @@ -2096,7 +2097,13 @@ command pfBoundsV pLeft, pRight, pTop, pBottom b2kWall pRight, pTop, pRight, pBottom b2kWall pLeft, pTop, pRight, pTop -- the ceiling b2kKillFloor pBottom + 300 -- below the floor slab; the climb never reaches it - b2kCamBounds pLeft, pTop, pRight, pBottom + -- the play column (pLeft..pRight) is NARROWER than the 1024 viewport, so the + -- camera's X bounds must be a viewport-wide range CENTRED on the column - if + -- they only spanned pLeft..pRight, the camera left-aligns to pLeft and the + -- whole tower sits shoved to the left with dead space on the right. The Y + -- bounds stay the full world height (that is what the vertical scroll rides). + put (pLeft + pRight) / 2 into tCx + b2kCamBounds tCx - 512, pTop, tCx + 512, pBottom put pTop + 320 into gCamTopY -- a 640-tall viewport sliding inside the tall world put pBottom - 320 into gCamBotY put pBottom + 300 into gKillPlaneY -- the explicit kill check is effectively off (contained) From f0a1901bb959fc2f4c9c646969106d0022129859 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 17:34:04 +0000 Subject: [PATCH 08/10] platformer: fill the vertical tower's side margins with stone walls The vertical keep's play column (x64..960) is narrower than the 1024px viewport, so a centred camera leaves a 64px margin each side that read as dead backdrop (the user saw it as "dead space on the right at ground level"). The side-wall colliders existed but were invisible. pfBoundsV now DRESSES each margin via a new pfDressWall helper: solid stone blocks (terrain_stone_block_center, matching the floor/ledges) tiled the full world height, lowest layer so torches/coins/ledges draw in front; the flat collider slab made visible is the no-art fallback. The shaft now reads as an enclosed keep edge-to-edge - no dead backdrop at ground level or up the climb. Example-side only (no Kit change, no harness bump). Static gates clean (check-livecodescript PASS, audit-platformer 0 findings, L7 still skipped as vertical). Needs the OXT pass to confirm the walls fill the margins. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01X92APxUJUMue3bAzXiX9V2 --- CHANGELOG.md | 7 ++++ examples/box2dxt-platformer.livecodescript | 41 ++++++++++++++++++---- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33292fa..6c5df5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,13 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `4`). - `pfMakeLedge` builds the one-way stone climb platforms (`b2kSmoothGround` + `stone_cloud`); a zig-zag of 8 ledges, 8 coins, a summit gem, contained walls (a fall drops you to a lower ledge, no respawn). + - **Centred + enclosed (two OXT rounds).** The play column (64..960) is + narrower than the 1024 viewport, so `pfBoundsV` now sets the camera's X + bounds to a viewport-wide range CENTRED on the column (was left-aligning the + tower), and `pfDressWall` fills the leftover margin EACH side with solid + stone (`terrain_stone_block_center`, matching the floor) so the shaft reads + as an enclosed keep edge-to-edge - no dead backdrop at ground level. The + side-wall colliders that were invisible are now the visible walls. The win moves to L7 (`gLevel >= 7`); L6's flag now ADVANCES. Example-side only (no Kit change, no harness bump). `tools/audit-platformer.py` learned to skip a vertical level (the y=576 model doesn't apply); L1-L6 still 0 findings. diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index ae9c120..2486fdd 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -317,8 +317,11 @@ -- respawn (the kill plane is off); (e) all 8 coins are grabbable up the -- climb, the summit GEM needs a double-jump off the top, the flag is atop -- the keep; (f) the backdrop is the dark stone keep and the chrome (HUD/ --- help/buttons/level-picker) stays put while the world scrolls. The win --- screen says ALL SEVEN LEVELS CLEAR. (Spinner + puzzles: slices 2-3.) +-- help/buttons/level-picker) stays put while the world scrolls; (g) the +-- tower is CENTRED and the stone SIDE-WALLS fill the viewport margins each +-- side (the play column is narrower than the view) - no dead backdrop at +-- ground level or up the climb. The win screen says ALL SEVEN LEVELS +-- CLEAR. (Spinner + puzzles: slices 2-3.) -- ===================================================================== local gStarted, gHero, gHeroSpr, gHudLast, gHurtLock @@ -2079,6 +2082,28 @@ command pfBounds pLeft, pRight put 780 into gKillPlaneY end pfBounds +-- Dress a vertical level's side MARGIN as a solid stone WALL so the gap beside +-- the (narrower-than-viewport) play column reads as the keep's wall, not dead +-- backdrop. pX1..pX2 is the margin span in world px, pTop..pBottom its height; +-- it is stone-tiled (terrain_stone_block_center, matching the floor/ledges) at +-- 64px, lowest layer so torches/coins/ledges draw in front. No stone art: the +-- flat collider slab pSlab is made visible (and camera-adopted) as the fallback. +command pfDressWall pSlab, pX1, pX2, pTop, pBottom + local tX, tY + if pX2 - pX1 < 1 then exit pfDressWall -- a zero-width margin (column == viewport): nothing to fill + if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_stone_block_center") then + repeat with tX = pX1 to pX2 - 64 step 64 + repeat with tY = pTop to pBottom - 64 step 64 + pfTile "terrain_stone_block_center", tX, tY + end repeat + end repeat + else if there is a graphic pSlab then + set the backgroundColor of graphic pSlab to "70,73,86" -- lit stone, lighter than the backdrop + set the visible of graphic pSlab to true + b2kCamAdopt the long id of graphic pSlab + end if +end pfDressWall + -- VERTICAL bounds (the CLIMBING levels): a TALL world (pTop..pBottom), walls -- around the full box + a ceiling, and the camera scrolls VERTICALLY between -- gCamTopY..gCamBotY as the hero climbs. The kill plane sits well BELOW the @@ -2098,12 +2123,16 @@ command pfBoundsV pLeft, pRight, pTop, pBottom b2kWall pLeft, pTop, pRight, pTop -- the ceiling b2kKillFloor pBottom + 300 -- below the floor slab; the climb never reaches it -- the play column (pLeft..pRight) is NARROWER than the 1024 viewport, so the - -- camera's X bounds must be a viewport-wide range CENTRED on the column - if - -- they only spanned pLeft..pRight, the camera left-aligns to pLeft and the - -- whole tower sits shoved to the left with dead space on the right. The Y - -- bounds stay the full world height (that is what the vertical scroll rides). + -- camera's X bounds are a viewport-wide range CENTRED on the column (a bound + -- of just pLeft..pRight would left-align the tower with dead space on the + -- right). The leftover margin EACH side is then DRESSED as the keep's stone + -- side-walls (the collider slabs made visible / stone-tiled) so the shaft + -- reads enclosed, edge to edge - no dead backdrop. The Y bounds stay the + -- full world height (that is what the vertical scroll rides). put (pLeft + pRight) / 2 into tCx b2kCamBounds tCx - 512, pTop, tCx + 512, pBottom + pfDressWall "pf_wallL", tCx - 512, pLeft, pTop, pBottom -- fill the LEFT margin + pfDressWall "pf_wallR", pRight, tCx + 512, pTop, pBottom -- fill the RIGHT margin put pTop + 320 into gCamTopY -- a 640-tall viewport sliding inside the tall world put pBottom - 320 into gCamBotY put pBottom + 300 into gKillPlaneY -- the explicit kill check is effectively off (contained) From 5732f16d6900a9afbef4b6e55e1550fbad954365 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 17:43:01 +0000 Subject: [PATCH 09/10] platformer: line the L7 windows up with the stone wall columns The two keep windows were placed at hardcoded x16/x912 - chosen when the side walls were invisible - so against the now-solid stone columns they straddled the wall/shaft edge instead of sitting in the stone. Move each to its wall column's tile origin (0 for the left column, 960 for the right) so the window centres flush in the stone, matching the wall tiles. Example-side only; static gates clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01X92APxUJUMue3bAzXiX9V2 --- examples/box2dxt-platformer.livecodescript | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index 2486fdd..2e35e38 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -3871,8 +3871,8 @@ command pfL7Scene pfMakeTorch 928, 440 pfMakeTorch 96, 264 if gAssetsOK is true and b2kSheetHasFrame("tiles", "window") then - pfTile "window", 16, 900 -- set into the tower walls - pfTile "window", 912, 500 + pfTile "window", 0, 900 -- centred IN the left wall column (origin 0, like its stone) + pfTile "window", 960, 500 -- centred IN the right wall column (origin 960) end if end pfL7Scene From 7411d96eb6a734c09b76f022a23f04e7f7534dcc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 17:50:20 +0000 Subject: [PATCH 10/10] platformer: add the SPINNER hazard to the keep (Phase C slice 2) The keep's hazard: pfMakeSpinner registers a bodiless spooks-sheet sprite (spinner.png / spinner_spin.png) that spins via ANIMATION - not body rotation (gotcha 23) - and sweeps a horizontal sine path, hurting by plain proximity exactly like the saw. An UNKILLABLE "saw-rule" hazard: you TIME it, never stomp it (knockback through pfOuch in pfTickMovers, never a respawn). Built on the existing mover table, so no new per-frame tick. pHalf selects the wall-mounted spinnerHalf for slice 3's puzzle wing. L7 deploys two full blades sweeping the shaft across a climb gap each (lower L1->L2 at y986, upper L4->L5 at y602), placed standing-SAFE on both adjacent ledges (the hero half-height is 38; each blade sits ~66px clear of a hero on either ledge) so you wait on the lower ledge and time the jump. Optional art (gated on gSpooksOK): no enemies.png = the maker no-ops and the spots are simply safe - the climb never depends on a hazard. Example-side only (no Kit change, no harness bump). Static gates clean (check-livecodescript PASS, audit-platformer 0 findings, L7 still skipped as vertical). The blade timing needs the OXT feel-pass (tune pPerX). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01X92APxUJUMue3bAzXiX9V2 --- CHANGELOG.md | 15 +++++++ docs/asset-expansion-plan.md | 9 +++- examples/box2dxt-platformer.livecodescript | 49 +++++++++++++++++++++- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c5df5d..9842530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,21 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `4`). ### Added +- **Platformer: the SPINNER - a spinning-blade hazard (asset-expansion Phase C, + slice 2).** The previously-unused `spinner*` art (the `spooks` / `enemies.png` + sheet) becomes the keep's hazard: `pfMakeSpinner pX, pY, pAmpX, pPerX, pHalf` + registers a bodiless sprite that spins via ANIMATION (not body rotation, + gotcha 23) and sweeps a horizontal sine path, hurting by plain proximity like + the saw - an UNKILLABLE "saw-rule" hazard you TIME, never stomp (knockback via + `pfOuch` in `pfTickMovers`, never a respawn). Built on the existing mover + table (no new per-frame tick). `pHalf` selects the wall-mounted half-blade + (`spinnerHalf`) for slice 3's puzzle wing. L7 deploys two full blades, each + sweeping the shaft across a climb gap (lower L1->L2, upper L4->L5) and placed + STANDING-SAFE on both adjacent ledges (the hero waits on the lower ledge and + times the jump). Optional art (gated on `gSpooksOK`): no `enemies.png` = the + maker no-ops and those spots are simply safe (the climb never depends on a + hazard). Example-side only (no Kit change, no harness bump); static gates + clean. **The blade timing needs the OXT feel-pass** (tune `pPerX` if frantic). - **Platformer: a LEVEL PICKER (dev/test convenience).** A top-right option-menu dropdown jumps straight to any level (`menuPick` -> `pfJumpToLevel`, a fresh run-state rebuild), so levels already approved need not be replayed after every diff --git a/docs/asset-expansion-plan.md b/docs/asset-expansion-plan.md index e5ae204..43908c3 100644 --- a/docs/asset-expansion-plan.md +++ b/docs/asset-expansion-plan.md @@ -181,8 +181,13 @@ needs an OXT eye. `gLevel >= 7`. The audit skips the vertical level. **The vertical camera scroll is the OXT unknown.** (Earlier horizontal passes were redesigned after the user asked for a true vertical level.) -- **Slices 2–3 — TODO:** the **spinner** hazard (slice 2), then the **multi-key / - switch puzzles** (slice 3) — the keep's real identity. +- **Slice 2 — DONE (pending OXT):** the **spinner** hazard. `pfMakeSpinner` (a + bodiless `spooks`-sheet sprite spinning via animation + sweeping a sine path, + proximity knockback like the saw — the saw-rule, you time it). L7 gets two + sweeping blades across the L1->L2 and L4->L5 climb gaps, standing-safe on the + adjacent ledges. `pHalf` provides the wall-mounted `spinnerHalf` for slice 3. + Gated on `gSpooksOK` (no `enemies.png` = safe). **Blade timing is the OXT feel-pass.** +- **Slice 3 — TODO:** the **multi-key / switch puzzles** — the keep's real identity. - **Assets:** `terrain_stone_*` (full), `switch_{blue,green,red}(_pressed)`, `key_{blue,green}`, `lock_{blue,green}`, `spinner*`/`spinnerHalf*`, `block_strong_*`. diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index 2e35e38..1019992 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -320,8 +320,13 @@ -- help/buttons/level-picker) stays put while the world scrolls; (g) the -- tower is CENTRED and the stone SIDE-WALLS fill the viewport margins each -- side (the play column is narrower than the view) - no dead backdrop at --- ground level or up the climb. The win screen says ALL SEVEN LEVELS --- CLEAR. (Spinner + puzzles: slices 2-3.) +-- ground level or up the climb; (h) SLICE 2 - two SPINNING BLADES (the +-- spooks sheet) sweep the shaft across a climb gap each (lower L1->L2, +-- upper L4->L5): you can stand SAFE on the lower ledge and time the jump, +-- a mistime is knockback (never lethal), and the blades spin via animation +-- (not body rotation). Confirm the timing is FAIR (tune pPerX if frantic); +-- no art (no enemies.png) = the spots are simply safe. The win screen says +-- ALL SEVEN LEVELS CLEAR. (Multi-key / switch puzzles: slice 3.) -- ===================================================================== local gStarted, gHero, gHeroSpr, gHudLast, gHurtLock @@ -599,6 +604,8 @@ function pfLoadSheets b2kSheetScale "spooks", 0.9 b2kAnimDef "spooks", "batfly", "bat_fly.png,bat.png", 8, true b2kAnimDef "spooks", "spiderwalk", "spider_walk1.png,spider_walk2.png", 8, true -- Wave 6 + b2kAnimDef "spooks", "spin", "spinner.png,spinner_spin.png", 12, true -- Phase C: the keep's blade + b2kAnimDef "spooks", "spinhalf", "spinnerHalf.png,spinnerHalf_spin.png", 12, true -- wall-mounted half-blade end if -- the hero (beige; swap the colour word to re-skin) b2kAnimDef "chars", "idle", "character_beige_idle", 2, true @@ -1327,6 +1334,36 @@ command pfAddMover pSpr, pX, pY, pAmpX, pPerX, pAmpY, pPerY, pHurtW, pHurtH, pFa put (pFaceTravel is true) into gMovFlip[gMovN] end pfAddMover +-- A SPINNING-BLADE hazard (asset-expansion Phase C, the keep's hazard): a +-- bodiless spooks-sheet sprite that spins in place (animation, not body +-- rotation - gotcha 23) and SWEEPS a horizontal sine path, hurting by plain +-- proximity like the saw. It is an UNKILLABLE hazard - the "saw rule": you +-- TIME it, you never stomp it (knockback via pfOuch in pfTickMovers, never a +-- respawn). pAmpX/pPerX set the sweep (amp 0 = a blade fixed in place); pHalf +-- selects the wall-mounted HALF-blade (spinnerHalf) over the full disc. The +-- art is OPTIONAL (enemies.png / gSpooksOK): absent, the maker no-ops and the +-- spot is simply safe (the climb never depends on a hazard being present). +command pfMakeSpinner pX, pY, pAmpX, pPerX, pHalf + local tRef, tFrame, tAnim, tHurtH + if gSpooksOK is not true then exit pfMakeSpinner + if pHalf is true then + put "spinnerHalf.png" into tFrame + put "spinhalf" into tAnim + put 15 into tHurtH -- the half-blade is flat (63x31): a short hurt box + else + put "spinner.png" into tFrame + put "spin" into tAnim + put 28 into tHurtH -- the full disc (~57px after the 0.9 scale): hurt ~ the blade + end if + if not b2kSheetHasFrame("spooks", tFrame) then exit pfMakeSpinner + b2kSpriteNew "spooks", tFrame, pX, pY + put the result into tRef + if tRef is empty then exit pfMakeSpinner + b2kSpritePlay tRef, tAnim + -- a spinning disc is symmetric: no facing flip (pFaceTravel false) + pfAddMover tRef, pX, pY, pAmpX, pPerX, 0, 1, 28, tHurtH, false +end pfMakeSpinner + command pfMakeCoin pX, pY local tRef add 1 to gCoinsTotal -- the total counts itself as the level builds @@ -3886,6 +3923,14 @@ command pfL7Cast pfMakeCoin 512, 272 -- ledge 7 pfMakeCoin 448, 144 -- on the keep top pfMakeGem 544, 80, "blue" -- BONUS gem: a double-jump above the keep top (the summit) + -- the keep's SPINNING BLADES (Phase C slice 2): two sweep the shaft across + -- a climb gap each - stand SAFE on the lower ledge, read the blade, jump the + -- gap when it has swept clear (the saw rule; a mistime is knockback, never + -- lethal). Each sits in the GAP (standing-safe on both adjacent ledges, ~66px + -- of the 38px-half hero clear): the lower across L1->L2, the upper L4->L5 + -- (tighter + faster). OXT: confirm the timing is fair; widen pPerX if frantic. + pfMakeSpinner 512, 986, 320, 1300, false -- lower: guards the L1->L2 crossing + pfMakeSpinner 512, 602, 300, 1050, false -- upper: a tighter, faster sweep pfMakeGoal 544, 160 -- the flag, atop the keep end pfL7Cast