Add retreating warship indicator and warship 2-color treatment

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.
This commit is contained in:
evanpelle
2026-06-08 17:32:21 -07:00
parent 611560a0b2
commit 65e99b25e7
4 changed files with 36 additions and 15 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 B

After

Width:  |  Height:  |  Size: 595 B

+14 -8
View File
@@ -93,7 +93,7 @@ const HYDROGEN_BOMB_COL = UNIT_ORDER.indexOf(UT_HYDROGEN_BOMB);
* Per-instance data (16 bytes): * Per-instance data (16 bytes):
* float x, y, ownerID — 12 bytes (3 floats) * float x, y, ownerID — 12 bytes (3 floats)
* uint8 atlasIdx — 1 byte (atlas column 011) * uint8 atlasIdx — 1 byte (atlas column 011)
* 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 * 2 bytes padding — aligns to 4-byte boundary
*/ */
const FLOATS_PER_INSTANCE = 4; const FLOATS_PER_INSTANCE = 4;
@@ -104,6 +104,7 @@ const FLAG_NORMAL = 0;
const FLAG_FLICKER = 1; const FLAG_FLICKER = 1;
const FLAG_ANGRY = 2; const FLAG_ANGRY = 2;
const FLAG_TRADE_FRIENDLY = 3; const FLAG_TRADE_FRIENDLY = 3;
const FLAG_RETREATING = 4;
/** Atlas column indices for train sub-types (resolved from trainType + loaded) */ /** Atlas column indices for train sub-types (resolved from trainType + loaded) */
const TRAIN_ENGINE_COL = UNIT_ORDER.indexOf("TrainEngine"); const TRAIN_ENGINE_COL = UNIT_ORDER.indexOf("TrainEngine");
@@ -395,6 +396,8 @@ export class UnitPass {
if (atlasIdx === undefined) continue; if (atlasIdx === undefined) continue;
const isRetreatingWarship =
unit.unitType === UT_WARSHIP && unit.retreating;
const isAngryWarship = const isAngryWarship =
unit.unitType === UT_WARSHIP && unit.targetUnitId !== null; unit.unitType === UT_WARSHIP && unit.targetUnitId !== null;
const isFlicker = FLICKER_TYPES.has(unit.unitType); const isFlicker = FLICKER_TYPES.has(unit.unitType);
@@ -416,13 +419,16 @@ export class UnitPass {
} }
} }
const flags = isTradeFriendly let flags = FLAG_NORMAL;
? FLAG_TRADE_FRIENDLY if (isTradeFriendly) {
: isAngryWarship flags = FLAG_TRADE_FRIENDLY;
? FLAG_ANGRY } else if (isRetreatingWarship) {
: isFlicker flags = FLAG_RETREATING;
? FLAG_FLICKER } else if (isAngryWarship) {
: FLAG_NORMAL; flags = FLAG_ANGRY;
} else if (isFlicker) {
flags = FLAG_FLICKER;
}
const isMissile = MISSILE_TYPES.has(unit.unitType); const isMissile = MISSILE_TYPES.has(unit.unitType);
const x = unit.pos % this.mapW; const x = unit.pos % this.mapW;
@@ -26,6 +26,7 @@ out vec4 fragColor;
const float FLAG_FLICKER = 1.0; const float FLAG_FLICKER = 1.0;
const float FLAG_ANGRY = 2.0; const float FLAG_ANGRY = 2.0;
const float FLAG_TRADE_FRIENDLY = 3.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) // Ally color for trade-friendly override (yellow — matches affiliation.ts ALLY)
const vec3 ALLY_COLOR = vec3(1.0, 1.0, 0.0); 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): // Flag states (uint8 passed as float via vertex attribute):
// 0 = normal // 0 = normal
// 1 = flicker (nukes/warheads — cycling hot colors) // 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) { 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; 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) { } else if (abs(vFlags - FLAG_FLICKER) < 0.1) {
// Flicker: cycle through hot colors, offset by position hash // Flicker: cycle through hot colors, offset by position hash
float phase = fract(uTick * uFlickerSpeed + vHash); float phase = fract(uTick * uFlickerSpeed + vHash);
@@ -95,19 +101,24 @@ void main() {
borderColor = FLICKER_COLORS[(idx + 2) % 4]; borderColor = FLICKER_COLORS[(idx + 2) % 4];
} }
// Three-band gray replacement: // Four-band gray replacement:
// 180/255 ~ 0.706 -> territory color (light band) // 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) // 70/255 ~ 0.275 -> border color (dark band)
vec3 spawnColor = mix(territoryColor, borderColor, 0.5); vec3 spawnColor = mix(territoryColor, borderColor, 0.5);
vec3 centerColor = mix(territoryColor, vec3(0.0), retreatBlink);
vec3 color; vec3 color;
if (gray > 0.6) { if (gray > 0.6) {
// Light band (180) -> territory color // Light band (180) -> territory color
color = territoryColor; color = territoryColor;
} else if (gray > 0.4) { } else if (gray > 0.45) {
// Mid band (130) -> spawn color // Mid band (130) -> spawn color
color = spawnColor; color = spawnColor;
} else if (gray > 0.34) {
// Center accent band (100) -> center color
color = centerColor;
} else { } else {
// Dark band (70) -> border color // Dark band (70) -> border color
color = borderColor; color = borderColor;
+6 -2
View File
@@ -53,7 +53,9 @@ function unitStateFromUpdate(u: UnitUpdate): UnitState {
lastPos: u.lastPos, lastPos: u.lastPos,
isActive: u.isActive, isActive: u.isActive,
reachedTarget: u.reachedTarget, reachedTarget: u.reachedTarget,
retreating: u.transportShipState?.isRetreating ?? false, retreating:
(u.transportShipState?.isRetreating ?? false) ||
u.warshipState?.state === "retreating",
targetable: u.targetable, targetable: u.targetable,
markedForDeletion: u.markedForDeletion, markedForDeletion: u.markedForDeletion,
health: u.health ?? null, health: u.health ?? null,
@@ -79,7 +81,9 @@ function applyUpdateInPlace(target: UnitState, u: UnitUpdate): void {
target.lastPos = u.lastPos; target.lastPos = u.lastPos;
target.isActive = u.isActive; target.isActive = u.isActive;
target.reachedTarget = u.reachedTarget; 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.targetable = u.targetable;
target.markedForDeletion = u.markedForDeletion; target.markedForDeletion = u.markedForDeletion;
target.health = u.health ?? null; target.health = u.health ?? null;