mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-10 01:25:42 +00:00
smoke trails
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { Fx } from "./Fx";
|
||||
|
||||
class SmokeParticle {
|
||||
public life: number = 0;
|
||||
public maxLife: number;
|
||||
public x: number;
|
||||
public y: number;
|
||||
public vx: number;
|
||||
public vy: number;
|
||||
public size: number;
|
||||
public maxSize: number;
|
||||
public opacity: number;
|
||||
public color: string;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
// Longer life for "puffy" trails
|
||||
this.maxLife = 1500 + Math.random() * 1000;
|
||||
|
||||
// Slow drift
|
||||
this.vx = (Math.random() - 0.5) * 0.02;
|
||||
this.vy = (Math.random() - 0.5) * 0.02;
|
||||
|
||||
// Start small, grow BIG
|
||||
this.size = 1 + Math.random() * 1;
|
||||
this.maxSize = 6 + Math.random() * 6;
|
||||
|
||||
// Bolder opacity
|
||||
this.opacity = 0.6 + Math.random() * 0.3;
|
||||
|
||||
// Varying shades of gray/white
|
||||
const gray = Math.floor(180 + Math.random() * 75);
|
||||
this.color = `rgba(${gray}, ${gray}, ${gray},`;
|
||||
}
|
||||
|
||||
update(delta: number): boolean {
|
||||
this.life += delta;
|
||||
if (this.life >= this.maxLife) return false;
|
||||
|
||||
const t = this.life / this.maxLife;
|
||||
this.x += this.vx * delta;
|
||||
this.y += this.vy * delta;
|
||||
|
||||
// Grow significantly over time
|
||||
this.size = 1 + t * (this.maxSize - 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
const t = this.life / this.maxLife;
|
||||
// Fade out towards the end
|
||||
const currentOpacity = this.opacity * (1 - t * t);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `${this.color} ${currentOpacity})`;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
export class SmokeTrailFx implements Fx {
|
||||
private particles: SmokeParticle[] = [];
|
||||
private lastEmitTime: number = 0;
|
||||
private emitInterval: number = 15; // Faster emission for denser trail
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private unitId: number,
|
||||
) {}
|
||||
|
||||
renderTick(duration: number, ctx: CanvasRenderingContext2D): boolean {
|
||||
const unit = this.game.unit(this.unitId);
|
||||
const isActive = unit?.isActive() ?? false;
|
||||
|
||||
if (isActive) {
|
||||
this.lastEmitTime += duration;
|
||||
while (this.lastEmitTime >= this.emitInterval) {
|
||||
this.emit(unit!);
|
||||
this.lastEmitTime -= this.emitInterval;
|
||||
}
|
||||
}
|
||||
|
||||
// Update and render particles
|
||||
for (let i = this.particles.length - 1; i >= 0; i--) {
|
||||
if (!this.particles[i].update(duration)) {
|
||||
this.particles.splice(i, 1);
|
||||
} else {
|
||||
this.particles[i].draw(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
return isActive || this.particles.length > 0;
|
||||
}
|
||||
|
||||
private emit(unit: UnitView) {
|
||||
// Add some randomness to spawn position so it's not a perfect line
|
||||
const offsetX = (Math.random() - 0.5) * 2;
|
||||
const offsetY = (Math.random() - 0.5) * 2;
|
||||
this.particles.push(
|
||||
new SmokeParticle(
|
||||
this.game.x(unit.tile()) + offsetX,
|
||||
this.game.y(unit.tile()) + offsetY,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,13 @@ import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
|
||||
import { conquestFxFactory } from "../fx/ConquestFx";
|
||||
import { Fx, FxType } from "../fx/Fx";
|
||||
import { nukeFxFactory, ShockwaveFx } from "../fx/NukeFx";
|
||||
import { SmokeTrailFx } from "../fx/SmokeTrailFx";
|
||||
import { SpriteFx } from "../fx/SpriteFx";
|
||||
import { UnitExplosionFx } from "../fx/UnitExplosionFx";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
import { RailTileChangedEvent } from "./RailroadLayer";
|
||||
|
||||
export class FxLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
@@ -25,6 +27,7 @@ export class FxLayer implements Layer {
|
||||
new AnimatedSpriteLoader();
|
||||
|
||||
private allFx: Fx[] = [];
|
||||
private missileTrails = new Map<number, SmokeTrailFx>();
|
||||
private hasBufferedFrame = false;
|
||||
|
||||
constructor(
|
||||
@@ -59,6 +62,22 @@ export class FxLayer implements Layer {
|
||||
}
|
||||
|
||||
onUnitEvent(unit: UnitView) {
|
||||
if (unit.isActive() && !this.missileTrails.has(unit.id())) {
|
||||
const type = unit.type();
|
||||
if (
|
||||
type === UnitType.AtomBomb ||
|
||||
type === UnitType.HydrogenBomb ||
|
||||
type === UnitType.MIRV ||
|
||||
type === UnitType.MIRVWarhead ||
|
||||
type === UnitType.SAMMissile
|
||||
) {
|
||||
this.missileTrails.set(
|
||||
unit.id(),
|
||||
new SmokeTrailFx(this.game, unit.id()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
switch (unit.type()) {
|
||||
case UnitType.AtomBomb: {
|
||||
this.onNukeEvent(unit, 70);
|
||||
@@ -71,6 +90,14 @@ export class FxLayer implements Layer {
|
||||
this.onNukeEvent(unit, 160);
|
||||
break;
|
||||
}
|
||||
case UnitType.MIRV: {
|
||||
this.onNukeEvent(unit, 70);
|
||||
break;
|
||||
}
|
||||
case UnitType.SAMMissile: {
|
||||
this.onNukeEvent(unit, 70);
|
||||
break;
|
||||
}
|
||||
case UnitType.Warship:
|
||||
this.onWarshipEvent(unit);
|
||||
break;
|
||||
@@ -256,12 +283,13 @@ export class FxLayer implements Layer {
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
const nowMs = performance.now();
|
||||
|
||||
const hasFx = this.allFx.length > 0;
|
||||
const hasFx = this.allFx.length > 0 || this.missileTrails.size > 0;
|
||||
if (!this.game.config().userSettings()?.fxLayer() || !hasFx) {
|
||||
if (this.hasBufferedFrame) {
|
||||
// Clear stale pixels once when fx ends/disabled so re-enabling doesn't
|
||||
// flash old frames.
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.missileTrails.clear();
|
||||
this.hasBufferedFrame = false;
|
||||
}
|
||||
this.lastRefreshMs = nowMs;
|
||||
@@ -320,5 +348,11 @@ export class FxLayer implements Layer {
|
||||
this.allFx.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [unitId, trail] of this.missileTrails) {
|
||||
if (!trail.renderTick(duration, this.context)) {
|
||||
this.missileTrails.delete(unitId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { EventBus } from "../../../core/EventBus";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { UnitType } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { UnitView } from "../../../core/game/GameView";
|
||||
import { BezenhamLine } from "../../../core/utilities/Line";
|
||||
import {
|
||||
AlternateViewEvent,
|
||||
|
||||
Reference in New Issue
Block a user