A Boomtown rank-up and a tower's step up grow in place from the old height, by a grow-from fraction in the instance texel #145

Owner

Tracker: #3789

Step 3 of plan #3561. A Boomtown rank-up, and a monument tower's step up, now grow the drawn building from the height the old version stands at to the new version's height over the whole rise, never from the ground (D5, D6, D11). Until step 4, a reduced-motion re-model still swaps at once and a tower's step down still re-rises from its base, as on main.

What changes

  • The rise program (src/client/render/units/paint.ts): the RISE line scales y by max(0.02, mix(fract(animTexel.y), 1.0, curve)). The slot channel's fraction is the share of its height the instance grows from; with 0 it is main's rise. The defines and the program cache key stay, so no program is added.
  • The instance texel (src/client/render/boomtown/anim.ts): InstanceAnim.set writes slot + from, setSlot (and so fadeBy) keeps the fraction, and rise writes a rise start with its from. riseScale(start, from, clock) is the program's CPU mirror. life.ts's risen moved here, and life's spinner poses a building's moving parts by it with the placement's from.
  • Batches (batch.ts): place and rise carry from. Moving parts still stand built.
  • Model heights (models.ts): modelHeight(key) is a building version's lookHeight or a tower's towerHeight(step).
  • The layer (layer.ts):
    • A Placement holds its from and remembers the version that stood (stood: model, rise start and from). A version that begins its rise is remembered as it begins. A version that stands at once (every placement of a city first presented, a monument first shown, and a reduced-motion begin) is remembered at full height. A re-model whose new version has not yet begun its rise or stood keeps the memory.
    • A rise grows from modelHeight(old) × riseScale(old start, old from, clock) / modelHeight(new) where that is below 1, and otherwise from the base. The monument takes the same path, so a tower's step up grows. A new placement in a city already laid rises from its base with its cage, as on main. A tier step places the instance with the placement's start and from.
    • A rise starts on the first frame that shows it. On main, begin ran in present or work and took the clock of the frame before, so a building's first drawn frame was already one frame into its rise (k = dt / 1.1). A rise begun there now starts at the next frame's clock, so the new version's first frame stands exactly at the old height, and a building rising from its base exactly at 0.02 of its height.
    • A rise begins only in view. A city out of view still draws its Far tier, unseen, so on main a rank-up there ran its whole rise off-screen. A placement now begins its rise on the first frame that shows it in view (place in a batch drawn in view, or showAt when its batch comes into view), so a rank-up made out of view grows when the city is next seen, from the version that stood in view before it. The started hook of TierLayer, which only this path used, leaves.
    • A queued rise begins in any batch its city draws. The layer queues each such placement with its city alone, and beginRises starts it in a batch the city draws in view that holds its model, Full or Mid before Far, so its cage goes up. A rank-up placed on a zoom-in fade's last frame therefore grows from the old height, with its cage, once the fade settles.
    • A town's liveness is settled before the frame's rises begin. Life.look sets each town live (in view, its monument tile in sight) at the start of the layer's animate, before beginRises, and Life.frame reads it. So a rise that begins on the frame its city comes into view raises its scaffold cage.
    • Three existing tests pinned the old start clock and now read the start on the first drawn frame: layer.test.ts's "a building rises from the first frame that draws it in the shown tier…", and life.test.ts's rise test and its 8-cage-cap test (the turned-away cage is born 0.02 s later).
  • The Wreck Yard (src/mock/wrecks.ts): a rankup scene. At stage time, before the host's first push, it sets the held city's roof building to &rank= (1–19, default 9) and takes the building nearest it out of the layout. At the death's time its cue installs a new layout object, { ...layout, buildings }, with the roof building at its next rank and a rank-1 building on the emptied plot, then syncs the game's blocks. The Stage's city type widens to the whole layout: CityLayout.

