survivors: adding first version

This commit is contained in:
Sheldan
2025-08-21 13:16:29 +02:00
parent 8a6e3b86df
commit 9b5ab25c4d
22 changed files with 2003 additions and 9 deletions

View File

@@ -29,59 +29,65 @@ jobs:
with:
node-version: 21
- name: Orbits Install dependencies
run: npm install
run: npm ci
working-directory: orbits
- name: Orbits Build
run: npx vite build
working-directory: orbits
- name: recBubbles Install dependencies
run: npm install
run: npm ci
working-directory: recBubbles
- name: recBubbles Build
run: npx vite build
working-directory: recBubbles
- name: balls Install dependencies
run: npm install
run: npm ci
working-directory: balls
- name: balls Build
run: npx vite build
working-directory: balls
- name: fireWorks Install dependencies
run: npm install
run: npm ci
working-directory: fireWorks
- name: fireWorks Build
run: npx vite build
working-directory: fireWorks
- name: bubbles Install dependencies
run: npm install
run: npm ci
working-directory: bubbles
- name: bubbles Build
run: npx vite build
working-directory: bubbles
- name: circleBs Install dependencies
run: npm install
run: npm ci
working-directory: circleBs
- name: circleBs Build
run: npx vite build
working-directory: circleBs
- name: clusterFilter Install dependencies
run: npm install
run: npm ci
working-directory: clusterFilter
- name: clusterFilter Build
run: npx vite build
working-directory: clusterFilter
- name: collatzConjecture Install dependencies
run: npm install
run: npm ci
working-directory: collatzConjecture
- name: collatzConjecture Build
run: npx vite build
working-directory: collatzConjecture
- name: dotLines Install dependencies
run: npm install
run: npm ci
working-directory: dotLines
- name: dotLines Build
run: npx vite build
working-directory: dotLines
- name: survivors install dependencies
run: npm ci
working-directory: absurd-survivors
- name: survivors build
run: npx vite build
working-directory: absurd-survivors
- name: Move index
run: cp index.html dist/
- name: Move overview images

24
absurd-survivors/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>overtuned survivors</title>
<meta name="description" content="">
</head>
<body>
<canvas id="canvas" style="display: block; background-color: black"></canvas>
<script type="module" src="src/main.ts"></script>
</body>
</html>

1088
absurd-survivors/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
{
"name": "overtuned-survivors",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"canvas-common": "file:../canvas-common",
"typescript": "~5.8.3",
"vite": "^7.1.2"
}
}

View File

@@ -0,0 +1,144 @@
import type {Acting, Drawable, Healthy, Moving, Shooting} from "./interfaces.ts";
import {drawDot, moveInDirectionOf} from "./utils.ts";
import {Vector} from "./base.ts";
import {World} from "./World.ts";
import type {Projectile} from "./projectile.ts";
import {HomingProjectile, StraightProjectile} from "./projectile.ts";
export abstract class Enemy implements Moving, Drawable, Acting, Healthy {
protected _position: Vector;
protected speed: number;
protected world: World;
protected status: EnemyStatus = new EnemyStatus(10);
constructor(position: Vector) {
this._position = position;
}
draw(ctx: CanvasRenderingContext2D) {
}
act() {
this.move()
}
move() {
}
getPosition(): Vector {
return this._position;
}
takeDamage(damage: number) {
this.status.health -= damage;
if(this.status.dead) {
this.world.removeEnemy(this)
}
}
}
export class BasicEnemy extends Enemy {
constructor(position: Vector) {
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)
}
move() {
this._position = moveInDirectionOf(this._position, this.world.player.position, this.speed)
}
act() {
super.act();
if(this._position.distanceTo(this.world.player.position) < this.size && this.impactCooldown <= 0) {
this.world.player.takeDamage(this.impactDamage)
this.impactCooldown = this.impactInterval;
}
this.impactCooldown -= 1;
}
static generateBasicEnemy(world: World, position?: Vector): BasicEnemy {
if(position === undefined) {
position = new Vector(250, 250)
}
let basicEnemy = new BasicEnemy(position);
basicEnemy.size = 5;
basicEnemy.world = world;
basicEnemy.speed = 0.5;
basicEnemy.color = 'orange'
basicEnemy.impactDamage = 2;
return basicEnemy;
}
}
export class ShootingEnemy extends BasicEnemy implements Shooting {
private shootCooldown: number = 0;
private shootInterval: number;
private projectiles: Projectile[] = []
constructor(position: Vector) {
super(position);
}
removeProjectile(projectile: Projectile) {
this.projectiles = this.projectiles.filter(item => item !== projectile)
}
act() {
super.act();
if(this.shootCooldown <= 0) {
this.createProjectile()
this.shootCooldown = this.shootInterval;
}
this.shootCooldown -= 1;
}
createProjectile() {
let projectile = StraightProjectile.createStraightProjectile(this.world, this._position, this.world.player.position, this)
this.projectiles.push(projectile)
return projectile
}
static generateShootingEnemy(world: World, position?: Vector) {
if(position === undefined) {
position = new Vector(250, 250)
}
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;
}
}
export class EnemyStatus {
constructor(private _health: number) {
}
get health(): number {
return this._health;
}
get dead(): boolean {
return this._health <= 0;
}
set health(value: number) {
this._health = value;
}
}

