survivors: adding functional container enemy with random chances on death

split between create and spawn methods
adding level drop
adding ability to player to level
fixing hud not showing the label value
adding current level display to hud
adding ability to weapons to level
This commit is contained in:
Sheldan
2025-08-29 22:58:44 +02:00
parent 59f1a4b164
commit a52754ce0d
8 changed files with 251 additions and 58 deletions

View File

@@ -3,13 +3,14 @@ import {drawDot, moveInDirectionOf} from "./utils.ts";
import {Vector} from "./base.ts"; import {Vector} from "./base.ts";
import {World} from "./World.ts"; import {World} from "./World.ts";
import type {Projectile} from "./projectile.ts"; import type {Projectile} from "./projectile.ts";
import {HomingProjectile, ProjectileStats, StraightProjectile} from "./projectile.ts"; import {ProjectileStats, StraightProjectile} from "./projectile.ts";
import {HealthPack, MoneyDrop} from "./drop.ts"; import {HealthPack, LevelDrop, MoneyDrop} from "./drop.ts";
export abstract class Enemy implements Placeable, Drawable, Acting, Healthy { export abstract class Enemy implements Placeable, Drawable, Acting, Healthy {
protected _position: Vector; protected _position: Vector;
protected speed: number; protected speed: number;
protected world: World; protected world: World;
protected size: number
protected status: EnemyStatus = new EnemyStatus(10); protected status: EnemyStatus = new EnemyStatus(10);
constructor(position: Vector) { constructor(position: Vector) {
@@ -40,10 +41,11 @@ export abstract class Enemy implements Placeable, Drawable, Acting, Healthy {
} }
die() { die() {
MoneyDrop.createMoneyDrop(this.world, this._position); MoneyDrop.spawnMoneyDrop(this.world, this._position);
} }
getSize() { getSize() {
return this.size;
} }
dead() { dead() {
@@ -58,14 +60,13 @@ export class BasicEnemy extends Enemy {
super(position); super(position);
} }
protected size: number;
protected color: string; protected color: string;
protected impactDamage: number; protected impactDamage: number;
protected impactCooldown: number = 0; protected impactCooldown: number = 0;
protected impactInterval: number = 60; protected impactInterval: number = 60;
draw(ctx: CanvasRenderingContext2D) { draw(ctx: CanvasRenderingContext2D) {
drawDot(this._position, this.size, this.color, ctx) drawDot(this._position, this.getSize(), this.color, ctx)
} }
@@ -75,13 +76,17 @@ export class BasicEnemy extends Enemy {
act() { act() {
super.act(); super.act();
if(this._position.distanceTo(this.world.player.position) < this.size && this.impactCooldown <= 0) { if(this._position.distanceTo(this.world.player.position) < this.getSize() && this.impactCooldown <= 0) {
this.world.player.takeDamage(this.impactDamage) this.world.player.takeDamage(this.impactDamage)
this.impactCooldown = this.impactInterval; this.impactCooldown = this.impactInterval;
} }
this.impactCooldown -= 1; this.impactCooldown -= 1;
} }
static spawnBasicEnemy(world: World, position?: Vector) {
world.addEnemy(this.generateBasicEnemy(world, position))
}
static generateBasicEnemy(world: World, position?: Vector): BasicEnemy { static generateBasicEnemy(world: World, position?: Vector): BasicEnemy {
if(position === undefined) { if(position === undefined) {
position = new Vector(250, 250) position = new Vector(250, 250)
@@ -95,9 +100,6 @@ export class BasicEnemy extends Enemy {
return basicEnemy; return basicEnemy;
} }
getSize() {
return this.size
}
} }
export class ShootingEnemy extends BasicEnemy implements Shooting { export class ShootingEnemy extends BasicEnemy implements Shooting {
@@ -133,18 +135,22 @@ export class ShootingEnemy extends BasicEnemy implements Shooting {
return projectile return projectile
} }
static spawnShootingEnemy(world: World, position?: Vector) {
world.addEnemy(this.generateShootingEnemy(world, position))
}
static generateShootingEnemy(world: World, position?: Vector) { static generateShootingEnemy(world: World, position?: Vector) {
if(position === undefined) { if(position === undefined) {
position = world.randomPlace() position = world.randomPlace()
} }
let basicEnemy = new ShootingEnemy(position); let shootingEnemy = new ShootingEnemy(position);
basicEnemy.size = 5; shootingEnemy.size = 5;
basicEnemy.world = world; shootingEnemy.world = world;
basicEnemy.speed = 0.5; shootingEnemy.speed = 0.5;
basicEnemy.color = 'green' shootingEnemy.color = 'green'
basicEnemy.impactDamage = 2; shootingEnemy.impactDamage = 2;
basicEnemy.shootInterval = 100 shootingEnemy.shootInterval = 100
return basicEnemy; return shootingEnemy
} }
} }
@@ -172,7 +178,6 @@ export class HealthEnemy extends Enemy {
super(position); super(position);
} }
protected size: number;
protected color: string; protected color: string;
draw(ctx: CanvasRenderingContext2D) { draw(ctx: CanvasRenderingContext2D) {
@@ -188,7 +193,11 @@ export class HealthEnemy extends Enemy {
} }
die() { die() {
HealthPack.createHealthPack(this.world, this._position) HealthPack.spawnHealthPack(this.world, this._position)
}
static spawnHealthEnemy(world: World, position?: Vector) {
world.addEnemy(this.createHealthEnemy(world, position))
} }
static createHealthEnemy(world: World, position?: Vector) { static createHealthEnemy(world: World, position?: Vector) {
@@ -208,3 +217,96 @@ export class HealthEnemy extends Enemy {
return this.size return this.size
} }
} }
export class ContainerEnemy extends Enemy {
private drops: KillChanceTable;
constructor(position: Vector) {
super(position);
this.status.health = 5;
this.drops = new KillChanceTable();
this.drops.addDrop( {chance: 50, creationMethod: this.spawnHealthPack})
this.drops.addDrop( {chance: 50, creationMethod: this.spawnLevelUp})
this.drops.addDrop( {chance: 10, creationMethod: this.spawnEnemy})
this.drops.calculateProbs()
}
protected color: string;
draw(ctx: CanvasRenderingContext2D) {
drawDot(this._position, this.size, this.color, ctx)
}
move() {
}
act() {
super.act();
}
die() {
this.drops.draw().creationMethod(this)
}
spawnHealthPack(enemy: ContainerEnemy) {
HealthPack.spawnHealthPack(enemy.world, enemy._position)
}
spawnLevelUp(enemy: ContainerEnemy) {
LevelDrop.spawnLevelDrop(enemy.world, enemy._position)
}
spawnEnemy(enemy: ContainerEnemy) {
ShootingEnemy.spawnShootingEnemy(enemy.world, enemy._position)
}
static spawnContainerEnemy(world: World, position?: Vector) {
world.addEnemy(this.createContainerEnemy(world, position))
}
static createContainerEnemy(world: World, position?: Vector) {
if(position === undefined) {
position = world.randomPlace()
}
let basicEnemy = new ContainerEnemy(position);
basicEnemy.size = 5;
basicEnemy.world = world;
basicEnemy.speed = 0;
basicEnemy.color = 'brown'
return basicEnemy;
}
getSize() {
return this.size
}
}
export interface ChanceEntry {
chance: number;
creationMethod: (any: any) => void;
}
export class KillChanceTable {
private chances: ChanceEntry[] = []
addDrop(entry: ChanceEntry) {
this.chances.push(entry)
}
calculateProbs() {
let sum = this.chances.reduce((sum, entry) => sum + entry.chance, 0)
this.chances.forEach(value => value.chance /= sum)
}
draw() {
let change = Math.random();
for (const value of this.chances) {
change -= value.chance;
if(change <= 0) {
return value;
}
}
return this.chances[this.chances.length - 1]
}
}

View File

@@ -1,11 +1,12 @@
import type {Acting, Drawable, Healthy, Weapon} from "./interfaces.ts"; import type {Acting, Drawable, Healthy, Leveling, Weapon} from "./interfaces.ts";
import {Vector} from "./base.ts"; import {Vector} from "./base.ts";
import {drawDot, getCoordinatesSplit} from "./utils.ts"; import {drawDot, getCoordinatesSplit} from "./utils.ts";
export class Player implements Drawable, Acting, Healthy { export class Player implements Drawable, Acting, Healthy {
private _position: Vector; private _position: Vector;
private _stats: PlayerStats; private _stats: PlayerStats;
private _color: string; private _color: string;
private _status: PlayerStatus; private _status: PlayerStatus;
private _weapons: [Weapon] = [] private _weapons: [Weapon] = []
@@ -30,13 +31,12 @@ export class Player implements Drawable, Acting, Healthy {
player._color = 'blue'; player._color = 'blue';
player._stats = PlayerStats.defaultPlayerStats(); player._stats = PlayerStats.defaultPlayerStats();
player._speed = new Vector(0, 0) player._speed = new Vector(0, 0)
player._status = new PlayerStatus(10, 0); player._status = new PlayerStatus(10, 0, 0);
return player; return player;
} }
addWeapon(weapon: Weapon) { addWeapon(weapon: Weapon) {
let weaponCount = this._weapons.length + 1; let weaponCount = this._weapons.length + 1;
let angle = 2 * Math.PI / weaponCount;
let points = getCoordinatesSplit(weaponCount) let points = getCoordinatesSplit(weaponCount)
for (let i = 0; i < points.length - 1; i++){ for (let i = 0; i < points.length - 1; i++){
const value = points[i]; const value = points[i];
@@ -102,10 +102,33 @@ export class Player implements Drawable, Acting, Healthy {
dead() { dead() {
return this.status.dead return this.status.dead
} }
increaseLevel() {
this.status.increaseLevel()
this.stats.increaseLevel()
this._weapons.forEach(weapon => {
weapon.increaseLevel()
})
}
level() {
return this.status.level
}
} }
export class PlayerStatus { export class PlayerStatus {
constructor(private _health: number, private _wealth: number) { constructor(private _health: number,
private _wealth: number,
private _level: number) {
}
get level(): number {
return this._level;
}
set level(value: number) {
this._level = value;
} }
get health(): number { get health(): number {
@@ -127,6 +150,10 @@ export class PlayerStatus {
set wealth(value: number) { set wealth(value: number) {
this._wealth = value; this._wealth = value;
} }
increaseLevel() {
this._level += 1
}
} }
export class PlayerStats { export class PlayerStats {
@@ -138,6 +165,14 @@ export class PlayerStats {
private _weaponRangeFactor: number) { private _weaponRangeFactor: number) {
} }
increaseLevel() {
this._speed *= 1.1;
this._health += 1
this._pullRange *= 1.1;
this._weaponRange *= 1.25
this._weaponRangeFactor += 0.1
}
get speed(): number { get speed(): number {
return this._speed; return this._speed;
} }

View File

@@ -1,13 +1,13 @@
import {Enemy} from "./Enemies.ts"; import {Enemy} from "./Enemies.ts";
import {Player} from "./Player.ts"; import {Player} from "./Player.ts";
import {Projectile} from "./projectile.ts"; import {Projectile, ProjectileStats} from "./projectile.ts";
import {Vector} from "./base.ts"; import {Vector} from "./base.ts";
import type {Drop, Placeable} from "./interfaces.ts"; import type {Drop, Placeable} from "./interfaces.ts";
export class World { export class World {
private _enemies: [Enemy] = []; private _enemies: Enemy[] = [];
private _projectiles: [Projectile] = []; private _projectiles: Projectile[] = [];
private _drops: [Drop] = []; private _drops: Drop[] = [];
private _player: Player; private _player: Player;
private _ctx: CanvasRenderingContext2D; private _ctx: CanvasRenderingContext2D;
private _size: Vector private _size: Vector

View File

@@ -7,7 +7,7 @@ export abstract class BasicDrop implements Drop {
protected world: World; protected world: World;
protected _position: Vector; protected _position: Vector;
protected _color: string; protected _color: string;
protected _size: number; protected size: number;
constructor(world: World, position: Vector) { constructor(world: World, position: Vector) {
this.world = world; this.world = world;
@@ -26,7 +26,7 @@ export abstract class BasicDrop implements Drop {
act() { act() {
let distanceToPlayer = this._position.distanceTo(this.world.player.position); let distanceToPlayer = this._position.distanceTo(this.world.player.position);
if(distanceToPlayer < (this.world.player.stats.size + this._size)) { if(distanceToPlayer < (this.world.player.stats.size + this.size)) {
this.pickup() this.pickup()
this.world.removeDrop(this) this.world.removeDrop(this)
} else if(distanceToPlayer < this.world.player.stats.pullRange) { } else if(distanceToPlayer < this.world.player.stats.pullRange) {
@@ -35,13 +35,12 @@ export abstract class BasicDrop implements Drop {
} }
} }
abstract draw(ctx: CanvasRenderingContext2D);
getSize() { getSize() {
return this._size return this.size
} }
abstract draw(ctx: CanvasRenderingContext2D);
} }
export class MoneyDrop extends BasicDrop { export class MoneyDrop extends BasicDrop {
@@ -49,48 +48,77 @@ export class MoneyDrop extends BasicDrop {
private worth: number; private worth: number;
draw(ctx: CanvasRenderingContext2D) { draw(ctx: CanvasRenderingContext2D) {
drawDot(this._position, this.getSize(), this._color, ctx) drawDot(this._position, this.size, this._color, ctx)
} }
pickup() { pickup() {
this.world.player.status.wealth += this.worth this.world.player.status.wealth += this.worth
} }
static spawnMoneyDrop(world: World, position?: Vector) {
world.addDrop(this.createMoneyDrop(world, position))
}
static createMoneyDrop(world: World, position?: Vector): MoneyDrop { static createMoneyDrop(world: World, position?: Vector): MoneyDrop {
if(!position) { if(!position) {
position = world.randomPlace() position = world.randomPlace()
} }
let drop = new MoneyDrop(world, position) let drop = new MoneyDrop(world, position)
drop.worth = 1; drop.worth = 1;
drop._size = 1; drop.size = 1;
drop._color = 'orange'; drop._color = 'orange';
world.addDrop(drop) world.addDrop(drop)
return drop; return drop;
} }
} }
export class HealthPack extends BasicDrop { export class HealthPack extends BasicDrop {
private healAmount: number; private healAmount: number;
draw(ctx: CanvasRenderingContext2D) { draw(ctx: CanvasRenderingContext2D) {
drawDot(this._position, this.getSize(), this._color, ctx) drawDot(this._position, this.size, this._color, ctx)
} }
pickup() { pickup() {
this.world.player.heal(this.healAmount) this.world.player.heal(this.healAmount)
} }
static createHealthPack(world: World, position?: Vector): HealthPack { static spawnHealthPack(world: World, position?: Vector) {
world.addDrop(this.createHealthPack(world, position))
}
static createHealthPack(world: World, position?: Vector) {
if(!position) { if(!position) {
position = world.randomPlace() position = world.randomPlace()
} }
let drop = new HealthPack(world, position) let drop = new HealthPack(world, position)
drop.healAmount = 5; drop.healAmount = 5;
drop._size = 2; drop.size = 2;
drop._color = 'green'; drop._color = 'green';
world.addDrop(drop)
return drop; return drop;
} }
} }
export class LevelDrop extends BasicDrop {
draw(ctx: CanvasRenderingContext2D) {
drawDot(this._position, this.size, this._color, ctx)
}
pickup() {
this.world.player.increaseLevel()
}
static spawnLevelDrop(world: World, position?: Vector) {
world.addDrop(this.createLevelDrop(world, position))
}
static createLevelDrop(world: World, position?: Vector): LevelDrop {
if(!position) {
position = world.randomPlace()
}
let drop = new LevelDrop(world, position)
drop.size = 5;
drop._color = 'blue';
return drop;
}
}

View File

@@ -10,6 +10,11 @@ export interface Healthy {
dead(); dead();
} }
export interface Leveling {
increaseLevel();
level()
}
export interface Drop extends Drawable, Acting { export interface Drop extends Drawable, Acting {
pickup() pickup()
} }
@@ -29,7 +34,7 @@ export interface Equipment {
setOffset(vec: Vector); setOffset(vec: Vector);
} }
export interface Weapon extends Drawable, Equipment { export interface Weapon extends Drawable, Equipment, Leveling {
act() act()
} }

View File

@@ -4,7 +4,7 @@ import {docReady} from "canvas-common";
import {World} from "./World.ts"; import {World} from "./World.ts";
import {Player} from "./Player.ts"; import {Player} from "./Player.ts";
import {Vector} from "./base.ts"; import {Vector} from "./base.ts";
import {BasicEnemy, Enemy, HealthEnemy, ShootingEnemy} from "./Enemies.ts"; import {BasicEnemy, ContainerEnemy, Enemy, HealthEnemy, ShootingEnemy} from "./Enemies.ts";
import {HUD} from "./ui.ts"; import {HUD} from "./ui.ts";
import {HomingPistol, Pistol} from "./weapons.ts"; import {HomingPistol, Pistol} from "./weapons.ts";
@@ -107,18 +107,22 @@ docReady(function () {
world = new World(player, ctx, new Vector(window.innerWidth, window.innerHeight)); world = new World(player, ctx, new Vector(window.innerWidth, window.innerHeight));
state = new WorldState(); state = new WorldState();
world.addEnemy(BasicEnemy.generateBasicEnemy(world)) BasicEnemy.spawnBasicEnemy(world)
world.addEnemy(ShootingEnemy.generateShootingEnemy(world, new Vector(350, 350))) ShootingEnemy.spawnShootingEnemy(world, new Vector(350, 350))
setInterval(() => { setInterval(() => {
world.addEnemy(ShootingEnemy.generateShootingEnemy(world)) ShootingEnemy.spawnShootingEnemy(world)
}, 1_000) }, 1_000)
setInterval(() => { setInterval(() => {
world.addEnemy(HealthEnemy.createHealthEnemy(world)) HealthEnemy.spawnHealthEnemy(world)
}, 15_000) }, 15_000)
player.addWeapon(Pistol.spawnPistol(world)) setInterval(() => {
player.addWeapon(HomingPistol.spawnPistol(world)) ContainerEnemy.spawnContainerEnemy(world)
}, 10_000)
player.addWeapon(Pistol.generatePistol(world))
player.addWeapon(HomingPistol.generatePistol(world))
hud = new HUD(world); hud = new HUD(world);

View File

@@ -3,13 +3,12 @@ import {World} from "./World.ts";
import {Vector} from "./base.ts"; import {Vector} from "./base.ts";
export class HUD implements DrawContainer { export class HUD implements DrawContainer {
private health: HealthInfo; private health: PlayerInfo;
private world: World; private world: World;
constructor(world: World) { constructor(world: World) {
this.world = world; this.world = world;
this.health = new HealthInfo(world); this.health = new PlayerInfo(world);
} }
draw(ctx: CanvasRenderingContext2D) { draw(ctx: CanvasRenderingContext2D) {
@@ -18,16 +17,17 @@ export class HUD implements DrawContainer {
} }
export class HealthInfo implements DrawContainer { export class PlayerInfo implements DrawContainer {
private bar: InfoBar; private bar: InfoBar;
private statLabels: [StatLabel] = [] private statLabels: StatLabel[] = []
private world: World; private world: World;
constructor(world: World) { constructor(world: World) {
this.world = world; this.world = world;
this.bar = new InfoBar(new Vector(0, 50), 50, 150, () => 'Health', () => this.world.player.status.health, () => this.world.player.stats.health) this.bar = new InfoBar(new Vector(0, 50), 50, 150, () => 'Health', () => this.world.player.status.health, () => this.world.player.stats.health)
this.statLabels = [ this.statLabels = [
new StatLabel(new Vector(0, 150), () => 'Money', () => this.world.player.status.wealth) new StatLabel(new Vector(0, 150), () => 'Money', () => this.world.player.status.wealth),
new StatLabel(new Vector(0, 160), () => 'Level', () => this.world.player.status.level)
] ]
} }
@@ -55,7 +55,8 @@ export class StatLabel implements Drawable {
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = this.borderColor ctx.strokeStyle = this.borderColor
let value = this.valueLambda(); let value = this.valueLambda();
ctx.fillText(`${value}`, this.position.x, this.position.y) let text = this.textLambda();
ctx.fillText(`${text}: ${value}`, this.position.x, this.position.y)
ctx.fill() ctx.fill()
} }

View File

@@ -12,16 +12,23 @@ export abstract class BasicWeapon implements Weapon {
protected color: string; protected color: string;
protected size: number; protected size: number;
protected stats: WeaponStats; protected stats: WeaponStats;
protected _level: number;
constructor(world: World, stats: WeaponStats) { constructor(world: World, stats: WeaponStats) {
this.player = world.player; this.player = world.player;
this.world = world; this.world = world;
this.stats = stats; this.stats = stats;
this._level = 1;
} }
act() { act() {
} }
increaseLevel() {
this._level += 1;
this.stats.increase()
}
draw(ctx: CanvasRenderingContext2D) { draw(ctx: CanvasRenderingContext2D) {
} }
@@ -43,6 +50,10 @@ export abstract class BasicWeapon implements Weapon {
setOffset(vec: Vector) { setOffset(vec: Vector) {
this.offset = vec; this.offset = vec;
} }
level() {
return this._level
}
} }
export abstract class RangeWeapon extends BasicWeapon { export abstract class RangeWeapon extends BasicWeapon {
@@ -89,7 +100,7 @@ export class HomingPistol extends RangeWeapon {
} }
} }
static spawnPistol(world: World, offset?: Vector) { static generatePistol(world: World, offset?: Vector) {
if(!offset) { if(!offset) {
offset = new Vector(5, 5) offset = new Vector(5, 5)
} }
@@ -128,7 +139,7 @@ export class Pistol extends RangeWeapon {
} }
} }
static spawnPistol(world: World, offset?: Vector) { static generatePistol(world: World, offset?: Vector) {
if(!offset) { if(!offset) {
offset = new Vector(5, 5) offset = new Vector(5, 5)
} }
@@ -158,6 +169,13 @@ export class WeaponStats {
this._weaponRangeFactor = 1 this._weaponRangeFactor = 1
} }
increase() {
this._weaponRange *= 1.1;
this._weaponRangeFactor += 0.05;
this._damage *= 1.25;
this._shootInterval *= 0.9
}
withWeaponRange(value: number) { withWeaponRange(value: number) {
this._weaponRange = value; this._weaponRange = value;
return this; return this;