Tests

  • layer.test.ts "Boomtown growth", each reading the drawn height as riseScale(start, from, clock) × modelHeight(model) from the still instance's start texel and its slot texel's fraction:
    • a growth step that ranks a standing building up at Full: on the new version's first frame it draws at the old version's body height (to 1e-4) with the old instance gone; it never draws below that height; it stays below its own height on every 0.02 s frame to 0.66 s and first reaches it at 0.68 s; it stands at its own height past RISE_SECONDS;
    • a rank-up 0.4 s into a growth grows on from the height the first had reached, with from below 0.98;
    • a rank-up whose new version is not yet built keeps the old version, at its full height, until it is, then grows from it;
    • a rank-up out of view leaves the Far batch at once and grows from the old height on the first frame back in view; after two rank-ups out of view it grows from the version that stood before them;
    • a rank-up presented on a Mid→Full fade's last frame draws at the old height with its cage at Full on the first frame Full shows alone, then grows as above;
    • a rank-up presented while a city fades from Far to Mid grows from the old height with its cage at Mid;
    • a rank-up made while its city is in the window but out of view at Full draws at the old height with its cage on the frame the city comes into view, then grows as above;
    • a tower's step up at Mid draws the new tower at the old tower's towerHeight on its first frame, never below it, with a cage;
    • a tier step during a growth places the new tier's instance with the same start and from, drawing the same height on the same frames, and a fadeBy keeps the fraction;
    • a building on an empty plot and one on the plot its fallen building left each draw at 0.02 of their height on their first frame, with a cage at Full;
    • a monument first shown at Mid, as its tile is explored after its city was laid, draws at its full towerHeight on its first frame with no cage.
  • life.test.ts: a Wind tower's sails ride its roof through a rank-up, posed at riseScale × their pivot on every frame; a building a rank-up grows keeps its lights off until its growth ends.
  • cities.phone.ts plays the rankup scene in every finish, with and without reduced motion, with no WebGL or shader error and no program added from before the rank-up to the scene's end.
  • Mutations, each in a whole-tree copy (control 49/49 on layer.test.ts and life.test.ts):
    • a growth rise started with from 0 fails the first-frame check (1.205 tiles off) and the tower's case;
    • the placement forgetting its remembered version when place removes the old instance from a batch not drawn in view fails the out-of-view case (and every other growth case, since a watched city's Far batch is such a batch);
    • main's curve started part-way along (from 0, start = clock − k_f × RISE_SECONDS) passes the first frame but fails the climb: it reaches the new height by 0.18 s;
    • setSlot writing the bare slot fails the fade-slot case alone;
    • beginRises taking the last batch the city draws, Far included, rather than Full or Mid first, fails the Far-to-Mid fade case alone.
  • A probe over whole-tree copies of 21673c8 and of this head ran a rank-up presented 0.005, 0.02 and 0.15 s before a zoom-in fade's end (Mid→Full, Far→Mid and Far→Full, with the new variant built beforehand or by work), then a zoom back out, and a rank-up at Full and at Mid as the city comes into view. On this head every case draws the old height (1.5076) on the new version's first frame with its cage, never draws below it, has begun its rise and remembers the new version 3 s later, and stands at its new height (1.6544) through the zoom back out. On 21673c8, 18 of the 20 cases failed: each fade-end case drew 0.0943 with no cage and never began its rise; the 0.15 s cases into Far→Mid and Far→Full grew with no cage; and both view entries grew with no cage.

Draw checks

The Map Yard bench (src/bench/map.ts, WebKit, iPhone 15 landscape) ran on m5 with 8 arms in one map.ts 7 … call per session: base 3d7dd93 and head 03c5436 at rest and with grown=full, and the head's ?doom=battle, battle&spare, nuke and nuke&spare. 5 sessions, 840 pages, 22:35–23:38. Both arms were built from git archive trees and served by vite preview(). Another project's gate (an sx build at about 99% of one core) ran throughout.

view calls base / head triangles base / head paired head − base (phone) pooled head − base
1× 68 / 68 336,400 / 336,400–336,480 +0.000 ms (+0.000) −0.027
0.5× 65 / 65 415,516 / 415,516 +0.003 ms (+0.025) +0.110
0.25× 41 / 41 518,984 / 518,984 +0.023 ms (+0.175) +0.743
1× full 68 / 68 463,939 / 463,939 −0.003 ms (−0.025) +0.000
0.5× full 66 / 66 526,887 / 526,887 −0.007 ms (−0.050) −0.123
0.25× full 42 / 42 668,691 / 668,691 +0.003 ms (+0.025) −0.023
  • Within D10 at every view and growth: the same calls, and a paired median at most +0.023 ms (phone +0.175 ms). The 80-triangle spread is one smoke puff between loads. The pooled medians swing with m5's two frame-time modes; two session-views (session 1 at 0.5×, session 2 at 0.25×) paired at +0.76 and +0.85 ms while the other 28 sat within ±0.06.
  • Doom peaks against their spare arms (paired, 1× / 0.5× / 0.25×): battle +0.020 / +0.037 / +0.043 ms, nuke +0.013 / +0.040 / +0.073 ms (phone up to +0.55 ms).
  • The commits after 03c5436 change only when a rise starts, not what draws at rest. The review of 21673c8 benched it on m5 against the base, 4 arms (base and head, at rest and grown=full), 5 sessions × 7 runs, 420 pages: the same calls and triangles in every load, and paired head − base +0.000 / +0.003 / +0.003 ms at rest and +0.000 / +0.003 / −0.003 ms full. The two commits after 21673c8 change only which batch a queued rise begins in and when a town's liveness is set, on the CPU. One load per view of 21673c8 and of the final head 11e5cac on s32, with motion as the bench loads it, drew the same calls and triangles: 68 / 65 / 41 calls with 336,400 / 415,516 / 518,984 triangles at rest (21673c8 drew 336,480 at 1×, one smoke puff), and 68 / 66 / 42 calls with 463,939 / 526,887 / 668,691 full.
  • The Map Yard's cities hold still while it benches, so the bench does not time a rank-up. In Node (tsx), a rank-up in a 16-building city grown 250 steps, at Full in Toybox, 15 interleaved runs: present 0.100 → 0.114 ms, the variant's build 0.484 → 0.448 ms, and 40 frames over the growth 0.363 → 0.446 ms (+0.002 ms a frame), base → head.

Renders for the Reviewer

The Wreck Yard's rankup scene at ?mode=frames&frames=-0.1,0.02,0.1,0.2,0.3,0.45,0.6,0.7,1.2 in Die-cast, Toybox and Micro-voxel, at the default zoom and at &zoom=3, with no page error and no program added. In every finish the watched building (rank 9 → 10) draws its new version squeezed to the old version's top on the 0.02 frame under its cage, and climbs to its own height by 0.7 s. The rank-1 building rises from its base on the emptied plot. Adding that rank-1 building lowers the city's median, so the monument's tower steps down and re-rises from its base, as on main until step 4. At &rank=1, 9 and 19 in Toybox, frames −0.1 and 1.2 show the same family's ground plan before and after. Classic swaps at once at 0.02, as on main.

Play

The esbuild bundles of src/ai/sim.ts and src/bench/net.ts from the base 3d7dd93 and the head 11e5cac are byte-identical (sim a3c510e5…, net 4ffd0007…), so the 14 AI matches and npm run bench were not run.

Gates

  • sak-gate fast (tracker gate run 365) on 03c5436: passed, npm test 986/986.
  • sak-gate full (tracker gate run 372) on 21673c8: passed, npm test 996/996 and test:phone 181/181, the rankup phone test included.
  • sak-gate full (tracker gate run 397) on 11e5cac: passed, npm test 999/999 and test:phone 181/181.

changelog.md and the in-game help are step 6's, for the whole plan.

Tracker: [#3789](https://sak.swipelab.com/tasks/3789) Step 3 of plan [#3561](https://sak.swipelab.com/tasks/3561). A Boomtown rank-up, and a monument tower's step up, now grow the drawn building from the height the old version stands at to the new version's height over the whole rise, never from the ground (D5, D6, D11). Until step 4, a reduced-motion re-model still swaps at once and a tower's step down still re-rises from its base, as on main. ## What changes - **The rise program** (src/client/render/units/paint.ts): the RISE line scales y by `max(0.02, mix(fract(animTexel.y), 1.0, curve))`. The slot channel's fraction is the share of its height the instance grows from; with 0 it is main's rise. The defines and the program cache key stay, so no program is added. - **The instance texel** (src/client/render/boomtown/anim.ts): `InstanceAnim.set` writes `slot + from`, `setSlot` (and so `fadeBy`) keeps the fraction, and `rise` writes a rise start with its `from`. `riseScale(start, from, clock)` is the program's CPU mirror. life.ts's `risen` moved here, and life's spinner poses a building's moving parts by it with the placement's `from`. - **Batches** (batch.ts): `place` and `rise` carry `from`. Moving parts still stand built. - **Model heights** (models.ts): `modelHeight(key)` is a building version's `lookHeight` or a tower's `towerHeight(step)`. - **The layer** (layer.ts): - A `Placement` holds its `from` and remembers the version that stood (`stood`: model, rise start and `from`). A version that begins its rise is remembered as it begins. A version that stands at once (every placement of a city first presented, a monument first shown, and a reduced-motion `begin`) is remembered at full height. A re-model whose new version has not yet begun its rise or stood keeps the memory. - A rise grows from `modelHeight(old) × riseScale(old start, old from, clock) / modelHeight(new)` where that is below 1, and otherwise from the base. The monument takes the same path, so a tower's step up grows. A new placement in a city already laid rises from its base with its cage, as on main. A tier step places the instance with the placement's start and `from`. - **A rise starts on the first frame that shows it.** On main, `begin` ran in `present` or `work` and took the clock of the frame before, so a building's first drawn frame was already one frame into its rise (k = dt / 1.1). A rise begun there now starts at the next frame's clock, so the new version's first frame stands exactly at the old height, and a building rising from its base exactly at 0.02 of its height. - **A rise begins only in view.** A city out of view still draws its Far tier, unseen, so on main a rank-up there ran its whole rise off-screen. A placement now begins its rise on the first frame that shows it in view (`place` in a batch drawn in view, or `showAt` when its batch comes into view), so a rank-up made out of view grows when the city is next seen, from the version that stood in view before it. The `started` hook of `TierLayer`, which only this path used, leaves. - **A queued rise begins in any batch its city draws.** The layer queues each such placement with its city alone, and `beginRises` starts it in a batch the city draws in view that holds its model, Full or Mid before Far, so its cage goes up. A rank-up placed on a zoom-in fade's last frame therefore grows from the old height, with its cage, once the fade settles. - **A town's liveness is settled before the frame's rises begin.** `Life.look` sets each town live (in view, its monument tile in sight) at the start of the layer's `animate`, before `beginRises`, and `Life.frame` reads it. So a rise that begins on the frame its city comes into view raises its scaffold cage. - Three existing tests pinned the old start clock and now read the start on the first drawn frame: layer.test.ts's "a building rises from the first frame that draws it in the shown tier…", and life.test.ts's rise test and its 8-cage-cap test (the turned-away cage is born 0.02 s later). - **The Wreck Yard** (src/mock/wrecks.ts): a `rankup` scene. At stage time, before the host's first push, it sets the held city's roof building to `&rank=` (1–19, default 9) and takes the building nearest it out of the layout. At the death's time its cue installs a new layout object, `{ ...layout, buildings }`, with the roof building at its next rank and a rank-1 building on the emptied plot, then syncs the game's blocks. The Stage's `city` type widens to the whole `layout: CityLayout`. ## Tests - layer.test.ts "Boomtown growth", each reading the drawn height as `riseScale(start, from, clock) × modelHeight(model)` from the still instance's start texel and its slot texel's fraction: - a growth step that ranks a standing building up at Full: on the new version's first frame it draws at the old version's body height (to 1e-4) with the old instance gone; it never draws below that height; it stays below its own height on every 0.02 s frame to 0.66 s and first reaches it at 0.68 s; it stands at its own height past RISE_SECONDS; - a rank-up 0.4 s into a growth grows on from the height the first had reached, with `from` below 0.98; - a rank-up whose new version is not yet built keeps the old version, at its full height, until it is, then grows from it; - a rank-up out of view leaves the Far batch at once and grows from the old height on the first frame back in view; after two rank-ups out of view it grows from the version that stood before them; - a rank-up presented on a Mid→Full fade's last frame draws at the old height with its cage at Full on the first frame Full shows alone, then grows as above; - a rank-up presented while a city fades from Far to Mid grows from the old height with its cage at Mid; - a rank-up made while its city is in the window but out of view at Full draws at the old height with its cage on the frame the city comes into view, then grows as above; - a tower's step up at Mid draws the new tower at the old tower's `towerHeight` on its first frame, never below it, with a cage; - a tier step during a growth places the new tier's instance with the same start and `from`, drawing the same height on the same frames, and a `fadeBy` keeps the fraction; - a building on an empty plot and one on the plot its fallen building left each draw at 0.02 of their height on their first frame, with a cage at Full; - a monument first shown at Mid, as its tile is explored after its city was laid, draws at its full `towerHeight` on its first frame with no cage. - life.test.ts: a Wind tower's sails ride its roof through a rank-up, posed at `riseScale` × their pivot on every frame; a building a rank-up grows keeps its lights off until its growth ends. - cities.phone.ts plays the `rankup` scene in every finish, with and without reduced motion, with no WebGL or shader error and no program added from before the rank-up to the scene's end. - Mutations, each in a whole-tree copy (control 49/49 on layer.test.ts and life.test.ts): - a growth rise started with `from` 0 fails the first-frame check (1.205 tiles off) and the tower's case; - the placement forgetting its remembered version when `place` removes the old instance from a batch not drawn in view fails the out-of-view case (and every other growth case, since a watched city's Far batch is such a batch); - main's curve started part-way along (`from` 0, start = clock − k_f × RISE_SECONDS) passes the first frame but fails the climb: it reaches the new height by 0.18 s; - `setSlot` writing the bare slot fails the fade-slot case alone; - `beginRises` taking the last batch the city draws, Far included, rather than Full or Mid first, fails the Far-to-Mid fade case alone. - A probe over whole-tree copies of 21673c8 and of this head ran a rank-up presented 0.005, 0.02 and 0.15 s before a zoom-in fade's end (Mid→Full, Far→Mid and Far→Full, with the new variant built beforehand or by `work`), then a zoom back out, and a rank-up at Full and at Mid as the city comes into view. On this head every case draws the old height (1.5076) on the new version's first frame with its cage, never draws below it, has begun its rise and remembers the new version 3 s later, and stands at its new height (1.6544) through the zoom back out. On 21673c8, 18 of the 20 cases failed: each fade-end case drew 0.0943 with no cage and never began its rise; the 0.15 s cases into Far→Mid and Far→Full grew with no cage; and both view entries grew with no cage. ## Draw checks The Map Yard bench (src/bench/map.ts, WebKit, iPhone 15 landscape) ran on m5 with 8 arms in one `map.ts 7 …` call per session: base 3d7dd93 and head 03c5436 at rest and with `grown=full`, and the head's `?doom=battle`, `battle&spare`, `nuke` and `nuke&spare`. 5 sessions, 840 pages, 22:35–23:38. Both arms were built from `git archive` trees and served by vite `preview()`. Another project's gate (an `sx` build at about 99% of one core) ran throughout. | view | calls base / head | triangles base / head | paired head − base (phone) | pooled head − base | |---|---|---|---|---| | 1× | 68 / 68 | 336,400 / 336,400–336,480 | +0.000 ms (+0.000) | −0.027 | | 0.5× | 65 / 65 | 415,516 / 415,516 | +0.003 ms (+0.025) | +0.110 | | 0.25× | 41 / 41 | 518,984 / 518,984 | +0.023 ms (+0.175) | +0.743 | | 1× full | 68 / 68 | 463,939 / 463,939 | −0.003 ms (−0.025) | +0.000 | | 0.5× full | 66 / 66 | 526,887 / 526,887 | −0.007 ms (−0.050) | −0.123 | | 0.25× full | 42 / 42 | 668,691 / 668,691 | +0.003 ms (+0.025) | −0.023 | - Within D10 at every view and growth: the same calls, and a paired median at most +0.023 ms (phone +0.175 ms). The 80-triangle spread is one smoke puff between loads. The pooled medians swing with m5's two frame-time modes; two session-views (session 1 at 0.5×, session 2 at 0.25×) paired at +0.76 and +0.85 ms while the other 28 sat within ±0.06. - Doom peaks against their spare arms (paired, 1× / 0.5× / 0.25×): battle +0.020 / +0.037 / +0.043 ms, nuke +0.013 / +0.040 / +0.073 ms (phone up to +0.55 ms). - The commits after 03c5436 change only when a rise starts, not what draws at rest. The review of 21673c8 benched it on m5 against the base, 4 arms (base and head, at rest and `grown=full`), 5 sessions × 7 runs, 420 pages: the same calls and triangles in every load, and paired head − base +0.000 / +0.003 / +0.003 ms at rest and +0.000 / +0.003 / −0.003 ms full. The two commits after 21673c8 change only which batch a queued rise begins in and when a town's liveness is set, on the CPU. One load per view of 21673c8 and of the final head 11e5cac on s32, with motion as the bench loads it, drew the same calls and triangles: 68 / 65 / 41 calls with 336,400 / 415,516 / 518,984 triangles at rest (21673c8 drew 336,480 at 1×, one smoke puff), and 68 / 66 / 42 calls with 463,939 / 526,887 / 668,691 full. - The Map Yard's cities hold still while it benches, so the bench does not time a rank-up. In Node (tsx), a rank-up in a 16-building city grown 250 steps, at Full in Toybox, 15 interleaved runs: `present` 0.100 → 0.114 ms, the variant's build 0.484 → 0.448 ms, and 40 frames over the growth 0.363 → 0.446 ms (+0.002 ms a frame), base → head. ## Renders for the Reviewer The Wreck Yard's `rankup` scene at `?mode=frames&frames=-0.1,0.02,0.1,0.2,0.3,0.45,0.6,0.7,1.2` in Die-cast, Toybox and Micro-voxel, at the default zoom and at `&zoom=3`, with no page error and no program added. In every finish the watched building (rank 9 → 10) draws its new version squeezed to the old version's top on the 0.02 frame under its cage, and climbs to its own height by 0.7 s. The rank-1 building rises from its base on the emptied plot. Adding that rank-1 building lowers the city's median, so the monument's tower steps down and re-rises from its base, as on main until step 4. At `&rank=1`, `9` and `19` in Toybox, frames −0.1 and 1.2 show the same family's ground plan before and after. Classic swaps at once at 0.02, as on main. ## Play The esbuild bundles of src/ai/sim.ts and src/bench/net.ts from the base 3d7dd93 and the head 11e5cac are byte-identical (sim a3c510e5…, net 4ffd0007…), so the 14 AI matches and `npm run bench` were not run. ## Gates - `sak-gate fast` (tracker gate run 365) on 03c5436: passed, `npm test` 986/986. - `sak-gate full` (tracker gate run 372) on 21673c8: passed, `npm test` 996/996 and `test:phone` 181/181, the rankup phone test included. - `sak-gate full` (tracker gate run 397) on 11e5cac: passed, `npm test` 999/999 and `test:phone` 181/181. changelog.md and the in-game help are step 6's, for the whole plan.
agra merged commit 776e4b6908 into task/3561-a-growing-building-keeps-its-style-and-g 2026-10-25 04:16:59 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
agra/mini-alert!145
No description provided.