diff --git a/CHANGELOG.md b/CHANGELOG.md index 2686111..675623e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,47 @@ The native shim's ABI is tracked separately by `b2Version()` (currently `4`). ### Added +- **Wave 4 (liquids) — SWIM, the Kit's first new player-action since + Wave 2 (statically verified + harness v12; user play-tested in the + platformer).** A new `b2kPlayerAddWater x1,y1,x2,y2` registers a polled + water zone (world state, wiped by `b2kClear`, exactly like the ladder + zones). While the player's centre is submerged the controller SWIMS: + gravity drops to `swimGravity` so you sink slowly, the sink caps at + `swimMaxFall` (far below the air terminal), UP/DOWN swim at `swimSpeed`, + and a JUMP press is a REPEATABLE upward STROKE (`swimJump`) with no ground + gate. A new `swim` state plus a `b2kPlayerAnims` swim slot (a 9th arg, + falling back to the fall pose, so three-arg calls still work) drive the + art. Swim is mutually exclusive with the climb (the tick starts only + one); leaving the zone, a hurt, or teardown restores the saved gravity + scale exactly once. The swim path costs ONE compare per frame when no + water zones exist. Two Opus correctness reviews found no blockers. + - **The platformer's L1 GREEN HILLS gains a HILLTOP POOL** — the swim + showcase, in the level the game is actually play-tested in. A RAISED + swim basin between two earth banks past the crusher alley: the 640-tall + world clamps the camera at its bottom, so a swim pool is a basin held + at the surface, never a sub-ground pit. Hop in, dive for three + underwater coins (the gate needs them), then stroke up + hold-forward + to HOP out the far bank to the flag. New `pfMakeWater` helper; per the + playtest the water was made heavier (`swimGravity` 0.6, `swimMaxFall` + 200, a trimmed `swimJump` 300 — `swimJump` alone sets the escape, so it + is the lever for "harder to climb out"). A debug warp (`0` on L1) + reaches the pool for fast iteration. + - **Fixed a pre-existing brick head-bump gap** the pool work surfaced: + the hero's 88px capsule was taller than its ~76px visible character + (128px frame headroom at a 0.75 down-scale), so heads "missed" bricks + by the difference even though the smash fired. The hitbox now matches + the art (feet-aligned, bind offset derived) and the bonk window reads + the real half-height instead of a hardcoded 44 (gotcha 28). + - **Harness v12** adds three swim tests: `stTestSwim` (dive / buoyant cap + / repeatable stroke / swim-up / gravity-restored-on-exit, every value + printed), `stTestSwimGrounded` (swim while resting on a submerged + floor — the pool-floor case), and `stTestSwimClear` (the level-rebuild + path: `b2kClear` must wipe the zone, or the next level's player is born + swimming in mid-air where the old pool was). + - The micro-game also gained an L3 "THE DEEP" swim level (data verbs + `water` + a `fish` pit-dweller), but it shows a white-world build issue + in its own example code and is set aside pending a focused pass — the + Kit swim itself is sound (it runs clean in the platformer). - **Platformer SHOWCASE polish round (statically verified; awaiting the OXT pass).** A pre-Wave-4 pass over the platformer to make it a polished demo of the kit *as it stands today* — longer, better-spaced diff --git a/CLAUDE.md b/CLAUDE.md index a0dae37..0f19739 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,7 +95,7 @@ failure. Run it after **every** `.livecodescript` edit. pass" and let the user confirm. **The self-test harness** (`examples/box2dxt-selftest.livecodescript`) is the runtime safety net: -~113 deterministic assertions (currently **v10**) driving the real Kit (paused world + +~125 deterministic assertions (currently **v12**) driving the real Kit (paused world + `b2kStepOnce` hand-stepping + `b2kInputInject` scripted keys). The workflow for every **Kit** change: (1) add/extend an assertion that captures the new behavior, (2) **bump `kStHarnessV`** (the report header prints it, so a @@ -226,6 +226,17 @@ OXT's compiler is **stricter than LiveCode's**. These are the recurring footguns 27. **`the result` is consumed by the NEXT command** — capture it into a local immediately after every `b2kSpawn*`/maker call before calling anything else (several past bugs were a stale `the result`). +28. **A physics hitbox taller than the VISIBLE sprite makes the head bump + things it never visually touches.** A capsule sized to the sprite FRAME + (not the character within it) tops out above the drawn head when the art + is bottom-aligned with frame headroom (Kenney's 128px characters at a + down-scale): the invisible "hat" hits the brick/ceiling while the visible + head stops short with a gap — even though the contact and the head-bump + poll both fire. Size the hitbox to the VISIBLE art, feet-aligned (derive + the bind offset from it), and have any head-reach logic read the body's + real half-height (a build-time global), never a hardcoded constant. The + platformer's brick-smash gap (round 7) was exactly this: an 88px capsule + over a ~76px visible character. ## The single-threaded performance playbook @@ -301,6 +312,18 @@ The rules, each earned by a measured regression: - **Ladders:** run the zone a little above a platform at the ladder's top (walk-off + DOWN grabs it); zones are world state (`b2kClear` wipes them). +- **Liquids / SWIM (Wave 4):** a swim pool CANNOT be a sub-ground pit — + `b2kCamBounds` clamps the camera at the world's bottom edge, so anything + below the ground line is off-screen. A swimmable pool is a RAISED basin + between two banks (or the whole ground raised, as the micro-game did): + hop in, dive for the underwater coins, stroke up + hold-forward to HOP + out the far bank. Water zones (`b2kPlayerAddWater`) are world state, + wiped by `b2kClear` like ladders. **Tuning:** `swimGravity` sets only the + between-stroke SINK; the single-stroke escape height is `swimJump` ALONE + (the stroke sets velocity directly, then full air-gravity governs the + apex once you break the surface). To make climbing out HARDER, lower + `swimJump` — raising `swimGravity` only makes you sink faster between + strokes. - **Hazard mercy patterns:** a riser never rises under the hero's feet (the piranha); proximity hazards give a sprint-speed telegraph (mimic wake ≥110px); unkillable hazards follow "the saw rule" (skip diff --git a/README.md b/README.md index 8bbb40b..4aeef3b 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ Box2D v3.1.0 (fetched by CMake) two-level platformer (menu → levels → win screen, embedded art, synthesized sound), the [platformer showcase](examples/box2dxt-platformer.livecodescript) is the - Game Kit pushed hard — player controller, scrolling camera, spritesheets, - coin puzzles — and the + Game Kit pushed hard — player controller (run/jump/climb/duck/swim), + scrolling camera, spritesheets, coin puzzles, a hilltop swim pool — and the [slingshot](examples/box2dxt-slingshot.livecodescript) is pure physics joy: catapult cannonballs into toppling towers, angry-birds style (three levels, ballistic aim preview, zero assets). And the diff --git a/docs/expansion-prep.md b/docs/expansion-prep.md index 065d5c9..8b34dc8 100644 --- a/docs/expansion-prep.md +++ b/docs/expansion-prep.md @@ -13,7 +13,8 @@ that keep the expansion as reliable as the engine underneath it. | Wave 2 | **COMPLETE — user-verified 2026-06-13** (player actions I, harness v10; see §9) | | Wave 3 | **BUILT — statically verified 2026-06-13** (bestiary I + HAUNTED HOLLOW; see §10) | | Showcase polish | **BUILT — statically verified 2026-06-13** (pre-Wave-4: longer/re-spaced levels, the kit's first JOINT mechanics — rope bridge + boulder + barrel; a prototyped wrecking ball was cut as un-sprite-able — and four variety species; all example-side, zero Kit change, no harness bump) | -| Next | **Wave 4** (liquids: swim zones + lava + pit dwellers + collapsing bridge) — see §7 | +| Wave 4 | **SWIM user play-tested in the platformer 2026-06-14** (harness **v12**, two Opus reviews clean; see §11). The Kit gained `b2kPlayerAddWater` + a buoyant `swim` mode/state/anim; the platformer's L1 GREEN HILLS gained a **HILLTOP POOL** (a raised-bank basin — the swim showcase, where it's tested), tuned heavier and with the hero hitbox fixed to match the art (gotcha 28), all per the user's OXT pass. DONE: swim zones, pit-dwellers (the micro-game `fish`, debut), lava (already in platformer L4). CARRY-OVER: the collapsing-bridge trap, and the micro-game's L3 "THE DEEP" (built but shows an example-side white-world build issue — set aside) | +| Next | **Wave 5 — player actions II** (wall-slide/jump, dash, double-jump; capsule reshape on duck) — see §7 and §12. Loose ends: the collapsing bridge + the micro-game L3 fix | | Companions | [plan.md](../plan.md) (history/decision log) · [game-engine-spec.md](game-engine-spec.md) (module design) | --- @@ -210,8 +211,9 @@ to Kit API (`b2kFoe…`). shape-def filter already covers chain creation). 3. **Wave 3 — bestiary I:** shelled (kickable!), ghost, bat, mimic, pipe plant, crusher-with-faces — into a platformer "haunted" section. -4. **Wave 4 — liquids:** swim zones + lava + pit dwellers + collapsing - bridge; a water level in the micro-game (level 3). +4. **Wave 4 — liquids:** swim zones (done — the platformer's hilltop pool) + + lava (already in L4) + pit dwellers (the micro-game fish) + the + collapsing bridge (carry-over). As-built record in §11. 5. **Wave 5 — player actions II:** wall-slide/jump, dash, double-jump powerup (boxItem delivers it), platform carry. 6. **Wave 6 — bestiary II + promotion:** chaser, lunger, spider, saws; @@ -453,3 +455,129 @@ knockback; only pits/kill-plane respawn. 3. No regression: L1-L3 + micro-game still complete (spacing pass verified at the same time). 4. Docs ride along: CHANGELOG, plan.md decision log, this section. + +## 11. Wave 4 — liquids (swim) — as built (2026-06-14) + +The first new player-action since Wave 2, built as a faithful parallel to +the ladder/climb system and **user play-tested in the platformer**. + +### 11.1 The swim feature (Kit) + +- **`b2kPlayerAddWater x1,y1,x2,y2`** — a polled water zone (flat + `sPlayWat*` arrays, parallel to `sPlayLad*`); world state, wiped by + `b2kClear`/`b2kPlayerForget`, surviving an attach — exactly like ladders. +- **`swim` mode** — while the centre is in a zone: gravity scales to + `swimGravity` (the body's own scale is saved and restored exactly once, + like the climb), the sink caps at `swimMaxFall`, UP/DOWN drive vy at + `swimSpeed`, and a JUMP press is a *repeatable* upward STROKE of + `swimJump` (no grounded/coyote/buffer gate). The `swim` state overrides + the grounded/airborne machine and clears `sPlayAir`, so surfacing is not + a phantom land. A 9th `pSwim` arg on `b2kPlayerAnims` falls back to fall. +- **Mutual exclusion with climb** — both park gravity via *separate* saves, + so the start gates check BOTH flags and two saves never fight. The tick + costs ONE compare/frame when no water zones exist. +- Knobs (`swimSpeed`/`swimJump`/`swimGravity`/`swimMaxFall`) cached in + `b2kPlayerTuneCache`. Two Opus correctness reviews (the Kit change; the + micro-game + harness) — no blockers. + +### 11.2 The layout law: a swim pool is a RAISED basin + +`b2kCamBounds` clamps the camera at the world's bottom edge, so a sub-ground +pit is off-screen. A swimmable pool is therefore a **raised basin between +two banks** (the platformer) or the whole ground raised (the micro-game): +hop in, dive for the underwater coins (which FORCE the swim via the +coin-gate), then stroke up + hold-forward to hop out the far bank. + +### 11.3 Playtest tuning + the swimGravity/swimJump lesson + +The pool felt too floaty (you could pop straight out), so it was made +heavier: `swimGravity` 0.6, `swimMaxFall` 200, `swimJump` 300. The lesson +worth keeping: **`swimGravity` sets only the between-stroke SINK; the +single-stroke escape height is `swimJump` ALONE** (the stroke writes +velocity directly, then full air-gravity governs the apex once you break the +surface). The lever for "harder to climb out" is `swimJump`, not gravity. + +### 11.4 The brick head-bump fix (gotcha 28) + +The pool work surfaced a pre-existing regression: the hero's 88px capsule +was taller than its ~76px visible character (128px frame headroom at a 0.75 +down-scale), so the invisible "hat" hit bricks while the visible head sat +~12px low (the bonk still fired). Fixed by sizing the hitbox to the art +(`tH` 88→76, `tDY` derived to keep the feet planted) and reading the body's +real half-height (`gHeroHalfH`) in the bonk window — see gotcha 28. + +### 11.5 Harness v12 + +- `stTestSwim` — dive / buoyant cap / repeatable stroke / swim-up / + gravity-restored-on-exit, every value printed. +- `stTestSwimGrounded` — swim while resting on a submerged floor (the + pool-floor case): still `swim`, a stroke lifts off with no grounded gate. +- `stTestSwimClear` — the level-rebuild path: `b2kClear` must wipe the zone, + or the next level's player is born swimming where the old pool was. + +### 11.6 Status / loose ends + +- The platformer's L1 hilltop pool is the user-tested swim showcase; a + `0`-key debug warp (delete-before-merge sentinel) reaches it fast. +- The micro-game's L3 "THE DEEP" (data verbs `water` + `fish`) is built but + shows an example-side **white-world build issue** (set aside — the Kit + swim is sound). To revisit: the L3 build path in `mgBuild` / the new verbs. +- The **collapsing-bridge** trap (the last named Wave 4 mechanic) is carried + to the loose-ends list. + +## 12. Wave 5 design — player actions II (prepared 2026-06-14) + +The next wave: four moves that extend the controller. All are KIT changes +(harness bumps), each a parallel to an existing tick path — reuse the +start-gate discipline that keeps swim/climb from fighting. + +### 12.1 Wall-slide / wall-jump + +- **Detect:** a side probe (mirror the ground ray) reports a wall contact on + the facing side while airborne and pressing toward it — a new `sPlayOnWall` + (+ side), polled like grounding. +- **Slide:** while wall-pressed and falling, cap the descent at a new + `wallSlideMax` (a velocity assert, like the swim sink). +- **Jump:** a JUMP press while sliding launches up-and-away (`wallJumpX`/ + `wallJumpY`), consuming the press, with a brief control-lock so the + away-velocity is not instantly cancelled (the knockback hand-off pattern). +- **State `wallslide`** + a 10th `b2kPlayerAnims` slot (falls back to fall). + +### 12.2 Dash + +- A direction + a new "dash" action (bound like "jump") gives a fast + horizontal burst (`dashSpeed`) for `dashMs`, then decays, gated by + `dashCooldownMs`; air-dash optional (one per airtime). **State `dash`** — + a velocity assert for its window (gotcha 17: it must keep moving). A dash + into water/ladder yields to swim/climb (those modes win, as over walk). + +### 12.3 Double-jump (powerup-delivered) + +- The "?-box" (boxItem) delivers a double-jump charge. An airborne + `b2kPlayerJump` gated on a charge count (`sPlayAirJumps`, reset on + landing) — reuses the gate-free `b2kPlayerJump`, so it is mostly + bookkeeping + the pickup. A pickup, not a permanent knob. + +### 12.4 Duck capsule reshape (deferred from Wave 2 by design) + +- Wave 2's duck brakes but keeps the hitbox; Wave 5 shrinks the capsule to + crawl under low gaps. The hard part is gotcha 28's cousin — a + **bottom-anchored** reshape (keep the FEET planted as the capsule + shortens) — and restoring it only when a ceiling probe finds headroom. + New `duckHeight` knob. + +### 12.5 Harness plan + +One self-diagnosing test per move: wall-slide caps the fall + wall-jump +launches away + leaves the state; dash bursts to `dashSpeed`, decays, and +respects the cooldown; double-jump fires once airborne and re-arms on +landing; duck-reshape shrinks the probed half-height and restores only under +headroom. Bump `kStHarnessV` per the rule. + +### 12.6 Exit criteria + +1. Harness all-pass (new states + the existing suite). +2. Each move feels right in the platformer (the test bed) and composes with + swim/climb/duck/knockback without fighting (the start-gate discipline). +3. Docs ride along (CHANGELOG, plan.md, this section). +4. No regression: every existing level still completes. diff --git a/docs/game-engine-spec.md b/docs/game-engine-spec.md index 0cac55d..adefed0 100644 --- a/docs/game-engine-spec.md +++ b/docs/game-engine-spec.md @@ -361,9 +361,9 @@ Input module and writes `b2kSetVelocity` x / preserves y. | `b2kPlayerMake pX, pY, pW, pH [,pSheet]` → control | One call: sprite (or plain capsule graphic if no sheet), capsule body, controller defaults, input armed. Reports the control. | | `b2kPlayerAttach pCtrl` | Adopt an existing control/sprite as the player (capsule body added if it has none). | | `b2kPlayerSet pKey, pValue` / `b2kPlayerGet(pKey)` | Tuning knobs (table below). | -| `b2kPlayerAnims pIdle, pRun, pJump, pFall [,pLand]` | Map controller states to sheet animations; auto-`FlipH` from facing. | +| `b2kPlayerAnims pIdle, pRun, pJump, pFall [,pLand] [,pDuck] [,pClimb] [,pHurt] [,pSwim]` | Map controller states to sheet animations; auto-`FlipH` from facing. (Wave 2-4 slots are optional fallbacks — see kit-reference.) | | `b2kPlayerOnGround()` → bool | Grounded this frame (post-tick). | -| `b2kPlayerState()` → word | `idle` / `run` / `jump` / `fall` (+ `land` transition tick). | +| `b2kPlayerState()` → word | `idle` / `run` / `jump` / `fall` / `duck` / `climb` / `hurt` / `swim` (+ `land` transition tick). | | `b2kPlayerFacing()` → 1 / -1 | Last horizontal intent. | | `b2kPlayerJump [pSpeed]` | Programmatic jump (springs, double-jump powerups) — respects the same state machine. | | `b2kPlayerControl pFlag` | Enable/disable input→motion (cutscenes; physics continues). | @@ -396,11 +396,11 @@ automatically when `b2kPlayerAnims` is set. segments are one-sided exactly the way platformers need — a capsule rises through from below and lands on top (`b2kChain`/`b2kSmoothGround`, top surface listed right-to-left; plain `b2kWall` segments are two-sided and cannot do -this). **Explicitly deferred** (designed-for, not in v1): moving-platform -velocity carry (v1 relies on friction), player-initiated drop-through -(pressing down to fall through a ledge — needs a brief collision-mask window), -wall-jump/slide, swim zones, multiple simultaneous players (state is -per-control already; only the input bindings are global). +this). **Explicitly deferred** (designed-for, not in v1) — several have +since landed: **drop-through** and **ladders** in Wave 2, **SWIM zones** in +Wave 4. Still ahead: moving-platform velocity carry (v1 relies on friction), +**wall-jump/slide** and **dash** (Wave 5), multiple simultaneous players +(state is per-control already; only the input bindings are global). ## 7. Module: Camera diff --git a/docs/kit-guide.md b/docs/kit-guide.md index 152f2de..6885669 100644 --- a/docs/kit-guide.md +++ b/docs/kit-guide.md @@ -673,13 +673,14 @@ end openCard ``` That is a complete, well-tuned character — arrows/WASD run, space jumps, -DOWN ducks, one-way decks drop through, ladders climb (§21). Feel lives in -`b2kPlayerSet` knobs (`moveSpeed`, `accel`, `airAccel`, `jumpSpeed`, -`jumpCut`, `coyoteMs`, `bufferMs`, `maxFall`, `maxSlopeDeg`, plus Wave 2's -`dropMs`, `climbSpeed`, `hurtPopX/Y`, `hurtMs`, `invulnMs`); read the +DOWN ducks, one-way decks drop through, ladders climb, water swims (§21). +Feel lives in `b2kPlayerSet` knobs (`moveSpeed`, `accel`, `airAccel`, +`jumpSpeed`, `jumpCut`, `coyoteMs`, `bufferMs`, `maxFall`, `maxSlopeDeg`, +plus Wave 2's `dropMs`, `climbSpeed`, `hurtPopX/Y`, `hurtMs`, `invulnMs`, +and Wave 4's `swimSpeed`/`swimJump`/`swimGravity`/`swimMaxFall`); read the character back with `b2kPlayerState()` -(`idle`/`run`/`jump`/`fall`/`duck`/`climb`/`hurt`, plus `land` for exactly -one frame on touch-down — perfect for dust and sound), +(`idle`/`run`/`jump`/`fall`/`duck`/`climb`/`hurt`/`swim`, plus `land` for +exactly one frame on touch-down — perfect for dust and sound), `b2kPlayerOnGround()` and `b2kPlayerFacing()`. Already have a body or sprite? `b2kPlayerAttach` adopts it instead of making one. Springs, bounces and powerups call `b2kPlayerJump 700` — always use it for external @@ -1076,7 +1077,7 @@ Play order: `openCard` builds level 1 and shows the menu → click → --- -## 21. Player actions: duck, drop-through, ladders, knockback +## 21. Player actions: duck, drop-through, ladders, knockback, swim Wave 2 builds four standard platformer verbs into the controller. They cost nothing until used (each idles at one compare per frame) and they @@ -1129,6 +1130,29 @@ knockback ends in a pit. One art note: if your game uses pattern), map a **looping** animation to the `hurt` anim slot — a non-looping pose would finish mid-knockback and fire your respawn. +**Swim** (Wave 4) is the buoyant parallel to the climb. `b2kPlayerAddWater +x1,y1,x2,y2` registers a water *zone* (polled presence, world state, wiped +by `b2kClear` like a ladder). While the player's centre is submerged the +`swim` state owns the controller: gravity scales to `swimGravity` (the +body's own scale is saved and restored, so a game-tuned floatiness +survives), the sink caps at `swimMaxFall`, UP/DOWN swim at `swimSpeed`, and +a JUMP press is a *repeatable* upward **stroke** of `swimJump` — no +grounded/coyote/buffer gate. Leaving the zone or a hurt restores gravity; +swim and climb are mutually exclusive (the tick starts only one). Map a +swim frame with `b2kPlayerAnims`'s ninth argument (it falls back to the +`fall`/`jump` pose, so a sheet without one still reads). + +Two things the OXT rounds taught: **(1) tuning** — `swimGravity` sets only +how fast you sink *between* strokes; the single-stroke escape height is +`swimJump` ALONE (the stroke sets velocity directly, then full air-gravity +governs the apex once you break the surface), so to make climbing out of a +pool harder, lower `swimJump`, not the gravity. **(2) layout** — a swim +pool can't be a pit *below* the ground, because `b2kCamBounds` clamps the +camera at the world's bottom edge and anything lower is off-screen; build +the pool as a RAISED basin between two banks (or raise the whole ground, as +the micro-game does), then hop in, dive for the coins, and stroke up + +hold-forward to hop out the far bank. + --- ## 22. xTalk gotchas worth knowing @@ -1244,11 +1268,11 @@ Optional arguments are in `[…]`. ### Player (the platformer controller) `b2kPlayerMake x,y,w,h [,sheet]` · `b2kPlayerAttach ctrl` · -`b2kPlayerAnims idle,run,jump [,fall] [,land] [,duck] [,climb] [,hurt]` · +`b2kPlayerAnims idle,run,jump [,fall] [,land] [,duck] [,climb] [,hurt] [,swim]` · `b2kPlayerSet key,value` · `b2kPlayerGet(key)` `[f]` · `b2kPlayerOnGround()` `[f]` · `b2kPlayerState()` `[f]` · `b2kPlayerFacing()` `[f]` · `b2kPlayerJump [speed]` · `b2kPlayerControl flag` · -`b2kPlayerAddLadder x1,y1,x2,y2` · `b2kPlayerHurt [fromX]` · +`b2kPlayerAddLadder x1,y1,x2,y2` · `b2kPlayerAddWater x1,y1,x2,y2` · `b2kPlayerHurt [fromX]` · `b2kPlayerHurtIs()` `[f]` · `b2kPlayer()` `[f]` · `b2kPlayerSprite()` `[f]` · `b2kPlayerRemove` diff --git a/docs/kit-reference.md b/docs/kit-reference.md index f05783d..788863c 100644 --- a/docs/kit-reference.md +++ b/docs/kit-reference.md @@ -334,17 +334,27 @@ inside a `b2kPlayerAddLadder` zone parks gravity at 0 and runs y at `climbSpeed`; JUMP exits with a normal jump), and the **hurt-knockback standard** (`b2kPlayerHurt`, below). +**Wave 4 action: swim.** While the player's centre is inside a +`b2kPlayerAddWater` zone the controller **swims** — gravity scales down to +`swimGravity` (buoyant), the sink caps at `swimMaxFall` (well below the air +terminal), UP/DOWN drive vy at `swimSpeed`, and a JUMP press is a +*repeatable* upward **stroke** (`swimJump`) with no ground gate. State reads +`swim`; leaving the zone (or a hurt) restores the saved gravity scale +exactly once. Mutually exclusive with the climb. Sized for a *raised-bank* +basin — a sub-ground pit falls below the camera (see kit-guide §21). + | Handler | Purpose | |---------|---------| | `b2kPlayerMake x, y, w, h [,sheet]` → control | One call: a capsule body host (`w`×`h` collision box — a visible capsule graphic, or invisible with a bound sprite of `sheet`'s first frame on top), controller armed, input on. Reports the player control. | | `b2kPlayerAttach ctrl` | Adopt an existing control (or sprite) as the player. A capsule body is added if it has none (then the controller also sets low friction); a body you made yourself keeps your material. Also sets fixed rotation + sleep-off and arms input. | -| `b2kPlayerAnims idle, run, jump [,fall] [,land] [,duck] [,climb] [,hurt]` | Map states to the art's animation names (`fall` defaults to `jump`; `duck` to `idle`; `climb` and `hurt` to `jump` — sheets without those frames still read correctly). `land` is an optional non-looping touch-down flourish, held for its own duration. **Map a LOOPING animation to `hurt`** if your game uses `b2kSpriteOnFinish` on the player's art — a non-looping hurt pose fires that finish message mid-knockback. The art is the player control itself if it is a sprite, else the first sprite `b2kSpriteBind`-pinned to it. | +| `b2kPlayerAnims idle, run, jump [,fall] [,land] [,duck] [,climb] [,hurt] [,swim]` | Map states to the art's animation names (`fall` defaults to `jump`; `duck` to `idle`; `climb` and `hurt` to `jump`; `swim` to `fall` — sheets without those frames still read correctly). `land` is an optional non-looping touch-down flourish, held for its own duration. **Map a LOOPING animation to `hurt`** if your game uses `b2kSpriteOnFinish` on the player's art — a non-looping hurt pose fires that finish message mid-knockback. The art is the player control itself if it is a sprite, else the first sprite `b2kSpriteBind`-pinned to it. | | `b2kPlayerSet key, value` / `b2kPlayerGet(key)` | Tuning knobs (table below). Settable any time; `b2kClear` keeps them (config, like input bindings), `b2kTeardown`/`b2kPlayerRemove` wipe them. | | `b2kPlayerOnGround()` | Grounded this frame (post-tick; false on the frame a jump launches). | -| `b2kPlayerState()` | `idle` / `run` / `jump` / `fall` / `duck` / `climb` / `hurt`, plus `land` for exactly one frame on touch-down (dust puffs, sounds — read it in `on b2kFrame`). A drop-through renders as `fall`; a knockback's own landing shows no `land` tick. | +| `b2kPlayerState()` | `idle` / `run` / `jump` / `fall` / `duck` / `climb` / `hurt` / `swim`, plus `land` for exactly one frame on touch-down (dust puffs, sounds — read it in `on b2kFrame`). A drop-through renders as `fall`; a knockback's own landing shows no `land` tick. | | `b2kPlayerFacing()` | 1 right / -1 left — the last horizontal intent. | | `b2kPlayerJump [speed]` | Programmatic jump (springs, double-jump powerups): the same launch as a pressed jump but **without** the grounded/coyote gate — the caller decides when it is allowed. | | `b2kPlayerAddLadder x1, y1, x2, y2` | Register a ladder **zone** (screen-px rect, any corner order; purely polled — no physics object). Zones are world state: `b2kClear` wipes them with everything else. Run the zone a little above a platform at the ladder's top so walking off that edge holding DOWN grabs it. | +| `b2kPlayerAddWater x1, y1, x2, y2` | Register a water/**swim zone** (screen-px rect, any corner order; purely polled). World state, wiped by `b2kClear` like ladders. Top the zone a little above the drawn surface so the dive-in and surface-out break the water where the art is. The pool is a *raised basin* between banks (a sub-ground pit clamps below the camera). | | `b2kPlayerHurt [fromX]` | The contact-damage knockback standard: an away-pop (`hurtPopX`/`hurtPopY`, ground-snap-exempt; the sign of `fromX` vs the player picks the direction — empty pops back off the facing), the `hurt` state/anim, input suppressed until `hurtMs` *or* the first landing after half of it (whichever is **later**), then an `invulnMs` mercy window in which this command no-ops. Keep your respawn flow for **lethal** hits (pits, kill planes): contact damage knocks back, falling dies. | | `b2kPlayerHurtIs()` | True through the knockback *and* the mercy window — the one gate your hazard checks need. | | `b2kPlayerControl flag` | `false` = the controller only observes (state/ground/facing stay fresh) and writes neither velocity nor animations — cutscenes, hit poses, scripted deaths. The `maxFall` clamp stays live. `true` re-asserts the state animation. An **explicit call (either way) cancels a knockback in flight** — your respawn flow takes the body over cleanly, and no mercy window is granted. | @@ -357,7 +367,11 @@ scale 40: `moveSpeed` 220 px/s · `accel` 1800 px/s² · `airAccel` 1100 px/s² `coyoteMs` 90 · `bufferMs` 110 · `maxFall` 900 px/s · `maxSlopeDeg` 50 (steeper than this is a wall, not ground) · `dropMs` 260 (the drop-through window) · `climbSpeed` 160 px/s (ladder rate; x runs at half `moveSpeed` -while climbing) · `hurtPopX` 220 / `hurtPopY` 320 px/s (knockback launch) · +while climbing) · `swimSpeed` 150 / `swimJump` 300 px/s (water move speed + +the repeatable stroke) · `swimGravity` 0.35 / `swimMaxFall` 150 (buoyancy: +the between-stroke sink scale and its cap — `swimJump` alone sets the escape +height, so lower IT to make climbing out harder) · `hurtPopX` 220 / +`hurtPopY` 320 px/s (knockback launch) · `hurtMs` 700 (control-off span) · `invulnMs` 900 (post-hurt mercy). ``` diff --git a/examples/box2dxt-contraption-builder.livecodescript b/examples/box2dxt-contraption-builder.livecodescript index 288a407..3f40fed 100644 --- a/examples/box2dxt-contraption-builder.livecodescript +++ b/examples/box2dxt-contraption-builder.livecodescript @@ -105,16 +105,18 @@ -- b2kSpriteFPS spr,fps · b2kSpriteOnFinish spr,msg · -- b2kSpriteBind spr,bodyCtrl[,dx,dy] · b2kSpriteRemove spr -- PLAYER b2kPlayerMake x,y,w,h[,sheet] · b2kPlayerAttach ctrl · --- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt] · +-- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt,swim] · -- b2kPlayerSet key,value · b2kPlayerGet(key) · b2kPlayerOnGround() · --- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt) · +-- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt|swim) · -- b2kPlayerFacing() · b2kPlayerJump [speed] · b2kPlayerControl flag · --- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerHurt [fromX] · +-- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerAddWater x1,y1,x2,y2 · +-- b2kPlayerHurt [fromX] · -- b2kPlayerHurtIs() (true through knockback + mercy window) · -- b2kPlayer() · b2kPlayerSprite() · b2kPlayerRemove -- (drives axes "moveX"/"moveY" + action "jump"; rebind to remap. -- DOWN ducks · DOWN+JUMP on a one-way chain drops through · --- UP/DOWN in a ladder zone climbs · JUMP exits a climb) +-- UP/DOWN in a ladder zone climbs · JUMP exits a climb · +-- in a water zone UP/DOWN swim, JUMP strokes (repeatable)) -- CAMERA b2kCamOn [rect] · b2kCamOff · b2kCamFollow ctrl[,lerp] · b2kCamUnfollow · -- b2kCamDeadzone w,h · b2kCamBounds x1,y1,x2,y2 · b2kCamGoto x,y · -- b2kCamPos() · b2kCamShake amp,ms · b2kCamMouseX/Y() · b2kCamGroup() · @@ -262,6 +264,12 @@ local sPlayClimb -- true while in the climb state (gravity scale parked at 0 local sPlayGravSave -- the body's gravity scale to restore on climb exit local sPlayLadN -- registered ladder zones (flat numeric arrays: the local sPlayLadL, sPlayLadT, sPlayLadR, sPlayLadB -- ...in-zone test is pure compares) +-- Wave 4: water zones + the swim state (a buoyant parallel to the climb) +local sPlaySwim -- true while submerged in a water zone (gravity scaled down) +local sPlaySwimGravSave -- the body's gravity scale to restore on swim exit +local sPlaySwimSpd, sPlaySwimJump, sPlaySwimGrav, sPlaySwimMaxFall -- swim tune caches +local sPlayWatN -- registered water zones (flat numeric arrays, like ladders) +local sPlayWatL, sPlayWatT, sPlayWatR, sPlayWatB local sPlayHurt -- true while knockback owns the controller local sPlayHurtEnd -- sim-clock when hurtMs has elapsed local sPlayHurtHalf -- sim-clock when half of hurtMs has elapsed (landings @@ -3182,6 +3190,8 @@ command b2kPlayerAttach pCtrl put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -3213,10 +3223,11 @@ end b2kPlayerResolveArt -- Tuning knobs (pixels, px/s, ms, degrees). Keys: moveSpeed, accel, -- airAccel, jumpSpeed, jumpCut, coyoteMs, bufferMs, maxFall, maxSlopeDeg, --- dropMs (drop-through window), climbSpeed (ladder px/s), hurtPopX/ --- hurtPopY (knockback launch px/s), hurtMs (control-off span), invulnMs --- (post-hurt mercy). Settable any time, before or after the player --- exists; unknown keys are stored verbatim for your own use. +-- dropMs (drop-through window), climbSpeed (ladder px/s), swimSpeed/ +-- swimJump (water px/s + stroke), swimGravity/swimMaxFall (buoyancy), +-- hurtPopX/hurtPopY (knockback launch px/s), hurtMs (control-off span), +-- invulnMs (post-hurt mercy). Settable any time, before or after the +-- player exists; unknown keys are stored verbatim for your own use. command b2kPlayerSet pKey, pValue put pValue into sPlayTune[toLower(pKey)] b2kPlayerTuneCache @@ -3235,6 +3246,10 @@ command b2kPlayerTuneCache put b2kPlayerGet("maxFall") into sPlayMaxFall put b2kPlayerGet("dropMs") into sPlayDropMS put b2kPlayerGet("climbSpeed") into sPlayClimbSpd + put b2kPlayerGet("swimSpeed") into sPlaySwimSpd + put b2kPlayerGet("swimJump") into sPlaySwimJump + put b2kPlayerGet("swimGravity") into sPlaySwimGrav + put b2kPlayerGet("swimMaxFall") into sPlaySwimMaxFall put b2kPlayerGet("hurtPopX") into sPlayHurtPopX put b2kPlayerGet("hurtPopY") into sPlayHurtPopY put b2kPlayerGet("hurtMs") into sPlayHurtMS @@ -3281,6 +3296,14 @@ function b2kPlayerDefault pKey return 260 case "climbspeed" return 160 + case "swimspeed" + return 150 + case "swimjump" + return 300 + case "swimgravity" + return 0.35 + case "swimmaxfall" + return 150 case "hurtpopx" return 220 case "hurtpopy" @@ -3299,9 +3322,10 @@ end b2kPlayerDefault -- idle/run resume. NOTE: a finishing land animation dispatches the art's -- b2kSpriteOnFinish message like any non-looping animation would. -- Wave 2 slots (all optional, so old five-argument calls keep working): --- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose -- --- sheets without those frames still read correctly. -command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt +-- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose; +-- the Wave 4 pSwim falls back to the fall pose -- sheets without those +-- frames still read correctly. +command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt, pSwim put pIdle into sPlayAnims["idle"] put pRun into sPlayAnims["run"] put pJump into sPlayAnims["jump"] @@ -3326,6 +3350,11 @@ command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt else put pHurt into sPlayAnims["hurt"] end if + if pSwim is empty then + put sPlayAnims["fall"] into sPlayAnims["swim"] + else + put pSwim into sPlayAnims["swim"] + end if put empty into sPlayAnimNow -- re-assert on the next tick if sPlayArt is empty then b2kPlayerResolveArt end b2kPlayerAnims @@ -3344,9 +3373,9 @@ function b2kPlayerOnGround return (sPlayGrounded is true) end b2kPlayerOnGround --- idle | run | jump | fall | duck | climb | hurt, plus "land" for exactly --- one frame on touch-down from jump/fall (dust puffs, landing sounds). --- A drop-through renders as "fall". Empty = no player. +-- idle | run | jump | fall | duck | climb | hurt | swim, plus "land" for +-- exactly one frame on touch-down from jump/fall (dust puffs, landing +-- sounds). A drop-through renders as "fall". Empty = no player. function b2kPlayerState return sPlayState end b2kPlayerState @@ -3391,6 +3420,25 @@ command b2kPlayerAddLadder pX1, pY1, pX2, pY2 put max(pY1, pY2) into sPlayLadB[sPlayLadN] end b2kPlayerAddLadder +-- Register a water ZONE (screen-px rect, any corner order). While the +-- player's centre is inside one, the controller SWIMS: gravity drops to +-- swimGravity (buoyant), the sink caps at swimMaxFall, UP/DOWN swim at +-- swimSpeed, and JUMP is a repeatable upward STROKE (swimJump) -- no +-- ground needed. Leaving the zone restores gravity. Pure polled geometry, +-- WORLD state (b2kClear wipes them), exactly like the ladder zones. +-- TIP: top the zone a little ABOVE the water's surface tiles, so the dive +-- in and the surface-out break the water where the surface art sits. +command b2kPlayerAddWater pX1, pY1, pX2, pY2 + if pX1 is not a number or pY1 is not a number \ + or pX2 is not a number or pY2 is not a number then exit b2kPlayerAddWater + if sPlayWatN is empty then put 0 into sPlayWatN + add 1 to sPlayWatN + put min(pX1, pX2) into sPlayWatL[sPlayWatN] + put max(pX1, pX2) into sPlayWatR[sPlayWatN] + put min(pY1, pY2) into sPlayWatT[sPlayWatN] + put max(pY1, pY2) into sPlayWatB[sPlayWatN] +end b2kPlayerAddWater + -- The knockback standard (spec section 9.4): control off, an away-pop -- (the sign of pFromX vs the player picks the direction; no/empty -- pFromX pops away from the facing), the hurt anim, control restored @@ -3405,6 +3453,7 @@ command b2kPlayerHurt pFromX if b2kPlayerHurtIs() then exit b2kPlayerHurt -- mercy window if sPlayControl is not true then exit b2kPlayerHurt -- a cutscene owns the body if sPlayClimb is true then b2kPlayerClimbEnd sBody[sPlayRef] + if sPlaySwim is true then b2kPlayerSwimEnd sBody[sPlayRef] if pFromX is a number then if pFromX > sPlayPX then put -1 into tDir @@ -3470,6 +3519,9 @@ command b2kPlayerForget pFull if sPlayClimb is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then b2kPlayerClimbEnd sBody[sPlayRef] end if + if sPlaySwim is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then + b2kPlayerSwimEnd sBody[sPlayRef] + end if if sPlayDropUntil is not empty and sPlayDropUntil > 0 then b2kPlayerDropRestore put empty into sPlayRef put empty into sPlayArt @@ -3492,6 +3544,8 @@ command b2kPlayerForget pFull put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -3502,6 +3556,11 @@ command b2kPlayerForget pFull put empty into sPlayLadT put empty into sPlayLadR put empty into sPlayLadB + put 0 into sPlayWatN + put empty into sPlayWatL + put empty into sPlayWatT + put empty into sPlayWatR + put empty into sPlayWatB if pFull is true then put empty into sPlayTune end b2kPlayerForget @@ -3568,6 +3627,24 @@ command b2kPlayerClimbEnd pBody put false into sPlayClimb end b2kPlayerClimbEnd +-- Internal: enter the swim -- gravity drops to swimGravity (buoyant; the +-- body's own scale is saved and restored, like the climb). Mutually +-- exclusive with the climb: the tick only ever starts one of them. +command b2kPlayerSwimStart pBody + if sPlaySwim is true then exit b2kPlayerSwimStart + put b2BodyGravityScale(pBody) into sPlaySwimGravSave + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGrav, 0.35) + put true into sPlaySwim + put false into sPlayJumping +end b2kPlayerSwimStart + +command b2kPlayerSwimEnd pBody + if sPlaySwim is not true then exit b2kPlayerSwimEnd + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGravSave, 1) + put empty into sPlaySwimGravSave + put false into sPlaySwim +end b2kPlayerSwimEnd + -- Internal: open the drop-through window -- take the reserved one-way -- bit out of the player's mask so chains (alone) stop colliding. command b2kPlayerDropStart @@ -3604,7 +3681,7 @@ end b2kPlayerDropRestore -- frame while idle -- the tick's steady-state budget is unchanged. command b2kPlayerTick local tNow, tDT, tB, tVX, tVY, tAxis, tAxisY, tTarget, tAcc, tStep - local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i + local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i, tInWater if sPlayRef is empty then exit b2kPlayerTick put sBody[sPlayRef] into tB if tB is empty then exit b2kPlayerTick @@ -3661,8 +3738,8 @@ command b2kPlayerTick put 1 into sPlayFacing end if end if - -- ladder presence: pure numeric compares on the probe's stashed - -- centre; zero zones = one compare and out + -- ladder + water presence: pure numeric compares on the probe's + -- stashed centre; zero zones of a kind = one compare and out put false into tInZone if sPlayLadN > 0 then repeat with i = 1 to sPlayLadN @@ -3673,13 +3750,26 @@ command b2kPlayerTick end if end repeat end if - if sPlayClimb is not true and tInZone is true then + put false into tInWater + if sPlayWatN > 0 then + repeat with i = 1 to sPlayWatN + if sPlayPX >= sPlayWatL[i] and sPlayPX <= sPlayWatR[i] \ + and sPlayPY >= sPlayWatT[i] and sPlayPY <= sPlayWatB[i] then + put true into tInWater + exit repeat + end if + end repeat + end if + if sPlayClimb is not true and sPlaySwim is not true and tInZone is true then -- enter: UP any time in-zone; DOWN only while AIRBORNE (a -- grounded DOWN is a duck -- or a drop-through on a chain) if tAxisY is -1 or (tAxisY is 1 and sPlayGrounded is not true) then b2kPlayerClimbStart tB end if end if + if sPlaySwim is not true and sPlayClimb is not true and tInWater is true then + b2kPlayerSwimStart tB -- submerged: buoyant gravity, stroke to rise + end if if sPlayClimb is true then if b2kActionPressed("jump") then -- JUMP exits with a normal jump. The same press edge is @@ -3714,7 +3804,33 @@ command b2kPlayerTick end if end if end if - if sPlayClimb is not true then + if sPlaySwim is true then + if tInWater is not true then + b2kPlayerSwimEnd tB -- surfaced / left the pool: gravity returns + else + -- horizontal: ease vx toward axis * swimSpeed (sluggish -- the + -- air-accel rate gives the underwater drag) + put tAxis * sPlaySwimSpd into tTarget + put sPlayAccelA * tDT into tStep + if tVX < tTarget then + put min(tTarget, tVX + tStep) into tVX + else + put max(tTarget, tVX - tStep) into tVX + end if + -- vertical: a JUMP press is a repeatable upward STROKE (no + -- ground gate -- the spec's water jump); else UP/DOWN swim at + -- swimSpeed; else the reduced gravity sinks you, capped low + if b2kActionPressed("jump") then + put 0 - sPlaySwimJump into tVY + else + if tAxisY is not 0 then + put tAxisY * sPlaySwimSpd into tVY + end if + end if + put true into tWrite + end if + end if + if sPlayClimb is not true and sPlaySwim is not true then -- horizontal: accelerate vx toward axis * moveSpeed (air = airAccel) put tAxis * sPlayMoveSpd into tTarget -- DUCK: down on the ground crouches and brakes to a stop at @@ -3769,9 +3885,18 @@ command b2kPlayerTick end if end if if sPlayJumping and tVY >= 0 then put false into sPlayJumping -- apex - if tVY > sPlayMaxFall then - put sPlayMaxFall into tVY - put true into tWrite + -- terminal velocity: the low swimMaxFall is the buoyant sink cap while + -- submerged; the normal maxFall (the character's terminal velocity) else + if sPlaySwim is true then + if tVY > sPlaySwimMaxFall then + put sPlaySwimMaxFall into tVY + put true into tWrite + end if + else + if tVY > sPlayMaxFall then + put sPlayMaxFall into tVY + put true into tWrite + end if end if -- GROUND-SNAP: grounded on FLAT ground, drifting upward, and we did -- not jump = the contact solver's push-out rebound after a hard @@ -3783,7 +3908,7 @@ command b2kPlayerTick -- must use b2kPlayerJump, which sets the jump flag (b2kPlayerHurt's -- pop rides the same flag). if sPlayGrounded and sPlayJumping is not true and sPlayClimb is not true \ - and tVY < 0 and abs(sPlayNormX) < 0.1 then + and sPlaySwim is not true and tVY < 0 and abs(sPlayNormX) < 0.1 then put 0 into tVY put true into tWrite end if @@ -3838,6 +3963,14 @@ command b2kPlayerTick end if end if end if + -- swim OWNS the state while submerged: the machine above ran the + -- grounded/airborne branch (sPlayClimb is false underwater), so override + -- to "swim" and keep the air counter clear -- surfacing must never read + -- as a long fall plus a phantom land tick. + if sPlaySwim is true and sPlayHurt is not true then + put "swim" into sPlayState + put 0 into sPlayAir + end if -- animations: only while controlling (manual poses own the art when -- control is off), and never let a vanished art control abort the -- frame -- the loop's ticks share one try block @@ -3856,7 +3989,7 @@ command b2kPlayerShowState pNow, pVX local tWant, tAnim, tAKey, tFPS, tFlip put sPlayState into tWant if pNow < sPlayHoldMS and tWant is not "jump" and tWant is not "fall" \ - and tWant is not "hurt" and tWant is not "climb" then + and tWant is not "hurt" and tWant is not "climb" and tWant is not "swim" then put empty into tAnim -- mid land-flourish: leave it playing else if tWant is "land" and sPlayAnims["land"] is empty then diff --git a/examples/box2dxt-demo.livecodescript b/examples/box2dxt-demo.livecodescript index aafe704..56a10f8 100644 --- a/examples/box2dxt-demo.livecodescript +++ b/examples/box2dxt-demo.livecodescript @@ -95,16 +95,18 @@ -- b2kSpriteFPS spr,fps · b2kSpriteOnFinish spr,msg · -- b2kSpriteBind spr,bodyCtrl[,dx,dy] · b2kSpriteRemove spr -- PLAYER b2kPlayerMake x,y,w,h[,sheet] · b2kPlayerAttach ctrl · --- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt] · +-- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt,swim] · -- b2kPlayerSet key,value · b2kPlayerGet(key) · b2kPlayerOnGround() · --- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt) · +-- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt|swim) · -- b2kPlayerFacing() · b2kPlayerJump [speed] · b2kPlayerControl flag · --- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerHurt [fromX] · +-- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerAddWater x1,y1,x2,y2 · +-- b2kPlayerHurt [fromX] · -- b2kPlayerHurtIs() (true through knockback + mercy window) · -- b2kPlayer() · b2kPlayerSprite() · b2kPlayerRemove -- (drives axes "moveX"/"moveY" + action "jump"; rebind to remap. -- DOWN ducks · DOWN+JUMP on a one-way chain drops through · --- UP/DOWN in a ladder zone climbs · JUMP exits a climb) +-- UP/DOWN in a ladder zone climbs · JUMP exits a climb · +-- in a water zone UP/DOWN swim, JUMP strokes (repeatable)) -- CAMERA b2kCamOn [rect] · b2kCamOff · b2kCamFollow ctrl[,lerp] · b2kCamUnfollow · -- b2kCamDeadzone w,h · b2kCamBounds x1,y1,x2,y2 · b2kCamGoto x,y · -- b2kCamPos() · b2kCamShake amp,ms · b2kCamMouseX/Y() · b2kCamGroup() · @@ -252,6 +254,12 @@ local sPlayClimb -- true while in the climb state (gravity scale parked at 0 local sPlayGravSave -- the body's gravity scale to restore on climb exit local sPlayLadN -- registered ladder zones (flat numeric arrays: the local sPlayLadL, sPlayLadT, sPlayLadR, sPlayLadB -- ...in-zone test is pure compares) +-- Wave 4: water zones + the swim state (a buoyant parallel to the climb) +local sPlaySwim -- true while submerged in a water zone (gravity scaled down) +local sPlaySwimGravSave -- the body's gravity scale to restore on swim exit +local sPlaySwimSpd, sPlaySwimJump, sPlaySwimGrav, sPlaySwimMaxFall -- swim tune caches +local sPlayWatN -- registered water zones (flat numeric arrays, like ladders) +local sPlayWatL, sPlayWatT, sPlayWatR, sPlayWatB local sPlayHurt -- true while knockback owns the controller local sPlayHurtEnd -- sim-clock when hurtMs has elapsed local sPlayHurtHalf -- sim-clock when half of hurtMs has elapsed (landings @@ -3172,6 +3180,8 @@ command b2kPlayerAttach pCtrl put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -3203,10 +3213,11 @@ end b2kPlayerResolveArt -- Tuning knobs (pixels, px/s, ms, degrees). Keys: moveSpeed, accel, -- airAccel, jumpSpeed, jumpCut, coyoteMs, bufferMs, maxFall, maxSlopeDeg, --- dropMs (drop-through window), climbSpeed (ladder px/s), hurtPopX/ --- hurtPopY (knockback launch px/s), hurtMs (control-off span), invulnMs --- (post-hurt mercy). Settable any time, before or after the player --- exists; unknown keys are stored verbatim for your own use. +-- dropMs (drop-through window), climbSpeed (ladder px/s), swimSpeed/ +-- swimJump (water px/s + stroke), swimGravity/swimMaxFall (buoyancy), +-- hurtPopX/hurtPopY (knockback launch px/s), hurtMs (control-off span), +-- invulnMs (post-hurt mercy). Settable any time, before or after the +-- player exists; unknown keys are stored verbatim for your own use. command b2kPlayerSet pKey, pValue put pValue into sPlayTune[toLower(pKey)] b2kPlayerTuneCache @@ -3225,6 +3236,10 @@ command b2kPlayerTuneCache put b2kPlayerGet("maxFall") into sPlayMaxFall put b2kPlayerGet("dropMs") into sPlayDropMS put b2kPlayerGet("climbSpeed") into sPlayClimbSpd + put b2kPlayerGet("swimSpeed") into sPlaySwimSpd + put b2kPlayerGet("swimJump") into sPlaySwimJump + put b2kPlayerGet("swimGravity") into sPlaySwimGrav + put b2kPlayerGet("swimMaxFall") into sPlaySwimMaxFall put b2kPlayerGet("hurtPopX") into sPlayHurtPopX put b2kPlayerGet("hurtPopY") into sPlayHurtPopY put b2kPlayerGet("hurtMs") into sPlayHurtMS @@ -3271,6 +3286,14 @@ function b2kPlayerDefault pKey return 260 case "climbspeed" return 160 + case "swimspeed" + return 150 + case "swimjump" + return 300 + case "swimgravity" + return 0.35 + case "swimmaxfall" + return 150 case "hurtpopx" return 220 case "hurtpopy" @@ -3289,9 +3312,10 @@ end b2kPlayerDefault -- idle/run resume. NOTE: a finishing land animation dispatches the art's -- b2kSpriteOnFinish message like any non-looping animation would. -- Wave 2 slots (all optional, so old five-argument calls keep working): --- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose -- --- sheets without those frames still read correctly. -command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt +-- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose; +-- the Wave 4 pSwim falls back to the fall pose -- sheets without those +-- frames still read correctly. +command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt, pSwim put pIdle into sPlayAnims["idle"] put pRun into sPlayAnims["run"] put pJump into sPlayAnims["jump"] @@ -3316,6 +3340,11 @@ command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt else put pHurt into sPlayAnims["hurt"] end if + if pSwim is empty then + put sPlayAnims["fall"] into sPlayAnims["swim"] + else + put pSwim into sPlayAnims["swim"] + end if put empty into sPlayAnimNow -- re-assert on the next tick if sPlayArt is empty then b2kPlayerResolveArt end b2kPlayerAnims @@ -3334,9 +3363,9 @@ function b2kPlayerOnGround return (sPlayGrounded is true) end b2kPlayerOnGround --- idle | run | jump | fall | duck | climb | hurt, plus "land" for exactly --- one frame on touch-down from jump/fall (dust puffs, landing sounds). --- A drop-through renders as "fall". Empty = no player. +-- idle | run | jump | fall | duck | climb | hurt | swim, plus "land" for +-- exactly one frame on touch-down from jump/fall (dust puffs, landing +-- sounds). A drop-through renders as "fall". Empty = no player. function b2kPlayerState return sPlayState end b2kPlayerState @@ -3381,6 +3410,25 @@ command b2kPlayerAddLadder pX1, pY1, pX2, pY2 put max(pY1, pY2) into sPlayLadB[sPlayLadN] end b2kPlayerAddLadder +-- Register a water ZONE (screen-px rect, any corner order). While the +-- player's centre is inside one, the controller SWIMS: gravity drops to +-- swimGravity (buoyant), the sink caps at swimMaxFall, UP/DOWN swim at +-- swimSpeed, and JUMP is a repeatable upward STROKE (swimJump) -- no +-- ground needed. Leaving the zone restores gravity. Pure polled geometry, +-- WORLD state (b2kClear wipes them), exactly like the ladder zones. +-- TIP: top the zone a little ABOVE the water's surface tiles, so the dive +-- in and the surface-out break the water where the surface art sits. +command b2kPlayerAddWater pX1, pY1, pX2, pY2 + if pX1 is not a number or pY1 is not a number \ + or pX2 is not a number or pY2 is not a number then exit b2kPlayerAddWater + if sPlayWatN is empty then put 0 into sPlayWatN + add 1 to sPlayWatN + put min(pX1, pX2) into sPlayWatL[sPlayWatN] + put max(pX1, pX2) into sPlayWatR[sPlayWatN] + put min(pY1, pY2) into sPlayWatT[sPlayWatN] + put max(pY1, pY2) into sPlayWatB[sPlayWatN] +end b2kPlayerAddWater + -- The knockback standard (spec section 9.4): control off, an away-pop -- (the sign of pFromX vs the player picks the direction; no/empty -- pFromX pops away from the facing), the hurt anim, control restored @@ -3395,6 +3443,7 @@ command b2kPlayerHurt pFromX if b2kPlayerHurtIs() then exit b2kPlayerHurt -- mercy window if sPlayControl is not true then exit b2kPlayerHurt -- a cutscene owns the body if sPlayClimb is true then b2kPlayerClimbEnd sBody[sPlayRef] + if sPlaySwim is true then b2kPlayerSwimEnd sBody[sPlayRef] if pFromX is a number then if pFromX > sPlayPX then put -1 into tDir @@ -3460,6 +3509,9 @@ command b2kPlayerForget pFull if sPlayClimb is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then b2kPlayerClimbEnd sBody[sPlayRef] end if + if sPlaySwim is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then + b2kPlayerSwimEnd sBody[sPlayRef] + end if if sPlayDropUntil is not empty and sPlayDropUntil > 0 then b2kPlayerDropRestore put empty into sPlayRef put empty into sPlayArt @@ -3482,6 +3534,8 @@ command b2kPlayerForget pFull put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -3492,6 +3546,11 @@ command b2kPlayerForget pFull put empty into sPlayLadT put empty into sPlayLadR put empty into sPlayLadB + put 0 into sPlayWatN + put empty into sPlayWatL + put empty into sPlayWatT + put empty into sPlayWatR + put empty into sPlayWatB if pFull is true then put empty into sPlayTune end b2kPlayerForget @@ -3558,6 +3617,24 @@ command b2kPlayerClimbEnd pBody put false into sPlayClimb end b2kPlayerClimbEnd +-- Internal: enter the swim -- gravity drops to swimGravity (buoyant; the +-- body's own scale is saved and restored, like the climb). Mutually +-- exclusive with the climb: the tick only ever starts one of them. +command b2kPlayerSwimStart pBody + if sPlaySwim is true then exit b2kPlayerSwimStart + put b2BodyGravityScale(pBody) into sPlaySwimGravSave + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGrav, 0.35) + put true into sPlaySwim + put false into sPlayJumping +end b2kPlayerSwimStart + +command b2kPlayerSwimEnd pBody + if sPlaySwim is not true then exit b2kPlayerSwimEnd + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGravSave, 1) + put empty into sPlaySwimGravSave + put false into sPlaySwim +end b2kPlayerSwimEnd + -- Internal: open the drop-through window -- take the reserved one-way -- bit out of the player's mask so chains (alone) stop colliding. command b2kPlayerDropStart @@ -3594,7 +3671,7 @@ end b2kPlayerDropRestore -- frame while idle -- the tick's steady-state budget is unchanged. command b2kPlayerTick local tNow, tDT, tB, tVX, tVY, tAxis, tAxisY, tTarget, tAcc, tStep - local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i + local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i, tInWater if sPlayRef is empty then exit b2kPlayerTick put sBody[sPlayRef] into tB if tB is empty then exit b2kPlayerTick @@ -3651,8 +3728,8 @@ command b2kPlayerTick put 1 into sPlayFacing end if end if - -- ladder presence: pure numeric compares on the probe's stashed - -- centre; zero zones = one compare and out + -- ladder + water presence: pure numeric compares on the probe's + -- stashed centre; zero zones of a kind = one compare and out put false into tInZone if sPlayLadN > 0 then repeat with i = 1 to sPlayLadN @@ -3663,13 +3740,26 @@ command b2kPlayerTick end if end repeat end if - if sPlayClimb is not true and tInZone is true then + put false into tInWater + if sPlayWatN > 0 then + repeat with i = 1 to sPlayWatN + if sPlayPX >= sPlayWatL[i] and sPlayPX <= sPlayWatR[i] \ + and sPlayPY >= sPlayWatT[i] and sPlayPY <= sPlayWatB[i] then + put true into tInWater + exit repeat + end if + end repeat + end if + if sPlayClimb is not true and sPlaySwim is not true and tInZone is true then -- enter: UP any time in-zone; DOWN only while AIRBORNE (a -- grounded DOWN is a duck -- or a drop-through on a chain) if tAxisY is -1 or (tAxisY is 1 and sPlayGrounded is not true) then b2kPlayerClimbStart tB end if end if + if sPlaySwim is not true and sPlayClimb is not true and tInWater is true then + b2kPlayerSwimStart tB -- submerged: buoyant gravity, stroke to rise + end if if sPlayClimb is true then if b2kActionPressed("jump") then -- JUMP exits with a normal jump. The same press edge is @@ -3704,7 +3794,33 @@ command b2kPlayerTick end if end if end if - if sPlayClimb is not true then + if sPlaySwim is true then + if tInWater is not true then + b2kPlayerSwimEnd tB -- surfaced / left the pool: gravity returns + else + -- horizontal: ease vx toward axis * swimSpeed (sluggish -- the + -- air-accel rate gives the underwater drag) + put tAxis * sPlaySwimSpd into tTarget + put sPlayAccelA * tDT into tStep + if tVX < tTarget then + put min(tTarget, tVX + tStep) into tVX + else + put max(tTarget, tVX - tStep) into tVX + end if + -- vertical: a JUMP press is a repeatable upward STROKE (no + -- ground gate -- the spec's water jump); else UP/DOWN swim at + -- swimSpeed; else the reduced gravity sinks you, capped low + if b2kActionPressed("jump") then + put 0 - sPlaySwimJump into tVY + else + if tAxisY is not 0 then + put tAxisY * sPlaySwimSpd into tVY + end if + end if + put true into tWrite + end if + end if + if sPlayClimb is not true and sPlaySwim is not true then -- horizontal: accelerate vx toward axis * moveSpeed (air = airAccel) put tAxis * sPlayMoveSpd into tTarget -- DUCK: down on the ground crouches and brakes to a stop at @@ -3759,9 +3875,18 @@ command b2kPlayerTick end if end if if sPlayJumping and tVY >= 0 then put false into sPlayJumping -- apex - if tVY > sPlayMaxFall then - put sPlayMaxFall into tVY - put true into tWrite + -- terminal velocity: the low swimMaxFall is the buoyant sink cap while + -- submerged; the normal maxFall (the character's terminal velocity) else + if sPlaySwim is true then + if tVY > sPlaySwimMaxFall then + put sPlaySwimMaxFall into tVY + put true into tWrite + end if + else + if tVY > sPlayMaxFall then + put sPlayMaxFall into tVY + put true into tWrite + end if end if -- GROUND-SNAP: grounded on FLAT ground, drifting upward, and we did -- not jump = the contact solver's push-out rebound after a hard @@ -3773,7 +3898,7 @@ command b2kPlayerTick -- must use b2kPlayerJump, which sets the jump flag (b2kPlayerHurt's -- pop rides the same flag). if sPlayGrounded and sPlayJumping is not true and sPlayClimb is not true \ - and tVY < 0 and abs(sPlayNormX) < 0.1 then + and sPlaySwim is not true and tVY < 0 and abs(sPlayNormX) < 0.1 then put 0 into tVY put true into tWrite end if @@ -3828,6 +3953,14 @@ command b2kPlayerTick end if end if end if + -- swim OWNS the state while submerged: the machine above ran the + -- grounded/airborne branch (sPlayClimb is false underwater), so override + -- to "swim" and keep the air counter clear -- surfacing must never read + -- as a long fall plus a phantom land tick. + if sPlaySwim is true and sPlayHurt is not true then + put "swim" into sPlayState + put 0 into sPlayAir + end if -- animations: only while controlling (manual poses own the art when -- control is off), and never let a vanished art control abort the -- frame -- the loop's ticks share one try block @@ -3846,7 +3979,7 @@ command b2kPlayerShowState pNow, pVX local tWant, tAnim, tAKey, tFPS, tFlip put sPlayState into tWant if pNow < sPlayHoldMS and tWant is not "jump" and tWant is not "fall" \ - and tWant is not "hurt" and tWant is not "climb" then + and tWant is not "hurt" and tWant is not "climb" and tWant is not "swim" then put empty into tAnim -- mid land-flourish: leave it playing else if tWant is "land" and sPlayAnims["land"] is empty then diff --git a/examples/box2dxt-microgame.livecodescript b/examples/box2dxt-microgame.livecodescript index 6254c3e..3aa48d3 100644 --- a/examples/box2dxt-microgame.livecodescript +++ b/examples/box2dxt-microgame.livecodescript @@ -65,6 +65,7 @@ local gLevel, gCoins, gCoinsTotal, gFalls, gRunStart, gWinSecs local gHero, gHeroSpr, gDoor, gDoorOpen, gSpawnX, gSpawnY local gHudLast, gHudNextMS, gHurtLock, gBuilding, gPrevState, gWorldW, gWorldH local gSwpN, gSwpRef, gSwpC, gSwpA, gSwpP, gSwpY, gSwpW, gSwpH +local gFishN, gFishRef, gFishX, gFishYC, gFishYA, gFishP, gFishW, gFishH -- Wave 4 pit-dwellers local gOuches -- Wave 2: knockback hits (falls stay falls) local gSkin -- "hero" (embedded) or an alien colour word local gSkinsOK -- true when the aliens atlas loaded (folder known) @@ -173,7 +174,7 @@ command mgBegin end mgBegin command mgAdvance - if gLevel is 1 then + if gLevel < 3 then -- rebuild OUTSIDE this physics frame: we are inside the sensor -- dispatch right now, and tearing the world down mid-frame is -- asking for trouble. The short beat also lets the door chime ring. @@ -185,8 +186,8 @@ end mgAdvance on mgGoNext if gMode is not "play" then exit mgGoNext - mgBuild 2 - b2kPlayerControl true -- straight into level 2, clocks keep running + mgBuild gLevel + 1 + b2kPlayerControl true -- straight into the next level, clocks keep running end mgGoNext command mgShowWin @@ -198,7 +199,7 @@ command mgShowWin b2kSetVelocity gHero, 0, item 2 of b2kVelocity(gHero) b2kSound "win" put "Y O U W I N !" & cr & cr into tMsg - put "both levels, every coin" & cr after tMsg + put "all three levels, every coin" & cr after tMsg put "time " & format("%d:%02d", gWinSecs div 60, gWinSecs mod 60) & " falls " & gFalls & " hits " & gOuches & cr & cr after tMsg if gFalls is 0 and gOuches is 0 then put "flawless - untouched, never fell" & cr & cr after tMsg put "C L I C K T O P L A Y A G A I N" after tMsg @@ -230,6 +231,8 @@ end mgMakeSounds -- sweep l,t,r,b,minx,maxx,period (patrolling proximity hazard) · -- ladder x,y1,y2 (a climb zone at x, y1 top to y2 bottom, rungs drawn -- - zero-asset; UP/DOWN climbs, JUMP lets go) · +-- water l,t,r,b (Wave 4 SWIM zone: buoyant, UP/DOWN swim, SPACE strokes) · +-- fish x,yTop,yBot,period (a pit-dweller bobbing in a pool; knockback) · -- door x,y. Add your own verbs in mgBuild's switch - that IS the -- pattern: levels are text, the interpreter is the engine of YOUR game. -- ===================================================================== @@ -250,6 +253,26 @@ function mgLevelText pLevel put "coin 740,372" & cr after t put "slab 880,512,1010,576" & cr after t put "door 945,478" & cr after t + else if pLevel is 3 then + -- Wave 4: THE DEEP. A wide pool you DIVE into; the only coins are + -- underwater, so the door makes you swim. UP/DOWN swim, SPACE strokes + -- (repeatable, no ground), and a stroke + hold-right HOPS you out onto + -- the far bank. A pit-dweller fish patrols the middle (knockback). + put "bounds 2000,640" & cr after t + put "spawn 90,320" & cr after t + put "text 1000,104,LEVEL 3 - THE DEEP: dive in and SWIM for every coin" & cr after t + put "text 330,208,UP / DOWN swim - SPACE strokes you up and out" & cr after t + put "slab 0,360,900,640" & cr after t + put "water 900,360,1500,640" & cr after t + put "slab 900,612,1500,640" & cr after t + put "coin 1000,452" & cr after t + put "coin 1140,540" & cr after t + put "coin 1290,452" & cr after t + put "coin 1430,540" & cr after t + put "fish 1210,340,560,1300" & cr after t + put "slab 1500,360,2000,640" & cr after t + put "coin 1700,326" & cr after t + put "door 1900,328" & cr after t else put "bounds 1664,640" & cr after t put "spawn 90,500" & cr after t @@ -301,6 +324,14 @@ command mgBuild pLevel put empty into gSwpY put empty into gSwpW put empty into gSwpH + put 0 into gFishN + put empty into gFishRef + put empty into gFishX + put empty into gFishYC + put empty into gFishYA + put empty into gFishP + put empty into gFishW + put empty into gFishH try b2kClear catch tErr @@ -358,6 +389,7 @@ command mgBuild pLevel b2kAnimDef "aliens", "jump", tCh & "_jump.png", 1, true b2kAnimDef "aliens", "duck", tCh & "_duck.png", 1, true b2kAnimDef "aliens", "climb", tCh & "_climb1.png," & tCh & "_climb2.png", 6, true + b2kAnimDef "aliens", "swim", tCh & "_swim1.png," & tCh & "_swim2.png", 6, true b2kAnimDef "aliens", "hit", tCh & "_hurt.png", 2, false b2kAnimDef "aliens", "hurtpose", tCh & "_hurt.png", 2, true put "aliens" into tSheet @@ -491,6 +523,50 @@ command mgBuild pLevel add 24 to tY end repeat break + case "water" + -- Wave 4: a SWIM zone. The Kit's controller polls it (buoyant + -- gravity, capped sink, UP/DOWN swim, SPACE strokes up). Drawn + -- as a blue pool with a surface line; coins/fish placed AFTER + -- it in the level text layer on top (later controls draw over). + b2kPlayerAddWater item 1 of tArgs, item 2 of tArgs, item 3 of tArgs, item 4 of tArgs + put "mg_water" & the milliseconds & random(100000) into tName + create graphic tName + set the style of it to "rectangle" + set the rect of it to item 1 of tArgs, item 2 of tArgs, item 3 of tArgs, item 4 of tArgs + set the filled of it to true + set the backgroundColor of it to "92,150,205" + set the foregroundColor of it to "60,110,170" + b2kCamAdopt the long id of graphic tName + put "mg_surf" & the milliseconds & random(100000) into tName + create graphic tName + set the style of it to "line" + set the points of it to (item 1 of tArgs) & comma & (item 2 of tArgs) & cr & (item 3 of tArgs) & comma & (item 2 of tArgs) + set the lineSize of it to 3 + set the foregroundColor of it to "165,210,245" + b2kCamAdopt the long id of graphic tName + break + case "fish" + -- Wave 4: a PIT-DWELLER. A bodiless mover (the sweep pattern, + -- vertical) bobbing between yTop and yBot - it breaches the + -- surface and dives, a knockback graze on contact (never a + -- respawn). Args: x,yTop,yBot,period. + put "mg_fish" & the milliseconds & random(100000) into tName + create graphic tName + set the style of it to "oval" + set the rect of it to (item 1 of tArgs) - 22, ((item 2 of tArgs) + (item 3 of tArgs)) / 2 - 13, (item 1 of tArgs) + 22, ((item 2 of tArgs) + (item 3 of tArgs)) / 2 + 13 + set the filled of it to true + set the backgroundColor of it to "95,165,150" + set the foregroundColor of it to "40,90,85" + b2kCamAdopt the long id of graphic tName + add 1 to gFishN + put the long id of graphic tName into gFishRef[gFishN] + put item 1 of tArgs into gFishX[gFishN] + put ((item 2 of tArgs) + (item 3 of tArgs)) / 2 into gFishYC[gFishN] + put ((item 3 of tArgs) - (item 2 of tArgs)) / 2 into gFishYA[gFishN] + put max(1, item 4 of tArgs) into gFishP[gFishN] + put 36 into gFishW[gFishN] + put 30 into gFishH[gFishN] + break case "door" put "mg_door" & the milliseconds & random(100000) into tName create graphic tName @@ -534,9 +610,9 @@ command mgBuild pLevel -- knockback pose is the LOOPING "hurtpose", never the one-shot -- "hit" whose finish message means RESPAWN here. if gSkin is "hero" then - b2kPlayerAnims "idle", "walk", "jump", "jump", "", "", "", "hurtpose" + b2kPlayerAnims "idle", "walk", "jump", "jump", "", "", "", "hurtpose", "" else - b2kPlayerAnims "idle", "walk", "jump", "jump", "", "duck", "climb", "hurtpose" + b2kPlayerAnims "idle", "walk", "jump", "jump", "", "duck", "climb", "hurtpose", "swim" end if b2kPlayerSet "moveSpeed", 250 b2kPlayerSet "jumpSpeed", 460 @@ -587,12 +663,18 @@ end mgWipeStage -- The per-frame game logic -- ===================================================================== on b2kFrame - local tHud, tSecs, tState + local tHud, tSecs, tState, tPos, tHX, tHY if gHero is empty then exit b2kFrame set the itemDelimiter to comma + -- ONE hero snapshot per frame: the kill plane and both hazard ticks + -- share it (each re-reading would be a needless FFI round-trip) + put b2kPosition(gHero) into tPos + put item 1 of tPos into tHX + put item 2 of tPos into tHY -- kill plane: off the bottom = a fall - if gHurtLock is not true and (item 2 of b2kPosition(gHero)) > gWorldH + 120 then mgHurt - mgTickSweeps + if gHurtLock is not true and tHY > gWorldH + 120 then mgHurt + mgTickSweeps tHX, tHY + mgTickFish tHX, tHY -- the door unlocks itself the moment the last coin is taken if gDoorOpen is not true and gDoor is not empty and gCoinsTotal > 0 and gCoins >= gCoinsTotal then put true into gDoorOpen @@ -624,25 +706,38 @@ on b2kFrame end if end b2kFrame -command mgTickSweeps - local i, tMS, tX, tHX, tHY, tPos +command mgTickSweeps pHX, pHY + local i, tMS, tX if gSwpN is 0 or gSwpN is empty then exit mgTickSweeps - set the itemDelimiter to comma put the milliseconds into tMS - put b2kPosition(gHero) into tPos - put item 1 of tPos into tHX - put item 2 of tPos into tHY repeat with i = 1 to gSwpN if gSwpRef[i] is empty then next repeat put gSwpC[i] + gSwpA[i] * sin(tMS / gSwpP[i]) into tX b2kSpriteMoveTo gSwpRef[i], tX, gSwpY[i] if gHurtLock is not true and gMode is "play" then -- a sweeper graze knocks back, never respawns (Wave 2 split) - if abs(tHX - tX) < gSwpW[i] and abs(tHY - gSwpY[i]) < gSwpH[i] then mgOuch tX + if abs(pHX - tX) < gSwpW[i] and abs(pHY - gSwpY[i]) < gSwpH[i] then mgOuch tX end if end repeat end mgTickSweeps +-- Wave 4 pit-dwellers: each fish bobs VERTICALLY between its yTop and yBot +-- (the sweep pattern on the y axis), breaching the surface and diving. A +-- graze knocks back (mgOuch), never respawns -- the Wave 2 hazard split. +command mgTickFish pHX, pHY + local i, tMS, tY + if gFishN is 0 or gFishN is empty then exit mgTickFish + put the milliseconds into tMS + repeat with i = 1 to gFishN + if gFishRef[i] is empty then next repeat + put gFishYC[i] + gFishYA[i] * sin(tMS / gFishP[i]) into tY + b2kSpriteMoveTo gFishRef[i], gFishX[i], tY + if gHurtLock is not true and gMode is "play" then + if abs(pHX - gFishX[i]) < gFishW[i] and abs(pHY - tY) < gFishH[i] then mgOuch gFishX[i] + end if + end repeat +end mgTickFish + -- ===================================================================== -- Events: pickups, hazards, the door -- ===================================================================== @@ -849,16 +944,18 @@ end rawKeyDown -- b2kSpriteFPS spr,fps · b2kSpriteOnFinish spr,msg · -- b2kSpriteBind spr,bodyCtrl[,dx,dy] · b2kSpriteRemove spr -- PLAYER b2kPlayerMake x,y,w,h[,sheet] · b2kPlayerAttach ctrl · --- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt] · +-- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt,swim] · -- b2kPlayerSet key,value · b2kPlayerGet(key) · b2kPlayerOnGround() · --- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt) · +-- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt|swim) · -- b2kPlayerFacing() · b2kPlayerJump [speed] · b2kPlayerControl flag · --- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerHurt [fromX] · +-- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerAddWater x1,y1,x2,y2 · +-- b2kPlayerHurt [fromX] · -- b2kPlayerHurtIs() (true through knockback + mercy window) · -- b2kPlayer() · b2kPlayerSprite() · b2kPlayerRemove -- (drives axes "moveX"/"moveY" + action "jump"; rebind to remap. -- DOWN ducks · DOWN+JUMP on a one-way chain drops through · --- UP/DOWN in a ladder zone climbs · JUMP exits a climb) +-- UP/DOWN in a ladder zone climbs · JUMP exits a climb · +-- in a water zone UP/DOWN swim, JUMP strokes (repeatable)) -- CAMERA b2kCamOn [rect] · b2kCamOff · b2kCamFollow ctrl[,lerp] · b2kCamUnfollow · -- b2kCamDeadzone w,h · b2kCamBounds x1,y1,x2,y2 · b2kCamGoto x,y · -- b2kCamPos() · b2kCamShake amp,ms · b2kCamMouseX/Y() · b2kCamGroup() · @@ -1006,6 +1103,12 @@ local sPlayClimb -- true while in the climb state (gravity scale parked at 0 local sPlayGravSave -- the body's gravity scale to restore on climb exit local sPlayLadN -- registered ladder zones (flat numeric arrays: the local sPlayLadL, sPlayLadT, sPlayLadR, sPlayLadB -- ...in-zone test is pure compares) +-- Wave 4: water zones + the swim state (a buoyant parallel to the climb) +local sPlaySwim -- true while submerged in a water zone (gravity scaled down) +local sPlaySwimGravSave -- the body's gravity scale to restore on swim exit +local sPlaySwimSpd, sPlaySwimJump, sPlaySwimGrav, sPlaySwimMaxFall -- swim tune caches +local sPlayWatN -- registered water zones (flat numeric arrays, like ladders) +local sPlayWatL, sPlayWatT, sPlayWatR, sPlayWatB local sPlayHurt -- true while knockback owns the controller local sPlayHurtEnd -- sim-clock when hurtMs has elapsed local sPlayHurtHalf -- sim-clock when half of hurtMs has elapsed (landings @@ -3926,6 +4029,8 @@ command b2kPlayerAttach pCtrl put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -3957,10 +4062,11 @@ end b2kPlayerResolveArt -- Tuning knobs (pixels, px/s, ms, degrees). Keys: moveSpeed, accel, -- airAccel, jumpSpeed, jumpCut, coyoteMs, bufferMs, maxFall, maxSlopeDeg, --- dropMs (drop-through window), climbSpeed (ladder px/s), hurtPopX/ --- hurtPopY (knockback launch px/s), hurtMs (control-off span), invulnMs --- (post-hurt mercy). Settable any time, before or after the player --- exists; unknown keys are stored verbatim for your own use. +-- dropMs (drop-through window), climbSpeed (ladder px/s), swimSpeed/ +-- swimJump (water px/s + stroke), swimGravity/swimMaxFall (buoyancy), +-- hurtPopX/hurtPopY (knockback launch px/s), hurtMs (control-off span), +-- invulnMs (post-hurt mercy). Settable any time, before or after the +-- player exists; unknown keys are stored verbatim for your own use. command b2kPlayerSet pKey, pValue put pValue into sPlayTune[toLower(pKey)] b2kPlayerTuneCache @@ -3979,6 +4085,10 @@ command b2kPlayerTuneCache put b2kPlayerGet("maxFall") into sPlayMaxFall put b2kPlayerGet("dropMs") into sPlayDropMS put b2kPlayerGet("climbSpeed") into sPlayClimbSpd + put b2kPlayerGet("swimSpeed") into sPlaySwimSpd + put b2kPlayerGet("swimJump") into sPlaySwimJump + put b2kPlayerGet("swimGravity") into sPlaySwimGrav + put b2kPlayerGet("swimMaxFall") into sPlaySwimMaxFall put b2kPlayerGet("hurtPopX") into sPlayHurtPopX put b2kPlayerGet("hurtPopY") into sPlayHurtPopY put b2kPlayerGet("hurtMs") into sPlayHurtMS @@ -4025,6 +4135,14 @@ function b2kPlayerDefault pKey return 260 case "climbspeed" return 160 + case "swimspeed" + return 150 + case "swimjump" + return 300 + case "swimgravity" + return 0.35 + case "swimmaxfall" + return 150 case "hurtpopx" return 220 case "hurtpopy" @@ -4043,9 +4161,10 @@ end b2kPlayerDefault -- idle/run resume. NOTE: a finishing land animation dispatches the art's -- b2kSpriteOnFinish message like any non-looping animation would. -- Wave 2 slots (all optional, so old five-argument calls keep working): --- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose -- --- sheets without those frames still read correctly. -command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt +-- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose; +-- the Wave 4 pSwim falls back to the fall pose -- sheets without those +-- frames still read correctly. +command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt, pSwim put pIdle into sPlayAnims["idle"] put pRun into sPlayAnims["run"] put pJump into sPlayAnims["jump"] @@ -4070,6 +4189,11 @@ command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt else put pHurt into sPlayAnims["hurt"] end if + if pSwim is empty then + put sPlayAnims["fall"] into sPlayAnims["swim"] + else + put pSwim into sPlayAnims["swim"] + end if put empty into sPlayAnimNow -- re-assert on the next tick if sPlayArt is empty then b2kPlayerResolveArt end b2kPlayerAnims @@ -4088,9 +4212,9 @@ function b2kPlayerOnGround return (sPlayGrounded is true) end b2kPlayerOnGround --- idle | run | jump | fall | duck | climb | hurt, plus "land" for exactly --- one frame on touch-down from jump/fall (dust puffs, landing sounds). --- A drop-through renders as "fall". Empty = no player. +-- idle | run | jump | fall | duck | climb | hurt | swim, plus "land" for +-- exactly one frame on touch-down from jump/fall (dust puffs, landing +-- sounds). A drop-through renders as "fall". Empty = no player. function b2kPlayerState return sPlayState end b2kPlayerState @@ -4135,6 +4259,25 @@ command b2kPlayerAddLadder pX1, pY1, pX2, pY2 put max(pY1, pY2) into sPlayLadB[sPlayLadN] end b2kPlayerAddLadder +-- Register a water ZONE (screen-px rect, any corner order). While the +-- player's centre is inside one, the controller SWIMS: gravity drops to +-- swimGravity (buoyant), the sink caps at swimMaxFall, UP/DOWN swim at +-- swimSpeed, and JUMP is a repeatable upward STROKE (swimJump) -- no +-- ground needed. Leaving the zone restores gravity. Pure polled geometry, +-- WORLD state (b2kClear wipes them), exactly like the ladder zones. +-- TIP: top the zone a little ABOVE the water's surface tiles, so the dive +-- in and the surface-out break the water where the surface art sits. +command b2kPlayerAddWater pX1, pY1, pX2, pY2 + if pX1 is not a number or pY1 is not a number \ + or pX2 is not a number or pY2 is not a number then exit b2kPlayerAddWater + if sPlayWatN is empty then put 0 into sPlayWatN + add 1 to sPlayWatN + put min(pX1, pX2) into sPlayWatL[sPlayWatN] + put max(pX1, pX2) into sPlayWatR[sPlayWatN] + put min(pY1, pY2) into sPlayWatT[sPlayWatN] + put max(pY1, pY2) into sPlayWatB[sPlayWatN] +end b2kPlayerAddWater + -- The knockback standard (spec section 9.4): control off, an away-pop -- (the sign of pFromX vs the player picks the direction; no/empty -- pFromX pops away from the facing), the hurt anim, control restored @@ -4149,6 +4292,7 @@ command b2kPlayerHurt pFromX if b2kPlayerHurtIs() then exit b2kPlayerHurt -- mercy window if sPlayControl is not true then exit b2kPlayerHurt -- a cutscene owns the body if sPlayClimb is true then b2kPlayerClimbEnd sBody[sPlayRef] + if sPlaySwim is true then b2kPlayerSwimEnd sBody[sPlayRef] if pFromX is a number then if pFromX > sPlayPX then put -1 into tDir @@ -4214,6 +4358,9 @@ command b2kPlayerForget pFull if sPlayClimb is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then b2kPlayerClimbEnd sBody[sPlayRef] end if + if sPlaySwim is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then + b2kPlayerSwimEnd sBody[sPlayRef] + end if if sPlayDropUntil is not empty and sPlayDropUntil > 0 then b2kPlayerDropRestore put empty into sPlayRef put empty into sPlayArt @@ -4236,6 +4383,8 @@ command b2kPlayerForget pFull put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -4246,6 +4395,11 @@ command b2kPlayerForget pFull put empty into sPlayLadT put empty into sPlayLadR put empty into sPlayLadB + put 0 into sPlayWatN + put empty into sPlayWatL + put empty into sPlayWatT + put empty into sPlayWatR + put empty into sPlayWatB if pFull is true then put empty into sPlayTune end b2kPlayerForget @@ -4312,6 +4466,24 @@ command b2kPlayerClimbEnd pBody put false into sPlayClimb end b2kPlayerClimbEnd +-- Internal: enter the swim -- gravity drops to swimGravity (buoyant; the +-- body's own scale is saved and restored, like the climb). Mutually +-- exclusive with the climb: the tick only ever starts one of them. +command b2kPlayerSwimStart pBody + if sPlaySwim is true then exit b2kPlayerSwimStart + put b2BodyGravityScale(pBody) into sPlaySwimGravSave + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGrav, 0.35) + put true into sPlaySwim + put false into sPlayJumping +end b2kPlayerSwimStart + +command b2kPlayerSwimEnd pBody + if sPlaySwim is not true then exit b2kPlayerSwimEnd + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGravSave, 1) + put empty into sPlaySwimGravSave + put false into sPlaySwim +end b2kPlayerSwimEnd + -- Internal: open the drop-through window -- take the reserved one-way -- bit out of the player's mask so chains (alone) stop colliding. command b2kPlayerDropStart @@ -4348,7 +4520,7 @@ end b2kPlayerDropRestore -- frame while idle -- the tick's steady-state budget is unchanged. command b2kPlayerTick local tNow, tDT, tB, tVX, tVY, tAxis, tAxisY, tTarget, tAcc, tStep - local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i + local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i, tInWater if sPlayRef is empty then exit b2kPlayerTick put sBody[sPlayRef] into tB if tB is empty then exit b2kPlayerTick @@ -4405,8 +4577,8 @@ command b2kPlayerTick put 1 into sPlayFacing end if end if - -- ladder presence: pure numeric compares on the probe's stashed - -- centre; zero zones = one compare and out + -- ladder + water presence: pure numeric compares on the probe's + -- stashed centre; zero zones of a kind = one compare and out put false into tInZone if sPlayLadN > 0 then repeat with i = 1 to sPlayLadN @@ -4417,13 +4589,26 @@ command b2kPlayerTick end if end repeat end if - if sPlayClimb is not true and tInZone is true then + put false into tInWater + if sPlayWatN > 0 then + repeat with i = 1 to sPlayWatN + if sPlayPX >= sPlayWatL[i] and sPlayPX <= sPlayWatR[i] \ + and sPlayPY >= sPlayWatT[i] and sPlayPY <= sPlayWatB[i] then + put true into tInWater + exit repeat + end if + end repeat + end if + if sPlayClimb is not true and sPlaySwim is not true and tInZone is true then -- enter: UP any time in-zone; DOWN only while AIRBORNE (a -- grounded DOWN is a duck -- or a drop-through on a chain) if tAxisY is -1 or (tAxisY is 1 and sPlayGrounded is not true) then b2kPlayerClimbStart tB end if end if + if sPlaySwim is not true and sPlayClimb is not true and tInWater is true then + b2kPlayerSwimStart tB -- submerged: buoyant gravity, stroke to rise + end if if sPlayClimb is true then if b2kActionPressed("jump") then -- JUMP exits with a normal jump. The same press edge is @@ -4458,7 +4643,33 @@ command b2kPlayerTick end if end if end if - if sPlayClimb is not true then + if sPlaySwim is true then + if tInWater is not true then + b2kPlayerSwimEnd tB -- surfaced / left the pool: gravity returns + else + -- horizontal: ease vx toward axis * swimSpeed (sluggish -- the + -- air-accel rate gives the underwater drag) + put tAxis * sPlaySwimSpd into tTarget + put sPlayAccelA * tDT into tStep + if tVX < tTarget then + put min(tTarget, tVX + tStep) into tVX + else + put max(tTarget, tVX - tStep) into tVX + end if + -- vertical: a JUMP press is a repeatable upward STROKE (no + -- ground gate -- the spec's water jump); else UP/DOWN swim at + -- swimSpeed; else the reduced gravity sinks you, capped low + if b2kActionPressed("jump") then + put 0 - sPlaySwimJump into tVY + else + if tAxisY is not 0 then + put tAxisY * sPlaySwimSpd into tVY + end if + end if + put true into tWrite + end if + end if + if sPlayClimb is not true and sPlaySwim is not true then -- horizontal: accelerate vx toward axis * moveSpeed (air = airAccel) put tAxis * sPlayMoveSpd into tTarget -- DUCK: down on the ground crouches and brakes to a stop at @@ -4513,9 +4724,18 @@ command b2kPlayerTick end if end if if sPlayJumping and tVY >= 0 then put false into sPlayJumping -- apex - if tVY > sPlayMaxFall then - put sPlayMaxFall into tVY - put true into tWrite + -- terminal velocity: the low swimMaxFall is the buoyant sink cap while + -- submerged; the normal maxFall (the character's terminal velocity) else + if sPlaySwim is true then + if tVY > sPlaySwimMaxFall then + put sPlaySwimMaxFall into tVY + put true into tWrite + end if + else + if tVY > sPlayMaxFall then + put sPlayMaxFall into tVY + put true into tWrite + end if end if -- GROUND-SNAP: grounded on FLAT ground, drifting upward, and we did -- not jump = the contact solver's push-out rebound after a hard @@ -4527,7 +4747,7 @@ command b2kPlayerTick -- must use b2kPlayerJump, which sets the jump flag (b2kPlayerHurt's -- pop rides the same flag). if sPlayGrounded and sPlayJumping is not true and sPlayClimb is not true \ - and tVY < 0 and abs(sPlayNormX) < 0.1 then + and sPlaySwim is not true and tVY < 0 and abs(sPlayNormX) < 0.1 then put 0 into tVY put true into tWrite end if @@ -4582,6 +4802,14 @@ command b2kPlayerTick end if end if end if + -- swim OWNS the state while submerged: the machine above ran the + -- grounded/airborne branch (sPlayClimb is false underwater), so override + -- to "swim" and keep the air counter clear -- surfacing must never read + -- as a long fall plus a phantom land tick. + if sPlaySwim is true and sPlayHurt is not true then + put "swim" into sPlayState + put 0 into sPlayAir + end if -- animations: only while controlling (manual poses own the art when -- control is off), and never let a vanished art control abort the -- frame -- the loop's ticks share one try block @@ -4600,7 +4828,7 @@ command b2kPlayerShowState pNow, pVX local tWant, tAnim, tAKey, tFPS, tFlip put sPlayState into tWant if pNow < sPlayHoldMS and tWant is not "jump" and tWant is not "fall" \ - and tWant is not "hurt" and tWant is not "climb" then + and tWant is not "hurt" and tWant is not "climb" and tWant is not "swim" then put empty into tAnim -- mid land-flourish: leave it playing else if tWant is "land" and sPlayAnims["land"] is empty then diff --git a/examples/box2dxt-platformer.livecodescript b/examples/box2dxt-platformer.livecodescript index 31ebcf4..5f5dc80 100644 --- a/examples/box2dxt-platformer.livecodescript +++ b/examples/box2dxt-platformer.livecodescript @@ -213,6 +213,7 @@ local gBonkMS, gPlateSpr -- the per-frame hero snapshot (read ONCE in b2kFrame; every tick -- shares it - each b2kPosition/b2kPlayerState is an FFI round-trip) local gHeroPX, gHeroPY, gHeroState, gJumpMS +local gHeroHalfH -- the hero capsule's half-height (head reach for bonks) local gDebrisNext, gDebrisUntil local gLeverSpr, gLeverX, gLeverArmed, gSawOn, gSawMov, gSawSpr local gHasKey, gKeySpr, gDoorOpen, gLockSpr, gDoorSprT, gDoorSprB @@ -674,13 +675,20 @@ command pfStartGame if gAssetsOK is true then b2kSheetScale "chars", 0.75 put 48 into tW - put 88 into tH - put -4 into tDY + -- the 128px character frame is bottom-aligned with headroom, so an + -- 88px hitbox topped out ABOVE the visible head -- heads "missed" + -- bricks by that headroom. 76 matches the art; tDY keeps the FEET on + -- the ground (sprite half = 128*0.75/2 = 48 vs hitbox half tH/2). + -- If the head still gaps under bricks, lower tH a touch; if it sinks + -- INTO them, raise it. tDY + gHeroHalfH follow automatically. + put 76 into tH + put (tH div 2) - 48 into tDY else put 36 into tW put 60 into tH put -2 into tDY end if + 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 @@ -710,6 +718,15 @@ command pfStartGame end if b2kPlayerSet "moveSpeed", kMoveSpeed b2kPlayerSet "jumpSpeed", kJumpSpeed + -- Wave 4 swim feel: heavier than the Kit defaults. swimGravity (the + -- sink between strokes) is well up so the water feels weighty; swimJump + -- (the stroke) is the ONLY thing that sets the escape height, so it is + -- trimmed back -- you must swim to the bank and stroke to climb out, not + -- bob straight up. All four are live; tune to taste in OXT. + b2kPlayerSet "swimSpeed", 165 + b2kPlayerSet "swimJump", 300 + b2kPlayerSet "swimGravity", 0.6 + b2kPlayerSet "swimMaxFall", 200 put 75 into gIntroPan -- a splash beat (~1.2 s), then control -- ===== LEVEL CAST (coins, enemies, checkpoint, the goal flag) ===== switch gLevel @@ -1220,6 +1237,29 @@ command pfMakeLava pL, pR b2kAddSensor tRef, "box" end pfMakeLava +-- Wave 4: a SWIM basin. Registers the Kit swim zone (the controller polls +-- it -- buoyant gravity, capped sink, UP/DOWN swim, SPACE strokes up) and +-- draws the water as a blue rect with a bright surface line. The caller +-- places the banks + floor slabs: the 640-tall world clamps the camera at +-- y640, so a swim pool cannot be a pit BELOW the ground -- it is a RAISED +-- basin between two banks (a hilltop pool). Coins layer on top (cast). +command pfMakeWater pL, pTop, pR, pBot + b2kPlayerAddWater pL, pTop, pR, pBot + create graphic ("pf_water" & pL) + set the style of it to "rectangle" + set the rect of it to pL, pTop, pR, pBot + set the filled of it to true + set the backgroundColor of it to "92,150,205" + set the foregroundColor of it to "60,110,170" + b2kCamAdopt the long id of graphic ("pf_water" & pL) + create graphic ("pf_surf" & pL) + set the style of it to "line" + set the points of it to pL & comma & pTop & cr & pR & comma & pTop + set the lineSize of it to 3 + set the foregroundColor of it to "165,210,245" + b2kCamAdopt the long id of graphic ("pf_surf" & pL) +end pfMakeWater + -- ===================================================================== -- Wave 1 builders: springboard, bonk row, lever, key + locked door. -- The pattern everywhere: an invisible static HOST graphic (named pf_*, @@ -1589,7 +1629,7 @@ end pfMakeWoodCrate command pfL1Scene local tX put "GREEN HILLS" into gLevelName - pfBounds 7552 + pfBounds 8640 -- extended for the Wave 4 HILLTOP POOL past the crusher alley pfSlab "pf_ground1", 0, 576, 2560, 640 pfSlab "pf_ground2", 2752, 576, 3968, 640 pfSlab "pf_ground3", 4288, 576, 7552, 640 -- the far meadow, the gauntlet, the homeward run + the GREEN CRUSHER alley @@ -1782,6 +1822,38 @@ command pfL1Scene pfMakeBonk "brick", 512 pfMakeBonk "box", 576 end if + -- ===== the Wave 4 HILLTOP POOL (past the crusher alley, before the + -- flag): a RAISED swim basin between two earth banks. Hop up onto the + -- left bank (a 96px step), DIVE in, swim DOWN for the coins, then stroke + -- up + hold-right to HOP out the far bank and on to the flag. Banks are + -- visible earth slabs; the floor hides under the water rect. ===== + pfSlab "pf_pondL", 7552, 480, 7760, 640 + pfSlab "pf_pondR", 8160, 480, 8368, 640 + pfSlab "pf_pondFloor", 7760, 616, 8160, 648 + pfSlab "pf_ground4", 8368, 576, 8640, 640 + set the backgroundColor of graphic "pf_pondL" to "150,120,82" + set the backgroundColor of graphic "pf_pondR" to "150,120,82" + set the visible of graphic "pf_pondL" to true + set the visible of graphic "pf_pondR" to true + b2kCamAdopt the long id of graphic "pf_pondL" + b2kCamAdopt the long id of graphic "pf_pondR" + if gAssetsOK is true and b2kSheetHasFrame("tiles", "terrain_grass_block_top") then + -- grass the bank tops + the far-bank flag approach so the pool reads + -- as a basin set into the hill (the floor stays under the water) + repeat with tX = 7552 to 7696 step 64 + pfTile "terrain_grass_block_top", tX, 480 + end repeat + repeat with tX = 8160 to 8304 step 64 + pfTile "terrain_grass_block_top", tX, 480 + end repeat + repeat with tX = 8368 to 8576 step 64 + pfTile "terrain_grass_block_top", tX, 576 + end repeat + else + set the visible of graphic "pf_ground4" to true + b2kCamAdopt the long id of graphic "pf_ground4" + end if + pfMakeWater 7760, 480, 8160, 640 end pfL1Scene command pfL1Cast @@ -1873,7 +1945,12 @@ command pfL1Cast -- then a quick retry at the brink, never a replay of the whole level pfMakeCheckpoint 3856 if gToysOK is true then pfMakeDebrisPool -- this level has bricks - pfMakeGoal 7540, 544 + -- Wave 4: the hilltop pool's coins are UNDERWATER, so the gate can only + -- be cleared by diving and swimming for them (then stroke out + on) + pfMakeCoin 7848, 556 + pfMakeCoin 7960, 588 + pfMakeCoin 8072, 556 + pfMakeGoal 8520, 544 end pfL1Cast -- ===================================================================== @@ -3052,7 +3129,7 @@ command pfTickBonks if gBonkN is 0 or gBonkN is empty then exit pfTickBonks if gHurtLock is true then exit pfTickBonks put gHeroPX into tHX - put gHeroPY - 44 into tHead + put gHeroPY - gHeroHalfH into tHead -- the real capsule top (head) if tHead < 440 or tHead > 468 then exit pfTickBonks if the milliseconds < gBonkMS then exit pfTickBonks if gHeroState is not "jump" and (gJumpMS is 0 or gJumpMS is empty or the milliseconds - gJumpMS > 160) then exit pfTickBonks @@ -3782,6 +3859,16 @@ on rawKeyDown pKeyCode b2kSoundMute (not b2kSoundMuted()) exit rawKeyDown end if + -- >>> DEBUG WARP (Wave 4 swim testing) -- delete this block before merge. + -- "0" drops the hero onto the L1 hilltop pool's left bank so the swim + -- is reachable without replaying the level. No-op on other levels. + if pKeyCode is 48 and gHero is not empty and gLevel is 1 then + b2kMoveTo gHero, 7620, 400 + b2kSetVelocity gHero, 0, 0 + b2kCamGoto 7620, 400 + exit rawKeyDown + end if + -- <<< END DEBUG WARP pass rawKeyDown end rawKeyDown @@ -3861,16 +3948,18 @@ end rawKeyDown -- b2kSpriteFPS spr,fps · b2kSpriteOnFinish spr,msg · -- b2kSpriteBind spr,bodyCtrl[,dx,dy] · b2kSpriteRemove spr -- PLAYER b2kPlayerMake x,y,w,h[,sheet] · b2kPlayerAttach ctrl · --- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt] · +-- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt,swim] · -- b2kPlayerSet key,value · b2kPlayerGet(key) · b2kPlayerOnGround() · --- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt) · +-- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt|swim) · -- b2kPlayerFacing() · b2kPlayerJump [speed] · b2kPlayerControl flag · --- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerHurt [fromX] · +-- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerAddWater x1,y1,x2,y2 · +-- b2kPlayerHurt [fromX] · -- b2kPlayerHurtIs() (true through knockback + mercy window) · -- b2kPlayer() · b2kPlayerSprite() · b2kPlayerRemove -- (drives axes "moveX"/"moveY" + action "jump"; rebind to remap. -- DOWN ducks · DOWN+JUMP on a one-way chain drops through · --- UP/DOWN in a ladder zone climbs · JUMP exits a climb) +-- UP/DOWN in a ladder zone climbs · JUMP exits a climb · +-- in a water zone UP/DOWN swim, JUMP strokes (repeatable)) -- CAMERA b2kCamOn [rect] · b2kCamOff · b2kCamFollow ctrl[,lerp] · b2kCamUnfollow · -- b2kCamDeadzone w,h · b2kCamBounds x1,y1,x2,y2 · b2kCamGoto x,y · -- b2kCamPos() · b2kCamShake amp,ms · b2kCamMouseX/Y() · b2kCamGroup() · @@ -4018,6 +4107,12 @@ local sPlayClimb -- true while in the climb state (gravity scale parked at 0 local sPlayGravSave -- the body's gravity scale to restore on climb exit local sPlayLadN -- registered ladder zones (flat numeric arrays: the local sPlayLadL, sPlayLadT, sPlayLadR, sPlayLadB -- ...in-zone test is pure compares) +-- Wave 4: water zones + the swim state (a buoyant parallel to the climb) +local sPlaySwim -- true while submerged in a water zone (gravity scaled down) +local sPlaySwimGravSave -- the body's gravity scale to restore on swim exit +local sPlaySwimSpd, sPlaySwimJump, sPlaySwimGrav, sPlaySwimMaxFall -- swim tune caches +local sPlayWatN -- registered water zones (flat numeric arrays, like ladders) +local sPlayWatL, sPlayWatT, sPlayWatR, sPlayWatB local sPlayHurt -- true while knockback owns the controller local sPlayHurtEnd -- sim-clock when hurtMs has elapsed local sPlayHurtHalf -- sim-clock when half of hurtMs has elapsed (landings @@ -6938,6 +7033,8 @@ command b2kPlayerAttach pCtrl put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -6969,10 +7066,11 @@ end b2kPlayerResolveArt -- Tuning knobs (pixels, px/s, ms, degrees). Keys: moveSpeed, accel, -- airAccel, jumpSpeed, jumpCut, coyoteMs, bufferMs, maxFall, maxSlopeDeg, --- dropMs (drop-through window), climbSpeed (ladder px/s), hurtPopX/ --- hurtPopY (knockback launch px/s), hurtMs (control-off span), invulnMs --- (post-hurt mercy). Settable any time, before or after the player --- exists; unknown keys are stored verbatim for your own use. +-- dropMs (drop-through window), climbSpeed (ladder px/s), swimSpeed/ +-- swimJump (water px/s + stroke), swimGravity/swimMaxFall (buoyancy), +-- hurtPopX/hurtPopY (knockback launch px/s), hurtMs (control-off span), +-- invulnMs (post-hurt mercy). Settable any time, before or after the +-- player exists; unknown keys are stored verbatim for your own use. command b2kPlayerSet pKey, pValue put pValue into sPlayTune[toLower(pKey)] b2kPlayerTuneCache @@ -6991,6 +7089,10 @@ command b2kPlayerTuneCache put b2kPlayerGet("maxFall") into sPlayMaxFall put b2kPlayerGet("dropMs") into sPlayDropMS put b2kPlayerGet("climbSpeed") into sPlayClimbSpd + put b2kPlayerGet("swimSpeed") into sPlaySwimSpd + put b2kPlayerGet("swimJump") into sPlaySwimJump + put b2kPlayerGet("swimGravity") into sPlaySwimGrav + put b2kPlayerGet("swimMaxFall") into sPlaySwimMaxFall put b2kPlayerGet("hurtPopX") into sPlayHurtPopX put b2kPlayerGet("hurtPopY") into sPlayHurtPopY put b2kPlayerGet("hurtMs") into sPlayHurtMS @@ -7037,6 +7139,14 @@ function b2kPlayerDefault pKey return 260 case "climbspeed" return 160 + case "swimspeed" + return 150 + case "swimjump" + return 300 + case "swimgravity" + return 0.35 + case "swimmaxfall" + return 150 case "hurtpopx" return 220 case "hurtpopy" @@ -7055,9 +7165,10 @@ end b2kPlayerDefault -- idle/run resume. NOTE: a finishing land animation dispatches the art's -- b2kSpriteOnFinish message like any non-looping animation would. -- Wave 2 slots (all optional, so old five-argument calls keep working): --- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose -- --- sheets without those frames still read correctly. -command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt +-- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose; +-- the Wave 4 pSwim falls back to the fall pose -- sheets without those +-- frames still read correctly. +command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt, pSwim put pIdle into sPlayAnims["idle"] put pRun into sPlayAnims["run"] put pJump into sPlayAnims["jump"] @@ -7082,6 +7193,11 @@ command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt else put pHurt into sPlayAnims["hurt"] end if + if pSwim is empty then + put sPlayAnims["fall"] into sPlayAnims["swim"] + else + put pSwim into sPlayAnims["swim"] + end if put empty into sPlayAnimNow -- re-assert on the next tick if sPlayArt is empty then b2kPlayerResolveArt end b2kPlayerAnims @@ -7100,9 +7216,9 @@ function b2kPlayerOnGround return (sPlayGrounded is true) end b2kPlayerOnGround --- idle | run | jump | fall | duck | climb | hurt, plus "land" for exactly --- one frame on touch-down from jump/fall (dust puffs, landing sounds). --- A drop-through renders as "fall". Empty = no player. +-- idle | run | jump | fall | duck | climb | hurt | swim, plus "land" for +-- exactly one frame on touch-down from jump/fall (dust puffs, landing +-- sounds). A drop-through renders as "fall". Empty = no player. function b2kPlayerState return sPlayState end b2kPlayerState @@ -7147,6 +7263,25 @@ command b2kPlayerAddLadder pX1, pY1, pX2, pY2 put max(pY1, pY2) into sPlayLadB[sPlayLadN] end b2kPlayerAddLadder +-- Register a water ZONE (screen-px rect, any corner order). While the +-- player's centre is inside one, the controller SWIMS: gravity drops to +-- swimGravity (buoyant), the sink caps at swimMaxFall, UP/DOWN swim at +-- swimSpeed, and JUMP is a repeatable upward STROKE (swimJump) -- no +-- ground needed. Leaving the zone restores gravity. Pure polled geometry, +-- WORLD state (b2kClear wipes them), exactly like the ladder zones. +-- TIP: top the zone a little ABOVE the water's surface tiles, so the dive +-- in and the surface-out break the water where the surface art sits. +command b2kPlayerAddWater pX1, pY1, pX2, pY2 + if pX1 is not a number or pY1 is not a number \ + or pX2 is not a number or pY2 is not a number then exit b2kPlayerAddWater + if sPlayWatN is empty then put 0 into sPlayWatN + add 1 to sPlayWatN + put min(pX1, pX2) into sPlayWatL[sPlayWatN] + put max(pX1, pX2) into sPlayWatR[sPlayWatN] + put min(pY1, pY2) into sPlayWatT[sPlayWatN] + put max(pY1, pY2) into sPlayWatB[sPlayWatN] +end b2kPlayerAddWater + -- The knockback standard (spec section 9.4): control off, an away-pop -- (the sign of pFromX vs the player picks the direction; no/empty -- pFromX pops away from the facing), the hurt anim, control restored @@ -7161,6 +7296,7 @@ command b2kPlayerHurt pFromX if b2kPlayerHurtIs() then exit b2kPlayerHurt -- mercy window if sPlayControl is not true then exit b2kPlayerHurt -- a cutscene owns the body if sPlayClimb is true then b2kPlayerClimbEnd sBody[sPlayRef] + if sPlaySwim is true then b2kPlayerSwimEnd sBody[sPlayRef] if pFromX is a number then if pFromX > sPlayPX then put -1 into tDir @@ -7226,6 +7362,9 @@ command b2kPlayerForget pFull if sPlayClimb is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then b2kPlayerClimbEnd sBody[sPlayRef] end if + if sPlaySwim is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then + b2kPlayerSwimEnd sBody[sPlayRef] + end if if sPlayDropUntil is not empty and sPlayDropUntil > 0 then b2kPlayerDropRestore put empty into sPlayRef put empty into sPlayArt @@ -7248,6 +7387,8 @@ command b2kPlayerForget pFull put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -7258,6 +7399,11 @@ command b2kPlayerForget pFull put empty into sPlayLadT put empty into sPlayLadR put empty into sPlayLadB + put 0 into sPlayWatN + put empty into sPlayWatL + put empty into sPlayWatT + put empty into sPlayWatR + put empty into sPlayWatB if pFull is true then put empty into sPlayTune end b2kPlayerForget @@ -7324,6 +7470,24 @@ command b2kPlayerClimbEnd pBody put false into sPlayClimb end b2kPlayerClimbEnd +-- Internal: enter the swim -- gravity drops to swimGravity (buoyant; the +-- body's own scale is saved and restored, like the climb). Mutually +-- exclusive with the climb: the tick only ever starts one of them. +command b2kPlayerSwimStart pBody + if sPlaySwim is true then exit b2kPlayerSwimStart + put b2BodyGravityScale(pBody) into sPlaySwimGravSave + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGrav, 0.35) + put true into sPlaySwim + put false into sPlayJumping +end b2kPlayerSwimStart + +command b2kPlayerSwimEnd pBody + if sPlaySwim is not true then exit b2kPlayerSwimEnd + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGravSave, 1) + put empty into sPlaySwimGravSave + put false into sPlaySwim +end b2kPlayerSwimEnd + -- Internal: open the drop-through window -- take the reserved one-way -- bit out of the player's mask so chains (alone) stop colliding. command b2kPlayerDropStart @@ -7360,7 +7524,7 @@ end b2kPlayerDropRestore -- frame while idle -- the tick's steady-state budget is unchanged. command b2kPlayerTick local tNow, tDT, tB, tVX, tVY, tAxis, tAxisY, tTarget, tAcc, tStep - local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i + local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i, tInWater if sPlayRef is empty then exit b2kPlayerTick put sBody[sPlayRef] into tB if tB is empty then exit b2kPlayerTick @@ -7417,8 +7581,8 @@ command b2kPlayerTick put 1 into sPlayFacing end if end if - -- ladder presence: pure numeric compares on the probe's stashed - -- centre; zero zones = one compare and out + -- ladder + water presence: pure numeric compares on the probe's + -- stashed centre; zero zones of a kind = one compare and out put false into tInZone if sPlayLadN > 0 then repeat with i = 1 to sPlayLadN @@ -7429,13 +7593,26 @@ command b2kPlayerTick end if end repeat end if - if sPlayClimb is not true and tInZone is true then + put false into tInWater + if sPlayWatN > 0 then + repeat with i = 1 to sPlayWatN + if sPlayPX >= sPlayWatL[i] and sPlayPX <= sPlayWatR[i] \ + and sPlayPY >= sPlayWatT[i] and sPlayPY <= sPlayWatB[i] then + put true into tInWater + exit repeat + end if + end repeat + end if + if sPlayClimb is not true and sPlaySwim is not true and tInZone is true then -- enter: UP any time in-zone; DOWN only while AIRBORNE (a -- grounded DOWN is a duck -- or a drop-through on a chain) if tAxisY is -1 or (tAxisY is 1 and sPlayGrounded is not true) then b2kPlayerClimbStart tB end if end if + if sPlaySwim is not true and sPlayClimb is not true and tInWater is true then + b2kPlayerSwimStart tB -- submerged: buoyant gravity, stroke to rise + end if if sPlayClimb is true then if b2kActionPressed("jump") then -- JUMP exits with a normal jump. The same press edge is @@ -7470,7 +7647,33 @@ command b2kPlayerTick end if end if end if - if sPlayClimb is not true then + if sPlaySwim is true then + if tInWater is not true then + b2kPlayerSwimEnd tB -- surfaced / left the pool: gravity returns + else + -- horizontal: ease vx toward axis * swimSpeed (sluggish -- the + -- air-accel rate gives the underwater drag) + put tAxis * sPlaySwimSpd into tTarget + put sPlayAccelA * tDT into tStep + if tVX < tTarget then + put min(tTarget, tVX + tStep) into tVX + else + put max(tTarget, tVX - tStep) into tVX + end if + -- vertical: a JUMP press is a repeatable upward STROKE (no + -- ground gate -- the spec's water jump); else UP/DOWN swim at + -- swimSpeed; else the reduced gravity sinks you, capped low + if b2kActionPressed("jump") then + put 0 - sPlaySwimJump into tVY + else + if tAxisY is not 0 then + put tAxisY * sPlaySwimSpd into tVY + end if + end if + put true into tWrite + end if + end if + if sPlayClimb is not true and sPlaySwim is not true then -- horizontal: accelerate vx toward axis * moveSpeed (air = airAccel) put tAxis * sPlayMoveSpd into tTarget -- DUCK: down on the ground crouches and brakes to a stop at @@ -7525,9 +7728,18 @@ command b2kPlayerTick end if end if if sPlayJumping and tVY >= 0 then put false into sPlayJumping -- apex - if tVY > sPlayMaxFall then - put sPlayMaxFall into tVY - put true into tWrite + -- terminal velocity: the low swimMaxFall is the buoyant sink cap while + -- submerged; the normal maxFall (the character's terminal velocity) else + if sPlaySwim is true then + if tVY > sPlaySwimMaxFall then + put sPlaySwimMaxFall into tVY + put true into tWrite + end if + else + if tVY > sPlayMaxFall then + put sPlayMaxFall into tVY + put true into tWrite + end if end if -- GROUND-SNAP: grounded on FLAT ground, drifting upward, and we did -- not jump = the contact solver's push-out rebound after a hard @@ -7539,7 +7751,7 @@ command b2kPlayerTick -- must use b2kPlayerJump, which sets the jump flag (b2kPlayerHurt's -- pop rides the same flag). if sPlayGrounded and sPlayJumping is not true and sPlayClimb is not true \ - and tVY < 0 and abs(sPlayNormX) < 0.1 then + and sPlaySwim is not true and tVY < 0 and abs(sPlayNormX) < 0.1 then put 0 into tVY put true into tWrite end if @@ -7594,6 +7806,14 @@ command b2kPlayerTick end if end if end if + -- swim OWNS the state while submerged: the machine above ran the + -- grounded/airborne branch (sPlayClimb is false underwater), so override + -- to "swim" and keep the air counter clear -- surfacing must never read + -- as a long fall plus a phantom land tick. + if sPlaySwim is true and sPlayHurt is not true then + put "swim" into sPlayState + put 0 into sPlayAir + end if -- animations: only while controlling (manual poses own the art when -- control is off), and never let a vanished art control abort the -- frame -- the loop's ticks share one try block @@ -7612,7 +7832,7 @@ command b2kPlayerShowState pNow, pVX local tWant, tAnim, tAKey, tFPS, tFlip put sPlayState into tWant if pNow < sPlayHoldMS and tWant is not "jump" and tWant is not "fall" \ - and tWant is not "hurt" and tWant is not "climb" then + and tWant is not "hurt" and tWant is not "climb" and tWant is not "swim" then put empty into tAnim -- mid land-flourish: leave it playing else if tWant is "land" and sPlayAnims["land"] is empty then diff --git a/examples/box2dxt-selftest.livecodescript b/examples/box2dxt-selftest.livecodescript index c6b9650..8a53957 100644 --- a/examples/box2dxt-selftest.livecodescript +++ b/examples/box2dxt-selftest.livecodescript @@ -28,6 +28,10 @@ -- climb (enter/hang/jump-exit, gravity restored), duck (brakes to a -- stop, down+jump on solid ground does NOT launch), and the hurt -- knockback standard (away-pop, control window, mercy no-op) +-- * Wave 4 liquids: SWIM in a water zone (buoyant sink capped low, a +-- repeatable JUMP stroke, UP/DOWN swim, gravity restored on exit); +-- swimming while RESTING on a submerged floor; and b2kClear wiping the +-- zone on a level rebuild (no one is born swimming where a pool was) -- * input axes/edges, sprites (sheet/anim/flip/remove), tones, -- camera adopt/goto/off, teardown hygiene -- @@ -43,7 +47,7 @@ local gRep, gPass, gFail, gFellCount, gRunning local gMsgEnters, gMsgContacts -- message-path counters (vs polling) constant kStUIVersion = "1" -constant kStHarnessV = "10" -- bump on EVERY harness change: the report +constant kStHarnessV = "12" -- bump on EVERY harness change: the report -- header prints it, so a stale paste is -- visible at a glance @@ -128,6 +132,9 @@ command stRunAll stRun "stTestLadderClimb" stRun "stTestDuck" stRun "stTestHurtKnockback" + stRun "stTestSwim" + stRun "stTestSwimGrounded" + stRun "stTestSwimClear" stRun "stTestTeardown" try b2kInputInjectOff @@ -991,6 +998,118 @@ command stTestLadderClimb (b2kPlayerOnGround() is true and stY(tRef) > 430) end stTestLadderClimb +-- Wave 4: SWIM. A deep water zone in open air; the player dives in, the +-- buoyant sink is capped far below the air terminal, a JUMP press strokes +-- up (repeatable, no ground), UP swims, and leaving the zone restores +-- gravity. All values printed, so a scale/arithmetic miss self-reports. +command stTestSwim + local tRef, i, tY1, tY2, tVwet + stNewWorld "player: swim a water zone (buoyant sink, stroke, surface-out)" + stSlab "st_floor", 50, 760, 800, 820 -- a deep catch floor below the pool + b2kPlayerAddWater 200, 260, 400, 740 -- a deep pool, surface at y260 + b2kPlayerMake 300, 150, 32, 48 -- spawn above the surface, in air + put the result into tRef + -- sink into the water; the swim state engages once the centre submerges + repeat with i = 1 to 90 + b2kStepOnce + if b2kPlayerState() is "swim" then exit repeat + end repeat + stAssert "diving in enters the swim state (got " & b2kPlayerState() & ")", \ + (b2kPlayerState() is "swim") + -- swim DOWN to a safe depth (headroom for the rise tests below) + b2kInputInject "down" + stStep 32 + b2kInputInject "" + -- buoyant: with no input the sink is capped LOW (vs the 900 air terminal) + stStep 16 + put stVY(tRef) into tVwet + stAssert "buoyant: the neutral sink is capped low (vy " & round(tVwet) & ")", \ + (tVwet > 0 and tVwet < 220) + -- a STROKE: a jump press is a repeatable upward burst, no ground needed + b2kInputInject "space" + stStep 2 + b2kInputInject "" + stAssert "a stroke (jump) pushes upward (vy " & round(stVY(tRef)) & ")", \ + (stVY(tRef) < -120) + -- hold UP: swims steadily up while submerged + put stY(tRef) into tY1 + b2kInputInject "up" + stStep 16 + put stY(tRef) into tY2 + b2kInputInject "" + stAssert "holding UP swims up (" & round(tY1 - tY2) & "px in 16 steps)", \ + (tY1 - tY2 > 20) + stAssert "still swimming while submerged (got " & b2kPlayerState() & ")", \ + (b2kPlayerState() is "swim") + -- out of the water (teleport to dry air): swim ends, gravity returns + b2kMoveTo tRef, 600, 200 + b2kSetVelocity tRef, 0, 0 + stStep 36 + stAssert "leaving the water leaves the swim state (got " & b2kPlayerState() & ")", \ + (b2kPlayerState() is not "swim") + stAssert "gravity restored out of the water: free-fall beats the swim cap (vy " \ + & round(stVY(tRef)) & ")", (stVY(tRef) > 180) +end stTestSwim + +-- Wave 4: SWIM while RESTING on the pool floor. The platformer's hilltop +-- pool has a solid floor INSIDE the water, so the swimmer sinks and stands +-- on it -- still submerged (centre in the zone), so still in the swim +-- state, and a stroke must lift off WITHOUT the grounded-jump gate. +command stTestSwimGrounded + local tRef, i, tY1 + stNewWorld "player: swims while RESTING on a submerged floor (the pool floor)" + stSlab "st_pondfloor", 200, 560, 600, 620 -- the floor sits INSIDE the water + b2kPlayerAddWater 200, 300, 600, 600 -- water covers the floor top (y560) + b2kPlayerMake 400, 200, 32, 48 + put the result into tRef + -- buoyant sink all the way to the floor (it never floats up unaided) + repeat with i = 1 to 160 + b2kStepOnce + if b2kPlayerOnGround() is true then exit repeat + end repeat + stAssert "sinks and rests on the submerged floor (grounded, y " & round(stY(tRef)) & ")", \ + (b2kPlayerOnGround() is true) + stAssert "...yet still SWIMMING (grounded but submerged, got " & b2kPlayerState() & ")", \ + (b2kPlayerState() is "swim") + -- a stroke lifts it off the floor: underwater there is NO grounded gate + put stY(tRef) into tY1 + b2kInputInject "space" + stStep 3 + b2kInputInject "" + stAssert "a stroke lifts it off the floor (rose " & round(tY1 - stY(tRef)) & "px)", \ + (tY1 - stY(tRef) > 6) +end stTestSwimGrounded + +-- Wave 4: the LEVEL-REBUILD path. A game calls b2kClear between levels; +-- it must wipe the water zone (via b2kPlayerForget), or the NEXT level's +-- player would be born swimming in mid-air where the old pool used to be. +-- This is the regression the play-tested games needed and the harness did +-- not have: zones are WORLD state and must not survive a rebuild. +command stTestSwimClear + local tRef, i + stNewWorld "player: b2kClear wipes the water zone (the level-rebuild path)" + stSlab "st_floor", 50, 760, 800, 820 + b2kPlayerAddWater 200, 200, 600, 740 + b2kPlayerMake 400, 120, 32, 48 + put the result into tRef + repeat with i = 1 to 90 + b2kStepOnce + if b2kPlayerState() is "swim" then exit repeat + end repeat + stAssert "world 1: the player swims in its water zone (got " & b2kPlayerState() & ")", \ + (b2kPlayerState() is "swim") + -- the rebuild: b2kClear (what mgBuild/pfStartGame call) must clear it + b2kClear + stSlab "st_floor2", 50, 500, 800, 560 + b2kPlayerMake 400, 200, 32, 48 -- spawned RIGHT where the old pool was + put the result into tRef + stStep 40 + stAssert "after b2kClear: the new player does NOT swim (no stale zone, got " \ + & b2kPlayerState() & ")", (b2kPlayerState() is not "swim") + stAssert "after b2kClear: gravity is normal -- it falls and lands (y " & round(stY(tRef)) & ")", \ + (b2kPlayerOnGround() is true and stY(tRef) > 400) +end stTestSwimClear + command stTestDuck local tRef stNewWorld "player: duck brakes to a stop; down+jump on solid = no launch" @@ -1145,16 +1264,18 @@ end stTestHurtKnockback -- b2kSpriteFPS spr,fps · b2kSpriteOnFinish spr,msg · -- b2kSpriteBind spr,bodyCtrl[,dx,dy] · b2kSpriteRemove spr -- PLAYER b2kPlayerMake x,y,w,h[,sheet] · b2kPlayerAttach ctrl · --- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt] · +-- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt,swim] · -- b2kPlayerSet key,value · b2kPlayerGet(key) · b2kPlayerOnGround() · --- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt) · +-- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt|swim) · -- b2kPlayerFacing() · b2kPlayerJump [speed] · b2kPlayerControl flag · --- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerHurt [fromX] · +-- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerAddWater x1,y1,x2,y2 · +-- b2kPlayerHurt [fromX] · -- b2kPlayerHurtIs() (true through knockback + mercy window) · -- b2kPlayer() · b2kPlayerSprite() · b2kPlayerRemove -- (drives axes "moveX"/"moveY" + action "jump"; rebind to remap. -- DOWN ducks · DOWN+JUMP on a one-way chain drops through · --- UP/DOWN in a ladder zone climbs · JUMP exits a climb) +-- UP/DOWN in a ladder zone climbs · JUMP exits a climb · +-- in a water zone UP/DOWN swim, JUMP strokes (repeatable)) -- CAMERA b2kCamOn [rect] · b2kCamOff · b2kCamFollow ctrl[,lerp] · b2kCamUnfollow · -- b2kCamDeadzone w,h · b2kCamBounds x1,y1,x2,y2 · b2kCamGoto x,y · -- b2kCamPos() · b2kCamShake amp,ms · b2kCamMouseX/Y() · b2kCamGroup() · @@ -1302,6 +1423,12 @@ local sPlayClimb -- true while in the climb state (gravity scale parked at 0 local sPlayGravSave -- the body's gravity scale to restore on climb exit local sPlayLadN -- registered ladder zones (flat numeric arrays: the local sPlayLadL, sPlayLadT, sPlayLadR, sPlayLadB -- ...in-zone test is pure compares) +-- Wave 4: water zones + the swim state (a buoyant parallel to the climb) +local sPlaySwim -- true while submerged in a water zone (gravity scaled down) +local sPlaySwimGravSave -- the body's gravity scale to restore on swim exit +local sPlaySwimSpd, sPlaySwimJump, sPlaySwimGrav, sPlaySwimMaxFall -- swim tune caches +local sPlayWatN -- registered water zones (flat numeric arrays, like ladders) +local sPlayWatL, sPlayWatT, sPlayWatR, sPlayWatB local sPlayHurt -- true while knockback owns the controller local sPlayHurtEnd -- sim-clock when hurtMs has elapsed local sPlayHurtHalf -- sim-clock when half of hurtMs has elapsed (landings @@ -4222,6 +4349,8 @@ command b2kPlayerAttach pCtrl put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -4253,10 +4382,11 @@ end b2kPlayerResolveArt -- Tuning knobs (pixels, px/s, ms, degrees). Keys: moveSpeed, accel, -- airAccel, jumpSpeed, jumpCut, coyoteMs, bufferMs, maxFall, maxSlopeDeg, --- dropMs (drop-through window), climbSpeed (ladder px/s), hurtPopX/ --- hurtPopY (knockback launch px/s), hurtMs (control-off span), invulnMs --- (post-hurt mercy). Settable any time, before or after the player --- exists; unknown keys are stored verbatim for your own use. +-- dropMs (drop-through window), climbSpeed (ladder px/s), swimSpeed/ +-- swimJump (water px/s + stroke), swimGravity/swimMaxFall (buoyancy), +-- hurtPopX/hurtPopY (knockback launch px/s), hurtMs (control-off span), +-- invulnMs (post-hurt mercy). Settable any time, before or after the +-- player exists; unknown keys are stored verbatim for your own use. command b2kPlayerSet pKey, pValue put pValue into sPlayTune[toLower(pKey)] b2kPlayerTuneCache @@ -4275,6 +4405,10 @@ command b2kPlayerTuneCache put b2kPlayerGet("maxFall") into sPlayMaxFall put b2kPlayerGet("dropMs") into sPlayDropMS put b2kPlayerGet("climbSpeed") into sPlayClimbSpd + put b2kPlayerGet("swimSpeed") into sPlaySwimSpd + put b2kPlayerGet("swimJump") into sPlaySwimJump + put b2kPlayerGet("swimGravity") into sPlaySwimGrav + put b2kPlayerGet("swimMaxFall") into sPlaySwimMaxFall put b2kPlayerGet("hurtPopX") into sPlayHurtPopX put b2kPlayerGet("hurtPopY") into sPlayHurtPopY put b2kPlayerGet("hurtMs") into sPlayHurtMS @@ -4321,6 +4455,14 @@ function b2kPlayerDefault pKey return 260 case "climbspeed" return 160 + case "swimspeed" + return 150 + case "swimjump" + return 300 + case "swimgravity" + return 0.35 + case "swimmaxfall" + return 150 case "hurtpopx" return 220 case "hurtpopy" @@ -4339,9 +4481,10 @@ end b2kPlayerDefault -- idle/run resume. NOTE: a finishing land animation dispatches the art's -- b2kSpriteOnFinish message like any non-looping animation would. -- Wave 2 slots (all optional, so old five-argument calls keep working): --- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose -- --- sheets without those frames still read correctly. -command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt +-- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose; +-- the Wave 4 pSwim falls back to the fall pose -- sheets without those +-- frames still read correctly. +command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt, pSwim put pIdle into sPlayAnims["idle"] put pRun into sPlayAnims["run"] put pJump into sPlayAnims["jump"] @@ -4366,6 +4509,11 @@ command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt else put pHurt into sPlayAnims["hurt"] end if + if pSwim is empty then + put sPlayAnims["fall"] into sPlayAnims["swim"] + else + put pSwim into sPlayAnims["swim"] + end if put empty into sPlayAnimNow -- re-assert on the next tick if sPlayArt is empty then b2kPlayerResolveArt end b2kPlayerAnims @@ -4384,9 +4532,9 @@ function b2kPlayerOnGround return (sPlayGrounded is true) end b2kPlayerOnGround --- idle | run | jump | fall | duck | climb | hurt, plus "land" for exactly --- one frame on touch-down from jump/fall (dust puffs, landing sounds). --- A drop-through renders as "fall". Empty = no player. +-- idle | run | jump | fall | duck | climb | hurt | swim, plus "land" for +-- exactly one frame on touch-down from jump/fall (dust puffs, landing +-- sounds). A drop-through renders as "fall". Empty = no player. function b2kPlayerState return sPlayState end b2kPlayerState @@ -4431,6 +4579,25 @@ command b2kPlayerAddLadder pX1, pY1, pX2, pY2 put max(pY1, pY2) into sPlayLadB[sPlayLadN] end b2kPlayerAddLadder +-- Register a water ZONE (screen-px rect, any corner order). While the +-- player's centre is inside one, the controller SWIMS: gravity drops to +-- swimGravity (buoyant), the sink caps at swimMaxFall, UP/DOWN swim at +-- swimSpeed, and JUMP is a repeatable upward STROKE (swimJump) -- no +-- ground needed. Leaving the zone restores gravity. Pure polled geometry, +-- WORLD state (b2kClear wipes them), exactly like the ladder zones. +-- TIP: top the zone a little ABOVE the water's surface tiles, so the dive +-- in and the surface-out break the water where the surface art sits. +command b2kPlayerAddWater pX1, pY1, pX2, pY2 + if pX1 is not a number or pY1 is not a number \ + or pX2 is not a number or pY2 is not a number then exit b2kPlayerAddWater + if sPlayWatN is empty then put 0 into sPlayWatN + add 1 to sPlayWatN + put min(pX1, pX2) into sPlayWatL[sPlayWatN] + put max(pX1, pX2) into sPlayWatR[sPlayWatN] + put min(pY1, pY2) into sPlayWatT[sPlayWatN] + put max(pY1, pY2) into sPlayWatB[sPlayWatN] +end b2kPlayerAddWater + -- The knockback standard (spec section 9.4): control off, an away-pop -- (the sign of pFromX vs the player picks the direction; no/empty -- pFromX pops away from the facing), the hurt anim, control restored @@ -4445,6 +4612,7 @@ command b2kPlayerHurt pFromX if b2kPlayerHurtIs() then exit b2kPlayerHurt -- mercy window if sPlayControl is not true then exit b2kPlayerHurt -- a cutscene owns the body if sPlayClimb is true then b2kPlayerClimbEnd sBody[sPlayRef] + if sPlaySwim is true then b2kPlayerSwimEnd sBody[sPlayRef] if pFromX is a number then if pFromX > sPlayPX then put -1 into tDir @@ -4510,6 +4678,9 @@ command b2kPlayerForget pFull if sPlayClimb is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then b2kPlayerClimbEnd sBody[sPlayRef] end if + if sPlaySwim is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then + b2kPlayerSwimEnd sBody[sPlayRef] + end if if sPlayDropUntil is not empty and sPlayDropUntil > 0 then b2kPlayerDropRestore put empty into sPlayRef put empty into sPlayArt @@ -4532,6 +4703,8 @@ command b2kPlayerForget pFull put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -4542,6 +4715,11 @@ command b2kPlayerForget pFull put empty into sPlayLadT put empty into sPlayLadR put empty into sPlayLadB + put 0 into sPlayWatN + put empty into sPlayWatL + put empty into sPlayWatT + put empty into sPlayWatR + put empty into sPlayWatB if pFull is true then put empty into sPlayTune end b2kPlayerForget @@ -4608,6 +4786,24 @@ command b2kPlayerClimbEnd pBody put false into sPlayClimb end b2kPlayerClimbEnd +-- Internal: enter the swim -- gravity drops to swimGravity (buoyant; the +-- body's own scale is saved and restored, like the climb). Mutually +-- exclusive with the climb: the tick only ever starts one of them. +command b2kPlayerSwimStart pBody + if sPlaySwim is true then exit b2kPlayerSwimStart + put b2BodyGravityScale(pBody) into sPlaySwimGravSave + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGrav, 0.35) + put true into sPlaySwim + put false into sPlayJumping +end b2kPlayerSwimStart + +command b2kPlayerSwimEnd pBody + if sPlaySwim is not true then exit b2kPlayerSwimEnd + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGravSave, 1) + put empty into sPlaySwimGravSave + put false into sPlaySwim +end b2kPlayerSwimEnd + -- Internal: open the drop-through window -- take the reserved one-way -- bit out of the player's mask so chains (alone) stop colliding. command b2kPlayerDropStart @@ -4644,7 +4840,7 @@ end b2kPlayerDropRestore -- frame while idle -- the tick's steady-state budget is unchanged. command b2kPlayerTick local tNow, tDT, tB, tVX, tVY, tAxis, tAxisY, tTarget, tAcc, tStep - local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i + local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i, tInWater if sPlayRef is empty then exit b2kPlayerTick put sBody[sPlayRef] into tB if tB is empty then exit b2kPlayerTick @@ -4701,8 +4897,8 @@ command b2kPlayerTick put 1 into sPlayFacing end if end if - -- ladder presence: pure numeric compares on the probe's stashed - -- centre; zero zones = one compare and out + -- ladder + water presence: pure numeric compares on the probe's + -- stashed centre; zero zones of a kind = one compare and out put false into tInZone if sPlayLadN > 0 then repeat with i = 1 to sPlayLadN @@ -4713,13 +4909,26 @@ command b2kPlayerTick end if end repeat end if - if sPlayClimb is not true and tInZone is true then + put false into tInWater + if sPlayWatN > 0 then + repeat with i = 1 to sPlayWatN + if sPlayPX >= sPlayWatL[i] and sPlayPX <= sPlayWatR[i] \ + and sPlayPY >= sPlayWatT[i] and sPlayPY <= sPlayWatB[i] then + put true into tInWater + exit repeat + end if + end repeat + end if + if sPlayClimb is not true and sPlaySwim is not true and tInZone is true then -- enter: UP any time in-zone; DOWN only while AIRBORNE (a -- grounded DOWN is a duck -- or a drop-through on a chain) if tAxisY is -1 or (tAxisY is 1 and sPlayGrounded is not true) then b2kPlayerClimbStart tB end if end if + if sPlaySwim is not true and sPlayClimb is not true and tInWater is true then + b2kPlayerSwimStart tB -- submerged: buoyant gravity, stroke to rise + end if if sPlayClimb is true then if b2kActionPressed("jump") then -- JUMP exits with a normal jump. The same press edge is @@ -4754,7 +4963,33 @@ command b2kPlayerTick end if end if end if - if sPlayClimb is not true then + if sPlaySwim is true then + if tInWater is not true then + b2kPlayerSwimEnd tB -- surfaced / left the pool: gravity returns + else + -- horizontal: ease vx toward axis * swimSpeed (sluggish -- the + -- air-accel rate gives the underwater drag) + put tAxis * sPlaySwimSpd into tTarget + put sPlayAccelA * tDT into tStep + if tVX < tTarget then + put min(tTarget, tVX + tStep) into tVX + else + put max(tTarget, tVX - tStep) into tVX + end if + -- vertical: a JUMP press is a repeatable upward STROKE (no + -- ground gate -- the spec's water jump); else UP/DOWN swim at + -- swimSpeed; else the reduced gravity sinks you, capped low + if b2kActionPressed("jump") then + put 0 - sPlaySwimJump into tVY + else + if tAxisY is not 0 then + put tAxisY * sPlaySwimSpd into tVY + end if + end if + put true into tWrite + end if + end if + if sPlayClimb is not true and sPlaySwim is not true then -- horizontal: accelerate vx toward axis * moveSpeed (air = airAccel) put tAxis * sPlayMoveSpd into tTarget -- DUCK: down on the ground crouches and brakes to a stop at @@ -4809,9 +5044,18 @@ command b2kPlayerTick end if end if if sPlayJumping and tVY >= 0 then put false into sPlayJumping -- apex - if tVY > sPlayMaxFall then - put sPlayMaxFall into tVY - put true into tWrite + -- terminal velocity: the low swimMaxFall is the buoyant sink cap while + -- submerged; the normal maxFall (the character's terminal velocity) else + if sPlaySwim is true then + if tVY > sPlaySwimMaxFall then + put sPlaySwimMaxFall into tVY + put true into tWrite + end if + else + if tVY > sPlayMaxFall then + put sPlayMaxFall into tVY + put true into tWrite + end if end if -- GROUND-SNAP: grounded on FLAT ground, drifting upward, and we did -- not jump = the contact solver's push-out rebound after a hard @@ -4823,7 +5067,7 @@ command b2kPlayerTick -- must use b2kPlayerJump, which sets the jump flag (b2kPlayerHurt's -- pop rides the same flag). if sPlayGrounded and sPlayJumping is not true and sPlayClimb is not true \ - and tVY < 0 and abs(sPlayNormX) < 0.1 then + and sPlaySwim is not true and tVY < 0 and abs(sPlayNormX) < 0.1 then put 0 into tVY put true into tWrite end if @@ -4878,6 +5122,14 @@ command b2kPlayerTick end if end if end if + -- swim OWNS the state while submerged: the machine above ran the + -- grounded/airborne branch (sPlayClimb is false underwater), so override + -- to "swim" and keep the air counter clear -- surfacing must never read + -- as a long fall plus a phantom land tick. + if sPlaySwim is true and sPlayHurt is not true then + put "swim" into sPlayState + put 0 into sPlayAir + end if -- animations: only while controlling (manual poses own the art when -- control is off), and never let a vanished art control abort the -- frame -- the loop's ticks share one try block @@ -4896,7 +5148,7 @@ command b2kPlayerShowState pNow, pVX local tWant, tAnim, tAKey, tFPS, tFlip put sPlayState into tWant if pNow < sPlayHoldMS and tWant is not "jump" and tWant is not "fall" \ - and tWant is not "hurt" and tWant is not "climb" then + and tWant is not "hurt" and tWant is not "climb" and tWant is not "swim" then put empty into tAnim -- mid land-flourish: leave it playing else if tWant is "land" and sPlayAnims["land"] is empty then diff --git a/examples/box2dxt-slingshot.livecodescript b/examples/box2dxt-slingshot.livecodescript index 7da8eab..a809a47 100644 --- a/examples/box2dxt-slingshot.livecodescript +++ b/examples/box2dxt-slingshot.livecodescript @@ -852,16 +852,18 @@ end rawKeyDown -- b2kSpriteFPS spr,fps · b2kSpriteOnFinish spr,msg · -- b2kSpriteBind spr,bodyCtrl[,dx,dy] · b2kSpriteRemove spr -- PLAYER b2kPlayerMake x,y,w,h[,sheet] · b2kPlayerAttach ctrl · --- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt] · +-- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt,swim] · -- b2kPlayerSet key,value · b2kPlayerGet(key) · b2kPlayerOnGround() · --- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt) · +-- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt|swim) · -- b2kPlayerFacing() · b2kPlayerJump [speed] · b2kPlayerControl flag · --- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerHurt [fromX] · +-- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerAddWater x1,y1,x2,y2 · +-- b2kPlayerHurt [fromX] · -- b2kPlayerHurtIs() (true through knockback + mercy window) · -- b2kPlayer() · b2kPlayerSprite() · b2kPlayerRemove -- (drives axes "moveX"/"moveY" + action "jump"; rebind to remap. -- DOWN ducks · DOWN+JUMP on a one-way chain drops through · --- UP/DOWN in a ladder zone climbs · JUMP exits a climb) +-- UP/DOWN in a ladder zone climbs · JUMP exits a climb · +-- in a water zone UP/DOWN swim, JUMP strokes (repeatable)) -- CAMERA b2kCamOn [rect] · b2kCamOff · b2kCamFollow ctrl[,lerp] · b2kCamUnfollow · -- b2kCamDeadzone w,h · b2kCamBounds x1,y1,x2,y2 · b2kCamGoto x,y · -- b2kCamPos() · b2kCamShake amp,ms · b2kCamMouseX/Y() · b2kCamGroup() · @@ -1009,6 +1011,12 @@ local sPlayClimb -- true while in the climb state (gravity scale parked at 0 local sPlayGravSave -- the body's gravity scale to restore on climb exit local sPlayLadN -- registered ladder zones (flat numeric arrays: the local sPlayLadL, sPlayLadT, sPlayLadR, sPlayLadB -- ...in-zone test is pure compares) +-- Wave 4: water zones + the swim state (a buoyant parallel to the climb) +local sPlaySwim -- true while submerged in a water zone (gravity scaled down) +local sPlaySwimGravSave -- the body's gravity scale to restore on swim exit +local sPlaySwimSpd, sPlaySwimJump, sPlaySwimGrav, sPlaySwimMaxFall -- swim tune caches +local sPlayWatN -- registered water zones (flat numeric arrays, like ladders) +local sPlayWatL, sPlayWatT, sPlayWatR, sPlayWatB local sPlayHurt -- true while knockback owns the controller local sPlayHurtEnd -- sim-clock when hurtMs has elapsed local sPlayHurtHalf -- sim-clock when half of hurtMs has elapsed (landings @@ -3929,6 +3937,8 @@ command b2kPlayerAttach pCtrl put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -3960,10 +3970,11 @@ end b2kPlayerResolveArt -- Tuning knobs (pixels, px/s, ms, degrees). Keys: moveSpeed, accel, -- airAccel, jumpSpeed, jumpCut, coyoteMs, bufferMs, maxFall, maxSlopeDeg, --- dropMs (drop-through window), climbSpeed (ladder px/s), hurtPopX/ --- hurtPopY (knockback launch px/s), hurtMs (control-off span), invulnMs --- (post-hurt mercy). Settable any time, before or after the player --- exists; unknown keys are stored verbatim for your own use. +-- dropMs (drop-through window), climbSpeed (ladder px/s), swimSpeed/ +-- swimJump (water px/s + stroke), swimGravity/swimMaxFall (buoyancy), +-- hurtPopX/hurtPopY (knockback launch px/s), hurtMs (control-off span), +-- invulnMs (post-hurt mercy). Settable any time, before or after the +-- player exists; unknown keys are stored verbatim for your own use. command b2kPlayerSet pKey, pValue put pValue into sPlayTune[toLower(pKey)] b2kPlayerTuneCache @@ -3982,6 +3993,10 @@ command b2kPlayerTuneCache put b2kPlayerGet("maxFall") into sPlayMaxFall put b2kPlayerGet("dropMs") into sPlayDropMS put b2kPlayerGet("climbSpeed") into sPlayClimbSpd + put b2kPlayerGet("swimSpeed") into sPlaySwimSpd + put b2kPlayerGet("swimJump") into sPlaySwimJump + put b2kPlayerGet("swimGravity") into sPlaySwimGrav + put b2kPlayerGet("swimMaxFall") into sPlaySwimMaxFall put b2kPlayerGet("hurtPopX") into sPlayHurtPopX put b2kPlayerGet("hurtPopY") into sPlayHurtPopY put b2kPlayerGet("hurtMs") into sPlayHurtMS @@ -4028,6 +4043,14 @@ function b2kPlayerDefault pKey return 260 case "climbspeed" return 160 + case "swimspeed" + return 150 + case "swimjump" + return 300 + case "swimgravity" + return 0.35 + case "swimmaxfall" + return 150 case "hurtpopx" return 220 case "hurtpopy" @@ -4046,9 +4069,10 @@ end b2kPlayerDefault -- idle/run resume. NOTE: a finishing land animation dispatches the art's -- b2kSpriteOnFinish message like any non-looping animation would. -- Wave 2 slots (all optional, so old five-argument calls keep working): --- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose -- --- sheets without those frames still read correctly. -command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt +-- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose; +-- the Wave 4 pSwim falls back to the fall pose -- sheets without those +-- frames still read correctly. +command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt, pSwim put pIdle into sPlayAnims["idle"] put pRun into sPlayAnims["run"] put pJump into sPlayAnims["jump"] @@ -4073,6 +4097,11 @@ command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt else put pHurt into sPlayAnims["hurt"] end if + if pSwim is empty then + put sPlayAnims["fall"] into sPlayAnims["swim"] + else + put pSwim into sPlayAnims["swim"] + end if put empty into sPlayAnimNow -- re-assert on the next tick if sPlayArt is empty then b2kPlayerResolveArt end b2kPlayerAnims @@ -4091,9 +4120,9 @@ function b2kPlayerOnGround return (sPlayGrounded is true) end b2kPlayerOnGround --- idle | run | jump | fall | duck | climb | hurt, plus "land" for exactly --- one frame on touch-down from jump/fall (dust puffs, landing sounds). --- A drop-through renders as "fall". Empty = no player. +-- idle | run | jump | fall | duck | climb | hurt | swim, plus "land" for +-- exactly one frame on touch-down from jump/fall (dust puffs, landing +-- sounds). A drop-through renders as "fall". Empty = no player. function b2kPlayerState return sPlayState end b2kPlayerState @@ -4138,6 +4167,25 @@ command b2kPlayerAddLadder pX1, pY1, pX2, pY2 put max(pY1, pY2) into sPlayLadB[sPlayLadN] end b2kPlayerAddLadder +-- Register a water ZONE (screen-px rect, any corner order). While the +-- player's centre is inside one, the controller SWIMS: gravity drops to +-- swimGravity (buoyant), the sink caps at swimMaxFall, UP/DOWN swim at +-- swimSpeed, and JUMP is a repeatable upward STROKE (swimJump) -- no +-- ground needed. Leaving the zone restores gravity. Pure polled geometry, +-- WORLD state (b2kClear wipes them), exactly like the ladder zones. +-- TIP: top the zone a little ABOVE the water's surface tiles, so the dive +-- in and the surface-out break the water where the surface art sits. +command b2kPlayerAddWater pX1, pY1, pX2, pY2 + if pX1 is not a number or pY1 is not a number \ + or pX2 is not a number or pY2 is not a number then exit b2kPlayerAddWater + if sPlayWatN is empty then put 0 into sPlayWatN + add 1 to sPlayWatN + put min(pX1, pX2) into sPlayWatL[sPlayWatN] + put max(pX1, pX2) into sPlayWatR[sPlayWatN] + put min(pY1, pY2) into sPlayWatT[sPlayWatN] + put max(pY1, pY2) into sPlayWatB[sPlayWatN] +end b2kPlayerAddWater + -- The knockback standard (spec section 9.4): control off, an away-pop -- (the sign of pFromX vs the player picks the direction; no/empty -- pFromX pops away from the facing), the hurt anim, control restored @@ -4152,6 +4200,7 @@ command b2kPlayerHurt pFromX if b2kPlayerHurtIs() then exit b2kPlayerHurt -- mercy window if sPlayControl is not true then exit b2kPlayerHurt -- a cutscene owns the body if sPlayClimb is true then b2kPlayerClimbEnd sBody[sPlayRef] + if sPlaySwim is true then b2kPlayerSwimEnd sBody[sPlayRef] if pFromX is a number then if pFromX > sPlayPX then put -1 into tDir @@ -4217,6 +4266,9 @@ command b2kPlayerForget pFull if sPlayClimb is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then b2kPlayerClimbEnd sBody[sPlayRef] end if + if sPlaySwim is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then + b2kPlayerSwimEnd sBody[sPlayRef] + end if if sPlayDropUntil is not empty and sPlayDropUntil > 0 then b2kPlayerDropRestore put empty into sPlayRef put empty into sPlayArt @@ -4239,6 +4291,8 @@ command b2kPlayerForget pFull put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -4249,6 +4303,11 @@ command b2kPlayerForget pFull put empty into sPlayLadT put empty into sPlayLadR put empty into sPlayLadB + put 0 into sPlayWatN + put empty into sPlayWatL + put empty into sPlayWatT + put empty into sPlayWatR + put empty into sPlayWatB if pFull is true then put empty into sPlayTune end b2kPlayerForget @@ -4315,6 +4374,24 @@ command b2kPlayerClimbEnd pBody put false into sPlayClimb end b2kPlayerClimbEnd +-- Internal: enter the swim -- gravity drops to swimGravity (buoyant; the +-- body's own scale is saved and restored, like the climb). Mutually +-- exclusive with the climb: the tick only ever starts one of them. +command b2kPlayerSwimStart pBody + if sPlaySwim is true then exit b2kPlayerSwimStart + put b2BodyGravityScale(pBody) into sPlaySwimGravSave + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGrav, 0.35) + put true into sPlaySwim + put false into sPlayJumping +end b2kPlayerSwimStart + +command b2kPlayerSwimEnd pBody + if sPlaySwim is not true then exit b2kPlayerSwimEnd + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGravSave, 1) + put empty into sPlaySwimGravSave + put false into sPlaySwim +end b2kPlayerSwimEnd + -- Internal: open the drop-through window -- take the reserved one-way -- bit out of the player's mask so chains (alone) stop colliding. command b2kPlayerDropStart @@ -4351,7 +4428,7 @@ end b2kPlayerDropRestore -- frame while idle -- the tick's steady-state budget is unchanged. command b2kPlayerTick local tNow, tDT, tB, tVX, tVY, tAxis, tAxisY, tTarget, tAcc, tStep - local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i + local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i, tInWater if sPlayRef is empty then exit b2kPlayerTick put sBody[sPlayRef] into tB if tB is empty then exit b2kPlayerTick @@ -4408,8 +4485,8 @@ command b2kPlayerTick put 1 into sPlayFacing end if end if - -- ladder presence: pure numeric compares on the probe's stashed - -- centre; zero zones = one compare and out + -- ladder + water presence: pure numeric compares on the probe's + -- stashed centre; zero zones of a kind = one compare and out put false into tInZone if sPlayLadN > 0 then repeat with i = 1 to sPlayLadN @@ -4420,13 +4497,26 @@ command b2kPlayerTick end if end repeat end if - if sPlayClimb is not true and tInZone is true then + put false into tInWater + if sPlayWatN > 0 then + repeat with i = 1 to sPlayWatN + if sPlayPX >= sPlayWatL[i] and sPlayPX <= sPlayWatR[i] \ + and sPlayPY >= sPlayWatT[i] and sPlayPY <= sPlayWatB[i] then + put true into tInWater + exit repeat + end if + end repeat + end if + if sPlayClimb is not true and sPlaySwim is not true and tInZone is true then -- enter: UP any time in-zone; DOWN only while AIRBORNE (a -- grounded DOWN is a duck -- or a drop-through on a chain) if tAxisY is -1 or (tAxisY is 1 and sPlayGrounded is not true) then b2kPlayerClimbStart tB end if end if + if sPlaySwim is not true and sPlayClimb is not true and tInWater is true then + b2kPlayerSwimStart tB -- submerged: buoyant gravity, stroke to rise + end if if sPlayClimb is true then if b2kActionPressed("jump") then -- JUMP exits with a normal jump. The same press edge is @@ -4461,7 +4551,33 @@ command b2kPlayerTick end if end if end if - if sPlayClimb is not true then + if sPlaySwim is true then + if tInWater is not true then + b2kPlayerSwimEnd tB -- surfaced / left the pool: gravity returns + else + -- horizontal: ease vx toward axis * swimSpeed (sluggish -- the + -- air-accel rate gives the underwater drag) + put tAxis * sPlaySwimSpd into tTarget + put sPlayAccelA * tDT into tStep + if tVX < tTarget then + put min(tTarget, tVX + tStep) into tVX + else + put max(tTarget, tVX - tStep) into tVX + end if + -- vertical: a JUMP press is a repeatable upward STROKE (no + -- ground gate -- the spec's water jump); else UP/DOWN swim at + -- swimSpeed; else the reduced gravity sinks you, capped low + if b2kActionPressed("jump") then + put 0 - sPlaySwimJump into tVY + else + if tAxisY is not 0 then + put tAxisY * sPlaySwimSpd into tVY + end if + end if + put true into tWrite + end if + end if + if sPlayClimb is not true and sPlaySwim is not true then -- horizontal: accelerate vx toward axis * moveSpeed (air = airAccel) put tAxis * sPlayMoveSpd into tTarget -- DUCK: down on the ground crouches and brakes to a stop at @@ -4516,9 +4632,18 @@ command b2kPlayerTick end if end if if sPlayJumping and tVY >= 0 then put false into sPlayJumping -- apex - if tVY > sPlayMaxFall then - put sPlayMaxFall into tVY - put true into tWrite + -- terminal velocity: the low swimMaxFall is the buoyant sink cap while + -- submerged; the normal maxFall (the character's terminal velocity) else + if sPlaySwim is true then + if tVY > sPlaySwimMaxFall then + put sPlaySwimMaxFall into tVY + put true into tWrite + end if + else + if tVY > sPlayMaxFall then + put sPlayMaxFall into tVY + put true into tWrite + end if end if -- GROUND-SNAP: grounded on FLAT ground, drifting upward, and we did -- not jump = the contact solver's push-out rebound after a hard @@ -4530,7 +4655,7 @@ command b2kPlayerTick -- must use b2kPlayerJump, which sets the jump flag (b2kPlayerHurt's -- pop rides the same flag). if sPlayGrounded and sPlayJumping is not true and sPlayClimb is not true \ - and tVY < 0 and abs(sPlayNormX) < 0.1 then + and sPlaySwim is not true and tVY < 0 and abs(sPlayNormX) < 0.1 then put 0 into tVY put true into tWrite end if @@ -4585,6 +4710,14 @@ command b2kPlayerTick end if end if end if + -- swim OWNS the state while submerged: the machine above ran the + -- grounded/airborne branch (sPlayClimb is false underwater), so override + -- to "swim" and keep the air counter clear -- surfacing must never read + -- as a long fall plus a phantom land tick. + if sPlaySwim is true and sPlayHurt is not true then + put "swim" into sPlayState + put 0 into sPlayAir + end if -- animations: only while controlling (manual poses own the art when -- control is off), and never let a vanished art control abort the -- frame -- the loop's ticks share one try block @@ -4603,7 +4736,7 @@ command b2kPlayerShowState pNow, pVX local tWant, tAnim, tAKey, tFPS, tFlip put sPlayState into tWant if pNow < sPlayHoldMS and tWant is not "jump" and tWant is not "fall" \ - and tWant is not "hurt" and tWant is not "climb" then + and tWant is not "hurt" and tWant is not "climb" and tWant is not "swim" then put empty into tAnim -- mid land-flourish: leave it playing else if tWant is "land" and sPlayAnims["land"] is empty then diff --git a/examples/box2dxt-spike-gamekit.livecodescript b/examples/box2dxt-spike-gamekit.livecodescript index 94ecc1d..8cc9873 100644 --- a/examples/box2dxt-spike-gamekit.livecodescript +++ b/examples/box2dxt-spike-gamekit.livecodescript @@ -1432,16 +1432,18 @@ end spikeNewGroup -- b2kSpriteFPS spr,fps · b2kSpriteOnFinish spr,msg · -- b2kSpriteBind spr,bodyCtrl[,dx,dy] · b2kSpriteRemove spr -- PLAYER b2kPlayerMake x,y,w,h[,sheet] · b2kPlayerAttach ctrl · --- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt] · +-- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt,swim] · -- b2kPlayerSet key,value · b2kPlayerGet(key) · b2kPlayerOnGround() · --- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt) · +-- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt|swim) · -- b2kPlayerFacing() · b2kPlayerJump [speed] · b2kPlayerControl flag · --- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerHurt [fromX] · +-- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerAddWater x1,y1,x2,y2 · +-- b2kPlayerHurt [fromX] · -- b2kPlayerHurtIs() (true through knockback + mercy window) · -- b2kPlayer() · b2kPlayerSprite() · b2kPlayerRemove -- (drives axes "moveX"/"moveY" + action "jump"; rebind to remap. -- DOWN ducks · DOWN+JUMP on a one-way chain drops through · --- UP/DOWN in a ladder zone climbs · JUMP exits a climb) +-- UP/DOWN in a ladder zone climbs · JUMP exits a climb · +-- in a water zone UP/DOWN swim, JUMP strokes (repeatable)) -- CAMERA b2kCamOn [rect] · b2kCamOff · b2kCamFollow ctrl[,lerp] · b2kCamUnfollow · -- b2kCamDeadzone w,h · b2kCamBounds x1,y1,x2,y2 · b2kCamGoto x,y · -- b2kCamPos() · b2kCamShake amp,ms · b2kCamMouseX/Y() · b2kCamGroup() · @@ -1589,6 +1591,12 @@ local sPlayClimb -- true while in the climb state (gravity scale parked at 0 local sPlayGravSave -- the body's gravity scale to restore on climb exit local sPlayLadN -- registered ladder zones (flat numeric arrays: the local sPlayLadL, sPlayLadT, sPlayLadR, sPlayLadB -- ...in-zone test is pure compares) +-- Wave 4: water zones + the swim state (a buoyant parallel to the climb) +local sPlaySwim -- true while submerged in a water zone (gravity scaled down) +local sPlaySwimGravSave -- the body's gravity scale to restore on swim exit +local sPlaySwimSpd, sPlaySwimJump, sPlaySwimGrav, sPlaySwimMaxFall -- swim tune caches +local sPlayWatN -- registered water zones (flat numeric arrays, like ladders) +local sPlayWatL, sPlayWatT, sPlayWatR, sPlayWatB local sPlayHurt -- true while knockback owns the controller local sPlayHurtEnd -- sim-clock when hurtMs has elapsed local sPlayHurtHalf -- sim-clock when half of hurtMs has elapsed (landings @@ -4509,6 +4517,8 @@ command b2kPlayerAttach pCtrl put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -4540,10 +4550,11 @@ end b2kPlayerResolveArt -- Tuning knobs (pixels, px/s, ms, degrees). Keys: moveSpeed, accel, -- airAccel, jumpSpeed, jumpCut, coyoteMs, bufferMs, maxFall, maxSlopeDeg, --- dropMs (drop-through window), climbSpeed (ladder px/s), hurtPopX/ --- hurtPopY (knockback launch px/s), hurtMs (control-off span), invulnMs --- (post-hurt mercy). Settable any time, before or after the player --- exists; unknown keys are stored verbatim for your own use. +-- dropMs (drop-through window), climbSpeed (ladder px/s), swimSpeed/ +-- swimJump (water px/s + stroke), swimGravity/swimMaxFall (buoyancy), +-- hurtPopX/hurtPopY (knockback launch px/s), hurtMs (control-off span), +-- invulnMs (post-hurt mercy). Settable any time, before or after the +-- player exists; unknown keys are stored verbatim for your own use. command b2kPlayerSet pKey, pValue put pValue into sPlayTune[toLower(pKey)] b2kPlayerTuneCache @@ -4562,6 +4573,10 @@ command b2kPlayerTuneCache put b2kPlayerGet("maxFall") into sPlayMaxFall put b2kPlayerGet("dropMs") into sPlayDropMS put b2kPlayerGet("climbSpeed") into sPlayClimbSpd + put b2kPlayerGet("swimSpeed") into sPlaySwimSpd + put b2kPlayerGet("swimJump") into sPlaySwimJump + put b2kPlayerGet("swimGravity") into sPlaySwimGrav + put b2kPlayerGet("swimMaxFall") into sPlaySwimMaxFall put b2kPlayerGet("hurtPopX") into sPlayHurtPopX put b2kPlayerGet("hurtPopY") into sPlayHurtPopY put b2kPlayerGet("hurtMs") into sPlayHurtMS @@ -4608,6 +4623,14 @@ function b2kPlayerDefault pKey return 260 case "climbspeed" return 160 + case "swimspeed" + return 150 + case "swimjump" + return 300 + case "swimgravity" + return 0.35 + case "swimmaxfall" + return 150 case "hurtpopx" return 220 case "hurtpopy" @@ -4626,9 +4649,10 @@ end b2kPlayerDefault -- idle/run resume. NOTE: a finishing land animation dispatches the art's -- b2kSpriteOnFinish message like any non-looping animation would. -- Wave 2 slots (all optional, so old five-argument calls keep working): --- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose -- --- sheets without those frames still read correctly. -command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt +-- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose; +-- the Wave 4 pSwim falls back to the fall pose -- sheets without those +-- frames still read correctly. +command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt, pSwim put pIdle into sPlayAnims["idle"] put pRun into sPlayAnims["run"] put pJump into sPlayAnims["jump"] @@ -4653,6 +4677,11 @@ command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt else put pHurt into sPlayAnims["hurt"] end if + if pSwim is empty then + put sPlayAnims["fall"] into sPlayAnims["swim"] + else + put pSwim into sPlayAnims["swim"] + end if put empty into sPlayAnimNow -- re-assert on the next tick if sPlayArt is empty then b2kPlayerResolveArt end b2kPlayerAnims @@ -4671,9 +4700,9 @@ function b2kPlayerOnGround return (sPlayGrounded is true) end b2kPlayerOnGround --- idle | run | jump | fall | duck | climb | hurt, plus "land" for exactly --- one frame on touch-down from jump/fall (dust puffs, landing sounds). --- A drop-through renders as "fall". Empty = no player. +-- idle | run | jump | fall | duck | climb | hurt | swim, plus "land" for +-- exactly one frame on touch-down from jump/fall (dust puffs, landing +-- sounds). A drop-through renders as "fall". Empty = no player. function b2kPlayerState return sPlayState end b2kPlayerState @@ -4718,6 +4747,25 @@ command b2kPlayerAddLadder pX1, pY1, pX2, pY2 put max(pY1, pY2) into sPlayLadB[sPlayLadN] end b2kPlayerAddLadder +-- Register a water ZONE (screen-px rect, any corner order). While the +-- player's centre is inside one, the controller SWIMS: gravity drops to +-- swimGravity (buoyant), the sink caps at swimMaxFall, UP/DOWN swim at +-- swimSpeed, and JUMP is a repeatable upward STROKE (swimJump) -- no +-- ground needed. Leaving the zone restores gravity. Pure polled geometry, +-- WORLD state (b2kClear wipes them), exactly like the ladder zones. +-- TIP: top the zone a little ABOVE the water's surface tiles, so the dive +-- in and the surface-out break the water where the surface art sits. +command b2kPlayerAddWater pX1, pY1, pX2, pY2 + if pX1 is not a number or pY1 is not a number \ + or pX2 is not a number or pY2 is not a number then exit b2kPlayerAddWater + if sPlayWatN is empty then put 0 into sPlayWatN + add 1 to sPlayWatN + put min(pX1, pX2) into sPlayWatL[sPlayWatN] + put max(pX1, pX2) into sPlayWatR[sPlayWatN] + put min(pY1, pY2) into sPlayWatT[sPlayWatN] + put max(pY1, pY2) into sPlayWatB[sPlayWatN] +end b2kPlayerAddWater + -- The knockback standard (spec section 9.4): control off, an away-pop -- (the sign of pFromX vs the player picks the direction; no/empty -- pFromX pops away from the facing), the hurt anim, control restored @@ -4732,6 +4780,7 @@ command b2kPlayerHurt pFromX if b2kPlayerHurtIs() then exit b2kPlayerHurt -- mercy window if sPlayControl is not true then exit b2kPlayerHurt -- a cutscene owns the body if sPlayClimb is true then b2kPlayerClimbEnd sBody[sPlayRef] + if sPlaySwim is true then b2kPlayerSwimEnd sBody[sPlayRef] if pFromX is a number then if pFromX > sPlayPX then put -1 into tDir @@ -4797,6 +4846,9 @@ command b2kPlayerForget pFull if sPlayClimb is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then b2kPlayerClimbEnd sBody[sPlayRef] end if + if sPlaySwim is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then + b2kPlayerSwimEnd sBody[sPlayRef] + end if if sPlayDropUntil is not empty and sPlayDropUntil > 0 then b2kPlayerDropRestore put empty into sPlayRef put empty into sPlayArt @@ -4819,6 +4871,8 @@ command b2kPlayerForget pFull put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -4829,6 +4883,11 @@ command b2kPlayerForget pFull put empty into sPlayLadT put empty into sPlayLadR put empty into sPlayLadB + put 0 into sPlayWatN + put empty into sPlayWatL + put empty into sPlayWatT + put empty into sPlayWatR + put empty into sPlayWatB if pFull is true then put empty into sPlayTune end b2kPlayerForget @@ -4895,6 +4954,24 @@ command b2kPlayerClimbEnd pBody put false into sPlayClimb end b2kPlayerClimbEnd +-- Internal: enter the swim -- gravity drops to swimGravity (buoyant; the +-- body's own scale is saved and restored, like the climb). Mutually +-- exclusive with the climb: the tick only ever starts one of them. +command b2kPlayerSwimStart pBody + if sPlaySwim is true then exit b2kPlayerSwimStart + put b2BodyGravityScale(pBody) into sPlaySwimGravSave + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGrav, 0.35) + put true into sPlaySwim + put false into sPlayJumping +end b2kPlayerSwimStart + +command b2kPlayerSwimEnd pBody + if sPlaySwim is not true then exit b2kPlayerSwimEnd + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGravSave, 1) + put empty into sPlaySwimGravSave + put false into sPlaySwim +end b2kPlayerSwimEnd + -- Internal: open the drop-through window -- take the reserved one-way -- bit out of the player's mask so chains (alone) stop colliding. command b2kPlayerDropStart @@ -4931,7 +5008,7 @@ end b2kPlayerDropRestore -- frame while idle -- the tick's steady-state budget is unchanged. command b2kPlayerTick local tNow, tDT, tB, tVX, tVY, tAxis, tAxisY, tTarget, tAcc, tStep - local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i + local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i, tInWater if sPlayRef is empty then exit b2kPlayerTick put sBody[sPlayRef] into tB if tB is empty then exit b2kPlayerTick @@ -4988,8 +5065,8 @@ command b2kPlayerTick put 1 into sPlayFacing end if end if - -- ladder presence: pure numeric compares on the probe's stashed - -- centre; zero zones = one compare and out + -- ladder + water presence: pure numeric compares on the probe's + -- stashed centre; zero zones of a kind = one compare and out put false into tInZone if sPlayLadN > 0 then repeat with i = 1 to sPlayLadN @@ -5000,13 +5077,26 @@ command b2kPlayerTick end if end repeat end if - if sPlayClimb is not true and tInZone is true then + put false into tInWater + if sPlayWatN > 0 then + repeat with i = 1 to sPlayWatN + if sPlayPX >= sPlayWatL[i] and sPlayPX <= sPlayWatR[i] \ + and sPlayPY >= sPlayWatT[i] and sPlayPY <= sPlayWatB[i] then + put true into tInWater + exit repeat + end if + end repeat + end if + if sPlayClimb is not true and sPlaySwim is not true and tInZone is true then -- enter: UP any time in-zone; DOWN only while AIRBORNE (a -- grounded DOWN is a duck -- or a drop-through on a chain) if tAxisY is -1 or (tAxisY is 1 and sPlayGrounded is not true) then b2kPlayerClimbStart tB end if end if + if sPlaySwim is not true and sPlayClimb is not true and tInWater is true then + b2kPlayerSwimStart tB -- submerged: buoyant gravity, stroke to rise + end if if sPlayClimb is true then if b2kActionPressed("jump") then -- JUMP exits with a normal jump. The same press edge is @@ -5041,7 +5131,33 @@ command b2kPlayerTick end if end if end if - if sPlayClimb is not true then + if sPlaySwim is true then + if tInWater is not true then + b2kPlayerSwimEnd tB -- surfaced / left the pool: gravity returns + else + -- horizontal: ease vx toward axis * swimSpeed (sluggish -- the + -- air-accel rate gives the underwater drag) + put tAxis * sPlaySwimSpd into tTarget + put sPlayAccelA * tDT into tStep + if tVX < tTarget then + put min(tTarget, tVX + tStep) into tVX + else + put max(tTarget, tVX - tStep) into tVX + end if + -- vertical: a JUMP press is a repeatable upward STROKE (no + -- ground gate -- the spec's water jump); else UP/DOWN swim at + -- swimSpeed; else the reduced gravity sinks you, capped low + if b2kActionPressed("jump") then + put 0 - sPlaySwimJump into tVY + else + if tAxisY is not 0 then + put tAxisY * sPlaySwimSpd into tVY + end if + end if + put true into tWrite + end if + end if + if sPlayClimb is not true and sPlaySwim is not true then -- horizontal: accelerate vx toward axis * moveSpeed (air = airAccel) put tAxis * sPlayMoveSpd into tTarget -- DUCK: down on the ground crouches and brakes to a stop at @@ -5096,9 +5212,18 @@ command b2kPlayerTick end if end if if sPlayJumping and tVY >= 0 then put false into sPlayJumping -- apex - if tVY > sPlayMaxFall then - put sPlayMaxFall into tVY - put true into tWrite + -- terminal velocity: the low swimMaxFall is the buoyant sink cap while + -- submerged; the normal maxFall (the character's terminal velocity) else + if sPlaySwim is true then + if tVY > sPlaySwimMaxFall then + put sPlaySwimMaxFall into tVY + put true into tWrite + end if + else + if tVY > sPlayMaxFall then + put sPlayMaxFall into tVY + put true into tWrite + end if end if -- GROUND-SNAP: grounded on FLAT ground, drifting upward, and we did -- not jump = the contact solver's push-out rebound after a hard @@ -5110,7 +5235,7 @@ command b2kPlayerTick -- must use b2kPlayerJump, which sets the jump flag (b2kPlayerHurt's -- pop rides the same flag). if sPlayGrounded and sPlayJumping is not true and sPlayClimb is not true \ - and tVY < 0 and abs(sPlayNormX) < 0.1 then + and sPlaySwim is not true and tVY < 0 and abs(sPlayNormX) < 0.1 then put 0 into tVY put true into tWrite end if @@ -5165,6 +5290,14 @@ command b2kPlayerTick end if end if end if + -- swim OWNS the state while submerged: the machine above ran the + -- grounded/airborne branch (sPlayClimb is false underwater), so override + -- to "swim" and keep the air counter clear -- surfacing must never read + -- as a long fall plus a phantom land tick. + if sPlaySwim is true and sPlayHurt is not true then + put "swim" into sPlayState + put 0 into sPlayAir + end if -- animations: only while controlling (manual poses own the art when -- control is off), and never let a vanished art control abort the -- frame -- the loop's ticks share one try block @@ -5183,7 +5316,7 @@ command b2kPlayerShowState pNow, pVX local tWant, tAnim, tAKey, tFPS, tFlip put sPlayState into tWant if pNow < sPlayHoldMS and tWant is not "jump" and tWant is not "fall" \ - and tWant is not "hurt" and tWant is not "climb" then + and tWant is not "hurt" and tWant is not "climb" and tWant is not "swim" then put empty into tAnim -- mid land-flourish: leave it playing else if tWant is "land" and sPlayAnims["land"] is empty then diff --git a/plan.md b/plan.md index 28db42b..69602e5 100644 --- a/plan.md +++ b/plan.md @@ -321,3 +321,5 @@ user-confirmed in OXT before the next begins. | 2026-06-13 | **Final layout pass: the thwomp "ride-the-head" coins were a gate-locking trap; moved on-path.** User found an L3 coin they couldn't find. Root cause: the new thwomps' reward coins were placed at the perch height (first embedded in the 60px block at y184, then lifted to y110 above it) and were reachable only by the obscure "hop onto the resting crusher and ride it up" beat - which a player avoiding the hazard never discovers, so a REQUIRED (collect-every-coin) coin could lock the goal-flag gate. Fix: all four new thwomp coins (L1 5700, L2 3600, L3 4280, L4 4600) moved to **y500, directly under each crusher on the critical path** - the crusher is too tall to jump over, so the hero MUST pass beneath it to progress and grabs the coin by passing (timing the crush), guaranteeing the gate can open. A full coin-reachability audit (every coin vs the slabs/clouds/pits/plats per level) confirmed every other coin is reachable via its mechanism (spring arcs, the rope-bridge deck, jump-over-pit arcs, the mound plateau, L2's original verified ride coin at 1840,64). Lesson: **a REQUIRED coin must sit on the critical path or an unskippable beat - never behind an optional, discover-it-yourself trick.** Still zero Kit changes. | User review; this commit | | 2026-06-13 | **Showcase round 4: a marquee CRUSHER ALLEY + cloud hop closes every level.** User: "make these longer - add a row of green/grass thwomps to run under/over, more clouds, longer levels in the same style; show off the engine with as much pizzazz as possible." A new `pTileFace` param on `pfMakeThwomp` makes a plain TILE-SPRITE crusher (no mood-face swaps; the same armed->drop->rest->rise->re-arm cycle), so a ROW of four becomes a timing gauntlet the hero dashes BENEATH (the 42px trigger << 180px spacing means only one drops at a time; coins sit at the 90px midpoints, y500). One alley per level, biome-matched so the mechanic reads at a glance: GREEN blocks (L1 last turn, + L2 grass), BLUE (L3 ice), RED (L4 haunted), each followed by a two-cloud HOP to the flag (clouds at y448 - a 154px jump clears the 128px - coins up top at y392), the clouds one-way chains ghost-padded a tile past the art each side (solid span == art span per the ghost rule). Finales (walled door + steps + flag + bounds) shifted RIGHT as whole units by +1280 (tile-aligned): L2 4672->5952, L3 5312->6592, L4 5376->6656 (L1 was already 7552). New snails in L2 (gSlime 4) and L3 (gSlime 5) keep snails liberal; new crushers are gBlock 5-8 (L2), 3-6 (L3), 4-7 (L4) - unique per namespace per level. Door-gated coins stay reachable (the keys are early on the main path, before each alley). Flags within plat2; coins/totals self-count as built. Static checker clean; awaiting the OXT pass. Still zero Kit changes (harness v10 holds). | User review; this commit | | 2026-06-13 | **Showcase round 4b: a per-frame optimization pass (user: "as fully optimized to the current kit/library as possible").** An Opus audit of `on b2kFrame` + all 14 `pfTick*` against the perf playbook (cost order: interpreter ops > FFI round-trips > property-set redraws) found one real regression and three free wins, all example-side. (1) HIGH - `pfTickThwomps` did an FFI `b2kPosition` + comma-split for EVERY block every frame, and the new crusher alleys had doubled the block count to ~7-8/level while only 0-1 are ever in motion. Fix: cache each block's perch x at make + re-arm (new `gBlockX[]`), gate the armed->falling trigger on the cached x + the hero snapshot (perch y is invariantly 200, so `tHY > tBY` becomes `tHY > 200`), and read the live position ONLY in the in-motion states - cutting thwomp FFI from ~8/frame to ~0-1/frame with byte-identical trigger semantics. (2) The `shellslide` tick read `b2kVelocity` twice (vx test + vy pass-through) -> read once into a local. (3) The HUD reused the already-snapshotted `gHeroState` instead of re-calling `b2kPlayerState()` and hoisted `the hScroll of b2kCamGroup()` (read twice) into one local - both 4 Hz so minor, but free. The audit confirmed every other tick already optimal (O(1) idle gates via `gXxxN is 0`/`is empty`, hoisted clocks, the shared snapshot, change-gated property/velocity writes, sleeping bodies left asleep) and the gotcha scan clean (no smart quotes; no `local` nested in a block; no per-frame velocity write to a resting body; no two velocity-asserting bodies sharing a band; no stale `the result`). Example-only, so NO harness bump (v10 holds; the harness drives the Kit, not the example's `pf*` ticks). Static checker clean; awaiting the OXT pass. | User direction; this commit | +| 2026-06-13 | **WAVE 4 begins (liquids): SWIM lands in the Kit + a water level in the micro-game.** User: "move on to wave 4" + (AskUserQuestion) "Kit + a playable level together" and water content in "Both" games. SWIM is the first new player-action since Wave 2, so it is a KIT change (harness bump), built as a faithful parallel to the ladder/climb system: a new `b2kPlayerAddWater x1,y1,x2,y2` polled zone (flat `sPlayWat*` arrays, wiped by `b2kClear` like ladders); while the centre is submerged the controller enters a `swim` mode - gravity scaled to `swimGravity` (0.35, saved/restored like the climb's), the fall capped at `swimMaxFall` (150) instead of the air terminal, UP/DOWN drive vy at +/-`swimSpeed`, and a JUMP press is a REPEATABLE upward stroke (`swimJump`, no grounded/coyote/buffer gate). New `swim` state (overrides the grounded/airborne machine while submerged, clears `sPlayAir` so surfacing is not a phantom land), a 9th `pSwim` arg on `b2kPlayerAnims` (falls back to fall), and mutual exclusion with the climb (start gates check BOTH flags, so two gravity-saves can never fight). Knobs cached in `b2kPlayerTuneCache`. Harness **v11**: `stTestSwim` hand-steps a dive/sink-cap/stroke/swim-up/exit-restore sequence, self-diagnosing. An Opus review traced every risk (gravity leak, double-save, climb/swim exclusion, same-tick surfacing handoff, state/anim, gotchas, knob round-trip, harness thresholds) - all SAFE, no blockers. Re-synced into all examples. Content: the micro-game gains L3 "THE DEEP" - dive a pool, every coin underwater (the door forces the swim), alien skins drive real `swim1/2`; new `water` and `fish` (vertical pit-dweller, knockback) data verbs; stroke+hold-right hops you out. The platformer water beat (the second half of "Both") needs a raised-bank basin (the 640-tall world clamps the camera at y640, so a deep pit is off-screen - the micro-game raised its land for the same reason) and lands next. Static gates clean; awaiting the OXT pass. | User direction; this commit | +| 2026-06-14 | **Wave 4 round 2 — swim PIVOTS to the platformer; two playtest fixes; harness v12; docs deep-dive.** User OXT pass: the micro-game L3 shows a "white empty world" (an L3-only build issue in the EXAMPLE's own code; the Kit swim runs fine), and "the platformer is where we have been testing" -> swim content MOVES there (AskUserQuestion: add a debug warp, leave the micro-game L3 for later). Built L1 GREEN HILLS' **HILLTOP POOL**: a raised-bank basin (banks y480, water 480..640, floor y616) past the crusher alley with 3 underwater coins, a finale shift (flag 7540->8520, `pfBounds` ->8640), a new `pfMakeWater` helper, swim anim = the fall-pose fallback (`character_beige` has no swim frame). A `0`-key debug warp (delete-before-merge sentinel) drops onto the pool for fast iteration. **Harness v12** adds `stTestSwimGrounded` (swim while grounded on a submerged floor) and `stTestSwimClear` (the level-rebuild path: `b2kClear` must wipe the zone). A second Opus audit (micro-game + harness) was clean and caught two polish items folded in earlier: the L3 fish never breaching the surface (widened its bob), and a triple `b2kPosition(gHero)` per frame (hoisted to one snapshot passed into both ticks). **Two playtest fixes:** (1) swim too floaty -> heavier (`swimGravity` 0.35->0.6, `swimMaxFall` ->200, `swimJump` 360->300); the lesson: `swimGravity` sets only the between-stroke SINK, `swimJump` ALONE sets the single-stroke escape height (it writes velocity directly), so trimming the stroke is the lever for "harder to climb out". (2) a brick head-bump GAP the user flagged as a regression -> traced to the hero's 88px hitbox being TALLER than its ~76px visible character (128px frame headroom at 0.75 scale): the invisible "hat" hit the brick while the visible head sat low (the bonk still FIRED). Fixed by sizing the hitbox to the art (`tH` 88->76, `tDY` derived to keep the feet planted) and reading the real half-height (`gHeroHalfH`) in the bonk window instead of a hardcoded 44. Logged as **gotcha 28** + a Liquids/SWIM layout law. Docs deep-dive: swim added across kit-reference + kit-guide §21 + the API index; CHANGELOG/expansion-prep repointed from the micro-game to the platformer; all harness refs ->v12. | User OXT feedback; this commit | diff --git a/src/box2dxt-kit.livecodescript b/src/box2dxt-kit.livecodescript index 0c635e8..0dd4d4e 100644 --- a/src/box2dxt-kit.livecodescript +++ b/src/box2dxt-kit.livecodescript @@ -69,16 +69,18 @@ -- b2kSpriteFPS spr,fps · b2kSpriteOnFinish spr,msg · -- b2kSpriteBind spr,bodyCtrl[,dx,dy] · b2kSpriteRemove spr -- PLAYER b2kPlayerMake x,y,w,h[,sheet] · b2kPlayerAttach ctrl · --- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt] · +-- b2kPlayerAnims idle,run,jump,fall[,land,duck,climb,hurt,swim] · -- b2kPlayerSet key,value · b2kPlayerGet(key) · b2kPlayerOnGround() · --- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt) · +-- b2kPlayerState() (idle|run|jump|fall|land|duck|climb|hurt|swim) · -- b2kPlayerFacing() · b2kPlayerJump [speed] · b2kPlayerControl flag · --- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerHurt [fromX] · +-- b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerAddWater x1,y1,x2,y2 · +-- b2kPlayerHurt [fromX] · -- b2kPlayerHurtIs() (true through knockback + mercy window) · -- b2kPlayer() · b2kPlayerSprite() · b2kPlayerRemove -- (drives axes "moveX"/"moveY" + action "jump"; rebind to remap. -- DOWN ducks · DOWN+JUMP on a one-way chain drops through · --- UP/DOWN in a ladder zone climbs · JUMP exits a climb) +-- UP/DOWN in a ladder zone climbs · JUMP exits a climb · +-- in a water zone UP/DOWN swim, JUMP strokes (repeatable)) -- CAMERA b2kCamOn [rect] · b2kCamOff · b2kCamFollow ctrl[,lerp] · b2kCamUnfollow · -- b2kCamDeadzone w,h · b2kCamBounds x1,y1,x2,y2 · b2kCamGoto x,y · -- b2kCamPos() · b2kCamShake amp,ms · b2kCamMouseX/Y() · b2kCamGroup() · @@ -226,6 +228,12 @@ local sPlayClimb -- true while in the climb state (gravity scale parked at 0 local sPlayGravSave -- the body's gravity scale to restore on climb exit local sPlayLadN -- registered ladder zones (flat numeric arrays: the local sPlayLadL, sPlayLadT, sPlayLadR, sPlayLadB -- ...in-zone test is pure compares) +-- Wave 4: water zones + the swim state (a buoyant parallel to the climb) +local sPlaySwim -- true while submerged in a water zone (gravity scaled down) +local sPlaySwimGravSave -- the body's gravity scale to restore on swim exit +local sPlaySwimSpd, sPlaySwimJump, sPlaySwimGrav, sPlaySwimMaxFall -- swim tune caches +local sPlayWatN -- registered water zones (flat numeric arrays, like ladders) +local sPlayWatL, sPlayWatT, sPlayWatR, sPlayWatB local sPlayHurt -- true while knockback owns the controller local sPlayHurtEnd -- sim-clock when hurtMs has elapsed local sPlayHurtHalf -- sim-clock when half of hurtMs has elapsed (landings @@ -3146,6 +3154,8 @@ command b2kPlayerAttach pCtrl put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -3177,10 +3187,11 @@ end b2kPlayerResolveArt -- Tuning knobs (pixels, px/s, ms, degrees). Keys: moveSpeed, accel, -- airAccel, jumpSpeed, jumpCut, coyoteMs, bufferMs, maxFall, maxSlopeDeg, --- dropMs (drop-through window), climbSpeed (ladder px/s), hurtPopX/ --- hurtPopY (knockback launch px/s), hurtMs (control-off span), invulnMs --- (post-hurt mercy). Settable any time, before or after the player --- exists; unknown keys are stored verbatim for your own use. +-- dropMs (drop-through window), climbSpeed (ladder px/s), swimSpeed/ +-- swimJump (water px/s + stroke), swimGravity/swimMaxFall (buoyancy), +-- hurtPopX/hurtPopY (knockback launch px/s), hurtMs (control-off span), +-- invulnMs (post-hurt mercy). Settable any time, before or after the +-- player exists; unknown keys are stored verbatim for your own use. command b2kPlayerSet pKey, pValue put pValue into sPlayTune[toLower(pKey)] b2kPlayerTuneCache @@ -3199,6 +3210,10 @@ command b2kPlayerTuneCache put b2kPlayerGet("maxFall") into sPlayMaxFall put b2kPlayerGet("dropMs") into sPlayDropMS put b2kPlayerGet("climbSpeed") into sPlayClimbSpd + put b2kPlayerGet("swimSpeed") into sPlaySwimSpd + put b2kPlayerGet("swimJump") into sPlaySwimJump + put b2kPlayerGet("swimGravity") into sPlaySwimGrav + put b2kPlayerGet("swimMaxFall") into sPlaySwimMaxFall put b2kPlayerGet("hurtPopX") into sPlayHurtPopX put b2kPlayerGet("hurtPopY") into sPlayHurtPopY put b2kPlayerGet("hurtMs") into sPlayHurtMS @@ -3245,6 +3260,14 @@ function b2kPlayerDefault pKey return 260 case "climbspeed" return 160 + case "swimspeed" + return 150 + case "swimjump" + return 300 + case "swimgravity" + return 0.35 + case "swimmaxfall" + return 150 case "hurtpopx" return 220 case "hurtpopy" @@ -3263,9 +3286,10 @@ end b2kPlayerDefault -- idle/run resume. NOTE: a finishing land animation dispatches the art's -- b2kSpriteOnFinish message like any non-looping animation would. -- Wave 2 slots (all optional, so old five-argument calls keep working): --- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose -- --- sheets without those frames still read correctly. -command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt +-- pDuck falls back to the idle pose, pClimb and pHurt to the jump pose; +-- the Wave 4 pSwim falls back to the fall pose -- sheets without those +-- frames still read correctly. +command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt, pSwim put pIdle into sPlayAnims["idle"] put pRun into sPlayAnims["run"] put pJump into sPlayAnims["jump"] @@ -3290,6 +3314,11 @@ command b2kPlayerAnims pIdle, pRun, pJump, pFall, pLand, pDuck, pClimb, pHurt else put pHurt into sPlayAnims["hurt"] end if + if pSwim is empty then + put sPlayAnims["fall"] into sPlayAnims["swim"] + else + put pSwim into sPlayAnims["swim"] + end if put empty into sPlayAnimNow -- re-assert on the next tick if sPlayArt is empty then b2kPlayerResolveArt end b2kPlayerAnims @@ -3308,9 +3337,9 @@ function b2kPlayerOnGround return (sPlayGrounded is true) end b2kPlayerOnGround --- idle | run | jump | fall | duck | climb | hurt, plus "land" for exactly --- one frame on touch-down from jump/fall (dust puffs, landing sounds). --- A drop-through renders as "fall". Empty = no player. +-- idle | run | jump | fall | duck | climb | hurt | swim, plus "land" for +-- exactly one frame on touch-down from jump/fall (dust puffs, landing +-- sounds). A drop-through renders as "fall". Empty = no player. function b2kPlayerState return sPlayState end b2kPlayerState @@ -3355,6 +3384,25 @@ command b2kPlayerAddLadder pX1, pY1, pX2, pY2 put max(pY1, pY2) into sPlayLadB[sPlayLadN] end b2kPlayerAddLadder +-- Register a water ZONE (screen-px rect, any corner order). While the +-- player's centre is inside one, the controller SWIMS: gravity drops to +-- swimGravity (buoyant), the sink caps at swimMaxFall, UP/DOWN swim at +-- swimSpeed, and JUMP is a repeatable upward STROKE (swimJump) -- no +-- ground needed. Leaving the zone restores gravity. Pure polled geometry, +-- WORLD state (b2kClear wipes them), exactly like the ladder zones. +-- TIP: top the zone a little ABOVE the water's surface tiles, so the dive +-- in and the surface-out break the water where the surface art sits. +command b2kPlayerAddWater pX1, pY1, pX2, pY2 + if pX1 is not a number or pY1 is not a number \ + or pX2 is not a number or pY2 is not a number then exit b2kPlayerAddWater + if sPlayWatN is empty then put 0 into sPlayWatN + add 1 to sPlayWatN + put min(pX1, pX2) into sPlayWatL[sPlayWatN] + put max(pX1, pX2) into sPlayWatR[sPlayWatN] + put min(pY1, pY2) into sPlayWatT[sPlayWatN] + put max(pY1, pY2) into sPlayWatB[sPlayWatN] +end b2kPlayerAddWater + -- The knockback standard (spec section 9.4): control off, an away-pop -- (the sign of pFromX vs the player picks the direction; no/empty -- pFromX pops away from the facing), the hurt anim, control restored @@ -3369,6 +3417,7 @@ command b2kPlayerHurt pFromX if b2kPlayerHurtIs() then exit b2kPlayerHurt -- mercy window if sPlayControl is not true then exit b2kPlayerHurt -- a cutscene owns the body if sPlayClimb is true then b2kPlayerClimbEnd sBody[sPlayRef] + if sPlaySwim is true then b2kPlayerSwimEnd sBody[sPlayRef] if pFromX is a number then if pFromX > sPlayPX then put -1 into tDir @@ -3434,6 +3483,9 @@ command b2kPlayerForget pFull if sPlayClimb is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then b2kPlayerClimbEnd sBody[sPlayRef] end if + if sPlaySwim is true and sPlayRef is not empty and sBody[sPlayRef] is not empty then + b2kPlayerSwimEnd sBody[sPlayRef] + end if if sPlayDropUntil is not empty and sPlayDropUntil > 0 then b2kPlayerDropRestore put empty into sPlayRef put empty into sPlayArt @@ -3456,6 +3508,8 @@ command b2kPlayerForget pFull put empty into sPlayDropMask put false into sPlayClimb put empty into sPlayGravSave + put false into sPlaySwim + put empty into sPlaySwimGravSave put false into sPlayHurt put 0 into sPlayHurtEnd put 0 into sPlayHurtHalf @@ -3466,6 +3520,11 @@ command b2kPlayerForget pFull put empty into sPlayLadT put empty into sPlayLadR put empty into sPlayLadB + put 0 into sPlayWatN + put empty into sPlayWatL + put empty into sPlayWatT + put empty into sPlayWatR + put empty into sPlayWatB if pFull is true then put empty into sPlayTune end b2kPlayerForget @@ -3532,6 +3591,24 @@ command b2kPlayerClimbEnd pBody put false into sPlayClimb end b2kPlayerClimbEnd +-- Internal: enter the swim -- gravity drops to swimGravity (buoyant; the +-- body's own scale is saved and restored, like the climb). Mutually +-- exclusive with the climb: the tick only ever starts one of them. +command b2kPlayerSwimStart pBody + if sPlaySwim is true then exit b2kPlayerSwimStart + put b2BodyGravityScale(pBody) into sPlaySwimGravSave + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGrav, 0.35) + put true into sPlaySwim + put false into sPlayJumping +end b2kPlayerSwimStart + +command b2kPlayerSwimEnd pBody + if sPlaySwim is not true then exit b2kPlayerSwimEnd + b2SetGravityScale pBody, b2kNumberOr(sPlaySwimGravSave, 1) + put empty into sPlaySwimGravSave + put false into sPlaySwim +end b2kPlayerSwimEnd + -- Internal: open the drop-through window -- take the reserved one-way -- bit out of the player's mask so chains (alone) stop colliding. command b2kPlayerDropStart @@ -3568,7 +3645,7 @@ end b2kPlayerDropRestore -- frame while idle -- the tick's steady-state budget is unchanged. command b2kPlayerTick local tNow, tDT, tB, tVX, tVY, tAxis, tAxisY, tTarget, tAcc, tStep - local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i + local tPrevState, tWrite, tInZone, tDuck, tClimbJump, i, tInWater if sPlayRef is empty then exit b2kPlayerTick put sBody[sPlayRef] into tB if tB is empty then exit b2kPlayerTick @@ -3625,8 +3702,8 @@ command b2kPlayerTick put 1 into sPlayFacing end if end if - -- ladder presence: pure numeric compares on the probe's stashed - -- centre; zero zones = one compare and out + -- ladder + water presence: pure numeric compares on the probe's + -- stashed centre; zero zones of a kind = one compare and out put false into tInZone if sPlayLadN > 0 then repeat with i = 1 to sPlayLadN @@ -3637,13 +3714,26 @@ command b2kPlayerTick end if end repeat end if - if sPlayClimb is not true and tInZone is true then + put false into tInWater + if sPlayWatN > 0 then + repeat with i = 1 to sPlayWatN + if sPlayPX >= sPlayWatL[i] and sPlayPX <= sPlayWatR[i] \ + and sPlayPY >= sPlayWatT[i] and sPlayPY <= sPlayWatB[i] then + put true into tInWater + exit repeat + end if + end repeat + end if + if sPlayClimb is not true and sPlaySwim is not true and tInZone is true then -- enter: UP any time in-zone; DOWN only while AIRBORNE (a -- grounded DOWN is a duck -- or a drop-through on a chain) if tAxisY is -1 or (tAxisY is 1 and sPlayGrounded is not true) then b2kPlayerClimbStart tB end if end if + if sPlaySwim is not true and sPlayClimb is not true and tInWater is true then + b2kPlayerSwimStart tB -- submerged: buoyant gravity, stroke to rise + end if if sPlayClimb is true then if b2kActionPressed("jump") then -- JUMP exits with a normal jump. The same press edge is @@ -3678,7 +3768,33 @@ command b2kPlayerTick end if end if end if - if sPlayClimb is not true then + if sPlaySwim is true then + if tInWater is not true then + b2kPlayerSwimEnd tB -- surfaced / left the pool: gravity returns + else + -- horizontal: ease vx toward axis * swimSpeed (sluggish -- the + -- air-accel rate gives the underwater drag) + put tAxis * sPlaySwimSpd into tTarget + put sPlayAccelA * tDT into tStep + if tVX < tTarget then + put min(tTarget, tVX + tStep) into tVX + else + put max(tTarget, tVX - tStep) into tVX + end if + -- vertical: a JUMP press is a repeatable upward STROKE (no + -- ground gate -- the spec's water jump); else UP/DOWN swim at + -- swimSpeed; else the reduced gravity sinks you, capped low + if b2kActionPressed("jump") then + put 0 - sPlaySwimJump into tVY + else + if tAxisY is not 0 then + put tAxisY * sPlaySwimSpd into tVY + end if + end if + put true into tWrite + end if + end if + if sPlayClimb is not true and sPlaySwim is not true then -- horizontal: accelerate vx toward axis * moveSpeed (air = airAccel) put tAxis * sPlayMoveSpd into tTarget -- DUCK: down on the ground crouches and brakes to a stop at @@ -3733,9 +3849,18 @@ command b2kPlayerTick end if end if if sPlayJumping and tVY >= 0 then put false into sPlayJumping -- apex - if tVY > sPlayMaxFall then - put sPlayMaxFall into tVY - put true into tWrite + -- terminal velocity: the low swimMaxFall is the buoyant sink cap while + -- submerged; the normal maxFall (the character's terminal velocity) else + if sPlaySwim is true then + if tVY > sPlaySwimMaxFall then + put sPlaySwimMaxFall into tVY + put true into tWrite + end if + else + if tVY > sPlayMaxFall then + put sPlayMaxFall into tVY + put true into tWrite + end if end if -- GROUND-SNAP: grounded on FLAT ground, drifting upward, and we did -- not jump = the contact solver's push-out rebound after a hard @@ -3747,7 +3872,7 @@ command b2kPlayerTick -- must use b2kPlayerJump, which sets the jump flag (b2kPlayerHurt's -- pop rides the same flag). if sPlayGrounded and sPlayJumping is not true and sPlayClimb is not true \ - and tVY < 0 and abs(sPlayNormX) < 0.1 then + and sPlaySwim is not true and tVY < 0 and abs(sPlayNormX) < 0.1 then put 0 into tVY put true into tWrite end if @@ -3802,6 +3927,14 @@ command b2kPlayerTick end if end if end if + -- swim OWNS the state while submerged: the machine above ran the + -- grounded/airborne branch (sPlayClimb is false underwater), so override + -- to "swim" and keep the air counter clear -- surfacing must never read + -- as a long fall plus a phantom land tick. + if sPlaySwim is true and sPlayHurt is not true then + put "swim" into sPlayState + put 0 into sPlayAir + end if -- animations: only while controlling (manual poses own the art when -- control is off), and never let a vanished art control abort the -- frame -- the loop's ticks share one try block @@ -3820,7 +3953,7 @@ command b2kPlayerShowState pNow, pVX local tWant, tAnim, tAKey, tFPS, tFlip put sPlayState into tWant if pNow < sPlayHoldMS and tWant is not "jump" and tWant is not "fall" \ - and tWant is not "hurt" and tWant is not "climb" then + and tWant is not "hurt" and tWant is not "climb" and tWant is not "swim" then put empty into tAnim -- mid land-flourish: leave it playing else if tWant is "land" and sPlayAnims["land"] is empty then