From 65e99b25e71263e3520bc836cd921e336137599e Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 8 Jun 2026 17:32:21 -0700 Subject: [PATCH] Add retreating warship indicator and warship 2-color treatment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Warships now render with a dedicated center accent band so their state reads at a glance: - Normal: center + outer ring share the territory color (2-color look), hull uses the border color. - Angry (attacking): outer ring and center turn red. - Retreating to repair: the center blinks black. The warship sprite center moved to its own gray value (100) in the unit atlas so the shader can drive it via a new fourth replacement band, with no per-unit-type branching — the missiles' shared 130 blend band is untouched. Warship repair-retreat (warshipState.state === "retreating") now feeds the existing UnitState.retreating boolean in UnitView, which UnitPass maps to a FLAG_RETREATING instance flag. --- resources/atlases/unit-atlas.png | Bin 298 -> 595 bytes src/client/render/gl/passes/UnitPass.ts | 22 +++++++++++------- .../render/gl/shaders/unit/unit.frag.glsl | 21 +++++++++++++---- src/client/view/UnitView.ts | 8 +++++-- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/resources/atlases/unit-atlas.png b/resources/atlases/unit-atlas.png index ceb8edcb0f2eeaae00c8203827a8d9429e1ce103..93df24b3211d4f431bf369aff54b45ff5662c934 100644 GIT binary patch delta 570 zcmV-A0>%BR0@DPLB!2;OQb$4o*~u(_00004XF*Lt006O%3;baP0000WV@Og>004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00009a7bBm000XU000XU0RWnu z7ytkO2XskIMF;8z2>=o~uCmaz00050Nkl> zVGopml*=AdgHIGe5z`1cdF2pZ*3Kj|l7DHmgTO3We;DIcO4OA1KFhDLuk?Q}008p3 zuKEg)-S2lT$XpOO0C+kzgrJ;r&%V9ks{zykXC|TvbQc4zgbl@k&U>F{9M6eXIQHZ5 z)tvA1{Y{{IQGd5qhqX4p0@Fwowpx1x(I^^~{l*x{&&~>*wKhBFSkM7L4k1iIN8wtQ zWg0r~eKy7@d}g6R*h=Vnkqo05eLu@ZBNK7#xIqK}GCq6R+*+{VF*mbFgbkr-eHk4` zVJo4l*|!`ya&QS*4Z7MKc(m4z*xp(=8-W6JG^FT9E`M~hC(o?SFW_Q~n!*!f9Of(Q zx@rn(D+i$ zM)!6EcX2Z%4vb#4U4RSi%|W9_&#|=}u7r)+m*Xj9<&i3vh664QT-dlJjdB;v?j&^f z$mwN&4>Pvjjg*tWiB4D+sg*{g4ky6Px#07*qo IM6N<$f@w+e6951J delta 271 zcmV+q0r39Q1gZj%B!BTqL_t(|+U?j;4uc>J24D}e$DM_PaVD;{GqCv3q-mhEXou6k z3CT284Ss9IZ9{Y;=Ug!hH;YnAy&AIKz|;C;WHeGrm2dw7&S_9$~k;6}e! zFElEP$RaZHs^iX)?agwNRy0~09p!6yG;sX*ULHqCuw-T9;IwOw{s=ul(ct*}o?S$a z<~+W_9cihkaFYwbr@q6r;9;eTRchEOwbfn$4&UMM9S$*%?@IRrY>1`!Fbn6#7a+fj Vu`OL_^Z)<=00>D%PDHLkV1iqdf1CgS diff --git a/src/client/render/gl/passes/UnitPass.ts b/src/client/render/gl/passes/UnitPass.ts index 4476ac5a7..bc96c1e9b 100644 --- a/src/client/render/gl/passes/UnitPass.ts +++ b/src/client/render/gl/passes/UnitPass.ts @@ -93,7 +93,7 @@ const HYDROGEN_BOMB_COL = UNIT_ORDER.indexOf(UT_HYDROGEN_BOMB); * Per-instance data (16 bytes): * float x, y, ownerID — 12 bytes (3 floats) * uint8 atlasIdx — 1 byte (atlas column 0–11) - * uint8 flags — 1 byte (0 = normal, 1 = flicker, 2 = angry) + * uint8 flags — 1 byte (0 = normal, 1 = flicker, 2 = angry, 3 = trade-friendly, 4 = retreating) * 2 bytes padding — aligns to 4-byte boundary */ const FLOATS_PER_INSTANCE = 4; @@ -104,6 +104,7 @@ const FLAG_NORMAL = 0; const FLAG_FLICKER = 1; const FLAG_ANGRY = 2; const FLAG_TRADE_FRIENDLY = 3; +const FLAG_RETREATING = 4; /** Atlas column indices for train sub-types (resolved from trainType + loaded) */ const TRAIN_ENGINE_COL = UNIT_ORDER.indexOf("TrainEngine"); @@ -395,6 +396,8 @@ export class UnitPass { if (atlasIdx === undefined) continue; + const isRetreatingWarship = + unit.unitType === UT_WARSHIP && unit.retreating; const isAngryWarship = unit.unitType === UT_WARSHIP && unit.targetUnitId !== null; const isFlicker = FLICKER_TYPES.has(unit.unitType); @@ -416,13 +419,16 @@ export class UnitPass { } } - const flags = isTradeFriendly - ? FLAG_TRADE_FRIENDLY - : isAngryWarship - ? FLAG_ANGRY - : isFlicker - ? FLAG_FLICKER - : FLAG_NORMAL; + let flags = FLAG_NORMAL; + if (isTradeFriendly) { + flags = FLAG_TRADE_FRIENDLY; + } else if (isRetreatingWarship) { + flags = FLAG_RETREATING; + } else if (isAngryWarship) { + flags = FLAG_ANGRY; + } else if (isFlicker) { + flags = FLAG_FLICKER; + } const isMissile = MISSILE_TYPES.has(unit.unitType); const x = unit.pos % this.mapW; diff --git a/src/client/render/gl/shaders/unit/unit.frag.glsl b/src/client/render/gl/shaders/unit/unit.frag.glsl index a04627b2d..03d70ec3b 100644 --- a/src/client/render/gl/shaders/unit/unit.frag.glsl +++ b/src/client/render/gl/shaders/unit/unit.frag.glsl @@ -26,6 +26,7 @@ out vec4 fragColor; const float FLAG_FLICKER = 1.0; const float FLAG_ANGRY = 2.0; const float FLAG_TRADE_FRIENDLY = 3.0; +const float FLAG_RETREATING = 4.0; // Ally color for trade-friendly override (yellow — matches affiliation.ts ALLY) const vec3 ALLY_COLOR = vec3(1.0, 1.0, 0.0); @@ -83,10 +84,15 @@ void main() { // Flag states (uint8 passed as float via vertex attribute): // 0 = normal // 1 = flicker (nukes/warheads — cycling hot colors) - // 2 = angry (warships attacking — solid red territory band) + // 2 = angry (warships attacking — outer ring (180 band) solid red) + // 4 = retreating (warships fleeing to port — blinking black center) + float retreatBlink = 0.0; if (abs(vFlags - FLAG_ANGRY) < 0.1) { - // Angry: solid red territory band + // Angry: the outer ring (180) and center (100) go red via territoryColor territoryColor = uAngryColor; + } else if (abs(vFlags - FLAG_RETREATING) < 0.1) { + // Retreating: slowly blink the center (100 band) black so the ship reads as fleeing + retreatBlink = step(0.5, fract(uTick * 0.07)); } else if (abs(vFlags - FLAG_FLICKER) < 0.1) { // Flicker: cycle through hot colors, offset by position hash float phase = fract(uTick * uFlickerSpeed + vHash); @@ -95,19 +101,24 @@ void main() { borderColor = FLICKER_COLORS[(idx + 2) % 4]; } - // Three-band gray replacement: + // Four-band gray replacement: // 180/255 ~ 0.706 -> territory color (light band) - // 130/255 ~ 0.510 -> spawn/mid color (interpolated) + // 130/255 ~ 0.510 -> spawn/mid color (interpolated; used by missiles) + // 100/255 ~ 0.392 -> center accent (warship center — tracks ring, blinks black) // 70/255 ~ 0.275 -> border color (dark band) vec3 spawnColor = mix(territoryColor, borderColor, 0.5); + vec3 centerColor = mix(territoryColor, vec3(0.0), retreatBlink); vec3 color; if (gray > 0.6) { // Light band (180) -> territory color color = territoryColor; - } else if (gray > 0.4) { + } else if (gray > 0.45) { // Mid band (130) -> spawn color color = spawnColor; + } else if (gray > 0.34) { + // Center accent band (100) -> center color + color = centerColor; } else { // Dark band (70) -> border color color = borderColor; diff --git a/src/client/view/UnitView.ts b/src/client/view/UnitView.ts index 46b2c24b0..2fc81132a 100644 --- a/src/client/view/UnitView.ts +++ b/src/client/view/UnitView.ts @@ -53,7 +53,9 @@ function unitStateFromUpdate(u: UnitUpdate): UnitState { lastPos: u.lastPos, isActive: u.isActive, reachedTarget: u.reachedTarget, - retreating: u.transportShipState?.isRetreating ?? false, + retreating: + (u.transportShipState?.isRetreating ?? false) || + u.warshipState?.state === "retreating", targetable: u.targetable, markedForDeletion: u.markedForDeletion, health: u.health ?? null, @@ -79,7 +81,9 @@ function applyUpdateInPlace(target: UnitState, u: UnitUpdate): void { target.lastPos = u.lastPos; target.isActive = u.isActive; target.reachedTarget = u.reachedTarget; - target.retreating = u.transportShipState?.isRetreating ?? false; + target.retreating = + (u.transportShipState?.isRetreating ?? false) || + u.warshipState?.state === "retreating"; target.targetable = u.targetable; target.markedForDeletion = u.markedForDeletion; target.health = u.health ?? null;