Skip to content

Commit

Permalink
[feat] Add gas grenade.
Browse files Browse the repository at this point in the history
  • Loading branch information
Half-Shot committed Mar 10, 2025
1 parent ae7f939 commit 248b48f
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 28 deletions.
4 changes: 4 additions & 0 deletions src/entities/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { RigidBody, Vector2 } from "@dimforge/rapier2d-compat";
import { UPDATE_PRIORITY } from "pixi.js";
import { MetersValue } from "../utils";
import { EntityType } from "./type";
import { PlayableCondition } from "./playable/conditions";

/**
* Base entity which all game objects implement
Expand All @@ -17,6 +18,9 @@ export interface IGameEntity {

export interface OnDamageOpts {
maxDamage?: number;
forceMultiplier?: number;
applyCondition?: PlayableCondition;
damagesTerrain?: boolean,
}

/**
Expand Down
57 changes: 57 additions & 0 deletions src/entities/phys/gas-grenade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Container } from "pixi.js";
import { GameWorld } from "../../world";
import { Coordinate, MetersValue } from "../../utils/coodinate";
import { WormInstance } from "../../logic";
import { EntityType } from "../type";
import { Grenade } from "./grenade";
import { getConditionTint, PlayableCondition } from "../playable/conditions";

/**
* Grenade projectile.
*/
export class GasGrenade extends Grenade {
static create(
parent: Container,
world: GameWorld,
position: Coordinate,
initialForce: { x: number; y: number },
timerSecs = 3,
worm?: WormInstance,
) {
const ent = new GasGrenade(
position,
initialForce,
world,
parent,
timerSecs,
worm,
);
parent.addChild(ent.sprite, ent.wireframe.renderable);
return ent;
}

private constructor(
position: Coordinate,
initialForce: { x: number; y: number },
world: GameWorld,
parent: Container,
timerSecs: number,
owner?: WormInstance,
) {
super(position, initialForce, world, parent, timerSecs, owner, {
applyCondition: PlayableCondition.Sickness,
maxDamage: 10,
explosionRadius: new MetersValue(3),
damagesTerrain: false,
explosionHue: getConditionTint([PlayableCondition.Sickness]) ?? 0xFFFFF,
forceMultiplier: 0.25,
});
}

recordState() {
return {
...super.recordState(),
type: EntityType.GasGrenade,
};
}
}
6 changes: 4 additions & 2 deletions src/entities/phys/grenade.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Container, Sprite, Text, Texture, Ticker } from "pixi.js";
import { TimedExplosive } from "./timedExplosive";
import { TimedExplosive, TimedExplosiveOpts } from "./timedExplosive";
import { IPhysicalEntity } from "../entity";
import { BitmapTerrain } from "../bitmapTerrain";
import { IMediaInstance, Sound } from "@pixi/sound";
Expand Down Expand Up @@ -62,13 +62,14 @@ export class Grenade extends TimedExplosive {
}
public bounceSoundPlayback?: IMediaInstance;

