mirror of
https://github.com/Sheldan/canvas.git
synced 2026-01-25 19:05:32 +00:00
Compare commits
48 Commits
8a6e3b86df
...
ef162a7dc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef162a7dc2 | ||
|
|
26ad150b59 | ||
|
|
9bb7ec99c0 | ||
|
|
db2110c921 | ||
|
|
b3dbc9cc80 | ||
|
|
ef2ea386c5 | ||
|
|
fa477afb9a | ||
|
|
28ef7b9c6f | ||
|
|
a8be84ebd3 | ||
|
|
27f3d2e916 | ||
|
|
9fea67dbcc | ||
|
|
add005d963 | ||
|
|
681ba1b632 | ||
|
|
272a86d7fc | ||
|
|
70c19f2851 | ||
|
|
c9c063b477 | ||
|
|
27862e19df | ||
|
|
70130f47a4 | ||
|
|
33310100f7 | ||
|
|
71b2afacc4 | ||
|
|
007d2568b3 | ||
|
|
7214a64b77 | ||
|
|
f2e62a7e74 | ||
|
|
c01ac53312 | ||
|
|
1124e62bb7 | ||
|
|
b591fc2dee | ||
|
|
e44355bf21 | ||
|
|
39da3d8abd | ||
|
|
a52754ce0d | ||
|
|
59f1a4b164 | ||
|
|
e714fc35f6 | ||
|
|
8ecfbf499f | ||
|
|
85c83a8827 | ||
|
|
dff1a6a760 | ||
|
|
e99b8b6bf8 | ||
|
|
e91368d380 | ||
|
|
dbf34061f0 | ||
|
|
7b8745d7d2 | ||
|
|
66ef2eaa31 | ||
|
|
07b64154e1 | ||
|
|
603bf3addc | ||
|
|
8ca64a19b7 | ||
|
|
71f48404c9 | ||
|
|
c8767f1119 | ||
|
|
18c323430c | ||
|
|
e75946d749 | ||
|
|
cffb10aed6 | ||
|
|
9b5ab25c4d |
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@@ -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
24
absurd-survivors/.gitignore
vendored
Normal 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?
|
||||
13
absurd-survivors/index.html
Normal file
13
absurd-survivors/index.html
Normal 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
1088
absurd-survivors/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
absurd-survivors/package.json
Normal file
16
absurd-survivors/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
321
absurd-survivors/src/Enemies.ts
Normal file
321
absurd-survivors/src/Enemies.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import type {Acting, ChanceEntry, Drawable, Healthy, Placeable, Shooting} from "./interfaces.ts";
|
||||
import {fillDot, moveInDirectionOf} from "./utils.ts";
|
||||
import {Vector} from "./base.ts";
|
||||
import {World} from "./World.ts";
|
||||
import type {Projectile} from "./projectile.ts";
|
||||
import {StraightProjectile} from "./projectile.ts";
|
||||
import {HealthPack, ItemDrop, LevelDrop, MoneyDrop} from "./drop.ts";
|
||||
import {ItemManagement} from "./items.ts";
|
||||
import {EnemyStats, ProjectileStats} from "./stats.ts";
|
||||
import {EnemyStatus} from "./status.ts";
|
||||
import {NumberDisplayParticle} from "./particles.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;
|
||||
protected drops: KillChanceTable;
|
||||
|
||||
constructor(position: Vector, enemyStats: EnemyStats) {
|
||||
this.drops = new KillChanceTable();
|
||||
this.drops.addDrop( {chance: 10, creationMethod: this.spawnMoney})
|
||||
this._position = position.clone();
|
||||
let health = enemyStats.getEffectiveHealth()
|
||||
this.status = new EnemyStatus(health);
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
}
|
||||
|
||||
act() {
|
||||
this.move()
|
||||
}
|
||||
|
||||
move() {
|
||||
}
|
||||
|
||||
|
||||
getPosition(): Vector {
|
||||
return this._position;
|
||||
}
|
||||
|
||||
takeDamage(damage: number) {
|
||||
this.status.health -= damage;
|
||||
NumberDisplayParticle.spawnNumberParticle(this.world, damage, this._position, 'white')
|
||||
if(this.status.dead) {
|
||||
this.die()
|
||||
this.world.removeEnemy(this)
|
||||
}
|
||||
}
|
||||
|
||||
die() {
|
||||
let draw = this.drops.draw();
|
||||
if(draw) {
|
||||
draw.creationMethod(this)
|
||||
}
|
||||
}
|
||||
|
||||
spawnMoney(enemy: Enemy) {
|
||||
MoneyDrop.spawnMoneyDrop(enemy.world, enemy._position);
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
dead() {
|
||||
return this.status.dead
|
||||
}
|
||||
|
||||
tick(seconds: number, tick: number) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class BasicEnemy extends Enemy {
|
||||
|
||||
constructor(position: Vector, enemyStats: EnemyStats) {
|
||||
super(position, enemyStats);
|
||||
}
|
||||
|
||||
protected color: string;
|
||||
protected impactDamage: number;
|
||||
protected impactCooldown: number = 0;
|
||||
protected impactInterval: number = 60;
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(this._position, this.getSize(), 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.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 = world.randomPlace()
|
||||
}
|
||||
let enemyStats = new EnemyStats().withHealthFactor(world.getEnemyHealthFactor());
|
||||
let basicEnemy = new BasicEnemy(position, enemyStats);
|
||||
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, enemyStats: EnemyStats) {
|
||||
super(position, enemyStats);
|
||||
}
|
||||
|
||||
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 stats = new ProjectileStats()
|
||||
.withPiercings(0)
|
||||
.withSize(1)
|
||||
.withDamage(5)
|
||||
.withSpeed(2)
|
||||
let projectile = StraightProjectile.createStraightProjectile(this.world, this._position, this.world.player.position, this, stats)
|
||||
this.projectiles.push(projectile)
|
||||
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 enemyStats = new EnemyStats().withHealthFactor(world.getEnemyHealthFactor());;
|
||||
let shootingEnemy = new ShootingEnemy(position, enemyStats);
|
||||
shootingEnemy.size = 5;
|
||||
shootingEnemy.world = world;
|
||||
shootingEnemy.speed = 0.5;
|
||||
shootingEnemy.color = 'green'
|
||||
shootingEnemy.impactDamage = 2;
|
||||
shootingEnemy.shootInterval = 100
|
||||
return shootingEnemy
|
||||
}
|
||||
}
|
||||
|
||||
export class HealthEnemy extends Enemy {
|
||||
|
||||
constructor(position: Vector, enemyStats: EnemyStats) {
|
||||
super(position, enemyStats);
|
||||
}
|
||||
|
||||
protected color: string;
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(this._position, this.size, this.color, ctx)
|
||||
}
|
||||
|
||||
|
||||
move() {
|
||||
}
|
||||
|
||||
act() {
|
||||
super.act();
|
||||
}
|
||||
|
||||
die() {
|
||||
HealthPack.spawnHealthPack(this.world, this._position)
|
||||
}
|
||||
|
||||
static spawnHealthEnemy(world: World, position?: Vector) {
|
||||
world.addEnemy(this.createHealthEnemy(world, position))
|
||||
}
|
||||
|
||||
static createHealthEnemy(world: World, position?: Vector) {
|
||||
if(position === undefined) {
|
||||
position = world.randomPlace()
|
||||
}
|
||||
let enemyStats = new EnemyStats().withHealthFactor(world.getEnemyHealthFactor());;
|
||||
let basicEnemy = new HealthEnemy(position, enemyStats);
|
||||
basicEnemy.size = 5;
|
||||
basicEnemy.world = world;
|
||||
basicEnemy.speed = 0;
|
||||
basicEnemy.color = 'purple'
|
||||
return basicEnemy;
|
||||
}
|
||||
|
||||
|
||||
getSize() {
|
||||
return this.size
|
||||
}
|
||||
}
|
||||
|
||||
export class ContainerEnemy extends Enemy {
|
||||
|
||||
private drops: KillChanceTable;
|
||||
constructor(position: Vector, enemyStats: EnemyStats) {
|
||||
super(position, enemyStats);
|
||||
this.status.health = 5;
|
||||
this.drops = new KillChanceTable();
|
||||
ItemManagement.getItemsWithRarityFactor().forEach(drop => {
|
||||
this.drops.addDrop(drop)
|
||||
})
|
||||
this.drops.calculateProbs()
|
||||
}
|
||||
|
||||
protected color: string;
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(this._position, this.size, this.color, ctx)
|
||||
}
|
||||
|
||||
move() {
|
||||
}
|
||||
|
||||
act() {
|
||||
super.act();
|
||||
}
|
||||
|
||||
die() {
|
||||
let draw = this.drops.draw();
|
||||
if(draw) {
|
||||
let item = draw.creationMethod(this);
|
||||
ItemDrop.spawnItemDrop(this.world, item, this._position)
|
||||
}
|
||||
}
|
||||
|
||||
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 enemyStats = new EnemyStats().withHealthFactor(world.getEnemyHealthFactor());;
|
||||
let basicEnemy = new ContainerEnemy(position, enemyStats);
|
||||
basicEnemy.size = 5;
|
||||
basicEnemy.world = world;
|
||||
basicEnemy.speed = 0;
|
||||
basicEnemy.color = 'brown'
|
||||
return basicEnemy;
|
||||
}
|
||||
|
||||
|
||||
getSize() {
|
||||
return this.size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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() {
|
||||
if(this.chances.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
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]
|
||||
}
|
||||
}
|
||||
177
absurd-survivors/src/Player.ts
Normal file
177
absurd-survivors/src/Player.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import type {Acting, Drawable, Healthy, Item, Weapon} from "./interfaces.ts";
|
||||
import {Vector} from "./base.ts";
|
||||
import {fillDot, getCoordinatesSplit} from "./utils.ts";
|
||||
import {PlayerStats} from "./stats.ts";
|
||||
import {PlayerStatus} from "./status.ts";
|
||||
import {World} from "./World.ts";
|
||||
import { NumberDisplayParticle} from "./particles.ts";
|
||||
|
||||
export class Player implements Drawable, Acting, Healthy {
|
||||
private _position: Vector;
|
||||
private _baseStats: PlayerStats;
|
||||
private _effectiveStats: PlayerStats;
|
||||
private _tempStats: PlayerStats;
|
||||
private _color: string;
|
||||
|
||||
private _status: PlayerStatus;
|
||||
private _weapons: Weapon[] = []
|
||||
private _items: Item[] = []
|
||||
private _healTick: number = 0;
|
||||
private static readonly HEAL_TICK_INTERVAL = 20;
|
||||
private _world: World;
|
||||
|
||||
// temp
|
||||
private _speed: Vector;
|
||||
private _toHeal: number = 0;
|
||||
|
||||
constructor(position: Vector) {
|
||||
this._position = position;
|
||||
this._effectiveStats = new PlayerStats()
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(this.position, this._effectiveStats.size, this._color, ctx)
|
||||
this._weapons.forEach(weapon => weapon.draw(ctx))
|
||||
}
|
||||
|
||||
public static generatePlayer(position?: Vector): Player {
|
||||
if(position === undefined) {
|
||||
position = new Vector(500, 500)
|
||||
}
|
||||
let player = new Player(position);
|
||||
player._color = 'blue';
|
||||
player._baseStats = PlayerStats.defaultPlayerStats();
|
||||
let tempStats = new PlayerStats();
|
||||
tempStats.resetToBasic()
|
||||
player._tempStats = tempStats;
|
||||
player.statsChanged()
|
||||
player._speed = new Vector(0, 0)
|
||||
player._status = new PlayerStatus(10, 0, 0);
|
||||
return player;
|
||||
}
|
||||
|
||||
addWeapon(weapon: Weapon) {
|
||||
let weaponCount = this._weapons.length + 1;
|
||||
let points = getCoordinatesSplit(weaponCount)
|
||||
for (let i = 0; i < points.length - 1; i++){
|
||||
const value = points[i];
|
||||
let affectedWeapon = this._weapons[i];
|
||||
affectedWeapon.setOffset(value.multiply(affectedWeapon.getSize()))
|
||||
}
|
||||
weapon.setOffset(points[points.length - 1].multiply(weapon.getSize()))
|
||||
this._weapons.push(weapon)
|
||||
}
|
||||
|
||||
addItem(item: Item) {
|
||||
this._items.push(item)
|
||||
}
|
||||
|
||||
set world(value: World) {
|
||||
this._world = value;
|
||||
}
|
||||
|
||||
move(direction: Vector) {
|
||||
this._position = this.position.add(direction)
|
||||
}
|
||||
|
||||
takeDamage(damage: number) {
|
||||
NumberDisplayParticle.spawnNumberParticle(this._world, damage, this._position, 'red')
|
||||
this._status.health -= damage;
|
||||
}
|
||||
|
||||
statsChanged() {
|
||||
this._effectiveStats.resetToBasic()
|
||||
this._effectiveStats.mergeStats(this._baseStats)
|
||||
this._effectiveStats.mergeStats(this._tempStats)
|
||||
}
|
||||
|
||||
heal(amount: number) {
|
||||
this._status.health += amount;
|
||||
this._status.health = Math.min(this._status.health, this._effectiveStats.health)
|
||||
}
|
||||
|
||||
get health(): number {
|
||||
return this._status.health;
|
||||
}
|
||||
|
||||
get position(): Vector {
|
||||
return this._position;
|
||||
}
|
||||
|
||||
get color(): string {
|
||||
return this._color;
|
||||
}
|
||||
|
||||
get effectiveStats(): PlayerStats {
|
||||
return this._effectiveStats;
|
||||
}
|
||||
|
||||
get status(): PlayerStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
get speed(): Vector {
|
||||
return this._speed;
|
||||
}
|
||||
|
||||
act() {
|
||||
this._weapons.forEach(weapon => weapon.act())
|
||||
}
|
||||
|
||||
die() {
|
||||
}
|
||||
|
||||
getPosition(): Vector {
|
||||
return this._position;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return this._effectiveStats.size
|
||||
}
|
||||
|
||||
dead() {
|
||||
return this.status.dead
|
||||
}
|
||||
|
||||
changeBaseStat(value: number, statFun: (stats: PlayerStats, value: number) => void) {
|
||||
this._baseStats.changeStat(value, statFun)
|
||||
this.statsChanged()
|
||||
}
|
||||
|
||||
changeTempStat(value: number, statFun: (stats: PlayerStats, value: number) => void) {
|
||||
this._tempStats.changeStat(value, statFun)
|
||||
this.statsChanged()
|
||||
}
|
||||
|
||||
increaseLevel() {
|
||||
this.status.increaseLevel()
|
||||
this._baseStats.increaseLevel()
|
||||
this._weapons.forEach(weapon => {
|
||||
weapon.increaseLevel()
|
||||
})
|
||||
this.statsChanged()
|
||||
}
|
||||
|
||||
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);
|
||||
NumberDisplayParticle.spawnNumberParticle(this._world, toHealNow, this.position, 'green')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
221
absurd-survivors/src/World.ts
Normal file
221
absurd-survivors/src/World.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import {Enemy} from "./Enemies.ts";
|
||||
import {Player} from "./Player.ts";
|
||||
import {Projectile } from "./projectile.ts";
|
||||
import {Vector} from "./base.ts";
|
||||
import type {Drop, Particle, Placeable} from "./interfaces.ts";
|
||||
|
||||
export class World {
|
||||
private _enemies: ObjectContainer<Enemy> = new ObjectContainer<Enemy>()
|
||||
private _projectiles: ObjectContainer<Projectile> = new ObjectContainer<Projectile>();
|
||||
private _drops: ObjectContainer<Drop> = new ObjectContainer<Drop>();
|
||||
private _particles: ObjectContainer<Particle> = new ObjectContainer();
|
||||
private _player: Player;
|
||||
private readonly _ctx: CanvasRenderingContext2D;
|
||||
private _size: Vector;
|
||||
private _tick: number = 0;
|
||||
private static readonly TICK_INTERVAL = 10;
|
||||
private timeStamp: Date;
|
||||
private startTimeStamp: Date;
|
||||
|
||||
constructor(player: Player, ctx: CanvasRenderingContext2D, size: Vector) {
|
||||
this._player = player;
|
||||
this._ctx = ctx;
|
||||
this._size = size;
|
||||
this.timeStamp = new Date();
|
||||
this.startTimeStamp = new Date();
|
||||
}
|
||||
|
||||
enemiesAct() {
|
||||
this._enemies.items.forEach(enemy => enemy.act())
|
||||
this._enemies.clean()
|
||||
this._projectiles.items.forEach(projectile => projectile.act())
|
||||
this._projectiles.clean()
|
||||
this._drops.items.forEach(drop => drop.act())
|
||||
this._drops.clean()
|
||||
this._particles.items.forEach(particle => particle.act())
|
||||
this._particles.clean()
|
||||
}
|
||||
|
||||
draw() {
|
||||
this._enemies.items.forEach(enemy => enemy.draw(this._ctx))
|
||||
this._drops.items.forEach(drop => drop.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);
|
||||
}
|
||||
|
||||
addProjectile(projectile: Projectile) {
|
||||
this._projectiles.add(projectile)
|
||||
}
|
||||
|
||||
addParticle(particle: Particle) {
|
||||
this._particles.add(particle)
|
||||
}
|
||||
|
||||
addDrop(drop: Drop) {
|
||||
this._drops.add(drop)
|
||||
}
|
||||
|
||||
removeDrop(drop: Drop) {
|
||||
this._drops.scheduleRemoval(drop)
|
||||
}
|
||||
|
||||
removeParticle(particle: Particle) {
|
||||
this._particles.scheduleRemoval(particle)
|
||||
}
|
||||
|
||||
removeEnemy(enemy: Enemy) {
|
||||
this._enemies.scheduleRemoval(enemy)
|
||||
}
|
||||
|
||||
removeProjectile(projectile: Projectile) {
|
||||
this._projectiles.scheduleRemoval(projectile)
|
||||
}
|
||||
|
||||
|
||||
movePlayer(vector: Vector) {
|
||||
this._player.position.x += vector.x;
|
||||
this._player.position.y += vector.y;
|
||||
this._player.position.x = Math.min(this.size.x - this._player.getSize(), this._player.position.x)
|
||||
this._player.position.x = Math.max(this._player.getSize(), this._player.position.x)
|
||||
this._player.position.y = Math.min(this.size.y -this._player.getSize(), this._player.position.y)
|
||||
this._player.position.y = Math.max(this._player.getSize(), this._player.position.y)
|
||||
}
|
||||
|
||||
maxValue() {
|
||||
return Math.max(this.size.x, this.size.y)
|
||||
}
|
||||
|
||||
get size(): Vector {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
get tick(): number {
|
||||
return this._tick;
|
||||
}
|
||||
|
||||
getSecondsPassed() {
|
||||
return (this.timeStamp.getTime() - this.startTimeStamp.getTime()) / 1000
|
||||
}
|
||||
|
||||
getEnemyHealthFactor() {
|
||||
return this.getSecondsPassed() / 30;
|
||||
}
|
||||
|
||||
triggerTick() {
|
||||
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 {
|
||||
return position.x > this.size.x || position.y > this.size.y || position.x < 0 || position.y < 0
|
||||
}
|
||||
|
||||
addEnemy(enemy: Enemy) {
|
||||
this._enemies.add(enemy)
|
||||
}
|
||||
|
||||
randomPlace(): Vector {
|
||||
return new Vector(this.size.x * Math.random(), this.size.y * Math.random())
|
||||
}
|
||||
|
||||
getClosestTargetTo(point: Vector, range?: number): [number, Placeable | undefined] | undefined {
|
||||
return this.getClosestTargetToButNot(point, undefined, range)
|
||||
}
|
||||
|
||||
getFarthestTargetButWithin(point: Vector, range?: number): [number, Placeable | undefined] | undefined {
|
||||
let currentTarget;
|
||||
let currentDistance = Number.MAX_SAFE_INTEGER;
|
||||
this._enemies.items.forEach(enemy => {
|
||||
let distance = point.distanceTo(enemy.getPosition());
|
||||
if(range && distance > range) {
|
||||
return;
|
||||
}
|
||||
if((range - distance) < currentDistance) {
|
||||
currentDistance = distance;
|
||||
currentTarget = enemy
|
||||
}
|
||||
})
|
||||
if(currentTarget) {
|
||||
return [currentDistance, currentTarget];
|
||||
}
|
||||
}
|
||||
|
||||
getClosestTargetToButNot(point: Vector, placeable?: Placeable, range?: number): [number, Placeable | undefined] | undefined {
|
||||
return this.getClosestTargetToButNotArray(point, [placeable], range)
|
||||
}
|
||||
|
||||
getClosestTargetToButNotArray(point: Vector, placeAbles?: [Placeable | undefined], range?: number): [number, Placeable | undefined] | undefined {
|
||||
let currentTarget;
|
||||
let currentDistance = Number.MAX_SAFE_INTEGER;
|
||||
this._enemies.items.forEach(enemy => {
|
||||
if(placeAbles && placeAbles.indexOf(enemy) !== -1) {
|
||||
return;
|
||||
}
|
||||
let distance = point.distanceTo(enemy.getPosition());
|
||||
if(range && distance > range) {
|
||||
return;
|
||||
}
|
||||
if(distance < currentDistance) {
|
||||
currentDistance = distance;
|
||||
currentTarget = enemy
|
||||
}
|
||||
})
|
||||
if(currentTarget) {
|
||||
return [currentDistance, currentTarget];
|
||||
}
|
||||
}
|
||||
|
||||
getAllInRange(point: Vector, range: number): Enemy[] {
|
||||
let found = [];
|
||||
this._enemies.items.forEach(enemy => {
|
||||
let distance = point.distanceTo(enemy.getPosition());
|
||||
if(range && distance < range) {
|
||||
found.push(enemy)
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
get player(): Player {
|
||||
return this._player;
|
||||
}
|
||||
}
|
||||
|
||||
class ObjectContainer<T> {
|
||||
private _items: T[] = [];
|
||||
private _itemsToRemove: T[] = [];
|
||||
|
||||
constructor() {
|
||||
this._items = []
|
||||
this._itemsToRemove = []
|
||||
}
|
||||
|
||||
scheduleRemoval(item: T) {
|
||||
this._itemsToRemove.push(item)
|
||||
}
|
||||
|
||||
clean() {
|
||||
this._itemsToRemove.forEach(value => this.remove(value))
|
||||
this._itemsToRemove = []
|
||||
}
|
||||
|
||||
private remove(itemToRemove: T) {
|
||||
this._items = this._items.filter(item => item !== itemToRemove)
|
||||
}
|
||||
|
||||
add(item: T) {
|
||||
this._items.push(item)
|
||||
}
|
||||
|
||||
|
||||
get items(): T[] {
|
||||
return this._items;
|
||||
}
|
||||
}
|
||||
167
absurd-survivors/src/base.ts
Normal file
167
absurd-survivors/src/base.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import type {Healthy, Placeable} from "./interfaces.ts";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static zero() {
|
||||
return new Vector(0, 0)
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Vector(this.x, 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)
|
||||
}
|
||||
|
||||
dotProduct(vector: Vector): number {
|
||||
return (this._x * vector._x + this._y * vector._y) / Math.pow(vector.vecLength(), 2);
|
||||
}
|
||||
|
||||
dotProduct2(vector: Vector): number {
|
||||
return (this._x * vector._x + this._y * vector._y)
|
||||
}
|
||||
|
||||
angleTo(vector: Vector): number {
|
||||
return Math.acos(this.dotProduct2(vector) / (this.vecLength() * vector.vecLength()))
|
||||
}
|
||||
|
||||
rotate(angle: number) {
|
||||
let c = Math.cos(angle);
|
||||
let s = Math.sin(angle)
|
||||
let x = this._x * c - this._y * s
|
||||
let y = this._x * s + this._y * c
|
||||
return new Vector(x, y)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export class Cooldown {
|
||||
private _currentValue;
|
||||
private _totalValue;
|
||||
|
||||
|
||||
constructor(totalValue) {
|
||||
this._totalValue = totalValue;
|
||||
this._currentValue = 0
|
||||
}
|
||||
|
||||
cooledDown(): boolean {
|
||||
return this.currentValue <= 0;
|
||||
}
|
||||
|
||||
get currentValue(): number {
|
||||
return this._currentValue;
|
||||
}
|
||||
|
||||
decreaseCooldown() {
|
||||
this._currentValue -= 1;
|
||||
}
|
||||
|
||||
resetCooldown() {
|
||||
this._currentValue = this._totalValue;
|
||||
}
|
||||
}
|
||||
|
||||
export class Point implements Placeable {
|
||||
|
||||
private position: Vector;
|
||||
|
||||
|
||||
constructor(position: Vector) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
getPosition(): Vector {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
}
|
||||
|
||||
move(any?: any) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class DeadPoint implements Placeable, Healthy {
|
||||
|
||||
private position: Vector;
|
||||
|
||||
|
||||
constructor(position: Vector) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
getPosition(): Vector {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
}
|
||||
|
||||
move(any?: any) {
|
||||
}
|
||||
|
||||
dead() {
|
||||
return true;
|
||||
}
|
||||
|
||||
die() {
|
||||
}
|
||||
|
||||
takeDamage(damage: number) {
|
||||
}
|
||||
|
||||
}
|
||||
160
absurd-survivors/src/drop.ts
Normal file
160
absurd-survivors/src/drop.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import type {Drop, Item} from "./interfaces.ts";
|
||||
import {World} from "./World.ts";
|
||||
import {fillDot, moveInDirectionOf} from "./utils.ts";
|
||||
import {Vector} from "./base.ts";
|
||||
import {rarityColor} from "./items.ts";
|
||||
|
||||
export abstract class BasicDrop implements Drop {
|
||||
protected world: World;
|
||||
protected _position: Vector;
|
||||
protected _color: string;
|
||||
protected size: number;
|
||||
|
||||
constructor(world: World, position: Vector) {
|
||||
this.world = world;
|
||||
this._position = position.clone();
|
||||
}
|
||||
|
||||
getPosition(): Vector {
|
||||
return this._position;
|
||||
}
|
||||
|
||||
move() {
|
||||
}
|
||||
|
||||
pickup() {
|
||||
}
|
||||
|
||||
act() {
|
||||
let distanceToPlayer = this._position.distanceTo(this.world.player.position);
|
||||
if(distanceToPlayer < (this.world.player.effectiveStats.size + this.size)) {
|
||||
this.pickup()
|
||||
this.world.removeDrop(this)
|
||||
} else if(distanceToPlayer < this.world.player.effectiveStats.pullRange) {
|
||||
let speedFactor = 125 / distanceToPlayer;
|
||||
this._position = moveInDirectionOf(this._position, this.world.player.position, speedFactor)
|
||||
}
|
||||
}
|
||||
|
||||
abstract draw(ctx: CanvasRenderingContext2D);
|
||||
|
||||
getSize() {
|
||||
return this.size
|
||||
}
|
||||
|
||||
tick(seconds: number, tick: number) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class MoneyDrop extends BasicDrop {
|
||||
|
||||
private worth: number;
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(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._color = 'orange';
|
||||
return drop;
|
||||
}
|
||||
}
|
||||
|
||||
export class HealthPack extends BasicDrop {
|
||||
private healAmount: number;
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(this._position, this.size, this._color, ctx)
|
||||
}
|
||||
|
||||
pickup() {
|
||||
this.world.player.heal(this.healAmount)
|
||||
}
|
||||
|
||||
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._color = 'green';
|
||||
return drop;
|
||||
}
|
||||
}
|
||||
|
||||
export class LevelDrop extends BasicDrop {
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ItemDrop extends BasicDrop {
|
||||
|
||||
private item: Item;
|
||||
|
||||
|
||||
constructor(world: World, position: Vector, item: Item) {
|
||||
super(world, position);
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
pickup() {
|
||||
this.item.pickup(this.world.player, this.world)
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
ctx.fillStyle = rarityColor(this.item.getRarity())
|
||||
ctx.fillText(this.item.name() + '', this._position.x, this._position.y)
|
||||
}
|
||||
|
||||
static spawnItemDrop(world: World, item: Item, position?: Vector) {
|
||||
world.addDrop(this.createItemDrop(world, item, position))
|
||||
}
|
||||
|
||||
static createItemDrop(world: World, item: Item, position?: Vector) {
|
||||
if(!position) {
|
||||
position = world.randomPlace()
|
||||
}
|
||||
let drop = new ItemDrop(world, position, item)
|
||||
drop.size = 3
|
||||
return drop
|
||||
}
|
||||
}
|
||||
7
absurd-survivors/src/instance.ts
Normal file
7
absurd-survivors/src/instance.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
80
absurd-survivors/src/interfaces.ts
Normal file
80
absurd-survivors/src/interfaces.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {Vector} from "./base.ts";
|
||||
import type {Player} from "./Player.ts";
|
||||
import type {Rarity} from "./items.ts";
|
||||
import type {World} from "./World.ts";
|
||||
|
||||
export interface Acting {
|
||||
act()
|
||||
tick(seconds: number, tick: number)
|
||||
}
|
||||
|
||||
export interface Healthy {
|
||||
takeDamage(damage: number);
|
||||
die();
|
||||
dead();
|
||||
}
|
||||
|
||||
export interface Leveling {
|
||||
increaseLevel();
|
||||
level()
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
pickup(player: Player, world: World);
|
||||
name(): string
|
||||
getRarity(): Rarity;
|
||||
}
|
||||
|
||||
export interface ChanceEntry {
|
||||
chance: number;
|
||||
creationMethod: (any: any) => any;
|
||||
}
|
||||
|
||||
export interface Drop extends Drawable, Acting {
|
||||
pickup()
|
||||
}
|
||||
|
||||
export interface Particle extends Drawable, Placeable, Acting {
|
||||
|
||||
}
|
||||
|
||||
export interface Placeable {
|
||||
move(any?: any)
|
||||
getSize();
|
||||
getPosition(): Vector;
|
||||
}
|
||||
|
||||
export interface MouseInteracting {
|
||||
hit(pos: Vector): boolean
|
||||
clickAction(pos: Vector): void
|
||||
}
|
||||
|
||||
export interface MouseInterActingContainer {
|
||||
mouseDown(pos: Vector)
|
||||
}
|
||||
|
||||
export interface Projectile extends Drawable {
|
||||
|
||||
}
|
||||
|
||||
export interface Equipment {
|
||||
getOffset(): Vector;
|
||||
setOffset(vec: Vector);
|
||||
}
|
||||
|
||||
export interface Weapon extends Drawable, Equipment, Leveling {
|
||||
act()
|
||||
}
|
||||
|
||||
export interface Shooting {
|
||||
createProjectile(): Projectile;
|
||||
removeProjectile(projectile: Projectile)
|
||||
}
|
||||
|
||||
export interface Drawable extends Placeable {
|
||||
draw(ctx: CanvasRenderingContext2D);
|
||||
}
|
||||
|
||||
export interface DrawContainer {
|
||||
draw(ctx: CanvasRenderingContext2D);
|
||||
}
|
||||
271
absurd-survivors/src/items.ts
Normal file
271
absurd-survivors/src/items.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import type {ChanceEntry, Item} from "./interfaces.ts";
|
||||
import {Player} from "./Player.ts";
|
||||
import {randomItem} from "./utils.ts";
|
||||
import {ChainBall, HomingPistol, Pistol, Spear, SpreadWeapon} from "./weapons.ts";
|
||||
import type {World} from "./World.ts";
|
||||
import {PlayerStats} from "./stats.ts";
|
||||
|
||||
export enum Rarity {
|
||||
GARBAGE= 'GARBAGE',
|
||||
COMMON = 'COMMON',
|
||||
UNCOMMON = 'UNCOMMON',
|
||||
RARE = 'RARE',
|
||||
EPIC = 'EPIC',
|
||||
LEGENDARY = 'LEGENDARY',
|
||||
GODLY = 'GODLY'
|
||||
}
|
||||
|
||||
export class ItemManagement {
|
||||
|
||||
private static ITEMS: Item[] = []
|
||||
|
||||
static addItem(item: Item) {
|
||||
this.ITEMS.push(item)
|
||||
}
|
||||
|
||||
static getItemsWithRarityFactor(): ChanceEntry[] {
|
||||
let items: ChanceEntry[] = []
|
||||
this.ITEMS.forEach((item) => {
|
||||
items.push({chance: rarityWeight(item.getRarity()), creationMethod: () => item})
|
||||
})
|
||||
return items;
|
||||
}
|
||||
|
||||
static getRandomItem() {
|
||||
return randomItem(this.ITEMS)
|
||||
}
|
||||
|
||||
static initializeItems() {
|
||||
this.ITEMS.push(new SpeedUp())
|
||||
this.ITEMS.push(new HealthUp())
|
||||
this.ITEMS.push(new HomingPistolItem())
|
||||
this.ITEMS.push(new PistolItem())
|
||||
this.ITEMS.push(new SpreadWeaponItem())
|
||||
this.ITEMS.push(new ChainBallWeaponItem())
|
||||
this.ITEMS.push(new PullRangeUp())
|
||||
this.ITEMS.push(new SpearWeaponItem())
|
||||
this.ITEMS.push(new DamageUpFactor())
|
||||
this.ITEMS.push(new DamageFactorUp())
|
||||
this.ITEMS.push(new DamageFactorUpFactor())
|
||||
this.ITEMS.push(new DamageUp())
|
||||
}
|
||||
}
|
||||
|
||||
export function rarityWeight(rarity: Rarity): number {
|
||||
switch (rarity) {
|
||||
case Rarity.GARBAGE: return 80;
|
||||
case Rarity.COMMON: return 65;
|
||||
case Rarity.UNCOMMON: return 50;
|
||||
case Rarity.RARE: return 30;
|
||||
case Rarity.EPIC: return 15;
|
||||
case Rarity.LEGENDARY: return 5;
|
||||
case Rarity.GODLY: return 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function rarityColor(rarity: Rarity): string {
|
||||
switch (rarity) {
|
||||
case Rarity.GARBAGE: return 'white';
|
||||
case Rarity.COMMON: return 'gray';
|
||||
case Rarity.UNCOMMON: return 'blue';
|
||||
case Rarity.RARE: return 'green';
|
||||
case Rarity.EPIC: return 'orange';
|
||||
case Rarity.LEGENDARY: return 'violett';
|
||||
case Rarity.GODLY: return 'red';
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class BaseItem implements Item {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
pickup(player: Player, world: World) {
|
||||
player.addItem(this)
|
||||
}
|
||||
|
||||
abstract name();
|
||||
abstract getRarity(): Rarity;
|
||||
}
|
||||
|
||||
export class SpeedUp extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
player.changeBaseStat(1, PlayerStats.increaseSpeed)
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'speed'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.LEGENDARY;
|
||||
}
|
||||
}
|
||||
|
||||
export class PullRangeUp extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
player.changeBaseStat(1.1, PlayerStats.factorPullRange)
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'pull range'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.COMMON;
|
||||
}
|
||||
}
|
||||
|
||||
export class HealthUp extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
let healthAmount = 1;
|
||||
player.changeBaseStat(healthAmount, PlayerStats.increaseHealth)
|
||||
player.heal(healthAmount)
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'health'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.COMMON;
|
||||
}
|
||||
}
|
||||
|
||||
export class HomingPistolItem extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
player.addWeapon(HomingPistol.generateHomingPistol(world))
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'homingp'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.RARE;
|
||||
}
|
||||
}
|
||||
|
||||
export class PistolItem extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
player.addWeapon(Pistol.generatePistol(world))
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'pistol'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.RARE;
|
||||
}
|
||||
}
|
||||
|
||||
export class SpreadWeaponItem extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
player.addWeapon(SpreadWeapon.generateSpreadWeapon(world))
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'spreadp'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.EPIC;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChainBallWeaponItem extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
player.addWeapon(ChainBall.createChainBall(world))
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'chain ball'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.EPIC;
|
||||
}
|
||||
}
|
||||
|
||||
export class SpearWeaponItem extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
player.addWeapon(Spear.createSpear(world))
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'spear'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.EPIC;
|
||||
}
|
||||
}
|
||||
|
||||
export class DamageFactorUpFactor extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
player.changeBaseStat(1, PlayerStats.factorDamageFactor)
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'damageFactorUpFactor'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.LEGENDARY;
|
||||
}
|
||||
}
|
||||
|
||||
export class DamageUp extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
player.changeBaseStat(1, PlayerStats.increaseDamage)
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'damageUp'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.COMMON;
|
||||
}
|
||||
}
|
||||
|
||||
export class DamageUpFactor extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
player.changeBaseStat(1, PlayerStats.factorDamage)
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'damageUpFactor'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.LEGENDARY;
|
||||
}
|
||||
}
|
||||
|
||||
export class DamageFactorUp extends BaseItem {
|
||||
pickup(player: Player, world: World) {
|
||||
player.changeBaseStat(1, PlayerStats.increaseDamageFactor)
|
||||
super.pickup(player, world)
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'damageFactorUp'
|
||||
}
|
||||
|
||||
getRarity(): Rarity {
|
||||
return Rarity.UNCOMMON;
|
||||
}
|
||||
}
|
||||
175
absurd-survivors/src/main.ts
Normal file
175
absurd-survivors/src/main.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
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, ContainerEnemy, HealthEnemy, ShootingEnemy} from "./Enemies.ts";
|
||||
import {HUD} from "./ui.ts";
|
||||
import {Pistol} from "./weapons.ts";
|
||||
import {ItemManagement} from "./items.ts";
|
||||
|
||||
let hud: HUD;
|
||||
let world: World;
|
||||
let config: Config;
|
||||
let state: WorldState;
|
||||
let ctx: CanvasRenderingContext2D;
|
||||
let canvas;
|
||||
|
||||
export class Config {
|
||||
private _fps: number = 60;
|
||||
|
||||
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, world.size.x, world.size.y);
|
||||
hud.draw(ctx)
|
||||
if(!state.ended) {
|
||||
world.triggerTick()
|
||||
world.enemiesAct()
|
||||
world.player.act()
|
||||
world.draw()
|
||||
for(let key in keys) {
|
||||
if(keys[key].state) {
|
||||
keys[key].fun(keys[key].intensity)
|
||||
}
|
||||
}
|
||||
} 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 (intensity: number) {
|
||||
world.movePlayer(new Vector(0, -world.player.effectiveStats.speed * intensity))
|
||||
})
|
||||
makeKey('s', function (intensity: number) {
|
||||
world.movePlayer(new Vector(0, world.player.effectiveStats.speed * intensity))
|
||||
})
|
||||
makeKey('a', function (intensity: number) {
|
||||
world.movePlayer(new Vector(-world.player.effectiveStats.speed * intensity, 0))
|
||||
})
|
||||
makeKey('d', function (intensity: number) {
|
||||
world.movePlayer(new Vector(world.player.effectiveStats.speed * intensity, 0))
|
||||
})
|
||||
|
||||
|
||||
function keyUp(event) {
|
||||
if(event.key in keys) {
|
||||
keys[event.key].state = false;
|
||||
keys[event.key].intensity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function keyDown(event) {
|
||||
if(event.key in keys) {
|
||||
keys[event.key].state = true;
|
||||
keys[event.key].intensity = 1;
|
||||
}
|
||||
}
|
||||
|
||||
document.onkeyup = keyUp;
|
||||
document.onkeydown = keyDown;
|
||||
docReady(function () {
|
||||
canvas = document.getElementById('canvas');
|
||||
|
||||
canvas.width = window.innerWidth;
|
||||
|
||||
canvas.height = window.innerHeight;
|
||||
|
||||
|
||||
ctx = canvas.getContext("2d");
|
||||
config = new Config();
|
||||
let player = Player.generatePlayer(new Vector(window.innerWidth /2, window.innerHeight / 2));
|
||||
|
||||
world = new World(player, ctx, new Vector(window.innerWidth, window.innerHeight));
|
||||
player.world = world; // not sure if this is great design
|
||||
state = new WorldState();
|
||||
|
||||
setInterval(() => {
|
||||
BasicEnemy.spawnBasicEnemy(world)
|
||||
}, 1_000)
|
||||
|
||||
setInterval(() => {
|
||||
ShootingEnemy.spawnShootingEnemy(world)
|
||||
}, 3_000)
|
||||
|
||||
setInterval(() => {
|
||||
HealthEnemy.spawnHealthEnemy(world)
|
||||
}, 15_000)
|
||||
|
||||
setInterval(() => {
|
||||
ContainerEnemy.spawnContainerEnemy(world)
|
||||
}, 10_000)
|
||||
|
||||
player.addWeapon(Pistol.generatePistol(world))
|
||||
hud = new HUD(world, keys);
|
||||
|
||||
canvas.onmousedown = (event) => {
|
||||
event.preventDefault()
|
||||
let pos = new Vector(event.x, event.y)
|
||||
hud.mouseDown(pos)
|
||||
}
|
||||
canvas.onmouseup = (event) => {
|
||||
event.preventDefault()
|
||||
let pos = new Vector(event.x, event.y)
|
||||
hud.mouseUp(pos)
|
||||
}
|
||||
|
||||
canvas.onmousemove = (event) => {
|
||||
event.preventDefault()
|
||||
let pos = new Vector(event.x, event.y)
|
||||
hud.mouseMove(pos)
|
||||
}
|
||||
|
||||
canvas.addEventListener("touchstart", (event) => {
|
||||
event.preventDefault()
|
||||
let touch = event.touches[0]
|
||||
let pos = new Vector(touch.clientX, touch.clientY)
|
||||
hud.mouseDown(pos)
|
||||
});
|
||||
|
||||
canvas.addEventListener("touchend", (event) => {
|
||||
event.preventDefault()
|
||||
let pos = new Vector(event.clientX, event.clientY)
|
||||
hud.mouseUp(pos)
|
||||
});
|
||||
|
||||
canvas.addEventListener("touchmove", (event) => {
|
||||
event.preventDefault()
|
||||
let touch = event.touches[0]
|
||||
let pos = new Vector(touch.clientX, touch.clientY)
|
||||
hud.mouseMove(pos)
|
||||
});
|
||||
|
||||
ItemManagement.initializeItems()
|
||||
|
||||
requestAnimationFrame(updateCanvas);
|
||||
|
||||
})
|
||||
|
||||
75
absurd-survivors/src/particles.ts
Normal file
75
absurd-survivors/src/particles.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
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 NumberDisplayParticle extends BaseParticle {
|
||||
private number: number;
|
||||
private secondsToDisplay: number = 2;
|
||||
private alreadyDisplayed: number = 0;
|
||||
private color: string;
|
||||
|
||||
constructor(position: Vector, world: World, healthAmount: number, color: string) {
|
||||
super(position, world);
|
||||
this.number = healthAmount;
|
||||
this.color = color
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.fillText(this.number + '', this._position.x, this._position.y);
|
||||
}
|
||||
|
||||
tick(seconds: number, tick: number) {
|
||||
this.alreadyDisplayed += seconds;
|
||||
if(this.alreadyDisplayed > this.secondsToDisplay) {
|
||||
this.world.removeParticle(this)
|
||||
}
|
||||
}
|
||||
|
||||
static spawnNumberParticle(world: World, health: number, position: Vector, color?: string) {
|
||||
if(!color) {
|
||||
color = 'red'
|
||||
}
|
||||
world.addParticle(this.createNumberParticle(world, health, position, color))
|
||||
}
|
||||
|
||||
static createNumberParticle(world: World, health: number, position: Vector, color?: string) {
|
||||
if(!color) {
|
||||
color = 'red'
|
||||
}
|
||||
let particle = new NumberDisplayParticle(position, world, health, color)
|
||||
return particle
|
||||
}
|
||||
}
|
||||
326
absurd-survivors/src/projectile.ts
Normal file
326
absurd-survivors/src/projectile.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
import type {Acting, Placeable, Healthy } from "./interfaces.ts";
|
||||
import type {Vector} from "./base.ts";
|
||||
import {World} from "./World.ts";
|
||||
import {DeadPoint, Vector} from "./base.ts";
|
||||
import {
|
||||
circleLineCollision,
|
||||
fillDot,
|
||||
getCoordinatesSplit,
|
||||
moveInDirectionOf,
|
||||
pointOnLineWithinLine,
|
||||
straightMove,
|
||||
toRad
|
||||
} from "./utils.ts";
|
||||
import {InstanceOfUtils} from "./instance.ts";
|
||||
import {ChainBall, MeleeWeapon} from "./weapons.ts";
|
||||
import type {Enemy} from "./Enemies.ts";
|
||||
import {ProjectileStats} from "./stats.ts";
|
||||
import {ProjectileStatus} from "./status.ts";
|
||||
|
||||
export abstract class Projectile implements Acting, Placeable {
|
||||
|
||||
protected position: Vector;
|
||||
protected speedVec: Vector;
|
||||
protected world: World;
|
||||
protected parent: any;
|
||||
protected color: string
|
||||
protected stats: ProjectileStats;
|
||||
protected status: ProjectileStatus;
|
||||
protected lastPosition: Vector;
|
||||
protected secondToLastPosition?: Vector
|
||||
protected lastColliding?: Placeable;
|
||||
|
||||
constructor(position: Vector, speedVec: Vector, stats: ProjectileStats, world: World, parent: any) {
|
||||
this.position = position.clone();
|
||||
this.speedVec = speedVec.clone();
|
||||
this.world = world;
|
||||
this.parent = parent;
|
||||
this.stats = stats;
|
||||
this.status = new ProjectileStatus(stats.piercings)
|
||||
}
|
||||
|
||||
die() {
|
||||
this.world.removeProjectile(this)
|
||||
}
|
||||
|
||||
act() {
|
||||
this.move()
|
||||
if(this.parent !== this.world.player) {
|
||||
if(this.position.distanceTo(this.world.player.position) < (this.stats.size + this.world.player.effectiveStats.size) && this.status.collisionCooldown.cooledDown()) {
|
||||
this.impactPlayer()
|
||||
this.status.collisionCooldown.resetCooldown()
|
||||
}
|
||||
} else if(this.parent === this.world.player) {
|
||||
let closestTargetTo = this.world.getClosestTargetToButNot(this.position, this.lastColliding);
|
||||
if(closestTargetTo !== undefined && closestTargetTo[1] !== undefined) {
|
||||
let target: Placeable = closestTargetTo[1]!;
|
||||
if(target.getPosition().distanceTo(this.position) < (this.stats.size + target.getSize())
|
||||
|| circleLineCollision(target.getPosition(), target.getSize(), this.position, this.lastPosition)) {
|
||||
if(target !== this.lastColliding) {
|
||||
if(InstanceOfUtils.instanceOfHealthy(target)) {
|
||||
let healthy = target as Healthy;
|
||||
healthy.takeDamage(this.stats.damage)
|
||||
if(!this.status.hasPiercingLeft()) {
|
||||
this.die()
|
||||
}
|
||||
this.status.decreasePiercings()
|
||||
} else {
|
||||
this.die()
|
||||
}
|
||||
}
|
||||
this.lastColliding = target;
|
||||
} else {
|
||||
this.lastColliding = undefined;
|
||||
}
|
||||
} else {
|
||||
this.lastColliding = undefined;
|
||||
}
|
||||
}
|
||||
this.status.collisionCooldown.decreaseCooldown()
|
||||
this.checkWorldBorder()
|
||||
}
|
||||
|
||||
checkWorldBorder() {
|
||||
if(this.world.outside(this.position)) {
|
||||
this.die()
|
||||
}
|
||||
}
|
||||
|
||||
impactPlayer() {
|
||||
this.world.player.takeDamage(this.stats.damage)
|
||||
this.die()
|
||||
};
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(this.position, this.stats.size, this.color, ctx)
|
||||
}
|
||||
|
||||
move() {
|
||||
this.secondToLastPosition = this.lastPosition;
|
||||
this.lastPosition = this.position.clone()
|
||||
}
|
||||
|
||||
getPosition(): Vector {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return this.stats.size
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class StraightProjectile extends Projectile {
|
||||
|
||||
|
||||
constructor(position: Vector, dirVector: Vector, stats: ProjectileStats, world: World, parent: any) {
|
||||
super(position, dirVector, stats, world, parent);
|
||||
}
|
||||
|
||||
move() {
|
||||
super.move()
|
||||
this.position = straightMove(this.position, this.speedVec)
|
||||
}
|
||||
|
||||
static createStraightProjectile(world: World, start: Vector, targetPosition: Vector, parent: any, stats: ProjectileStats, color?: string) {
|
||||
let dirVector = Vector.createVector(targetPosition, start).normalize().multiply(stats.speed);
|
||||
let projectile = new StraightProjectile(start, dirVector, stats, world, parent)
|
||||
projectile.color = color === undefined ? 'red' : color!;
|
||||
world.addProjectile(projectile)
|
||||
return projectile;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChainBallProjectile extends Projectile {
|
||||
private weapon: ChainBall;
|
||||
private movingBack: boolean = false;
|
||||
private target: Vector;
|
||||
private lastHit: Enemy[] = []
|
||||
|
||||
constructor(position: Vector, speedVec: Vector, stats: ProjectileStats, world: World, parent: any, weapon: ChainBall, target: Vector) {
|
||||
super(position, speedVec, stats, world, parent);
|
||||
this.weapon = weapon;
|
||||
this.target = target.clone()
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(this.position, this.stats.size, 'pink', ctx) // todo render the weapon instead
|
||||
}
|
||||
|
||||
act() {
|
||||
this.move()
|
||||
// TODO It seems that the projectile doesnt _quite_ hit the enemy, but is just getting close to it
|
||||
let hitEnemies = this.world.getAllInRange(this.position, this.stats.size * 2);
|
||||
hitEnemies.forEach(value => {
|
||||
if(this.lastHit.indexOf(value) !== -1) {
|
||||
if(InstanceOfUtils.instanceOfHealthy(value)) {
|
||||
let healthy = value as Healthy;
|
||||
healthy.takeDamage(this.stats.damage)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.lastHit = hitEnemies;
|
||||
}
|
||||
|
||||
move() {
|
||||
super.move()
|
||||
if(!this.movingBack) {
|
||||
this.position = straightMove(this.position, this.speedVec)
|
||||
if(this.position.distanceTo(this.target) < this.stats.size) {
|
||||
this.movingBack = true;
|
||||
this.speedVec = this.speedVec.multiply(3)
|
||||
}
|
||||
} else {
|
||||
this.position = moveInDirectionOf(this.position, this.world.player.position, this.speedVec.vecLength())
|
||||
}
|
||||
if(this.movingBack && this.position.distanceTo(this.world.player.position) < (this.stats.size + this.world.player.effectiveStats.size)) {
|
||||
this.weapon.reset();
|
||||
this.die()
|
||||
}
|
||||
}
|
||||
|
||||
static createChainBallProjectile(world: World, start: Vector, targetPosition: Vector, parent: any, stats: ProjectileStats, weapon: MeleeWeapon, color?: string) {
|
||||
let dirVector = Vector.createVector(targetPosition, start).normalize().multiply(stats.speed);
|
||||
let projectile = new ChainBallProjectile(start, dirVector, stats, world, parent, weapon, targetPosition)
|
||||
projectile.color = color === undefined ? 'red' : color!;
|
||||
world.addProjectile(projectile)
|
||||
return projectile;
|
||||
}
|
||||
}
|
||||
|
||||
export class StraightMeleeWeaponProjectile extends Projectile {
|
||||
|
||||
private weapon: MeleeWeapon;
|
||||
private movingBack: boolean = false;
|
||||
private target: Vector;
|
||||
private lastHit: Enemy[] = []
|
||||
|
||||
constructor(position: Vector, speedVec: Vector, stats: ProjectileStats, world: World, parent: any, weapon: ChainBall, target: Vector) {
|
||||
super(position, speedVec, stats, world, parent);
|
||||
this.weapon = weapon;
|
||||
this.target = target.clone()
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
let position = this.getRealPosition();
|
||||
fillDot(position, this.stats.size, 'brown', ctx) // todo render the weapon instead
|
||||
}
|
||||
|
||||
getRealPosition(): Vector {
|
||||
return this.world.player.getPosition().add(this.position)
|
||||
}
|
||||
|
||||
act() {
|
||||
this.move()
|
||||
let hitEnemies = this.world.getAllInRange(this.getRealPosition(), this.stats.size * 2);
|
||||
hitEnemies.forEach(value => {
|
||||
if(this.lastHit.indexOf(value) !== -1) {
|
||||
if(InstanceOfUtils.instanceOfHealthy(value)) {
|
||||
let healthy = value as Healthy;
|
||||
healthy.takeDamage(this.stats.damage)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.lastHit = hitEnemies;
|
||||
}
|
||||
|
||||
move() {
|
||||
super.move()
|
||||
if(!this.movingBack) {
|
||||
this.position = straightMove(this.position, this.speedVec)
|
||||
if(this.position.distanceTo(this.target) < this.stats.size) {
|
||||
this.movingBack = true;
|
||||
this.speedVec = this.speedVec.multiply(3)
|
||||
}
|
||||
} else {
|
||||
this.position = moveInDirectionOf(this.position, new Vector(0, 0), this.speedVec.vecLength())
|
||||
}
|
||||
if(this.movingBack && this.position.distanceTo(new Vector(0, 0)) < (this.stats.size + this.world.player.effectiveStats.size)) {
|
||||
this.weapon.reset();
|
||||
this.die()
|
||||
}
|
||||
}
|
||||
|
||||
static createStraightMeleeProjectile(world: World, start: Vector, targetPosition: Vector, parent: any, stats: ProjectileStats, weapon: MeleeWeapon, color?: string) {
|
||||
let dirVector = Vector.createVector(targetPosition, start).normalize().multiply(stats.speed);
|
||||
let projectile = new StraightMeleeWeaponProjectile(start, dirVector, stats, world, parent, weapon, targetPosition)
|
||||
projectile.color = color === undefined ? 'red' : color!;
|
||||
world.addProjectile(projectile)
|
||||
return projectile;
|
||||
}
|
||||
}
|
||||
|
||||
export class HomingProjectile extends Projectile {
|
||||
|
||||
private target: Placeable;
|
||||
|
||||
constructor(position: Vector, speedVec: Vector, stats: ProjectileStats, world: World, parent: any, target: Placeable) {
|
||||
super(position, speedVec, stats, world, parent);
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
move() {
|
||||
super.move()
|
||||
this.position = moveInDirectionOf(this.position, this.target.getPosition(), this.speedVec.vecLength())
|
||||
if(InstanceOfUtils.instanceOfHealthy(this.target)) {
|
||||
let target = this.target as Healthy
|
||||
if(target.dead()) {
|
||||
let closestTargetTo = this.world.getClosestTargetTo(this.position)
|
||||
|
||||
let oldTargetDirection = Vector.createVector(this.target.getPosition(), this.position)
|
||||
let justMovedDirection = Vector.createVector(this.position, this.lastPosition).normalize()
|
||||
let olderMovedDirection: Vector;
|
||||
if(this.secondToLastPosition !== undefined) {
|
||||
olderMovedDirection = Vector.createVector(this.lastPosition, this.secondToLastPosition).normalize();
|
||||
} else {
|
||||
olderMovedDirection = new Vector(0, 1)
|
||||
}
|
||||
if (closestTargetTo !== undefined && closestTargetTo[1] !== undefined) {
|
||||
let newTargetPosition = closestTargetTo[1]!.getPosition();
|
||||
let newDir = Vector.createVector(newTargetPosition, this.position)
|
||||
let newDirAngle = newDir.angleTo(olderMovedDirection);
|
||||
if(Math.abs(newDirAngle) <= toRad(30)) {
|
||||
this.target = closestTargetTo[1]!;
|
||||
} else {
|
||||
if(pointOnLineWithinLine(this.target.getPosition(), this.lastPosition, this.position)) {
|
||||
justMovedDirection = olderMovedDirection
|
||||
}
|
||||
this.target = new DeadPoint(this.position.add(justMovedDirection.multiply(this.world.maxValue())))
|
||||
}
|
||||
} else {
|
||||
if(pointOnLineWithinLine(this.target.getPosition(), this.lastPosition, this.position)) {
|
||||
justMovedDirection = olderMovedDirection
|
||||
}
|
||||
this.target = new DeadPoint(this.position.add(justMovedDirection.multiply(Math.max(this.world.size.x, this.world.size.y))))
|
||||
}
|
||||
}
|
||||
}
|
||||
this.checkWorldBorder()
|
||||
}
|
||||
|
||||
static createHomingProjectile(world: World, start: Vector, parent: any, target: Placeable, stats: ProjectileStats, color?: string) {
|
||||
|
||||
let projectile = new HomingProjectile(start, new Vector(0, stats.speed), stats, world, parent, target)
|
||||
projectile.color = color === undefined ? 'red' : color!;
|
||||
world.addProjectile(projectile)
|
||||
return projectile;
|
||||
}
|
||||
|
||||
|
||||
die() {
|
||||
if(this.stats.deathSplit && this.stats.deathSplit > 0 && Math.random() > this.stats.deathSplitChance) {
|
||||
let splits = this.stats.deathSplit;
|
||||
let directionalVectors = getCoordinatesSplit(splits);
|
||||
let stats = new ProjectileStats()
|
||||
.withSize(this.stats.size / 2)
|
||||
.withDamage(this.stats.damage / 2)
|
||||
.withSpeed(this.stats.speed / 2)
|
||||
directionalVectors.forEach(value => {
|
||||
let target = this.position.add(value.multiply(this.world.maxValue()))
|
||||
StraightProjectile.createStraightProjectile(this.world, this.position, target, this.parent, stats, 'white')
|
||||
})
|
||||
}
|
||||
super.die();
|
||||
}
|
||||
}
|
||||
|
||||
235
absurd-survivors/src/stats.ts
Normal file
235
absurd-survivors/src/stats.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
|
||||
export class PlayerStats {
|
||||
|
||||
private _speed: number;
|
||||
private _size: number;
|
||||
private _health: number;
|
||||
private _pullRange: number;
|
||||
private _weaponRange: number;
|
||||
private _weaponRangeFactor: number;
|
||||
private _healthRegen: number;
|
||||
private _damage: number;
|
||||
private _damageFactor: number;
|
||||
|
||||
constructor() {
|
||||
this._speed = 3;
|
||||
this._size = 5;
|
||||
this._health = 10;
|
||||
this._pullRange = 150;
|
||||
this._weaponRange = 250;
|
||||
this._weaponRangeFactor = 1;
|
||||
this._healthRegen = 0.001;
|
||||
this._damage = 0;
|
||||
this._damageFactor = 0;
|
||||
}
|
||||
|
||||
resetToBasic() {
|
||||
this._speed = 0;
|
||||
this._health = 0;
|
||||
this._pullRange = 0;
|
||||
this._weaponRange = 0;
|
||||
this._weaponRangeFactor = 1;
|
||||
this._healthRegen = 0.001;
|
||||
this._damage = 0;
|
||||
this._damageFactor = 0;
|
||||
}
|
||||
|
||||
increaseLevel() {
|
||||
this._speed *= 1.1;
|
||||
this._health += 1
|
||||
this._pullRange *= 1.1;
|
||||
this._weaponRange *= 1.25
|
||||
this._weaponRangeFactor += 0.1
|
||||
this._healthRegen += 0.1
|
||||
this._damage += 1;
|
||||
this._damageFactor += 0.1;
|
||||
}
|
||||
|
||||
mergeStats(otherStats: PlayerStats) {
|
||||
this._speed += otherStats._speed;
|
||||
this._health += otherStats._health;
|
||||
this._pullRange += otherStats._pullRange;
|
||||
this._weaponRange += otherStats._weaponRange
|
||||
this._weaponRangeFactor += otherStats._weaponRangeFactor;
|
||||
this._healthRegen += otherStats._healthRegen;
|
||||
this._damage += otherStats._damage;
|
||||
this._damageFactor += otherStats._damageFactor
|
||||
}
|
||||
|
||||
clone() {
|
||||
let newStats = new PlayerStats();
|
||||
newStats.mergeStats(this)
|
||||
return newStats;
|
||||
}
|
||||
|
||||
changeStat(value: number, statFun: (stats: PlayerStats, value: number) => void) {
|
||||
statFun(this, value)
|
||||
}
|
||||
|
||||
static increaseSpeed(stats: PlayerStats, value: number) {
|
||||
stats._speed += value
|
||||
}
|
||||
|
||||
static factorSpeed(stats: PlayerStats, value: number) {
|
||||
stats._speed *= value
|
||||
}
|
||||
|
||||
static factorDamage(stats: PlayerStats, value: number) {
|
||||
stats._damage *= value
|
||||
}
|
||||
|
||||
static increaseDamage(stats: PlayerStats, value: number) {
|
||||
stats._damage += value
|
||||
}
|
||||
|
||||
static factorDamageFactor(stats: PlayerStats, value: number) {
|
||||
stats._damageFactor *= value
|
||||
}
|
||||
|
||||
static increaseDamageFactor(stats: PlayerStats, value: number) {
|
||||
stats._damageFactor += value
|
||||
}
|
||||
|
||||
static increasePullRange(stats: PlayerStats, value: number) {
|
||||
stats._pullRange += value
|
||||
}
|
||||
|
||||
static factorPullRange(stats: PlayerStats, value: number) {
|
||||
stats._pullRange *= value
|
||||
}
|
||||
|
||||
static increaseHealth(stats: PlayerStats, value: number) {
|
||||
stats._health += value
|
||||
}
|
||||
|
||||
get speed(): number {
|
||||
return this._speed;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
get pullRange(): number {
|
||||
return this._pullRange;
|
||||
}
|
||||
|
||||
get health(): number {
|
||||
return this._health;
|
||||
}
|
||||
|
||||
get weaponRange(): number {
|
||||
return this._weaponRange
|
||||
}
|
||||
|
||||
get healthRegen(): number {
|
||||
return this._healthRegen;
|
||||
}
|
||||
|
||||
get effectiveWeaponRange(): number {
|
||||
return this._weaponRange * this._weaponRangeFactor;
|
||||
}
|
||||
|
||||
get effectiveDamage(): number {
|
||||
return this._damage * this._damageFactor;
|
||||
}
|
||||
|
||||
public static defaultPlayerStats(): PlayerStats {
|
||||
return new PlayerStats();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ProjectileStats {
|
||||
|
||||
private _piercings: number;
|
||||
private _size: number;
|
||||
private _damage: number;
|
||||
private _speed: number;
|
||||
private _deathSplit: number;
|
||||
private _deathSplitChance: number;
|
||||
|
||||
constructor() {
|
||||
this._size = 1
|
||||
}
|
||||
|
||||
withPiercings(value: number) {
|
||||
this._piercings = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
withSize(value: number) {
|
||||
this._size = Math.max(value, 1);
|
||||
return this;
|
||||
}
|
||||
|
||||
withDamage(value: number) {
|
||||
this._damage = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
withSpeed(value: number) {
|
||||
this._speed = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
withDeathSplit(value: number) {
|
||||
this._deathSplit = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
withDeathSplitChance(value: number) {
|
||||
this._deathSplitChance = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
get piercings(): number {
|
||||
return this._piercings;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
|
||||
get speed(): number {
|
||||
return this._speed;
|
||||
}
|
||||
|
||||
get damage(): number {
|
||||
return this._damage;
|
||||
}
|
||||
|
||||
|
||||
get deathSplitChance(): number {
|
||||
return this._deathSplitChance;
|
||||
}
|
||||
|
||||
get deathSplit(): number {
|
||||
return this._deathSplit;
|
||||
}
|
||||
}
|
||||
|
||||
export class EnemyStats {
|
||||
private _healthFactor: number;
|
||||
private _baseHealth: number;
|
||||
|
||||
constructor() {
|
||||
this._healthFactor = 1;
|
||||
this._baseHealth = 10
|
||||
}
|
||||
|
||||
withHealthFactor(healthFactor: number) {
|
||||
this._healthFactor = healthFactor;
|
||||
return this
|
||||
}
|
||||
|
||||
withBaseHealth(baseHealth: number) {
|
||||
this._baseHealth = baseHealth;
|
||||
return this
|
||||
}
|
||||
|
||||
getEffectiveHealth() {
|
||||
return this._baseHealth * this._healthFactor
|
||||
}
|
||||
}
|
||||
88
absurd-survivors/src/status.ts
Normal file
88
absurd-survivors/src/status.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {Cooldown} from "./base.ts";
|
||||
|
||||
export class PlayerStatus {
|
||||
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 {
|
||||
return this._health;
|
||||
}
|
||||
|
||||
set health(value: number) {
|
||||
this._health = value;
|
||||
}
|
||||
|
||||
get dead(): boolean {
|
||||
return this._health <= 0
|
||||
}
|
||||
|
||||
get wealth(): number {
|
||||
return this._wealth;
|
||||
}
|
||||
|
||||
set wealth(value: number) {
|
||||
this._wealth = value;
|
||||
}
|
||||
|
||||
increaseLevel() {
|
||||
this._level += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectileStatus {
|
||||
private _piercingsLeft: number;
|
||||
private _collisionCooldown: Cooldown;
|
||||
|
||||
constructor(piercingsLeft: number) {
|
||||
this._piercingsLeft = piercingsLeft;
|
||||
this._collisionCooldown = new Cooldown(10)
|
||||
}
|
||||
|
||||
get piercingsLeft(): number {
|
||||
return this._piercingsLeft;
|
||||
}
|
||||
|
||||
hasPiercingLeft(): boolean {
|
||||
return this.piercingsLeft > 0;
|
||||
}
|
||||
|
||||
|
||||
get collisionCooldown(): Cooldown {
|
||||
return this._collisionCooldown;
|
||||
}
|
||||
|
||||
decreasePiercings() {
|
||||
this._piercingsLeft -= 1;
|
||||
}
|
||||
|
||||
}
|
||||
4
absurd-survivors/src/style.css
Normal file
4
absurd-survivors/src/style.css
Normal file
@@ -0,0 +1,4 @@
|
||||
html, body, div, canvas {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
295
absurd-survivors/src/ui.ts
Normal file
295
absurd-survivors/src/ui.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import type {Drawable, DrawContainer, MouseInteracting, MouseInterActingContainer} from "./interfaces.ts";
|
||||
import {World} from "./World.ts";
|
||||
import {Vector} from "./base.ts";
|
||||
import {drawDot, fillDot, isMobile, rectPointIntersection} from "./utils.ts";
|
||||
|
||||
export class HUD implements DrawContainer {
|
||||
private health: PlayerInfo;
|
||||
private controls: Controls | undefined;
|
||||
private world: World;
|
||||
|
||||
constructor(world: World, keys: any) {
|
||||
this.world = world;
|
||||
this.health = new PlayerInfo(world);
|
||||
if (isMobile()) {
|
||||
this.controls = new Controls(world, keys);
|
||||
}
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
this.health.draw(ctx)
|
||||
if(this.controls) {
|
||||
this.controls.draw(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
mouseDown(pos: Vector) {
|
||||
if(this.controls) {
|
||||
this.controls.mouseDown(pos)
|
||||
}
|
||||
}
|
||||
|
||||
mouseUp(pos: Vector) {
|
||||
if(this.controls) {
|
||||
this.controls.mouseUp(pos)
|
||||
}
|
||||
}
|
||||
|
||||
mouseMove(pos: Vector) {
|
||||
if(this.controls) {
|
||||
this.controls.mouseMove(pos)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Controls implements DrawContainer, MouseInterActingContainer {
|
||||
|
||||
private world: World;
|
||||
private centerPosition: Vector | undefined;
|
||||
private keys: any;
|
||||
private size: number;
|
||||
|
||||
constructor(world: World, keys: any) {
|
||||
this.world = world;
|
||||
this.keys = keys;
|
||||
this.size = 60;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
if(this.isPressed()) {
|
||||
drawDot(this.centerPosition!, this.size, 'white', ctx)
|
||||
}
|
||||
}
|
||||
|
||||
isPressed() {
|
||||
return this.centerPosition !== undefined;
|
||||
}
|
||||
|
||||
mouseDown(pos: Vector) {
|
||||
this.centerPosition = pos
|
||||
}
|
||||
|
||||
mouseUp(pos: Vector) {
|
||||
this.centerPosition = undefined;
|
||||
let keys = ['a', 's', 'd', 'w']
|
||||
keys.forEach(key => {
|
||||
this.keys[key].state = false;
|
||||
this.keys[key].intensity = 0;
|
||||
})
|
||||
this.keys['a'].state = false
|
||||
}
|
||||
|
||||
setKeyTo(key: string, state: boolean, value: number) {
|
||||
this.keys[key].state = state;
|
||||
this.keys[key].intensity = value;
|
||||
}
|
||||
|
||||
mouseMove(pos: Vector) {
|
||||
if(this.isPressed()) {
|
||||
let diff = Vector.createVector(pos, this.centerPosition!);
|
||||
if(isNaN(diff.x) || isNaN(diff.y)) {
|
||||
return;
|
||||
}
|
||||
diff = diff.normalize();
|
||||
if(diff.x > 0) {
|
||||
this.setKeyTo('a', false, 0)
|
||||
this.setKeyTo('d', true, Math.abs(diff.x))
|
||||
} else if(diff.x < 0){
|
||||
this.setKeyTo('d', false, 0)
|
||||
this.setKeyTo('a', true, Math.abs(diff.x))
|
||||
}
|
||||
if(diff.y > 0) {
|
||||
this.setKeyTo('w', false, 0)
|
||||
this.setKeyTo('s', true, Math.abs(diff.y))
|
||||
} else if(diff.y < 0) {
|
||||
this.setKeyTo('s', false, 0)
|
||||
this.setKeyTo('w', true, Math.abs(diff.y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PlayerInfo implements DrawContainer {
|
||||
private bar: InfoBar;
|
||||
private statLabels: StatLabel[] = []
|
||||
private world: World;
|
||||
private statLabelBase: Vector;
|
||||
private static readonly STAT_LABEL_HEIGHT_OFFSET: number = 4;
|
||||
|
||||
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.effectiveStats.health)
|
||||
this.statLabelBase = new Vector(0, 150)
|
||||
this.statLabels = [
|
||||
new StatLabel(() => 'Money', () => this.world.player.status.wealth),
|
||||
new StatLabel(() => 'Level', () => this.world.player.status.level),
|
||||
new StatLabel(() => 'Speed', () => Math.floor(this.world.player.effectiveStats.speed)),
|
||||
new StatLabel(() => 'Pull range', () => Math.floor(this.world.player.effectiveStats.pullRange)),
|
||||
new StatLabel(() => 'Weapon range', () => Math.floor(this.world.player.effectiveStats.effectiveWeaponRange)),
|
||||
new StatLabel(() => 'Damage', () => Math.floor(this.world.player.effectiveStats.effectiveDamage))
|
||||
]
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
this.bar.draw(ctx)
|
||||
let metrics = ctx.measureText('I')
|
||||
let upperCaseCharacterSize = new Vector(0, metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent).add(new Vector(0, PlayerInfo.STAT_LABEL_HEIGHT_OFFSET))
|
||||
for (let i = 0; i < this.statLabels.length; i++){
|
||||
const label = this.statLabels[i];
|
||||
label.move(this.statLabelBase.add(upperCaseCharacterSize.multiply(i)))
|
||||
label.draw(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class RectButton implements Drawable, MouseInteracting {
|
||||
protected position: Vector;
|
||||
protected size: Vector;
|
||||
protected borderColor: string = 'light-gray';
|
||||
protected contentColor: string = 'gray';
|
||||
protected label: string;
|
||||
protected action: () => void;
|
||||
protected releaseLambda?: () => void;
|
||||
|
||||
constructor(position: Vector, size: Vector, label: string, actionLambda: () => void, releaseLambda?: () => void) {
|
||||
this.position = position;
|
||||
this.size = size;
|
||||
this.label = label;
|
||||
this.action = actionLambda;
|
||||
this.releaseLambda = releaseLambda;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = this.contentColor;
|
||||
ctx.strokeStyle = this.borderColor;
|
||||
ctx.rect(this.position.x, this.position.y, this.size.x, this.size.y)
|
||||
ctx.fill()
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.fillText(this.label, this.position.x + this.size.x / 2, this.position.y + this.size.y / 2)
|
||||
ctx.closePath()
|
||||
}
|
||||
|
||||
getPosition(): Vector {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
}
|
||||
|
||||
move(any?: any) {
|
||||
}
|
||||
|
||||
clickAction(pos: Vector) {
|
||||
this.action()
|
||||
}
|
||||
|
||||
releaseAction(pos: Vector) {
|
||||
if(this.releaseLambda) {
|
||||
this.releaseLambda()
|
||||
}
|
||||
}
|
||||
|
||||
hit(pos: Vector): boolean {
|
||||
return rectPointIntersection(this.position, this.size, pos)
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyboardButton extends RectButton {
|
||||
constructor(position: Vector, size: Vector, key: string, keys: any) {
|
||||
super(position, size, key.toUpperCase(),
|
||||
() => { keys[key.toLowerCase()].state = true},
|
||||
() => { keys[key.toLowerCase()].state = false}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class StatLabel implements Drawable {
|
||||
private position: Vector;
|
||||
private borderColor: string = 'white';
|
||||
private textLambda: () => string;
|
||||
private valueLambda: () => number;
|
||||
|
||||
|
||||
constructor(textLambda: () => string, valueLambda: () => number);
|
||||
constructor(textLambda: () => string, valueLambda: () => number, position?: Vector) {
|
||||
if(!position) {
|
||||
position = new Vector(0, 0)
|
||||
}
|
||||
this.position = position;
|
||||
this.textLambda = textLambda;
|
||||
this.valueLambda = valueLambda;
|
||||
}
|
||||
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = this.borderColor
|
||||
let value = this.valueLambda();
|
||||
let text = this.textLambda();
|
||||
ctx.fillText(`${text}: ${value}`, this.position.x, this.position.y)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
getPosition(): Vector {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
move(position: Vector) {
|
||||
this.position = position
|
||||
}
|
||||
|
||||
getSize() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class InfoBar implements Drawable {
|
||||
private position: Vector;
|
||||
private height: number;
|
||||
private width: number;
|
||||
private fillColor: string = 'green';
|
||||
private borderColor: string = 'white';
|
||||
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()
|
||||
}
|
||||
|
||||
getPosition(): Vector {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
move(any?: any) {
|
||||
}
|
||||
|
||||
getSize() {
|
||||
}
|
||||
|
||||
}
|
||||
109
absurd-survivors/src/utils.ts
Normal file
109
absurd-survivors/src/utils.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import {Vector} from "./base.ts";
|
||||
|
||||
export function fillDot(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 isMobile() {
|
||||
// https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
|
||||
let check = false;
|
||||
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor);
|
||||
return check;
|
||||
}
|
||||
|
||||
export function drawDot(position: Vector, size: number, color: string, ctx: CanvasRenderingContext2D) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = color;
|
||||
ctx.arc(position.x, position.y, size, 0, 2 * Math.PI);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
export function moveInDirectionOf(position: Vector, target: Vector, speedFactor: number): 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)
|
||||
}
|
||||
|
||||
export function toRad(angle) {
|
||||
return angle / 180 * Math.PI;
|
||||
}
|
||||
|
||||
export function toDegrees(angle) {
|
||||
return angle * 180 / Math.PI
|
||||
}
|
||||
|
||||
export function pointInsideCircle(circleCenter: Vector, radius: number, point: Vector) {
|
||||
return circleCenter.distanceTo(point) < radius;
|
||||
}
|
||||
|
||||
export function linePointCollision(point: Vector, lineStart: Vector, lineEnd: Vector) {
|
||||
let lineLength = Vector.createVector(lineEnd, lineStart).vecLength();
|
||||
let distanceStart = Vector.createVector(lineStart, point).vecLength()
|
||||
let distanceEnd = Vector.createVector(lineEnd, point).vecLength();
|
||||
let buffer = 0.001
|
||||
if((distanceStart + distanceEnd) >= (lineLength - buffer) && (distanceEnd + distanceStart) <= (lineLength + buffer)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function pointOnLineWithinLine(circleCenter: Vector, lineStart: Vector, lineEnd: Vector) {
|
||||
let lineVector = Vector.createVector(lineEnd, lineStart)
|
||||
let vectorCenterLine = Vector.createVector(circleCenter, lineStart)
|
||||
let dot = vectorCenterLine.dotProduct(lineVector)
|
||||
let closestX = lineStart.x + (dot * (lineVector.x))
|
||||
let closestY = lineStart.y + (dot * (lineVector.y))
|
||||
let closestPoint = new Vector(closestX, closestY);
|
||||
return linePointCollision(closestPoint, lineStart, lineEnd);
|
||||
}
|
||||
|
||||
export function circleLineCollision(circleCenter: Vector, radius: number, lineStart: Vector, lineEnd: Vector) {
|
||||
if(pointInsideCircle(circleCenter, radius, lineStart) || pointInsideCircle(circleCenter, radius, lineEnd)) {
|
||||
return true;
|
||||
}
|
||||
let lineVector = Vector.createVector(lineEnd, lineStart)
|
||||
let vectorCenterLine = Vector.createVector(circleCenter, lineStart)
|
||||
let dot = vectorCenterLine.dotProduct(lineVector)
|
||||
let closestX = lineStart.x + (dot * (lineVector.x))
|
||||
let closestY = lineStart.y + (dot * (lineVector.y))
|
||||
let closestPoint = new Vector(closestX, closestY);
|
||||
let onSegment = linePointCollision(closestPoint, lineStart, lineEnd)
|
||||
if(!onSegment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pointInsideCircle(circleCenter, radius, closestPoint);
|
||||
}
|
||||
|
||||
export function getCoordinatesSplit(amount: number) {
|
||||
let angle = 2 * Math.PI / amount;
|
||||
let points: Vector[] = [];
|
||||
for (let i = 0; i < amount; i++) {
|
||||
let x = Math.cos(angle * i)
|
||||
let y = Math.sin(angle * i)
|
||||
points.push(new Vector(x, y))
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
export function randomItem(items: any[]) {
|
||||
return items[Math.floor(Math.random() * items.length)]
|
||||
}
|
||||
|
||||
export function rectPointIntersection(topLeft: Vector, size: Vector, point: Vector) {
|
||||
if (topLeft.x + size.x >= point.x &&
|
||||
topLeft.x <= point.x &&
|
||||
topLeft.y + size.y >= point.y &&
|
||||
topLeft.y <= point.y) {
|
||||
return true
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
1
absurd-survivors/src/vite-env.d.ts
vendored
Normal file
1
absurd-survivors/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
406
absurd-survivors/src/weapons.ts
Normal file
406
absurd-survivors/src/weapons.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
import type {Weapon} from "./interfaces.ts";
|
||||
import {fillDot, toRad} from "./utils.ts";
|
||||
import {Player} from "./Player.ts";
|
||||
import {
|
||||
HomingProjectile,
|
||||
Projectile,
|
||||
StraightProjectile,
|
||||
ChainBallProjectile,
|
||||
StraightMeleeWeaponProjectile
|
||||
} from "./projectile.ts";
|
||||
import {World} from "./World.ts";
|
||||
import {Vector} from "./base.ts";
|
||||
import {ProjectileStats} from "./stats.ts";
|
||||
|
||||
export abstract class BasicWeapon implements Weapon {
|
||||
protected offset: Vector;
|
||||
protected readonly player: Player
|
||||
protected readonly world: World;
|
||||
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) {
|
||||
}
|
||||
|
||||
getPosition(): Vector {
|
||||
return this.player.position.add(this.offset);
|
||||
}
|
||||
|
||||
move(any?: any) {
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
getDamage() {
|
||||
return this.stats.damage + this.player.effectiveStats.effectiveDamage;
|
||||
}
|
||||
|
||||
getOffset(): Vector {
|
||||
return this.offset;
|
||||
}
|
||||
|
||||
setOffset(vec: Vector) {
|
||||
this.offset = vec;
|
||||
}
|
||||
|
||||
level() {
|
||||
return this._level
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class RangeWeapon extends BasicWeapon {
|
||||
|
||||
protected projectiles: [Projectile] = []
|
||||
protected shootCooldown: number = 0;
|
||||
|
||||
abstract createProjectile(): boolean;
|
||||
|
||||
act() {
|
||||
if(this.shootCooldown <= 0) {
|
||||
if(this.createProjectile()) {
|
||||
this.shootCooldown = this.stats.shootInterval;
|
||||
}
|
||||
}
|
||||
this.shootCooldown -= 1;
|
||||
}
|
||||
|
||||
calculateRange(): number {
|
||||
return this.world.player.effectiveStats.effectiveWeaponRange + this.stats.effectiveWeaponRange;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export abstract class MeleeWeapon extends RangeWeapon {
|
||||
protected launched: boolean;
|
||||
|
||||
act() {
|
||||
if(this.shootCooldown <= 0) {
|
||||
if(this.createProjectile()) {
|
||||
this.launched = true;
|
||||
this.shootCooldown = 1;
|
||||
}
|
||||
}
|
||||
if(!this.launched) {
|
||||
this.shootCooldown -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.shootCooldown = this.stats.shootInterval
|
||||
this.launched = false;
|
||||
}
|
||||
|
||||
calculateRange(): number {
|
||||
return this.stats.effectiveWeaponRange;
|
||||
}
|
||||
}
|
||||
|
||||
export class Spear extends MeleeWeapon {
|
||||
protected launched: boolean;
|
||||
|
||||
createProjectile(): boolean {
|
||||
let range = this.calculateRange()
|
||||
let closestTargetTo = this.world.getFarthestTargetButWithin(this.world.player.position, range);
|
||||
if(closestTargetTo !== undefined && closestTargetTo[1] !== undefined) {
|
||||
let stats = new ProjectileStats()
|
||||
.withPiercings(1000)
|
||||
.withSize(3)
|
||||
.withDamage(this.getDamage())
|
||||
.withSpeed(this.stats.projectileSpeed);
|
||||
let offsetVector = Vector.createVector(closestTargetTo[1]!.getPosition(), this.player.position).multiply(1.1);
|
||||
if(offsetVector.vecLength() < 15) {
|
||||
offsetVector = offsetVector.normalize().multiply(40)
|
||||
}
|
||||
let projectile = StraightMeleeWeaponProjectile.createStraightMeleeProjectile(this.world, new Vector(0, 0), offsetVector, this.player, stats, this)
|
||||
this.projectiles.push(projectile)
|
||||
return true
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static createSpear(world: World, offset?: Vector) {
|
||||
if(!offset) {
|
||||
offset = new Vector(5, 5)
|
||||
}
|
||||
let stats = new WeaponStats()
|
||||
.withProjectileSpeed(5)
|
||||
.withDamage(15)
|
||||
.withWeaponRange(150)
|
||||
.withShootInterval(50)
|
||||
let pistol = new Spear(world, stats)
|
||||
pistol.offset = offset;
|
||||
pistol.size = 7;
|
||||
pistol.color = 'brown';
|
||||
return pistol;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ChainBall extends MeleeWeapon {
|
||||
|
||||
createProjectile(): boolean {
|
||||
let range = this.calculateRange()
|
||||
let closestTargetTo = this.world.getFarthestTargetButWithin(this.world.player.position, range);
|
||||
if(closestTargetTo !== undefined && closestTargetTo[1] !== undefined) {
|
||||
let stats = new ProjectileStats()
|
||||
.withPiercings(1000)
|
||||
.withSize(3)
|
||||
.withDamage(this.getDamage())
|
||||
.withSpeed(this.stats.projectileSpeed);
|
||||
let projectile = ChainBallProjectile.createChainBallProjectile(this.world, this.getPosition(), closestTargetTo[1]!.getPosition(), this.player, stats, this)
|
||||
this.projectiles.push(projectile)
|
||||
return true
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static createChainBall(world: World, offset?: Vector) {
|
||||
if(!offset) {
|
||||
offset = new Vector(5, 5)
|
||||
}
|
||||
let stats = new WeaponStats()
|
||||
.withProjectileSpeed(3)
|
||||
.withDamage(15)
|
||||
.withWeaponRange(150)
|
||||
.withShootInterval(20)
|
||||
let pistol = new ChainBall(world, stats)
|
||||
pistol.offset = offset;
|
||||
pistol.size = 4;
|
||||
pistol.color = 'gray';
|
||||
return pistol;
|
||||
}
|
||||
}
|
||||
|
||||
export class HomingPistol extends RangeWeapon {
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(this.getPosition(), 1, this.color, ctx)
|
||||
}
|
||||
|
||||
createProjectile(): boolean {
|
||||
let range = this.calculateRange()
|
||||
let closestTargetTo = this.world.getClosestTargetTo(this.world.player.position, range);
|
||||
if(closestTargetTo !== undefined && closestTargetTo[1] !== undefined) {
|
||||
let stats = new ProjectileStats()
|
||||
.withPiercings(this.stats.projectilePiercings)
|
||||
.withSize(1)
|
||||
.withDamage(this.getDamage())
|
||||
.withSpeed(this.stats.projectileSpeed);
|
||||
let projectile = HomingProjectile.createHomingProjectile(this.world, this.getPosition(), this.player, closestTargetTo[1]!, stats, 'yellow')
|
||||
this.projectiles.push(projectile)
|
||||
return true
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static generateHomingPistol(world: World, offset?: Vector) {
|
||||
if(!offset) {
|
||||
offset = new Vector(5, 5)
|
||||
}
|
||||
let stats = new WeaponStats()
|
||||
.withProjectileSpeed(3)
|
||||
.withDamage(3)
|
||||
.withShootInterval(50)
|
||||
let pistol = new HomingPistol(world, stats)
|
||||
pistol.offset = offset;
|
||||
pistol.size = 5;
|
||||
pistol.color = 'yellow';
|
||||
return pistol;
|
||||
}
|
||||
}
|
||||
|
||||
export class Pistol extends RangeWeapon {
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(this.getPosition(), 1, this.color, ctx)
|
||||
}
|
||||
|
||||
private createProjectile(): boolean {
|
||||
let range = this.calculateRange()
|
||||
let closestTargetTo = this.world.getClosestTargetTo(this.world.player.position, range);
|
||||
if(closestTargetTo !== undefined && closestTargetTo[1] !== undefined) {
|
||||
let stats = new ProjectileStats()
|
||||
.withPiercings(this.stats.projectilePiercings)
|
||||
.withSize(1)
|
||||
.withDamage(this.getDamage())
|
||||
.withSpeed(this.stats.projectileSpeed);
|
||||
let projectile = StraightProjectile.createStraightProjectile(this.world, this.getPosition(), closestTargetTo[1]!.getPosition(), this.player, stats, 'pink')
|
||||
this.projectiles.push(projectile)
|
||||
return true
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static generatePistol(world: World, offset?: Vector) {
|
||||
if(!offset) {
|
||||
offset = new Vector(5, 5)
|
||||
}
|
||||
let stats = new WeaponStats()
|
||||
.withProjectileSpeed(4)
|
||||
.withDamage(4)
|
||||
.withShootInterval(25)
|
||||
let pistol = new Pistol(world, stats)
|
||||
pistol.offset = offset;
|
||||
pistol.size = 5;
|
||||
pistol.color = 'brown';
|
||||
return pistol;
|
||||
}
|
||||
}
|
||||
|
||||
export class SpreadWeapon extends RangeWeapon {
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
fillDot(this.getPosition(), 1, this.color, ctx)
|
||||
}
|
||||
|
||||
private createProjectile(): boolean {
|
||||
let range = this.calculateRange()
|
||||
let closestTargetTo = this.world.getClosestTargetTo(this.world.player.position, range);
|
||||
if(closestTargetTo !== undefined && closestTargetTo[1] !== undefined) {
|
||||
let stats = new ProjectileStats()
|
||||
.withPiercings(this.stats.projectilePiercings)
|
||||
.withSize(1)
|
||||
.withDamage(this.getDamage())
|
||||
.withSpeed(this.stats.projectileSpeed);
|
||||
let targetPosition = closestTargetTo[1]!.getPosition();
|
||||
let weaponPosition = this.getPosition();
|
||||
let mainVector = Vector.createVector(targetPosition, weaponPosition)
|
||||
let mainProjectile = StraightProjectile.createStraightProjectile(this.world, weaponPosition, targetPosition, this.player, stats, 'gray')
|
||||
this.projectiles.push(mainProjectile)
|
||||
let upperVector = mainVector.rotate(toRad(-30))
|
||||
let upperTarget = weaponPosition.add(upperVector.multiply(this.world.maxValue()))
|
||||
let upperProjectile = StraightProjectile.createStraightProjectile(this.world, weaponPosition, upperTarget, this.player, stats, 'gray')
|
||||
this.projectiles.push(upperProjectile)
|
||||
let lowerVector = mainVector.rotate(toRad(30))
|
||||
let lowerTarget = weaponPosition.add(lowerVector.multiply(this.world.maxValue()))
|
||||
let lowerProjectile = StraightProjectile.createStraightProjectile(this.world, weaponPosition, lowerTarget, this.player, stats, 'gray')
|
||||
this.projectiles.push(lowerProjectile)
|
||||
return true
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static generateSpreadWeapon(world: World, offset?: Vector) {
|
||||
if(!offset) {
|
||||
offset = new Vector(5, 5)
|
||||
}
|
||||
let stats = new WeaponStats()
|
||||
.withProjectileSpeed(3)
|
||||
.withDamage(3)
|
||||
.withShootInterval(40)
|
||||
let pistol = new SpreadWeapon(world, stats)
|
||||
pistol.offset = offset;
|
||||
pistol.size = 5;
|
||||
pistol.color = 'gray';
|
||||
return pistol;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class WeaponStats {
|
||||
|
||||
private _weaponRange: number;
|
||||
private _weaponRangeFactor: number;
|
||||
private _projectileSpeed: number;
|
||||
private _projectilePiercings: number;
|
||||
private _damage: number;
|
||||
private _shootInterval: number;
|
||||
|
||||
constructor() {
|
||||
this._weaponRangeFactor = 1
|
||||
this._weaponRange = 50
|
||||
this._projectilePiercings = 0
|
||||
this._projectileSpeed = 100
|
||||
this._damage = 1
|
||||
this._shootInterval = 50
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
withWeaponRangeFactor(value: number) {
|
||||
this._weaponRangeFactor = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
withProjectileSpeed(value: number) {
|
||||
this._projectileSpeed = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
withShootInterval(value: number) {
|
||||
this._shootInterval = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
withProjectilePiercings(value: number) {
|
||||
this._projectilePiercings = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
withDamage(value: number) {
|
||||
this._damage = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
get weaponRange(): number {
|
||||
return this._weaponRange;
|
||||
}
|
||||
|
||||
get weaponRangeFactor(): number {
|
||||
return this._weaponRangeFactor
|
||||
}
|
||||
|
||||
|
||||
get projectilePiercings(): number {
|
||||
return this._projectilePiercings;
|
||||
}
|
||||
|
||||
get damage(): number {
|
||||
return this._damage;
|
||||
}
|
||||
|
||||
get projectileSpeed(): number {
|
||||
return this._projectileSpeed;
|
||||
}
|
||||
|
||||
get shootInterval(): number {
|
||||
return this._shootInterval;
|
||||
}
|
||||
|
||||
get effectiveWeaponRange(): number {
|
||||
return this._weaponRange * this._weaponRangeFactor
|
||||
}
|
||||
}
|
||||
25
absurd-survivors/tsconfig.json
Normal file
25
absurd-survivors/tsconfig.json
Normal 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"]
|
||||
}
|
||||
8
absurd-survivors/vite.config.js
Normal file
8
absurd-survivors/vite.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
build: {
|
||||
outDir: '../dist/survivors'
|
||||
},
|
||||
})
|
||||
BIN
img/survivors.png
Normal file
BIN
img/survivors.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -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>
|
||||
Reference in New Issue
Block a user