mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 00:36:10 +00:00
006f1690a5
## Description: Warship veterancy! This is an idea inspired by the unit veterancy feature of games like C&C: Red Alert 2 in which unit eliminations increases the level of individual units. I've been trying to build this mechanic for months with different ideas, and I finally landed on this being one of the more balanced implementation. Warships can earn up to three levels, represented by the gold bar insignia in the bottom right of their warship sprite. <img width="622" height="202" alt="image" src="https://github.com/user-attachments/assets/a8c31a45-4ae9-41a9-b054-9c4a7f4ab1f1" /> A veterancy bar grants 20% health from the base amount, and a 20% increase in shell damage applied _after_ the random damage roll. For example, a level 3 warship will apply a 60% damage boost on top of the random shell damage value (something between 200-325. If the random value is 250, the final damage output will be `250 * 1.60 = 400`. There are three ways to achieve a veteran level: 1. **Eliminate another warship:** any time a warship neutralizes another warship, it immediately get's a veterancy increase. https://github.com/user-attachments/assets/6a9e0958-5171-4ca3-94f6-9c2300a12f8b 2. **Eliminate transport boats:** Destroying 10 transport boats will level a warship to the next veterancy bar. https://github.com/user-attachments/assets/619ce0c0-033c-4e0b-9c64-b41eabaa791b 3. **Steal trade ships:** If the warship captures 25 trade ships, it will earn a veterancy bar. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: bijx
115 lines
3.3 KiB
TypeScript
115 lines
3.3 KiB
TypeScript
/**
|
|
* TrailManager stamps a unit's path into the per-tile "last owner" texture.
|
|
*
|
|
* Smoothed nukes are interpolated lastPos→pos per render frame (UnitPass), so
|
|
* their trail must stamp only up to `lastPos` — otherwise the tail would lead
|
|
* the smoothly-moving missile sprite. Every other unit stamps up to `pos`.
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
NUKE_TRAIL_BIT,
|
|
TrailManager,
|
|
} from "../../../../src/client/render/frame/TrailManager";
|
|
import type { UnitState } from "../../../../src/client/render/types";
|
|
import {
|
|
UT_ATOM_BOMB,
|
|
UT_TRADE_SHIP,
|
|
} from "../../../../src/client/render/types";
|
|
|
|
const MAP_W = 50;
|
|
const MAP_H = 50;
|
|
|
|
function unit(overrides: Partial<UnitState> = {}): UnitState {
|
|
return {
|
|
id: 1,
|
|
unitType: UT_ATOM_BOMB,
|
|
ownerID: 7,
|
|
lastOwnerID: null,
|
|
pos: 0,
|
|
lastPos: 0,
|
|
isActive: true,
|
|
reachedTarget: false,
|
|
retreating: false,
|
|
targetable: true,
|
|
markedForDeletion: false,
|
|
health: null,
|
|
underConstruction: false,
|
|
targetUnitId: null,
|
|
targetTile: null,
|
|
troops: 0,
|
|
missileTimerQueue: [],
|
|
level: 1,
|
|
veterancy: 0,
|
|
hasTrainStation: false,
|
|
trainType: null,
|
|
loaded: null,
|
|
constructionStartTick: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function units(...us: UnitState[]): Map<number, UnitState> {
|
|
return new Map(us.map((u) => [u.id, u]));
|
|
}
|
|
|
|
const ref = (x: number, y: number) => y * MAP_W + x;
|
|
|
|
describe("TrailManager", () => {
|
|
it("stamps a smoothed nuke's trail only up to lastPos, not pos", () => {
|
|
const tm = new TrailManager(MAP_W, MAP_H);
|
|
const trail = tm.getTrailState();
|
|
|
|
// A nuke's texel carries the owner smallID plus the nuke bit (bit 12).
|
|
const nukeTexel = 7 | NUKE_TRAIL_BIT;
|
|
|
|
// First sighting: lastPos === pos at spawn.
|
|
tm.update(units(unit({ pos: ref(2, 2), lastPos: ref(2, 2) })), [1]);
|
|
expect(trail[ref(2, 2)]).toBe(nukeTexel);
|
|
|
|
// Move: lastPos trails pos by a tile. The trail head must reach lastPos
|
|
// (3,2) but NOT the current pos (4,2) — the smoothed sprite occupies the
|
|
// lastPos→pos span this frame.
|
|
tm.update(units(unit({ pos: ref(4, 2), lastPos: ref(3, 2) })), [1]);
|
|
expect(trail[ref(3, 2)]).toBe(nukeTexel);
|
|
expect(trail[ref(4, 2)]).toBe(0);
|
|
});
|
|
|
|
it("stamps a non-smoothed unit's trail up to pos", () => {
|
|
const tm = new TrailManager(MAP_W, MAP_H);
|
|
const trail = tm.getTrailState();
|
|
|
|
tm.update(
|
|
units(
|
|
unit({ unitType: UT_TRADE_SHIP, pos: ref(2, 2), lastPos: ref(2, 2) }),
|
|
),
|
|
[1],
|
|
);
|
|
tm.update(
|
|
units(
|
|
unit({ unitType: UT_TRADE_SHIP, pos: ref(4, 2), lastPos: ref(3, 2) }),
|
|
),
|
|
[1],
|
|
);
|
|
|
|
// Trade ships are not interpolated, so the trail reaches the current pos.
|
|
expect(trail[ref(4, 2)]).toBe(7);
|
|
});
|
|
|
|
it("clears a unit's trail when it disappears", () => {
|
|
const tm = new TrailManager(MAP_W, MAP_H);
|
|
const trail = tm.getTrailState();
|
|
|
|
const nukeTexel = 7 | NUKE_TRAIL_BIT;
|
|
tm.update(units(unit({ pos: ref(5, 5), lastPos: ref(5, 5) })), [1]);
|
|
tm.update(units(unit({ pos: ref(7, 5), lastPos: ref(6, 5) })), [1]);
|
|
expect(trail[ref(5, 5)]).toBe(nukeTexel);
|
|
expect(trail[ref(6, 5)]).toBe(nukeTexel);
|
|
|
|
// Unit gone from the map → its tiles are cleared.
|
|
tm.update(new Map(), []);
|
|
expect(trail[ref(5, 5)]).toBe(0);
|
|
expect(trail[ref(6, 5)]).toBe(0);
|
|
});
|
|
});
|