View File

@@ -0,0 +1,116 @@
import type {Acting, Drawable, Healthy, Weapon} from "./interfaces.ts";
import {Vector} from "./base.ts";
import {drawDot} from "./utils.ts";
export class Player implements Drawable, Acting, Healthy {
private _position: Vector;
private _stats: Stats;
private _color: string;
private _status: Status;
private _weapons: [Weapon] = []
// temp
private _speed: Vector;
constructor(position: Vector) {
this._position = position;
}
draw(ctx: CanvasRenderingContext2D) {
drawDot(this.position, this._stats.size, this._color, ctx)
this._weapons.forEach(weapon => weapon.draw(ctx))
}
public static generatePlayer(): Player {
let player = new Player(new Vector(500, 500));
player._color = 'blue';
player._stats = Stats.defaultPlayerStats();
player._speed = new Vector(0, 0)
player._status = new Status(10);
return player;
}
addWeapon(weapon: Weapon) {
this._weapons.push(weapon)
}
move(direction: Vector) {
this._position = this.position.add(direction)
}
takeDamage(damage: number) {
this._status.health -= damage;
}
get health(): number {
return this._status.health;
}
get position(): Vector {
return this._position;
}
get color(): string {
return this._color;
}
get stats(): Stats {
return this._stats;
}
get speed(): Vector {
return this._speed;
}
act() {
this._weapons.forEach(weapon => weapon.act())
}
}
export class Status {
constructor(private _health: number) {
}
get health(): number {
return this._health;
}
set health(value: number) {
this._health = value;
}
}
export class Stats {
constructor(private _speed: number,
private _size: number,
private _health: number) {
}
get speed(): number {
return this._speed;
}
set speed(value: number) {
this._speed = value;
}
get size(): number {
return this._size;
}
set size(value: number) {
this._size = value;
}
get health(): number {
return this._health;
}
public static defaultPlayerStats(): Stats {
return new Stats(2, 5, 10);
}
}

View File

@@ -0,0 +1,70 @@
import {Enemy} from "./Enemies.ts";
import type {Player} from "./Player.ts";
import {Player} from "./Player.ts";
import {Projectile} from "./projectile.ts";
import {Vector} from "./base.ts";
import type {Moving} from "./interfaces.ts";
export class World {
private _enemies: [Enemy] = [];
private _projectiles: [Projectile] = [];
private _player: Player;
private _ctx: CanvasRenderingContext2D;
constructor(player: Player, ctx: CanvasRenderingContext2D) {
this._player = player;
this._ctx = ctx;
}
enemiesAct() {
this._enemies.forEach(enemy => enemy.act())
this._projectiles.forEach(projectile => projectile.act())
}
draw() {
this._enemies.forEach(enemy => enemy.draw(this._ctx))
this._projectiles.forEach(projectile => projectile.draw(this._ctx))
this._player.draw(this._ctx);
}
addProjectile(projectile: Projectile) {
this._projectiles.push(projectile)
}
removeProjectile(projectile: Projectile) {
this._projectiles = this._projectiles.filter(item => item !== projectile)
}
removeEnemy(enemy: Enemy) {
this._enemies = this._enemies.filter(item => item !== enemy)
}
get enemies(): [Enemy] {
return this._enemies;
}
addEnemy(enemy: Enemy) {
this._enemies.push(enemy)
}
getClosestTargetTo(point: Vector): [number, Moving | undefined] | undefined {
let currentTarget;
let currentDistance = Number.MAX_SAFE_INTEGER;
this._enemies.forEach(enemy => {
let distance = point.distanceTo(enemy.getPosition());
if(distance < currentDistance) {
currentDistance = distance;
currentTarget = enemy
}
})
if(currentTarget) {
return [currentDistance, currentTarget];
}
}
get player(): Player {
return this._player;
}
}

