survivors: adding health regen and simple particle system

This commit is contained in:
Sheldan
2025-09-14 18:09:12 +02:00
parent db2110c921
commit 9bb7ec99c0
7 changed files with 145 additions and 5 deletions

View File

@@ -3,6 +3,8 @@ import {Vector} from "./base.ts";
import {fillDot, getCoordinatesSplit} from "./utils.ts"; import {fillDot, getCoordinatesSplit} from "./utils.ts";
import {PlayerStats} from "./stats.ts"; import {PlayerStats} from "./stats.ts";
import {PlayerStatus} from "./status.ts"; import {PlayerStatus} from "./status.ts";
import {World} from "./World.ts";
import {HealingParticle} from "./particles.ts";
export class Player implements Drawable, Acting, Healthy { export class Player implements Drawable, Acting, Healthy {
private _position: Vector; private _position: Vector;
@@ -14,10 +16,13 @@ export class Player implements Drawable, Acting, Healthy {
private _status: PlayerStatus; private _status: PlayerStatus;
private _weapons: Weapon[] = [] private _weapons: Weapon[] = []
private _items: Item[] = [] private _items: Item[] = []
private _healTick: number = 0;
private static readonly HEAL_TICK_INTERVAL = 20;
private _world: World;
// temp // temp
private _speed: Vector; private _speed: Vector;
private _toHeal: number = 0;
constructor(position: Vector) { constructor(position: Vector) {
this._position = position; this._position = position;
@@ -61,6 +66,10 @@ export class Player implements Drawable, Acting, Healthy {
this._items.push(item) this._items.push(item)
} }
set world(value: World) {
this._world = value;
}
move(direction: Vector) { move(direction: Vector) {
this._position = this.position.add(direction) this._position = this.position.add(direction)
} }
@@ -145,5 +154,23 @@ export class Player implements Drawable, Acting, Healthy {
level() { level() {
return this.status.level return this.status.level
} }
isHurt() {
return this.health < this._effectiveStats.health
}
tick(seconds: number, tick: number) {
this._healTick += 1;
if((this._healTick % Player.HEAL_TICK_INTERVAL) == 0 && this.isHurt()) {
let healed = this._effectiveStats.healthRegen / seconds
this._toHeal += healed;
if(this._toHeal >= 1) {
let toHealNow = this._toHeal - (this._toHeal % 1);
this._toHeal -= toHealNow;
this.heal(toHealNow);
HealingParticle.spawnHealingParticle(this._world, toHealNow, this.position)
}
}
}
} }

View File

@@ -2,20 +2,25 @@ import {Enemy} from "./Enemies.ts";
import {Player} from "./Player.ts"; import {Player} from "./Player.ts";
import {Projectile } from "./projectile.ts"; import {Projectile } from "./projectile.ts";
import {Vector} from "./base.ts"; import {Vector} from "./base.ts";
import type {Drop, Placeable} from "./interfaces.ts"; import type {Drop, Particle, Placeable} from "./interfaces.ts";
export class World { export class World {
private _enemies: ObjectContainer<Enemy> = new ObjectContainer<Enemy>() private _enemies: ObjectContainer<Enemy> = new ObjectContainer<Enemy>()
private _projectiles: ObjectContainer<Projectile> = new ObjectContainer<Projectile>(); private _projectiles: ObjectContainer<Projectile> = new ObjectContainer<Projectile>();
private _drops: ObjectContainer<Drop> = new ObjectContainer<Drop>(); private _drops: ObjectContainer<Drop> = new ObjectContainer<Drop>();
private _particles: ObjectContainer<Particle> = new ObjectContainer();
private _player: Player; private _player: Player;
private readonly _ctx: CanvasRenderingContext2D; private readonly _ctx: CanvasRenderingContext2D;
private _size: Vector; private _size: Vector;
private _tick: number = 0;
private static readonly TICK_INTERVAL = 10;
private timeStamp: Date;
constructor(player: Player, ctx: CanvasRenderingContext2D, size: Vector) { constructor(player: Player, ctx: CanvasRenderingContext2D, size: Vector) {
this._player = player; this._player = player;
this._ctx = ctx; this._ctx = ctx;
this._size = size; this._size = size;
this.timeStamp = new Date();
} }
enemiesAct() { enemiesAct() {
@@ -25,12 +30,15 @@ export class World {
this._projectiles.clean() this._projectiles.clean()
this._drops.items.forEach(drop => drop.act()) this._drops.items.forEach(drop => drop.act())
this._drops.clean() this._drops.clean()
this._particles.items.forEach(particle => particle.act())
this._particles.clean()
} }
draw() { draw() {
this._enemies.items.forEach(enemy => enemy.draw(this._ctx)) this._enemies.items.forEach(enemy => enemy.draw(this._ctx))
this._drops.items.forEach(drop => drop.draw(this._ctx)) this._drops.items.forEach(drop => drop.draw(this._ctx))
this._projectiles.items.forEach(projectile => projectile.draw(this._ctx)) this._projectiles.items.forEach(projectile => projectile.draw(this._ctx))
this._particles.items.forEach(particle => particle.draw(this._ctx))
this._player.draw(this._ctx); this._player.draw(this._ctx);
} }
@@ -38,6 +46,10 @@ export class World {
this._projectiles.add(projectile) this._projectiles.add(projectile)
} }
addParticle(particle: Particle) {
this._particles.add(particle)
}
addDrop(drop: Drop) { addDrop(drop: Drop) {
this._drops.add(drop) this._drops.add(drop)
} }
@@ -46,6 +58,10 @@ export class World {
this._drops.scheduleRemoval(drop) this._drops.scheduleRemoval(drop)
} }
removeParticle(particle: Particle) {
this._particles.scheduleRemoval(particle)
}
removeEnemy(enemy: Enemy) { removeEnemy(enemy: Enemy) {
this._enemies.scheduleRemoval(enemy) this._enemies.scheduleRemoval(enemy)
} }
@@ -69,11 +85,21 @@ export class World {
return Math.max(this.size.x, this.size.y) return Math.max(this.size.x, this.size.y)
} }
get size(): Vector { get size(): Vector {
return this._size; return this._size;
} }
tick() {
this._tick += 1;
if((this._tick % World.TICK_INTERVAL) == 0) {
let currentTimeStamp = new Date();
let seconds = (currentTimeStamp.getTime() - this.timeStamp.getTime()) / 1000;
this._player.tick(seconds, this._tick);
this._particles.items.forEach(particle => particle.tick(seconds, this._tick))
this.timeStamp = currentTimeStamp;
}
}
outside(position: Vector): boolean { outside(position: Vector): boolean {
return position.x > this.size.x || position.y > this.size.y || position.x < 0 || position.y < 0 return position.x > this.size.x || position.y > this.size.y || position.x < 0 || position.y < 0
} }

View File

@@ -42,6 +42,9 @@ export abstract class BasicDrop implements Drop {
return this.size return this.size
} }
tick(seconds: number, tick: number) {
}
} }
export class MoneyDrop extends BasicDrop { export class MoneyDrop extends BasicDrop {

View File

@@ -5,6 +5,7 @@ import type {World} from "./World.ts";
export interface Acting { export interface Acting {
act() act()
tick(seconds: number, tick: number)
} }
export interface Healthy { export interface Healthy {
@@ -33,6 +34,10 @@ export interface Drop extends Drawable, Acting {
pickup() pickup()
} }
export interface Particle extends Drawable, Placeable, Acting {
}
export interface Placeable { export interface Placeable {
move(any?: any) move(any?: any)
getSize(); getSize();

View File

@@ -9,7 +9,6 @@ import {HUD} from "./ui.ts";
import {Pistol} from "./weapons.ts"; import {Pistol} from "./weapons.ts";
import {ItemManagement} from "./items.ts"; import {ItemManagement} from "./items.ts";
let hud: HUD; let hud: HUD;
let world: World; let world: World;
let config: Config; let config: Config;
@@ -39,6 +38,7 @@ function updateCanvas() {
ctx.clearRect(0, 0, world.size.x, world.size.y); ctx.clearRect(0, 0, world.size.x, world.size.y);
hud.draw(ctx) hud.draw(ctx)
if(!state.ended) { if(!state.ended) {
world.tick()
world.enemiesAct() world.enemiesAct()
world.player.act() world.player.act()
world.draw() world.draw()
@@ -108,6 +108,7 @@ docReady(function () {
let player = Player.generatePlayer(new Vector(window.innerWidth /2, window.innerHeight / 2)); let player = Player.generatePlayer(new Vector(window.innerWidth /2, window.innerHeight / 2));
world = new World(player, ctx, new Vector(window.innerWidth, window.innerHeight)); world = new World(player, ctx, new Vector(window.innerWidth, window.innerHeight));
player.world = world; // not sure if this is great design
state = new WorldState(); state = new WorldState();
setInterval(() => { setInterval(() => {

View File

@@ -0,0 +1,69 @@
import type {Particle} from "./interfaces.ts";
import {Vector} from "./base.ts";
import {World} from "./World.ts";
abstract class BaseParticle implements Particle {
protected _position: Vector;
protected world: World;
constructor(position: Vector, world: World) {
this._position = position.clone();
this.world = world;
}
getPosition(): Vector {
return this._position;
}
getSize() {
}
move(any?: any) {
this._position = this._position.add(new Vector(0, -0.5))
}
draw(ctx: CanvasRenderingContext2D) {
}
act() {
this.move()
}
tick(seconds: number, tick: number) {
}
}
export class HealingParticle extends BaseParticle {
private healthAmount: number;
private secondsToDisplay: number = 2;
private alreadyDisplayed: number = 0;
constructor(position: Vector, world: World, healthAmount: number) {
super(position, world);
this.healthAmount = healthAmount;
}
draw(ctx: CanvasRenderingContext2D) {
ctx.fillStyle = 'green';
ctx.fillText(this.healthAmount + '', this._position.x, this._position.y);
}
static spawnHealingParticle(world: World, health: number, position: Vector) {
world.addParticle(this.createHealingParticle(world, health, position))
}
static createHealingParticle(world: World, health: number, position: Vector) {
let healingParticle = new HealingParticle(position, world, health)
return healingParticle
}
tick(seconds: number, tick: number) {
this.alreadyDisplayed += seconds;
if(this.alreadyDisplayed > this.secondsToDisplay) {
this.world.removeParticle(this)
}
}
}

View File

@@ -7,6 +7,7 @@ export class PlayerStats {
private _pullRange: number; private _pullRange: number;
private _weaponRange: number; private _weaponRange: number;
private _weaponRangeFactor: number; private _weaponRangeFactor: number;
private _healthRegen: number;
constructor() { constructor() {
this._speed = 3; this._speed = 3;
@@ -15,6 +16,7 @@ export class PlayerStats {
this._pullRange = 150; this._pullRange = 150;
this._weaponRange = 250; this._weaponRange = 250;
this._weaponRangeFactor = 1; this._weaponRangeFactor = 1;
this._healthRegen = 0.001;
} }
resetToBasic() { resetToBasic() {
@@ -22,7 +24,8 @@ export class PlayerStats {
this._health = 0; this._health = 0;
this._pullRange = 0; this._pullRange = 0;
this._weaponRange = 0; this._weaponRange = 0;
this._weaponRangeFactor = 1 this._weaponRangeFactor = 1;
this._healthRegen = 0.1;
} }
increaseLevel() { increaseLevel() {
@@ -31,6 +34,7 @@ export class PlayerStats {
this._pullRange *= 1.1; this._pullRange *= 1.1;
this._weaponRange *= 1.25 this._weaponRange *= 1.25
this._weaponRangeFactor += 0.1 this._weaponRangeFactor += 0.1
this._healthRegen += 0.1
} }
mergeStats(otherStats: PlayerStats) { mergeStats(otherStats: PlayerStats) {
@@ -39,6 +43,7 @@ export class PlayerStats {
this._pullRange += otherStats._pullRange; this._pullRange += otherStats._pullRange;
this._weaponRange += otherStats._weaponRange this._weaponRange += otherStats._weaponRange
this._weaponRangeFactor += otherStats._weaponRangeFactor; this._weaponRangeFactor += otherStats._weaponRangeFactor;
this._healthRegen += otherStats._healthRegen;
} }
clone() { clone() {
@@ -91,6 +96,10 @@ export class PlayerStats {
return this._weaponRange return this._weaponRange
} }
get healthRegen(): number {
return this._healthRegen;
}
get effectiveWeaponRange(): number { get effectiveWeaponRange(): number {
return this._weaponRange * this._weaponRangeFactor; return this._weaponRange * this._weaponRangeFactor;
} }