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 {World} from "./World.ts";
import type {Projectile} from "./projectile.ts";
import {HomingProjectile, ProjectileStats, StraightProjectile} from "./projectile.ts";
import {HealthPack, MoneyDrop} from "./drop.ts";
import {ProjectileStats, StraightProjectile} from "./projectile.ts";
import {HealthPack, LevelDrop, MoneyDrop} from "./drop.ts";
export abstract class Enemy implements Placeable, Drawable, Acting, Healthy {
protected _position: Vector;
protected speed: number;
protected world: World;
protected size: number
protected status: EnemyStatus = new EnemyStatus(10);
constructor(position: Vector) {
@@ -40,10 +41,11 @@ export abstract class Enemy implements Placeable, Drawable, Acting, Healthy {
}
die() {
MoneyDrop.createMoneyDrop(this.world, this._position);
MoneyDrop.spawnMoneyDrop(this.world, this._position);
}
getSize() {
return this.size;
}
dead() {
@@ -58,14 +60,13 @@ export class BasicEnemy extends Enemy {
super(position);
}
protected size: number;
protected color: string;
protected impactDamage: number;
protected impactCooldown: number = 0;
protected impactInterval: number = 60;
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() {
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.impactCooldown = this.impactInterval;
}
this.impactCooldown -= 1;
}
static spawnBasicEnemy(world: World, position?: Vector) {
world.addEnemy(this.generateBasicEnemy(world, position))
}
static generateBasicEnemy(world: World, position?: Vector): BasicEnemy {
if(position === undefined) {
position = new Vector(250, 250)
@@ -95,9 +100,6 @@ export class BasicEnemy extends Enemy {
return basicEnemy;
}
getSize() {
return this.size
}
}
export class ShootingEnemy extends BasicEnemy implements Shooting {
@@ -133,18 +135,22 @@ export class ShootingEnemy extends BasicEnemy implements Shooting {
return projectile
}
static spawnShootingEnemy(world: World, position?: Vector) {
world.addEnemy(this.generateShootingEnemy(world, position))
}
static generateShootingEnemy(world: World, position?: Vector) {
if(position === undefined) {
position = world.randomPlace()
}
let basicEnemy = new ShootingEnemy(position);
basicEnemy.size = 5;
basicEnemy.world = world;
basicEnemy.speed = 0.5;
basicEnemy.color = 'green'
basicEnemy.impactDamage = 2;
basicEnemy.shootInterval = 100
return basicEnemy;
let shootingEnemy = new ShootingEnemy(position);
shootingEnemy.size = 5;
shootingEnemy.world = world;
shootingEnemy.speed = 0.5;
shootingEnemy.color = 'green'
shootingEnemy.impactDamage = 2;
shootingEnemy.shootInterval = 100
return shootingEnemy
}
}
@@ -172,7 +178,6 @@ export class HealthEnemy extends Enemy {
super(position);
}
protected size: number;
protected color: string;
draw(ctx: CanvasRenderingContext2D) {
@@ -188,7 +193,11 @@ export class HealthEnemy extends Enemy {
}
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) {
@@ -207,4 +216,97 @@ export class HealthEnemy extends Enemy {
getSize() {
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 {drawDot, getCoordinatesSplit} from "./utils.ts";
export class Player implements Drawable, Acting, Healthy {
export class Player implements Drawable, Acting, Healthy {
private _position: Vector;
private _stats: PlayerStats;
private _color: string;
private _status: PlayerStatus;
private _weapons: [Weapon] = []
@@ -30,13 +31,12 @@ export class Player implements Drawable, Acting, Healthy {
player._color = 'blue';
player._stats = PlayerStats.defaultPlayerStats();
player._speed = new Vector(0, 0)
player._status = new PlayerStatus(10, 0);
player._status = new PlayerStatus(10, 0, 0);
return player;
}
addWeapon(weapon: Weapon) {
let weaponCount = this._weapons.length + 1;
let angle = 2 * Math.PI / weaponCount;
let points = getCoordinatesSplit(weaponCount)
for (let i = 0; i < points.length - 1; i++){
const value = points[i];
@@ -102,10 +102,33 @@ export class Player implements Drawable, Acting, Healthy {
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 {
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 {
@@ -127,6 +150,10 @@ export class PlayerStatus {
set wealth(value: number) {
this._wealth = value;
}
increaseLevel() {
this._level += 1
}
}
export class PlayerStats {
@@ -138,6 +165,14 @@ export class PlayerStats {
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 {
return this._speed;
}

View File

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

View File

@@ -7,7 +7,7 @@ export abstract class BasicDrop implements Drop {
protected world: World;
protected _position: Vector;
protected _color: string;
protected _size: number;
protected size: number;
constructor(world: World, position: Vector) {
this.world = world;
@@ -26,7 +26,7 @@ export abstract class BasicDrop implements Drop {
act() {
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.world.removeDrop(this)
} else if(distanceToPlayer < this.world.player.stats.pullRange) {
@@ -35,13 +35,12 @@ export abstract class BasicDrop implements Drop {
}
}
abstract draw(ctx: CanvasRenderingContext2D);
getSize() {
return this._size
return this.size
}
abstract draw(ctx: CanvasRenderingContext2D);
}
export class MoneyDrop extends BasicDrop {
@@ -49,48 +48,77 @@ export class MoneyDrop extends BasicDrop {
private worth: number;
draw(ctx: CanvasRenderingContext2D) {
drawDot(this._position, this.getSize(), this._color, ctx)
drawDot(this._position, this.size, this._color, ctx)
}
pickup() {
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 {
if(!position) {
position = world.randomPlace()
}
let drop = new MoneyDrop(world, position)
drop.worth = 1;
drop._size = 1;
drop.size = 1;
drop._color = 'orange';
world.addDrop(drop)
return drop;
}
}
export class HealthPack extends BasicDrop {
private healAmount: number;
draw(ctx: CanvasRenderingContext2D) {
drawDot(this._position, this.getSize(), this._color, ctx)
drawDot(this._position, this.size, this._color, ctx)
}
pickup() {
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) {
position = world.randomPlace()
}
let drop = new HealthPack(world, position)
drop.healAmount = 5;
drop._size = 2;
drop.size = 2;
drop._color = 'green';
world.addDrop(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();
}
export interface Leveling {
increaseLevel();
level()
}
export interface Drop extends Drawable, Acting {
pickup()
}
@@ -29,7 +34,7 @@ export interface Equipment {
setOffset(vec: Vector);
}
export interface Weapon extends Drawable, Equipment {
export interface Weapon extends Drawable, Equipment, Leveling {
act()
}

View File

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

View File

@@ -3,13 +3,12 @@ import {World} from "./World.ts";
import {Vector} from "./base.ts";
export class HUD implements DrawContainer {
private health: HealthInfo;
private health: PlayerInfo;
private world: World;
constructor(world: World) {
this.world = world;
this.health = new HealthInfo(world);
this.health = new PlayerInfo(world);
}
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 statLabels: [StatLabel] = []
private statLabels: StatLabel[] = []
private world: World;
constructor(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.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.strokeStyle = this.borderColor
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()
}

View File

@@ -12,16 +12,23 @@ export abstract class BasicWeapon implements Weapon {
protected color: string;
protected size: number;
protected stats: WeaponStats;
protected _level: number;
constructor(world: World, stats: WeaponStats) {
this.player = world.player;
this.world = world;
this.stats = stats;
this._level = 1;
}
act() {
}
increaseLevel() {
this._level += 1;
this.stats.increase()
}
draw(ctx: CanvasRenderingContext2D) {
}
@@ -43,6 +50,10 @@ export abstract class BasicWeapon implements Weapon {
setOffset(vec: Vector) {
this.offset = vec;
}
level() {
return this._level
}
}
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) {
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) {
offset = new Vector(5, 5)
}
@@ -158,6 +169,13 @@ export class WeaponStats {
this._weaponRangeFactor = 1
}
increase() {
this._weaponRange *= 1.1;
this._weaponRangeFactor += 0.05;
this._damage *= 1.25;
this._shootInterval *= 0.9
}
withWeaponRange(value: number) {
this._weaponRange = value;
return this;