From ea416d80b5d85e65d894c86426eeb129764b78c1 Mon Sep 17 00:00:00 2001 From: qbisi Date: Sun, 11 Oct 2026 15:48:31 +0800 Subject: [PATCH] feat(player): draw the Phoenix The page drew a Phoenix as the generic disc under its name. It now has a sprite after the model's top view in its Move state (Mech_Default_16_1, 18.2 m by 12.6 m): a long pointed nose striped in the team's colour over a short body, a tail boom each side ending in fins, a white pylon outboard of each boom, and from each pylon a long panel in the team's colour swept back and out. It hovers; it has no attack animation. Its exhausts under the body's back glow #00eaff. That is read off the model: its one material, Phoenix_lv1, has an _EmissionColor of 1.5 and an _EmissionMap whose brightest texels average (0, 156, 205); scaled by 1.5, that is (0, 234, 255+). Rendering the emission map over the model shows it lit only in two small patches under the back of the body, where the glow is drawn, and a few yellow texels at the pylons' feet, not drawn. models/phoenix.svg is new and listed in models/README.md; export-models.mjs --check passes on all twelve. The demo still fields six kinds, so its comment and the crate README now say six of the units with sprites. Co-Authored-By: Claude Opus 5.5 --- crates/player/README.md | 2 +- crates/player/scenes/six-units.yaml | 2 +- crates/player/web/player.js | 2 +- crates/player/web/sprites.js | 63 +++++++++++++++++++++++++++-- models/README.md | 1 + models/phoenix.svg | 58 ++++++++++++++++++++++++++ scripts/player/export-models.mjs | 2 +- 7 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 models/phoenix.svg diff --git a/crates/player/README.md b/crates/player/README.md index c50654554..7b575bfcb 100644 --- a/crates/player/README.md +++ b/crates/player/README.md @@ -69,5 +69,5 @@ not. | --- | --- | | `src/` | the timeline the page plays and the page that carries it | | `web/` | the page's shell, style, sprites and player, built into the binary | -| `scenes/` | `six-units.yaml`, the demo the test fights: the six units with sprites, each side's constructions and a shield | +| `scenes/` | `six-units.yaml`, the demo the test fights: six of the units with sprites, each side's constructions and a shield | | `tests/` | the demo fought by the simulator and read back from the page | diff --git a/crates/player/scenes/six-units.yaml b/crates/player/scenes/six-units.yaml index 897ae54c8..82ae28781 100644 --- a/crates/player/scenes/six-units.yaml +++ b/crates/player/scenes/six-units.yaml @@ -1,4 +1,4 @@ -# The player's demo: each side fields the six units the page has sprites +# The player's demo: each side fields six of the units the page has sprites # for (Rhino, Crawler, Sledgehammer, Wasp, Arclight and Marksman), a # Defensive Wall, an Anti-Armor Turret, a Rapid-Fire Turret and a shield, # beside the map's two towers; both sides place the same board. The crate's diff --git a/crates/player/web/player.js b/crates/player/web/player.js index b9eecedab..981bb644a 100644 --- a/crates/player/web/player.js +++ b/crates/player/web/player.js @@ -17,7 +17,7 @@ // The game's English names, as docs/terminology/ lists them. const NAMES = { marksman: 'Marksman', arclight: 'Arclight', rhino: 'Rhino', crawler: 'Crawler', - sledgehammer: 'Sledgehammer', wasp: 'Wasp', energy_tower: 'Energy Tower', + sledgehammer: 'Sledgehammer', wasp: 'Wasp', phoenix: 'Phoenix', energy_tower: 'Energy Tower', research_center: 'Research Center', anti_armor_turret: 'Anti-Armor Cannon', rapid_fire_turret: 'Rapid-Fire Cannon', defensive_wall: 'Defensive Wall', }; diff --git a/crates/player/web/sprites.js b/crates/player/web/sprites.js index 8310770ad..781b5d56b 100644 --- a/crates/player/web/sprites.js +++ b/crates/player/web/sprites.js @@ -29,12 +29,13 @@ const Sprites = (() => { // Each unit's emissive colour, read off its model's emission texture. const GLOW = { marksman: '#5ff2ff', arclight: '#b3f2ff', rhino: '#ffa236', crawler: '#ffb648', - sledgehammer: '#55e8f4', wasp: '#ffd84e', + sledgehammer: '#55e8f4', wasp: '#ffd84e', phoenix: '#00eaff', }; // The model's footprint in metres, width by length, which frames an icon. const SIZE = { marksman: [12, 22], arclight: [18, 13], rhino: [28, 24], crawler: [4.2, 5.2], - sledgehammer: [7.6, 13], wasp: [7.4, 6.6], energy_tower: [22, 22], research_center: [22, 22], + sledgehammer: [7.6, 13], wasp: [7.4, 6.6], phoenix: [18.4, 12.8], + energy_tower: [22, 22], research_center: [22, 22], anti_armor_turret: [24, 24], rapid_fire_turret: [14, 20], defensive_wall: [10, 10], }; @@ -679,6 +680,62 @@ const Sprites = (() => { } } + // Phoenix, as its model flies: a long pointed nose over a short body, a + // tail boom each side of it ending in fins, a white pylon outboard of + // each boom, and from each pylon a long panel in the team's colour swept + // back and out. Its exhausts glow in its emissive colour. + function phoenix(c, p) { + const T = p.team; + const bob = Math.sin(p.t * 2.4 + p.walk) * 0.03; + c.scale(1 + bob, 1 + bob); + for (const sx of [-1, 1]) { + // the swept panel, from the pylon back and out to its capped tip + poly(c, [sx * 4.4, -0.3, sx * 8.7, 2.2, sx * 7.9, 3.8, sx * 4.1, 1.5]); + paint(c, T.accent); + line(c, sx * 4.6, 0.6, sx * 8.3, 2.9, T.deep, 0.25); + poly(c, [sx * 8.55, 2.0, sx * 9.2, 2.4, sx * 8.4, 4.05, sx * 7.75, 3.7]); + paint(c, T.deep); + // the strut out to the pylon + poly(c, [sx * 1.0, -0.9, sx * 4.0, -0.7, sx * 4.0, 0.5, sx * 1.0, 0.6]); + paint(c, M.dark); + rr(c, sx * 2.75 - 0.25, -1.7, 0.5, 1.0, 0.1); + paint(c, T.accent, M.line, 0.6); + // the pylon + poly(c, [sx * 3.95, -3.5, sx * 4.6, -3.5, sx * 4.65, 1.9, sx * 4.3, 3.2, sx * 3.9, 2.2]); + paint(c, M.white); + rr(c, sx * 4.25 - 0.3, -4.1, 0.6, 0.7, 0.1); + paint(c, M.mid); + // the tail boom, widening into its tailplane, red fins at its end + poly(c, [sx * 1.25, -0.8, sx * 2.15, -0.8, sx * 2.25, 3.2, sx * 3.9, 5.3, sx * 3.8, 5.9, sx * 2.1, 5.2, sx * 1.6, 6.3, sx * 1.35, 3.3]); + paint(c, M.white); + poly(c, [sx * 1.5, -0.5, sx * 1.9, -0.5, sx * 1.95, 2.8, sx * 1.6, 2.8]); + paint(c, M.mid, null); + for (const [x, y0, y1] of [[1.75, 4.7, 6.3], [3.85, 4.6, 6.0]]) { + rr(c, sx * x - 0.17, y0, 0.34, y1 - y0, 0.1); + paint(c, T.accent, M.line, 0.6); + } + } + // the body behind the cockpit, its dark intake under it + rr(c, -0.65, 0.4, 1.3, 1.6, 0.25); + paint(c, '#2c3a55'); + // the exhausts under the intake's back, which glow + for (const sx of [-1, 1]) glow(c, sx * 0.45, 1.9, 0.9, GLOW.phoenix, 0.7 + 0.3 * Math.sin(p.t * 23 + sx)); + rr(c, -0.5, -0.9, 1.0, 1.35, 0.15); + paint(c, M.white); + // the collar around the nose's root + poly(c, sym([0, -1.9, 0.9, -1.6, 1.5, -0.95, 0.9, -0.85, 0, -1.05])); + paint(c, M.white); + // the nose, striped in the team's colour, its canopy along the middle + poly(c, sym([0, -6.3, 0.3, -5.2, 0.75, -2.6, 0.95, -1.0, 0, -0.9])); + paint(c, M.white); + for (const sx of [-1, 1]) { + poly(c, [sx * 0.32, -5.0, sx * 0.55, -3.9, sx * 0.85, -1.9, sx * 0.62, -2.0, sx * 0.4, -3.9]); + paint(c, T.accent, null); + } + poly(c, sym([0, -2.7, 0.24, -2.4, 0.24, -1.0, 0, -0.95])); + paint(c, '#9fd6f5', M.line, 0.6); + } + // A unit the page has no sprite for: a disc of its collision size, marked. function generic(c, p, radius, label) { circle(c, 0, 0, radius); @@ -700,7 +757,7 @@ const Sprites = (() => { } } - const UNITS = { marksman, arclight, rhino, crawler, sledgehammer, wasp }; + const UNITS = { marksman, arclight, rhino, crawler, sledgehammer, wasp, phoenix }; function drawUnit(c, kind, p, radius) { const draw = UNITS[kind]; diff --git a/models/README.md b/models/README.md index 1e6273989..c7b1f9bf4 100644 --- a/models/README.md +++ b/models/README.md @@ -30,6 +30,7 @@ the game is in them. | `crawler` | Crawler | ![](crawler.svg) | | `sledgehammer` | Sledgehammer | ![](sledgehammer.svg) | | `wasp` | Wasp | ![](wasp.svg) | +| `phoenix` | Phoenix | ![](phoenix.svg) | | `energy_tower` | Energy Tower | ![](energy_tower.svg) | | `research_center` | Research Center | ![](research_center.svg) | | `anti_armor_turret` | Anti-Armor Cannon | ![](anti_armor_turret.svg) | diff --git a/models/phoenix.svg b/models/phoenix.svg new file mode 100644 index 000000000..2e203601f --- /dev/null +++ b/models/phoenix.svg @@ -0,0 +1,58 @@ + +phoenix + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/player/export-models.mjs b/scripts/player/export-models.mjs index f810b50e0..29dfd98ed 100644 --- a/scripts/player/export-models.mjs +++ b/scripts/player/export-models.mjs @@ -23,7 +23,7 @@ const OUT = join(ROOT, 'models'); // The sprites the page draws, and whether each is a building. const MODELS = [ ['marksman', false], ['arclight', false], ['rhino', false], ['crawler', false], - ['sledgehammer', false], ['wasp', false], ['energy_tower', true], + ['sledgehammer', false], ['wasp', false], ['phoenix', false], ['energy_tower', true], ['research_center', true], ['anti_armor_turret', true], ['rapid_fire_turret', true], ['defensive_wall', true], ];