Add naval combat animations (#858)

## Description:


https://github.com/user-attachments/assets/b46f949a-eb50-4656-8492-216cf820ac46

Add a couple animations for naval combat:

- shell hit
- ship explosion
- ship sinking

Added a simple `Timeline` class to spread FX animations over time.
Added a `ColoredAnimatedSprite` similar to the existing `ColoredSprite`.
Refactored the latter to avoid code duplication.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

IngloriousTom
This commit is contained in:
DevelopingTom
2025-05-27 01:59:11 +02:00
committed by Scott Anderson
parent 182a42e0db
commit cea22c8220
20 changed files with 431 additions and 178 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

+137 -42
View File
@@ -1,11 +1,17 @@
import miniBigSmoke from "../../../resources/sprites/bigsmoke.png";
import miniExplosion from "../../../resources/sprites/miniExplosion.png";
import miniFire from "../../../resources/sprites/minifire.png";
import nuke from "../../../resources/sprites/nukeExplosion.png";
import SAMExplosion from "../../../resources/sprites/samExplosion.png";
import sinkingShip from "../../../resources/sprites/sinkingShip.png";
import miniSmoke from "../../../resources/sprites/smoke.png";
import miniSmokeAndFire from "../../../resources/sprites/smokeAndFire.png";
import unitExplosion from "../../../resources/sprites/unitExplosion.png";
import { Theme } from "../../core/configuration/Config";
import { PlayerView } from "../../core/game/GameView";
import { AnimatedSprite } from "./AnimatedSprite";
import { FxType } from "./fx/Fx";
import { colorizeCanvas } from "./SpriteLoader";
type AnimatedSpriteConfig = {
url: string;
@@ -54,6 +60,33 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
originX: 9,
originY: 14,
},
[FxType.MiniExplosion]: {
url: miniExplosion,
frameWidth: 13,
frameCount: 4,
frameDuration: 70,
looping: false,
originX: 6,
originY: 6,
},
[FxType.UnitExplosion]: {
url: unitExplosion,
frameWidth: 19,
frameCount: 4,
frameDuration: 70,
looping: false,
originX: 9,
originY: 9,
},
[FxType.SinkingShip]: {
url: sinkingShip,
frameWidth: 16,
frameCount: 14,
frameDuration: 90,
looping: false,
originX: 7,
originY: 7,
},
[FxType.Nuke]: {
url: nuke,
frameWidth: 60,
@@ -74,53 +107,115 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
},
};
const animatedSpriteImageMap: Map<FxType, CanvasImageSource> = new Map();
export class AnimatedSpriteLoader {
private animatedSpriteImageMap: Map<FxType, HTMLCanvasElement> = new Map();
// Do not color the same sprite twice
private coloredAnimatedSpriteCache: Map<string, HTMLCanvasElement> =
new Map();
export const loadAllAnimatedSpriteImages = async (): Promise<void> => {
const entries = Object.entries(ANIMATED_SPRITE_CONFIG);
public async loadAllAnimatedSpriteImages(): Promise<void> {
const entries = Object.entries(ANIMATED_SPRITE_CONFIG);
await Promise.all(
entries.map(async ([fxType, config]) => {
const typedFxType = fxType as FxType;
if (!config?.url) return;
await Promise.all(
entries.map(async ([fxType, config]) => {
const typedFxType = fxType as FxType;
if (!config?.url) return;
try {
const img = new Image();
img.crossOrigin = "anonymous";
img.src = config.url;
try {
const img = new Image();
img.crossOrigin = "anonymous";
img.src = config.url;
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = (e) => reject(e);
});
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = (e) => reject(e);
});
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext("2d")!.drawImage(img, 0, 0);
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext("2d")!.drawImage(img, 0, 0);
animatedSpriteImageMap.set(typedFxType, canvas);
} catch (err) {
console.error(`Failed to load sprite for ${typedFxType}:`, err);
}
}),
);
};
this.animatedSpriteImageMap.set(typedFxType, canvas);
} catch (err) {
console.error(`Failed to load sprite for ${typedFxType}:`, err);
}
}),
);
}
export const createAnimatedSpriteForUnit = (
fxType: FxType,
): AnimatedSprite | null => {
const config = ANIMATED_SPRITE_CONFIG[fxType];
const image = animatedSpriteImageMap.get(fxType);
if (!config || !image) return null;
private createRegularAnimatedSprite(fxType: FxType): AnimatedSprite | null {
const config = ANIMATED_SPRITE_CONFIG[fxType];
const image = this.animatedSpriteImageMap.get(fxType);
if (!config || !image) return null;
return new AnimatedSprite(
image,
config.frameWidth,
config.frameCount,
config.frameDuration,
config.looping ?? true,
config.originX,
config.originY,
);
};
return new AnimatedSprite(
image,
config.frameWidth,
config.frameCount,
config.frameDuration,
config.looping ?? true,
config.originX,
config.originY,
);
}
private getColoredAnimatedSprite(
owner: PlayerView,
fxType: FxType,
theme: Theme,
): HTMLCanvasElement | null {
const baseImage = this.animatedSpriteImageMap.get(fxType);
const config = ANIMATED_SPRITE_CONFIG[fxType];
if (!baseImage || !config) return null;
const territoryColor = theme.territoryColor(owner);
const borderColor = theme.borderColor(owner);
const spawnHighlightColor = theme.spawnHighlightColor();
const key = `${fxType}-${owner.id()}`;
let coloredCanvas: HTMLCanvasElement;
if (this.coloredAnimatedSpriteCache.has(key)) {
coloredCanvas = this.coloredAnimatedSpriteCache.get(key)!;
} else {
coloredCanvas = colorizeCanvas(
baseImage,
territoryColor,
borderColor,
spawnHighlightColor,
);
this.coloredAnimatedSpriteCache.set(key, coloredCanvas);
}
return coloredCanvas;
}
private createColoredAnimatedSpriteForUnit(
fxType: FxType,
owner: PlayerView,
theme: Theme,
): AnimatedSprite | null {
const config = ANIMATED_SPRITE_CONFIG[fxType];
const image = this.getColoredAnimatedSprite(owner, fxType, theme);
if (!config || !image) return null;
return new AnimatedSprite(
image,
config.frameWidth,
config.frameCount,
config.frameDuration,
config.looping ?? true,
config.originX,
config.originY,
);
}
public createAnimatedSprite(
fxType: FxType,
owner?: PlayerView,
theme?: Theme,
): AnimatedSprite | null {
if (owner && theme) {
return this.createColoredAnimatedSpriteForUnit(fxType, owner, theme);
}
return this.createRegularAnimatedSprite(fxType);
}
}
+56 -43
View File
@@ -71,7 +71,53 @@ export const isSpriteReady = (unitType: UnitType): boolean => {
const coloredSpriteCache: Map<string, HTMLCanvasElement> = new Map();
// puts the sprite in an canvas colors it and caches the colored canvas
/**
* Load a canvas and replace grayscale with border colors
*/
export const colorizeCanvas = (
source: CanvasImageSource & { width: number; height: number },
colorA: Colord,
colorB: Colord,
colorC: Colord,
): HTMLCanvasElement => {
const canvas = document.createElement("canvas");
canvas.width = source.width;
canvas.height = source.height;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(source, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const colorARgb = colorA.toRgb();
const colorBRgb = colorB.toRgb();
const colorCRgb = colorC.toRgb();
for (let i = 0; i < data.length; i += 4) {
const r = data[i],
g = data[i + 1],
b = data[i + 2];
if (r === 180 && g === 180 && b === 180) {
data[i] = colorARgb.r;
data[i + 1] = colorARgb.g;
data[i + 2] = colorARgb.b;
} else if (r === 70 && g === 70 && b === 70) {
data[i] = colorBRgb.r;
data[i + 1] = colorBRgb.g;
data[i + 2] = colorBRgb.b;
} else if (r === 130 && g === 130 && b === 130) {
data[i] = colorCRgb.r;
data[i + 1] = colorCRgb.g;
data[i + 2] = colorCRgb.b;
}
}
ctx.putImageData(imageData, 0, 0);
return canvas;
};
export const getColoredSprite = (
unit: UnitView,
theme: Theme,
@@ -82,8 +128,7 @@ export const getColoredSprite = (
const territoryColor = customTerritoryColor ?? theme.territoryColor(owner);
const borderColor = customBorderColor ?? theme.borderColor(owner);
const spawnHighlightColor = theme.spawnHighlightColor();
const colorKey = territoryColor.toRgbString() + borderColor.toRgbString();
const key = unit.type() + colorKey;
const key = `${unit.type()}-${owner.id()}`;
if (coloredSpriteCache.has(key)) {
return coloredSpriteCache.get(key)!;
@@ -94,45 +139,13 @@ export const getColoredSprite = (
throw new Error(`Failed to load sprite for ${unit.type()}`);
}
const territoryRgb = territoryColor.toRgb();
const borderRgb = borderColor.toRgb();
const spawnHighlightRgb = spawnHighlightColor.toRgb();
const coloredCanvas = colorizeCanvas(
sprite,
territoryColor,
borderColor,
spawnHighlightColor,
);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
canvas.width = sprite.width;
canvas.height = sprite.height;
ctx.drawImage(sprite, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
if (r === 180 && g === 180 && b === 180) {
data[i] = territoryRgb.r;
data[i + 1] = territoryRgb.g;
data[i + 2] = territoryRgb.b;
}
if (r === 70 && g === 70 && b === 70) {
data[i] = borderRgb.r;
data[i + 1] = borderRgb.g;
data[i + 2] = borderRgb.b;
}
if (r === 130 && g === 130 && b === 130) {
data[i] = spawnHighlightRgb.r;
data[i + 1] = spawnHighlightRgb.g;
data[i + 2] = spawnHighlightRgb.b;
}
}
ctx.putImageData(imageData, 0.5, 0.5);
coloredSpriteCache.set(key, canvas);
return canvas;
coloredSpriteCache.set(key, coloredCanvas);
return coloredCanvas;
};
+3
View File
@@ -7,6 +7,9 @@ export enum FxType {
MiniSmoke = "MiniSmoke",
MiniBigSmoke = "MiniBigSmoke",
MiniSmokeAndFire = "MiniSmokeAndFire",
MiniExplosion = "MiniExplosion",
UnitExplosion = "UnitExplosion",
SinkingShip = "SinkingShip",
Nuke = "Nuke",
SAMExplosion = "SAMExplosion",
}
+11 -3
View File
@@ -1,6 +1,7 @@
import { GameView } from "../../../core/game/GameView";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { Fx, FxType } from "./Fx";
import { SpriteFX } from "./SpriteFx";
import { FadeFx, SpriteFx } from "./SpriteFx";
/**
* Shockwave effect: draw a growing 1px white circle
@@ -34,6 +35,7 @@ export class ShockwaveFx implements Fx {
* Spawn @p number of @p type animation within a perimeter
*/
function addSpriteInCircle(
animatedSpriteLoader: AnimatedSpriteLoader,
x: number,
y: number,
radius: number,
@@ -52,7 +54,11 @@ function addSpriteInCircle(
game.isValidCoord(spawnX, spawnY) &&
game.isLand(game.ref(spawnX, spawnY))
) {
const sprite = new SpriteFX(spawnX, spawnY, type, 6000, 0.1, 0.8);
const sprite = new FadeFx(
new SpriteFx(animatedSpriteLoader, spawnX, spawnY, type, 6000),
0.1,
0.8,
);
result.push(sprite as Fx);
}
}
@@ -65,6 +71,7 @@ function addSpriteInCircle(
* - ruins and desolation fx
*/
export function nukeFxFactory(
animatedSpriteLoader: AnimatedSpriteLoader,
x: number,
y: number,
radius: number,
@@ -72,7 +79,7 @@ export function nukeFxFactory(
): Fx[] {
const nukeFx: Fx[] = [];
// Explosion animation
nukeFx.push(new SpriteFX(x, y, FxType.Nuke) as Fx);
nukeFx.push(new SpriteFx(animatedSpriteLoader, x, y, FxType.Nuke));
// Shockwave animation
nukeFx.push(new ShockwaveFx(x, y, 1500, radius * 1.5));
// Ruins and desolation sprites
@@ -89,6 +96,7 @@ export function nukeFxFactory(
for (const { type, radiusFactor, density } of debrisPlan) {
addSpriteInCircle(
animatedSpriteLoader,
x,
y,
radius * radiusFactor,
-34
View File
@@ -1,34 +0,0 @@
import { AnimatedSprite } from "../AnimatedSprite";
import { createAnimatedSpriteForUnit } from "../AnimatedSpriteLoader";
import { Fx, FxType } from "./Fx";
/**
* Explosion effect: sprite animation of an explosion
*/
export class SAMExplosionFx implements Fx {
private lifeTime: number = 0;
private explosionSprite: AnimatedSprite | null;
constructor(
private x: number,
private y: number,
private duration: number,
) {
this.explosionSprite = createAnimatedSpriteForUnit(FxType.SAMExplosion);
}
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
if (this.explosionSprite) {
this.lifeTime += frameTime;
if (this.lifeTime >= this.duration) {
return false;
}
if (this.explosionSprite.isActive()) {
this.explosionSprite.update(frameTime);
this.explosionSprite.draw(ctx, this.x, this.y);
return true;
}
return false;
}
return false;
}
}
+58 -34
View File
@@ -1,6 +1,8 @@
import { Theme } from "../../../core/configuration/Config";
import { consolex } from "../../../core/Consolex";
import { PlayerView } from "../../../core/game/GameView";
import { AnimatedSprite } from "../AnimatedSprite";
import { createAnimatedSpriteForUnit } from "../AnimatedSpriteLoader";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { Fx, FxType } from "./Fx";
function fadeInOut(
@@ -18,51 +20,73 @@ function fadeInOut(
return 1 - f * f;
}
}
/**
* Fade in/out another FX
*/
export class FadeFx implements Fx {
constructor(
private fxToFade: SpriteFx,
private fadeIn: number,
private fadeOut: number,
) {}
renderTick(duration: number, ctx: CanvasRenderingContext2D): boolean {
const t = this.fxToFade.getElapsedTime() / this.fxToFade.getDuration();
ctx.save();
ctx.globalAlpha = fadeInOut(t, this.fadeIn, this.fadeOut);
const result = this.fxToFade.renderTick(duration, ctx);
ctx.restore();
return result;
}
}
/**
* A simple FX displaying an animated sprite
* Animated sprite. Can be colored if provided an owner/theme
*/
export class SpriteFX implements Fx {
private lifeTime: number = 0;
private animatedSprite: AnimatedSprite | null;
private totalLifeTime: number = 0;
export class SpriteFx implements Fx {
protected animatedSprite: AnimatedSprite | null;
protected elapsedTime = 0;
protected duration = 1000;
constructor(
private x: number,
private y: number,
animatedSpriteLoader: AnimatedSpriteLoader,
protected x: number,
protected y: number,
fxType: FxType,
duration?: number,
private fadeIn?: number,
private fadeOut?: number,
private owner?: PlayerView,
private theme?: Theme,
) {
this.animatedSprite = createAnimatedSpriteForUnit(fxType);
this.animatedSprite = animatedSpriteLoader.createAnimatedSprite(
fxType,
owner,
theme,
);
if (!this.animatedSprite) {
consolex.error("Could not load animated sprite ", fxType);
this.totalLifeTime = 0;
} else if (!duration) {
// When no duration set, rely on the sprite lifetime
this.totalLifeTime = this.animatedSprite.lifeTime() ?? 1000; // 1s by default
consolex.error("Could not load animated sprite", fxType);
} else {
this.totalLifeTime = duration;
this.duration = duration ?? this.animatedSprite.lifeTime() ?? 1000;
}
}
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
if (this.animatedSprite) {
this.lifeTime += frameTime;
if (this.lifeTime >= this.totalLifeTime) {
return false;
}
if (this.animatedSprite.isActive()) {
ctx.save();
const t = this.lifeTime / this.totalLifeTime;
ctx.globalAlpha = fadeInOut(t, this.fadeIn ?? 0, this.fadeOut ?? 0.7);
this.animatedSprite.update(frameTime);
this.animatedSprite.draw(ctx, this.x, this.y);
ctx.restore();
return true;
}
return false;
}
return false;
if (!this.animatedSprite) return false;
this.elapsedTime += frameTime;
if (this.elapsedTime >= this.duration) return false;
if (!this.animatedSprite.isActive()) return false;
const t = this.elapsedTime / this.duration;
this.animatedSprite.update(frameTime);
this.animatedSprite.draw(ctx, this.x, this.y);
return true;
}
getElapsedTime(): number {
return this.elapsedTime;
}
getDuration(): number {
return this.duration;
}
}
+33
View File
@@ -0,0 +1,33 @@
type TimedTask = {
delay: number;
action: () => void;
triggered: boolean;
};
/**
* Basic timeline to chain actions
*/
export class Timeline {
private tasks: TimedTask[] = [];
private timeElapsed = 0;
add(delay: number, action: () => void): Timeline {
this.tasks.push({ delay, action, triggered: false });
return this;
}
update(dt: number) {
this.timeElapsed += dt;
for (const task of this.tasks) {
if (!task.triggered && this.timeElapsed >= task.delay) {
task.action();
task.triggered = true;
}
}
}
isComplete() {
return this.tasks.every((t) => t.triggered);
}
}
+47
View File
@@ -0,0 +1,47 @@
import { GameView } from "../../../core/game/GameView";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { Fx, FxType } from "./Fx";
import { SpriteFx } from "./SpriteFx";
import { Timeline } from "./Timeline";
/**
* Explosion Effect: a few timed explosions
*/
export class UnitExplosionFx implements Fx {
private timeline = new Timeline();
private explosions: Fx[] = [];
constructor(
animatedSpriteLoader: AnimatedSpriteLoader,
private x: number,
private y: number,
game: GameView,
) {
const config = [
{ dx: 0, dy: 0, delay: 0, type: FxType.UnitExplosion },
{ dx: 4, dy: -6, delay: 80, type: FxType.UnitExplosion },
{ dx: -6, dy: 4, delay: 160, type: FxType.UnitExplosion },
];
for (const { dx, dy, delay, type } of config) {
this.timeline.add(delay, () => {
if (game.isValidCoord(x + dx, y + dy)) {
this.explosions.push(
new SpriteFx(animatedSpriteLoader, x + dx, y + dy, type),
);
}
});
}
}
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
this.timeline.update(frameTime);
let allDone = true;
for (const fx of this.explosions) {
if (fx.renderTick(frameTime, ctx)) {
allDone = false;
}
}
return !allDone || !this.timeline.isComplete();
}
}
+73 -9
View File
@@ -1,10 +1,12 @@
import { Theme } from "../../../core/configuration/Config";
import { UnitType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
import { loadAllAnimatedSpriteImages } from "../AnimatedSpriteLoader";
import { Fx } from "../fx/Fx";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { Fx, FxType } from "../fx/Fx";
import { nukeFxFactory, ShockwaveFx } from "../fx/NukeFx";
import { SAMExplosionFx } from "../fx/SAMExplosionFx";
import { SpriteFx } from "../fx/SpriteFx";
import { UnitExplosionFx } from "../fx/UnitExplosionFx";
import { Layer } from "./Layer";
export class FxLayer implements Layer {
@@ -13,10 +15,15 @@ export class FxLayer implements Layer {
private lastRefresh: number = 0;
private refreshRate: number = 10;
private theme: Theme;
private animatedSpriteLoader: AnimatedSpriteLoader =
new AnimatedSpriteLoader();
private allFx: Fx[] = [];
constructor(private game: GameView) {}
constructor(private game: GameView) {
this.theme = this.game.config().theme();
}
shouldTransform(): boolean {
return true;
@@ -41,12 +48,58 @@ export class FxLayer implements Layer {
case UnitType.HydrogenBomb:
this.onNukeEvent(unit, 160);
break;
case UnitType.Warship:
this.onWarshipEvent(unit);
break;
case UnitType.Shell:
this.onShellEvent(unit);
break;
}
}
onShellEvent(unit: UnitView) {
if (!unit.isActive()) {
if (unit.reachedTarget()) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const shipExplosion = new SpriteFx(
this.animatedSpriteLoader,
x,
y,
FxType.MiniExplosion,
);
this.allFx.push(shipExplosion);
}
}
}
onWarshipEvent(unit: UnitView) {
if (!unit.isActive()) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const shipExplosion = new UnitExplosionFx(
this.animatedSpriteLoader,
x,
y,
this.game,
);
this.allFx.push(shipExplosion);
const sinkingShip = new SpriteFx(
this.animatedSpriteLoader,
x,
y,
FxType.SinkingShip,
undefined,
unit.owner(),
this.theme,
);
this.allFx.push(sinkingShip);
}
}
onNukeEvent(unit: UnitView, radius: number) {
if (!unit.isActive()) {
if (unit.wasInterceptedBySAM()) {
if (!unit.reachedTarget()) {
this.handleSAMInterception(unit);
} else {
// Kaboom
@@ -58,15 +111,26 @@ export class FxLayer implements Layer {
handleNukeExplosion(unit: UnitView, radius: number) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const nukeFx = nukeFxFactory(x, y, radius, this.game);
const nukeFx = nukeFxFactory(
this.animatedSpriteLoader,
x,
y,
radius,
this.game,
);
this.allFx = this.allFx.concat(nukeFx);
}
handleSAMInterception(unit: UnitView) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const interception = new SAMExplosionFx(x, y, 1000);
this.allFx.push(interception);
const explosion = new SpriteFx(
this.animatedSpriteLoader,
x,
y,
FxType.SAMExplosion,
);
this.allFx.push(explosion);
const shockwave = new ShockwaveFx(x, y, 800, 40);
this.allFx.push(shockwave);
}
@@ -74,7 +138,7 @@ export class FxLayer implements Layer {
async init() {
this.redraw();
try {
await loadAllAnimatedSpriteImages();
this.animatedSpriteLoader.loadAllAnimatedSpriteImages();
console.log("FX sprites loaded successfully");
} catch (err) {
console.error("Failed to load FX sprites:", err);
+1
View File
@@ -239,6 +239,7 @@ export class NukeExecution implements Execution {
}
}
this.active = false;
this.nuke.setReachedTarget();
this.nuke.delete(false);
// Record stats
@@ -181,7 +181,6 @@ export class SAMLauncherExecution implements Execution {
);
// Delete warheads
mirvWarheadTargets.forEach((u) => {
u.setInterceptedBySam();
u.delete();
});
} else if (target !== null) {
@@ -66,7 +66,6 @@ export class SAMMissileExecution implements Execution {
this._owner.id(),
);
this.active = false;
this.target.setInterceptedBySam();
this.target.delete(true, this._owner);
this.SAMMissile.delete(false);
+1
View File
@@ -52,6 +52,7 @@ export class ShellExecution implements Execution {
if (result === true) {
this.active = false;
this.target.modifyHealth(-this.effectOnTarget(), this._owner);
this.shell.setReachedTarget();
this.shell.delete(false);
return;
} else {
+2 -2
View File
@@ -369,8 +369,8 @@ export interface Unit {
targetUnit(): Unit | undefined;
setTargetedBySAM(targeted: boolean): void;
targetedBySAM(): boolean;
setInterceptedBySam(): void;
interceptedBySam(): boolean;
setReachedTarget(): void;
reachedTarget(): boolean;
// Health
hasHealth(): boolean;
+1 -1
View File
@@ -73,7 +73,7 @@ export interface UnitUpdate {
pos: TileRef;
lastPos: TileRef;
isActive: boolean;
wasIntercepted: boolean;
reachedTarget: boolean;
retreating: boolean;
targetUnitId?: number; // Only for trade ships
targetTile?: TileRef; // Only for nukes
+2 -2
View File
@@ -93,8 +93,8 @@ export class UnitView {
isActive(): boolean {
return this.data.isActive;
}
wasInterceptedBySAM(): boolean {
return this.data.wasIntercepted;
reachedTarget(): boolean {
return this.data.reachedTarget;
}
hasHealth(): boolean {
return this.data.health !== undefined;
+6 -6
View File
@@ -21,7 +21,7 @@ export class UnitImpl implements Unit {
private _lastTile: TileRef;
private _retreating: boolean = false;
private _targetedBySAM = false;
private _interceptedBySAM = false;
private _reachedTarget = false;
private _lastSetSafeFromPirates: number; // Only for trade ships
private _constructionType: UnitType | undefined;
private _lastOwner: PlayerImpl | null = null;
@@ -104,7 +104,7 @@ export class UnitImpl implements Unit {
ownerID: this._owner.smallID(),
lastOwnerID: this._lastOwner?.smallID(),
isActive: this._active,
wasIntercepted: this._interceptedBySAM,
reachedTarget: this._reachedTarget,
retreating: this._retreating,
pos: this._tile,
lastPos: this._lastTile,
@@ -324,12 +324,12 @@ export class UnitImpl implements Unit {
return this._targetedBySAM;
}
setInterceptedBySam(): void {
this._interceptedBySAM = true;
setReachedTarget(): void {
this._reachedTarget = true;
}
interceptedBySam(): boolean {
return this._interceptedBySAM;
reachedTarget(): boolean {
return this._reachedTarget;
}
setSafeFromPirates(): void {