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):
* float x, y, ownerID — 12 bytes (3 floats)
* 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
*/
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;
@@ -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;
+6 -2
View File
@@ -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;