View File

@@ -0,0 +1,59 @@
export class Vector {
constructor(private _x: number, private _y: number) {
}
static createVector(tip: Vector, shaft: Vector): Vector {
return new Vector(tip.x - shaft.x, tip.y - shaft.y);
}
normalize(): Vector {
let length = this.vecLength();
return new Vector(this.x / length, this.y / length)
}
vecLength(): number {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
distanceTo(point: Vector): number {
return Math.sqrt(Math.pow(this.x - point.x, 2) + Math.pow(this.y - point.y, 2));
}
add(vec: Vector): Vector {
return new Vector(this._x + vec._x, this._y + vec._y)
}
minus(vec: Vector): Vector {
return new Vector(this.x - vec._x, this.y - vec.y)
}
multiply(number: number): Vector {
return new Vector(this.x * number, this.y * number)
}
multiplyVec(vec: Vector): Vector {
return new Vector(this.x * vec._x, this.y * vec.y)
}
negate(): Vector {
return this.multiply(-1)
}
get x(): number {
return this._x;
}
set x(value: number) {
this._x = value;
}
get y(): number {
return this._y;
}
set y(value: number) {
this._y = value;
}
}

View File

@@ -0,0 +1,7 @@
import type {Healthy} from "./interfaces.ts";
export class InstanceOfUtils {
static instanceOfHealthy(object: any): object is Healthy {
return 'takeDamage' in object;
}
}

View File

@@ -0,0 +1,31 @@
import {Vector} from "./base.ts";
export interface Acting {
act()
}
export interface Healthy {
takeDamage(damage: number);
}
export interface Moving {
move()
getPosition(): Vector;
}
export interface Projectile extends Drawable {
}
export interface Weapon extends Drawable{
act()
}
export interface Shooting {
createProjectile(): Projectile;
removeProjectile(projectile: Projectile)
}
export interface Drawable {
draw(ctx: CanvasRenderingContext2D);
}

View File

@@ -0,0 +1,133 @@
import './style.css'
import {docReady} from "canvas-common";
import {World} from "./World.ts";
import {Player} from "./Player.ts";
import {Vector} from "./base.ts";
import {BasicEnemy, Enemy, ShootingEnemy} from "./Enemies.ts";
import {HUD} from "./ui.ts";
import {Pistol} from "./weapons.ts";
let hud: HUD;
let world: World;
let config: Config;
let state: WorldState;
let ctx: CanvasRenderingContext2D;
let canvas;
export class Config {
private _size: Vector = new Vector(window.innerWidth, window.innerHeight)
private _fps: number = 60;
get size(): Vector {
return this._size;
}
get fps(): number {
return this._fps;
}
}
export class WorldState {
private _ended: boolean = false;
get ended(): boolean {
return this._ended;
}
}
function updateCanvas() {
ctx.clearRect(0, 0, config.size.x, config.size.y);
hud.draw(ctx)
if(!state.ended) {
world.enemiesAct()
world.player.act()
world.draw()
for(let key in keys) {
if(keys[key].state) {
keys[key].fun()
}
}
} else {
ctx.fillText('End', 15, 15)
}
setTimeout(function () {
requestAnimationFrame(updateCanvas);
}, 1000 / config.fps)
}
function makeKey(char, fun) {
keys[char] = {
state: false,
fun: fun
}
}
let keys = {};
makeKey('w', function () {
world.player.position.y += -world.player.stats.speed
})
makeKey('s', function () {
world.player.position.y += world.player.stats.speed
})
makeKey('a', function () {
world.player.position.x += -world.player.stats.speed
})
makeKey('d', function () {
world.player.position.x += world.player.stats.speed
})
function keyUp(event) {
if(event.key in keys) {
keys[event.key].state = false;
}
}
function keyDown(event) {
if(event.key in keys) {
keys[event.key].state = true;
}
}
document.onkeyup = keyUp;
document.onkeydown = keyDown;
docReady(function () {
canvas = document.getElementById('canvas');
config = new Config();
canvas.width = config.size.x;
canvas.height = config.size.y;
ctx = canvas.getContext("2d");
let player = Player.generatePlayer();
world = new World(player, ctx);
state = new WorldState();
world.addEnemy(BasicEnemy.generateBasicEnemy(world))
world.addEnemy(ShootingEnemy.generateShootingEnemy(world, new Vector(350, 350)))
setInterval(() => {
world.addEnemy(ShootingEnemy.generateShootingEnemy(world, new Vector(Math.random() * config.size.x, Math.random() * config.size.y)))
}, 1000)
player.addWeapon(Pistol.spawnPistol(world))
let secondPistol = Pistol.spawnPistol(world, new Vector(-5, -5));
player.addWeapon(secondPistol)
hud = new HUD(world);
requestAnimationFrame(updateCanvas);
})

View File

@@ -0,0 +1,100 @@
import type {Acting, Moving, Healthy} from "./interfaces.ts";
import type {Vector} from "./base.ts";
import {World} from "./World.ts";
import {Vector} from "./base.ts";
import {drawDot, moveInDirectionOf, straightMove} from "./utils.ts";
import {InstanceOfUtils} from "./instance.ts";
export abstract class Projectile implements Acting, Moving {
protected position: Vector;
protected speedVec: Vector;
protected impact: number;
protected world: World;
protected size: number;
protected parent: any;
protected color: string
constructor(position: Vector, speedVec: Vector, world: World, parent: any) {
this.position = position;
this.speedVec = speedVec;
this.world = world;
this.parent = parent;
}
act() {
this.move()
if(this.parent != this.world.player) {
if(this.position.distanceTo(this.world.player.position) < (this.size + this.world.player.stats.size)) {
this.impactPlayer()
}
}
if(this.parent == this.world.player) {
let closestTargetTo = this.world.getClosestTargetTo(this.position);
if(closestTargetTo !== undefined && closestTargetTo[1] !== undefined && closestTargetTo[1]?.getPosition().distanceTo(this.position) < (this.size + this.world.player.stats.size)) {
let target: Moving = closestTargetTo[1]!;
if(InstanceOfUtils.instanceOfHealthy(target)) {
let healthy = target as Healthy;
healthy.takeDamage(this.impact)
}
}
}
}
impactPlayer() {
this.world.player.takeDamage(this.impact)
this.world.removeProjectile(this)
};
draw(ctx: CanvasRenderingContext2D) {
drawDot(this.position, this.size, this.color, ctx)
}
move() {
}
getPosition(): Vector {
return this.position;
}
}
export class StraightProjectile extends Projectile {
constructor(position: Vector, dirVector: Vector, world: World, parent: any) {
super(position, dirVector, world, parent);
}
move() {
this.position = straightMove(this.position, this.speedVec)
}
static createStraightProjectile(world: World, start: Vector, targetPosition: Vector, parent: any) {
let projectile = new StraightProjectile(start, Vector.createVector(targetPosition, start).normalize().multiply(5), world, parent)
projectile.impact = 1;
projectile.size = 1
projectile.color = 'red';
world.addProjectile(projectile)
return projectile;
}
}
export class HomingProjectile extends Projectile {
move() {
this.position = moveInDirectionOf(this.position, this.world.player.position, this.speedVec.vecLength())
}
static createHomingProjectile(world: World, start: Vector, parent: any) {
let projectile = new HomingProjectile(start, new Vector(5, 1), world, parent)
projectile.impact = 1;
projectile.size = 1
projectile.color = 'red';
world.addProjectile(projectile)
return projectile;
}
}

View File

@@ -0,0 +1,4 @@
html, body, div, canvas {
margin: 0;
padding: 0;
}

View File

@@ -0,0 +1,72 @@
import type {World} from "./World.ts";
import type {Drawable} from "./interfaces.ts";
import {World} from "./World.ts";
import type {Vector} from "./base.ts";
import {Vector} from "./base.ts";
export class HUD implements Drawable{
private health: HealthInfo;
private world: World;
constructor(world: World) {
this.world = world;
this.health = new HealthInfo(world);
}
draw(ctx: CanvasRenderingContext2D) {
this.health.draw(ctx)
}
}
export class HealthInfo implements Drawable{
private bar: InfoBar;
private world: World;
constructor(world: World) {
this.world = world;
this.bar = new InfoBar(new Vector(0, 50), 50, 150, () => 'Health', () => this.world.player.health, () => this.world.player.stats.health)
}
draw(ctx: CanvasRenderingContext2D) {
this.bar.draw(ctx)
}
}
export class InfoBar implements Drawable {
private position: Vector;
private height: number;
private width: number;
private fillColor: string = 'green';
private borderColor: string = 'black';
private textLambda: () => string;
private valueLambda: () => number;
private totalValueLambda: () => number;
constructor(position: Vector, height: number, width: number, textLambda: () => string, valueLambda: () => number, totalValueLambda: () => number) {
this.position = position;
this.height = height;
this.width = width;
this.textLambda = textLambda;
this.valueLambda = valueLambda;
this.totalValueLambda = totalValueLambda;
}
draw(ctx: CanvasRenderingContext2D) {
ctx.beginPath();
ctx.strokeStyle = this.borderColor
ctx.strokeRect(this.position.x, this.position.y, this.width, this.height)
ctx.fillStyle = this.fillColor;
let value = this.valueLambda();
let totalValue = this.totalValueLambda();
let usedWidth = (value / totalValue) * this.width;
ctx.fillRect(this.position.x, this.position.y, usedWidth, this.height)
ctx.fillStyle = this.borderColor
ctx.fillText(`${value}/${totalValue}`, this.position.x + this.width / 2, this.position.y + this.height / 2)
ctx.fill()
}
}

View File

@@ -0,0 +1,18 @@
import {Vector} from "./base.ts";
export function drawDot(position: Vector, size: number, color: string, ctx: CanvasRenderingContext2D) {
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(position.x, position.y, size, 0, 2 * Math.PI);
ctx.fill();
}
export function moveInDirectionOf(position: Vector, target: Vector, speedFactor): Vector {
let playerVector = Vector.createVector(target, position);
let direction = playerVector.normalize()
return position.add(direction.multiply(speedFactor))
}
export function straightMove(position: Vector, speed: Vector): Vector {
return position.add(speed)
}

1
absurd-survivors/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,59 @@
import type {Weapon} from "./interfaces.ts";
import {drawDot} from "./utils.ts";
import {Player} from "./Player.ts";
import {Projectile, StraightProjectile} from "./projectile.ts";
import {World} from "./World.ts";
import {Vector} from "./base.ts";
export class Pistol implements Weapon {
private player: Player
private shootInterval: number;
private shootCooldown: number = 0;
private world: World;
private offset: Vector;
private projectiles: [Projectile] = []
private color: string;
private size: number;
constructor(world: World) {
this.player = world.player;
this.world = world;
}
draw(ctx: CanvasRenderingContext2D) {
drawDot(this.player.position.add(this.offset), this.size, this.color, ctx)
}
act() {
if(this.shootCooldown <= 0) {
if(this.createProjectile()) {
this.shootCooldown = this.shootInterval;
}
}
this.shootCooldown -= 1;
}
private createProjectile(): boolean {
let closestTargetTo = this.world.getClosestTargetTo(this.world.player.position);
if(closestTargetTo !== undefined && closestTargetTo[1] !== undefined) {
let projectile = StraightProjectile.createStraightProjectile(this.world, this.player.position.add(this.offset), closestTargetTo[1]!.getPosition(), this.player)
this.projectiles.push(projectile)
return true
} else {
return false;
}
}
static spawnPistol(world: World, offset?: Vector) {
if(!offset) {
offset = new Vector(5, 5)
}
let pistol = new Pistol(world)
pistol.offset = offset;
pistol.size = 1;
pistol.color = 'yellow';
pistol.shootInterval = 10;
return pistol;
}
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
export default defineConfig({
build: {
outDir: '../../dist/survivors'
},
})

BIN
img/survivors.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -18,5 +18,6 @@
<a href="/clusterFilter"><img src="img/clusterFilter.png" alt="clusterFilter" class="preview" title="clusterFilter"></a>
<a href="/collatzConjecture"><img src="img/collatzConjecture.png" alt="collatzConjecture" class="preview" title="collatzConjecture"></a>
<a href="/dotLines"><img src="img/dotLines.png" alt="dotLines" class="preview" title="dotLines"></a>
<a href="/survivors"><img src="img/survivors.png" alt="survivors" class="preview" title="survivors"></a>
</body>
</html>