Add veterancy system for units and update health modification logic

This commit is contained in:
bijx
2026-02-17 00:44:58 -05:00
parent 86e51ab790
commit 52c44ba610
7 changed files with 122 additions and 8 deletions
+65
View File
@@ -19,6 +19,10 @@ const COLOR_PROGRESSION = [
const HEALTHBAR_WIDTH = 11; // Width of the health bar
const LOADINGBAR_WIDTH = 14; // Width of the loading bar
const PROGRESSBAR_HEIGHT = 3; // Height of a bar
const VETERANCY_DOT_RADIUS = 1.4;
const VETERANCY_DOT_SPACING = 5;
const VETERANCY_DOT_Y_OFFSET = 10;
const VETERANCY_DOT_CLEAR_PADDING = 2;
/**
* Layer responsible for drawing UI elements that overlay the game
@@ -35,6 +39,10 @@ export class UILayer implements Layer {
{ unit: UnitView; progressBar: ProgressBar }
> = new Map();
private allHealthBars: Map<number, ProgressBar> = new Map();
private allVeterancyDots: Map<
number,
{ x: number; y: number; bars: number }
> = new Map();
// Keep track of currently selected unit
private selectedUnit: UnitView | null = null;
@@ -103,6 +111,10 @@ export class UILayer implements Layer {
}
onUnitEvent(unit: UnitView) {
if (!unit.isActive()) {
this.clearVeterancyDots(unit.id());
return;
}
const underConst = unit.isUnderConstruction();
if (underConst) {
this.createLoadingBar(unit);
@@ -111,6 +123,7 @@ export class UILayer implements Layer {
switch (unit.type()) {
case UnitType.Warship: {
this.drawHealthBar(unit);
this.drawVeterancyDots(unit);
break;
}
case UnitType.City:
@@ -297,6 +310,58 @@ export class UILayer implements Layer {
}
}
private clearVeterancyDots(unitID: number): void {
const previous = this.allVeterancyDots.get(unitID);
if (previous === undefined || this.context === null) {
return;
}
const width =
previous.bars <= 0
? 0
: (previous.bars - 1) * VETERANCY_DOT_SPACING +
VETERANCY_DOT_RADIUS * 2;
const startX =
previous.x -
width / 2 -
VETERANCY_DOT_RADIUS -
VETERANCY_DOT_CLEAR_PADDING;
const startY =
previous.y - VETERANCY_DOT_RADIUS - VETERANCY_DOT_CLEAR_PADDING;
const clearWidth =
width + (VETERANCY_DOT_RADIUS + VETERANCY_DOT_CLEAR_PADDING) * 2;
const clearHeight =
(VETERANCY_DOT_RADIUS + VETERANCY_DOT_CLEAR_PADDING) * 2;
this.context.clearRect(startX, startY, clearWidth, clearHeight);
this.allVeterancyDots.delete(unitID);
}
private drawVeterancyDots(unit: UnitView): void {
if (this.context === null) {
return;
}
this.clearVeterancyDots(unit.id());
const bars = Math.min(3, Math.max(0, unit.veterancyLevel()));
if (bars === 0) {
return;
}
const centerX = this.game.x(unit.tile());
const y = this.game.y(unit.tile()) - VETERANCY_DOT_Y_OFFSET;
const totalWidth =
(bars - 1) * VETERANCY_DOT_SPACING + VETERANCY_DOT_RADIUS * 2;
const startX = centerX - totalWidth / 2 + VETERANCY_DOT_RADIUS;
this.context.fillStyle = "#d4af37";
for (let i = 0; i < bars; i++) {
const x = startX + i * VETERANCY_DOT_SPACING;
this.context.beginPath();
this.context.arc(x, y, VETERANCY_DOT_RADIUS, 0, Math.PI * 2);
this.context.fill();
}
this.allVeterancyDots.set(unit.id(), { x: centerX, y, bars });
}
private updateProgressBars() {
this.allProgressBars.forEach((progressBarInfo, unitId) => {
const progress = this.getProgress(progressBarInfo.unit);
+7 -2
View File
@@ -52,7 +52,11 @@ export class ShellExecution implements Execution {
);
if (result.status === PathStatus.COMPLETE) {
this.active = false;
this.target.modifyHealth(-this.effectOnTarget(), this._owner);
this.target.modifyHealth(
-this.effectOnTarget(),
this._owner,
this.ownerUnit,
);
this.shell.setReachedTarget();
this.shell.delete(false);
return;
@@ -64,7 +68,8 @@ export class ShellExecution implements Execution {
private effectOnTarget(): number {
const { damage } = this.mg.config().unitInfo(UnitType.Shell);
const baseDamage = damage ?? 250;
const veterancyBonus = this.ownerUnit.veterancyLevel() * 25;
const baseDamage = (damage ?? 250) + veterancyBonus;
const roll = this.random.nextInt(1, 6);
const damageMultiplier = (roll - 1) * 25 + 200;
+3 -1
View File
@@ -151,7 +151,9 @@ export class WarshipExecution implements Execution {
}
private shootTarget() {
const shellAttackRate = this.mg.config().warshipShellAttackRate();
const baseAttackRate = this.mg.config().warshipShellAttackRate();
const veterancyReduction = this.warship.veterancyLevel() * 2;
const shellAttackRate = Math.max(1, baseAttackRate - veterancyReduction);
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
if (this.warship.targetUnit()?.type() !== UnitType.TransportShip) {
// Warships don't need to reload when attacking transport ships.
+8 -2
View File
@@ -495,7 +495,11 @@ export interface Unit {
isMarkedForDeletion(): boolean;
markForDeletion(): void;
isOverdueDeletion(): boolean;
delete(displayMessage?: boolean, destroyer?: Player): void;
delete(
displayMessage?: boolean,
destroyer?: Player,
destroyerUnit?: Unit,
): void;
tile(): TileRef;
lastTile(): TileRef;
move(tile: TileRef): void;
@@ -534,7 +538,7 @@ export interface Unit {
retreating(): boolean;
orderBoatRetreat(): void;
health(): number;
modifyHealth(delta: number, attacker?: Player): void;
modifyHealth(delta: number, attacker?: Player, attackerUnit?: Unit): void;
// Troops
setTroops(troops: number): void;
@@ -564,6 +568,8 @@ export interface Unit {
// Warships
setPatrolTile(tile: TileRef): void;
patrolTile(): TileRef | undefined;
veterancyLevel(): number;
gainVeterancy(): void;
}
export interface TerraNullius {
+1
View File
@@ -141,6 +141,7 @@ export interface UnitUpdate {
hasTrainStation: boolean;
trainType?: TrainType; // Only for trains
loaded?: boolean; // Only for trains
veterancyLevel?: number; // Only for warships
}
export interface AttackUpdate {
+3
View File
@@ -180,6 +180,9 @@ export class UnitView {
isLoaded(): boolean | undefined {
return this.data.loaded;
}
veterancyLevel(): number {
return this.data.veterancyLevel ?? 0;
}
}
export class PlayerView {
+35 -3
View File
@@ -38,6 +38,7 @@ export class UnitImpl implements Unit {
private _targetable: boolean = true;
private _loaded: boolean | undefined;
private _trainType: TrainType | undefined;
private _veterancyLevel = 0;
// Nuke only
private _trajectoryIndex: number = 0;
private _trajectory: TrajectoryTile[];
@@ -142,6 +143,8 @@ export class UnitImpl implements Unit {
hasTrainStation: this._hasTrainStation,
trainType: this._trainType,
loaded: this._loaded,
veterancyLevel:
this._type === UnitType.Warship ? this._veterancyLevel : undefined,
};
}
@@ -221,14 +224,14 @@ export class UnitImpl implements Unit {
);
}
modifyHealth(delta: number, attacker?: Player): void {
modifyHealth(delta: number, attacker?: Player, attackerUnit?: Unit): void {
this._health = withinInt(
this._health + toInt(delta),
0n,
toInt(this.info().maxHealth ?? 1),
);
if (this._health === 0n) {
this.delete(true, attacker);
this.delete(true, attacker, attackerUnit);
}
}
@@ -256,7 +259,11 @@ export class UnitImpl implements Unit {
return this._deletionAt !== null && this.mg.ticks() - this._deletionAt > 0;
}
delete(displayMessage?: boolean, destroyer?: Player): void {
delete(
displayMessage?: boolean,
destroyer?: Player,
destroyerUnit?: Unit,
): void {
if (!this.isActive()) {
throw new Error(`cannot delete ${this} not active`);
}
@@ -275,6 +282,15 @@ export class UnitImpl implements Unit {
}
if (destroyer !== undefined) {
if (
this._type === UnitType.Warship &&
destroyerUnit !== undefined &&
destroyerUnit.type() === UnitType.Warship &&
destroyerUnit.owner() === destroyer &&
destroyerUnit.isActive()
) {
destroyerUnit.gainVeterancy();
}
switch (this._type) {
case UnitType.TransportShip:
this.mg
@@ -483,4 +499,20 @@ export class UnitImpl implements Unit {
this.mg.addUpdate(this.toUpdate());
}
}
veterancyLevel(): number {
return this._veterancyLevel;
}
gainVeterancy(): void {
if (this._type !== UnitType.Warship) {
return;
}
const updated = Math.min(3, this._veterancyLevel + 1);
if (updated === this._veterancyLevel) {
return;
}
this._veterancyLevel = updated;
this.mg.addUpdate(this.toUpdate());
}
}