mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +00:00
Add veterancy system for units and update health modification logic
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -180,6 +180,9 @@ export class UnitView {
|
||||
isLoaded(): boolean | undefined {
|
||||
return this.data.loaded;
|
||||
}
|
||||
veterancyLevel(): number {
|
||||
return this.data.veterancyLevel ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class PlayerView {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user