private constructor(
protected constructor(
position: Coordinate,
initialForce: { x: number; y: number },
world: GameWorld,
parent: Container,
timerSecs: number,
owner?: WormInstance,
optsOverrides?: Partial<TimedExplosiveOpts>,
) {
const sprite = new Sprite(Grenade.texture);
sprite.scale.set(0.5);
Expand All @@ -89,6 +90,7 @@ export class Grenade extends TimedExplosive {
autostartTimer: true,
ownerWorm: owner,
maxDamage: 40,
...optsOverrides,
});
this.timerText = new Text({
text: "",
Expand Down
11 changes: 9 additions & 2 deletions src/entities/phys/timedExplosive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import { MetersValue } from "../../utils/coodinate";
import { WormInstance } from "../../logic";
import { handleDamageInRadius } from "../../utils/damage";
import { RecordedEntityState } from "../../state/model";
import { PlayableCondition } from "../playable/conditions";

interface Opts {
export interface TimedExplosiveOpts {
explosionRadius: MetersValue;
explodeOnContact: boolean;
explosionHue?: ColorSource;
Expand All @@ -23,6 +24,9 @@ interface Opts {
timerSecs?: number;
ownerWorm?: WormInstance;
maxDamage: number;
applyCondition?: PlayableCondition,
damagesTerrain?: boolean;
forceMultiplier?: number;
}

export interface TimedExplosiveRecordedState extends RecordedEntityState {
Expand All @@ -48,7 +52,7 @@ export abstract class TimedExplosive<
body: RapierPhysicsObject,
gameWorld: GameWorld,
private readonly parent: Container,
protected readonly opts: Opts,
protected readonly opts: TimedExplosiveOpts,
) {
super(sprite, body, gameWorld);
this.gameWorld.addBody(this, body.collider);
Expand Down Expand Up @@ -93,6 +97,9 @@ export abstract class TimedExplosive<
hue: this.opts.explosionHue ?? 0xffffff,
shrapnelHue: this.opts.explosionShrapnelHue ?? 0xffffff,
maxDamage: this.opts.maxDamage,
applyCondition: this.opts.applyCondition,
damagesTerrain: this.opts.damagesTerrain,
forceMultiplier: this.opts.forceMultiplier,
},
this.physObject.collider,
);
Expand Down
29 changes: 22 additions & 7 deletions src/entities/playable/playable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { HEALTH_CHANGE_TENSION_TIMER_MS } from "../../consts";
import { first, skip, Subscription } from "rxjs";
import Logger from "../../log";
import { TiledSpriteAnimated } from "../../utils/tiledspriteanimated";
import { getConditionEffect, PlayableCondition } from "./conditions";
import { getConditionEffect, getConditionTint, PlayableCondition } from "./conditions";

interface Opts {
explosionRadius: MetersValue;
Expand Down Expand Up @@ -250,15 +250,15 @@ export abstract class PlayableEntity<
this.conditions
.values()
.map((v) => getConditionEffect(v).damageMultiplier)
.reduce((v, c) => {
.reduce<number>((v, c) => {
if (c === undefined) {
return v;
}
if (v === undefined) {
return c;
}
return Math.min(v, c);
}) ?? 1;
}, 1);
this.wormIdent.setHealth(this.wormIdent.health - damage * damageMultiplier);
}

Expand All @@ -284,14 +284,26 @@ export abstract class PlayableEntity<
cannotDieFromDamage: v.cannotDieFromDamage || c.cannotDieFromDamage,
takeDamagePerRound: v.takeDamagePerRound + c.takeDamagePerRound,
};
}) ?? 0;
}, { takeDamagePerRound: 0, cannotDieFromDamage: false });
if (takeDamagePerRound) {
if (cannotDieFromDamage && this.wormIdent.health > takeDamagePerRound) {
this.reduceHealth(takeDamagePerRound);
}
}
}

public addCondition(condition: PlayableCondition) {
this.conditions.add(condition);
const teamColor = teamGroupToColorSet(this.wormIdent.team.group).fg;
this.sprite.tint = getConditionTint(this.conditions) ?? teamColor;
}

public removeCondition(condition: PlayableCondition) {
this.conditions.delete(condition);
const teamColor = teamGroupToColorSet(this.wormIdent.team.group).fg;
this.sprite.tint = getConditionTint(this.conditions) ?? teamColor;
}

public onDamage(
point: Vector2,
radius: MetersValue,
Expand All @@ -310,17 +322,17 @@ export abstract class PlayableEntity<
this.conditions
.values()
.map((v) => getConditionEffect(v).forceMultiplier)
.reduce((v, c) => {
.reduce<number>((v, c) => {
if (c === undefined) {
return v;
}
if (v === undefined) {
return c;
}
return Math.min(v, c);
}) ?? 1;
}, 1);
const forceMag =
Math.abs((radius.value * 10) / (1 / distance)) * forceMultiplier;
Math.abs((radius.value * 10) / (1 / distance)) * forceMultiplier * (opts.forceMultiplier ?? 1);
const massagedY = point.y + 5;
const force = mult(
{
Expand All @@ -331,6 +343,9 @@ export abstract class PlayableEntity<
);
log.info("onDamage force", "=>", force);
this.physObject.body.applyImpulse(force, true);
if (opts.applyCondition) {
this.addCondition(opts.applyCondition);
}
}

public recordState() {
Expand Down
16 changes: 3 additions & 13 deletions src/entities/playable/worm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,18 +264,6 @@ export class Worm extends PlayableEntity<WormRecordedState> {
});
}

public addCondition(condition: PlayableCondition) {
this.conditions.add(condition);
const teamColor = teamGroupToColorSet(this.wormIdent.team.group).fg;
this.sprite.tint = getConditionTint(this.conditions) ?? teamColor;
}

public removeCondition(condition: PlayableCondition) {
this.conditions.delete(condition);
const teamColor = teamGroupToColorSet(this.wormIdent.team.group).fg;
this.sprite.tint = getConditionTint(this.conditions) ?? teamColor;
}

