diff --git a/resources/atlases/unit-atlas.png b/resources/atlases/unit-atlas.png index ceb8edcb0..93df24b32 100644 Binary files a/resources/atlases/unit-atlas.png and b/resources/atlases/unit-atlas.png differ 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;