public selectWeapon(weapon: IWeaponDefinition) {
if (this.perRoundState.shotsTaken > 0) {
// Worm is already in progress of shooting things.
Expand Down Expand Up @@ -307,6 +295,8 @@ export class Worm extends PlayableEntity<WormRecordedState> {
Controller.on("inputBegin", this.onInputBegin);
Controller.on("inputEnd", this.onInputEnd);
}
// TODO: Switch weapon if there is no more ammo.
// if (this.wormIdent.team.availableWeapons
}

onEndOfTurn() {
Expand Down Expand Up @@ -750,7 +740,7 @@ export class Worm extends PlayableEntity<WormRecordedState> {
} else {
this.weaponSprite.position.set(
this.sprite.x -
(this.sprite.width + this.currentWeapon.sprite.offset.x),
(this.sprite.width + this.currentWeapon.sprite.offset.x),
this.sprite.y + this.currentWeapon.sprite.offset.y,
);
this.weaponSprite.rotation = this.fireAngle - Math.PI;
Expand Down
1 change: 1 addition & 0 deletions src/entities/type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum EntityType {
Worm = "wormgine.worm",
Grenade = "wormgine.grenade",
GasGrenade = "wormgine.grenade_gas",
BazookaShell = "wormgine.bazooka_shell",
HomingMissile = "wormgine.homing_missile",
Mine = "wormgine.mine",
Expand Down
6 changes: 5 additions & 1 deletion src/utils/damage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { GameWorld, PIXELS_PER_METER } from "../world";
import { Explosion, ExplosionsOptions } from "../entities/explosion";
import { Container } from "pixi.js";
import { OnDamageOpts } from "../entities/entity";
import { BitmapTerrain } from "../entities/bitmapTerrain";

interface Opts extends Partial<ExplosionsOptions>, OnDamageOpts {}
interface Opts extends Partial<ExplosionsOptions>, OnDamageOpts { }

export function handleDamageInRadius(
gameWorld: GameWorld,
Expand All @@ -22,6 +23,9 @@ export function handleDamageInRadius(
ignoreCollider,
);
for (const element of explosionCollidesWith) {
if (element instanceof BitmapTerrain && opts.damagesTerrain === false) {
continue;
}
element.onDamage?.(point, radius, opts);
}
gameWorld.addEntity(
Expand Down
52 changes: 52 additions & 0 deletions src/weapons/gas-grenade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Container, Point } from "pixi.js";
import {
FireOpts,
IWeaponCode,
IWeaponDefiniton,
projectileWeaponHelper,
} from "./weapon";
import { Worm } from "../entities/playable/worm";
import { GameWorld } from "../world";
import icon from "../assets/grenade.png";
import { GasGrenade } from "../entities/phys/gas-grenade";

export const WeaponGasGrenade: IWeaponDefiniton = {
name: "Gas Grenade",
icon,
code: IWeaponCode.GasGrenade,
maxDuration: 50,
allowGetaway: true,
timerAdjustable: true,
showTargetGuide: true,
loadAssets(assets) {
this.sprite = {
texture: assets.textures.grenade,
scale: new Point(0.33, 0.33),
offset: new Point(3, -10),
};
},
fireFn(parent: Container, world: GameWorld, worm: Worm, opts: FireOpts) {
if (!opts.duration) {
throw Error("Duration expected but not given");
}
if (!opts.timer) {
throw Error("Timer expected but not given");
}
if (opts.angle === undefined) {
throw Error("Angle expected but not given");
}
const { position, force } = projectileWeaponHelper(
worm.position,
opts.duration,
opts.angle,
);
return GasGrenade.create(
parent,
world,
position,
force,
opts.timer,
worm.wormIdent,
);
},
};
3 changes: 3 additions & 0 deletions src/weapons/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AssetPack } from "../assets";
import { WeaponBazooka } from "./bazooka";
import { WeaponGrenade } from "./grenade";
import { WeaponGasGrenade } from "./gas-grenade";
import WeaponShotgun from "./shotgun";
import WeaponFireworkLauncher from "./firework";
import WeaponHomingMissile from "./homingMissile";
Expand Down Expand Up @@ -31,6 +32,8 @@ export function getDefinitionForCode(code: IWeaponCode) {
return WeaponMine;
case IWeaponCode.HomingMissile:
return WeaponHomingMissile;
case IWeaponCode.GasGrenade:
return WeaponGasGrenade;
default:
throw Error(`Unknown weapon code '${code}'`);
}
Expand Down
7 changes: 4 additions & 3 deletions src/weapons/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { IWeaponCode } from "./weapon";

export const DefaultWeaponSchema: Record<IWeaponCode, number> = Object.freeze({
[IWeaponCode.Bazooka]: -1,
[IWeaponCode.FireworkLauncher]: -1,
[IWeaponCode.FireworkLauncher]: 1,
[IWeaponCode.Grenade]: -1,
[IWeaponCode.HomingMissile]: -1,
[IWeaponCode.Mine]: -1,
[IWeaponCode.HomingMissile]: 1,
[IWeaponCode.Mine]: 2,
[IWeaponCode.Shotgun]: -1,
[IWeaponCode.GasGrenade]: 3,
});
1 change: 1 addition & 0 deletions src/weapons/weapon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Vector2 } from "@dimforge/rapier2d-compat";

export enum IWeaponCode {
Grenade = "wep_grenade",
GasGrenade = "wep_grenade_gas",
Bazooka = "wep_bazooka",
Shotgun = "wep_shotgun",
FireworkLauncher = "wep_firework",
Expand Down

0 comments on commit 248b48f

Please sign in to comment.