diff --git a/src/ability.js b/src/ability.js index ea61f8d..e1d0c80 100644 --- a/src/ability.js +++ b/src/ability.js @@ -1,3 +1,4 @@ +import Buff from './buff.js' import Projectile from './projectile.js' // major damage OR minor self sustain / crowd control @@ -169,6 +170,40 @@ export default class Ability { }, }) + static expose = new Ability({ + id: 'expose', + name: 'Expose', + castTime: 0.25, + cooldown: 8, + radius: 80, + range: 1200, + speed: 1700, + visualRadius: 50, + effect: function exposeEffect(caster, cursor) { + const ability = this + const exposeCollision = function exposeCollision(projectile, collidingEntity) { + if (collidingEntity == null) { return } + if (collidingEntity.team == (projectile.owner?.team ?? 'unknown')) { return } + + collidingEntity.applyBuff(Buff.exposed.id) + projectile.despawn() + } + + const projectile = new Projectile({ + onCollide: exposeCollision, + owner: caster, + position: caster.position.clone(), + radius: ability.radius, + speed: ability.speed, + visualRadius: ability.visualRadius, + }) + + projectile.destination = caster.position.clone().add(cursor.clone().sub(caster.position).normalize().multiplyScalar(ability.range + caster.radius)) + caster.game?.spawnProjectile(projectile) + caster.cooldown(ability.id) + }, + }) + static control = new Ability({ id: 'control', name: 'Control', diff --git a/src/buff.js b/src/buff.js new file mode 100644 index 0000000..9c19565 --- /dev/null +++ b/src/buff.js @@ -0,0 +1,20 @@ +export default class Buff { + id = crypto.randomUUID() + name = 'Buff' + duration = 0 + + #effect = () => {} + + get effect() { return this.#effect } + set effect(value) { this.#effect = value } + + constructor(options = {}) { + Object.entries(options).forEach(([key, value]) => this[key] = value) + } + + static exposed = new Buff({ + id: 'exposed', + name: 'Exposed', + duration: 4, + }) +} diff --git a/src/entity.js b/src/entity.js index 344092a..2ae2f53 100644 --- a/src/entity.js +++ b/src/entity.js @@ -3,17 +3,19 @@ import Pathfind from './pathfind.js' import SAT from 'sat' import SATX from './satx.js' import Team from './team.js' +import Buff from './buff.js' export default class Entity { id = crypto.randomUUID() abilities = {} + buffs = [] casting = null cooldowns = {} dead = false health = null height = 40 maxHealth = 1 - memory = {} + memory = {} // TODO: hide from reports but keep public position = null radius = 0 speed = 400 @@ -151,6 +153,17 @@ export default class Entity { ) } + applyBuff(id) { + const index = this.buffs.findIndex((it) => it.id == id) + const timestamp = this.game?.currentTick ?? 0 + if (index > -1) { + this.buffs[index].timestamp = timestamp + } + else { + this.buffs.push({ id, timestamp }) + } + } + collidables() { const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id).map((e) => e.collider()) const terrainColliders = (this.game?.terrains ?? []).map((t) => t.colliders()).flat() @@ -179,7 +192,13 @@ export default class Entity { } damage(amount) { - this.health = Math.min(Math.max(0, this.health - amount), this.maxHealth) + let damage = amount + if (this.hasBuff(Buff.exposed.id)) { + damage *= 3 // TODO: move to buff, make generic + this.removeBuff(Buff.exposed.id) + } + + this.health = Math.min(Math.max(0, this.health - damage), this.maxHealth) } despawn() { @@ -190,6 +209,10 @@ export default class Entity { return this.position.distanceTo(cursor) } + hasBuff(id) { + return this.buffs.some((it) => it.id == id) + } + heal(amount) { this.health = Math.min(Math.max(0, this.health + amount), this.maxHealth) } @@ -202,6 +225,10 @@ export default class Entity { return SATX.collideObjects(this.collider(), colliders) } + removeBuff(id) { + this.buffs = this.buffs.filter((it) => it.id != id) + } + respawn() { this.position = this.#spawnPosition.clone() this.health = this.maxHealth @@ -221,6 +248,7 @@ export default class Entity { this.#cast() this.#checkHealth() this.#move() + this.#tickBuffs() this.fixPosition() } @@ -375,4 +403,24 @@ export default class Entity { } } } + + #tickBuff(index) { + const entityBuff = this.buffs[index] + if (entityBuff == null) { return } + + const buffDefinition = this.game?.buffs.find((it) => it.id == entityBuff.id) + if (buffDefinition == null) { return } + + const buff = { ...buffDefinition, ...entityBuff } + const duration = this.game?.secToTick(buff.duration) ?? 0 + const currentTick = this.game?.currentTick ?? 0 + + if (buff.timestamp + duration < currentTick) { + this.removeBuff(buff.id) + } + } + + #tickBuffs() { + this.buffs.forEach((_v, i) => this.#tickBuff(i)) + } } diff --git a/src/game.js b/src/game.js index 846151b..91ce3bf 100644 --- a/src/game.js +++ b/src/game.js @@ -1,11 +1,13 @@ import { EventEmitter } from 'node:events' import Ability from './ability.js' +import Buff from './buff.js' import Entity from './entity.js' import Projectile from './projectile.js' import Terrain from './terrain.js' export default class Game { abilities = Object.values({...Ability}) + buffs = Object.values({...Buff}) averageTick = 0 currentTick = 0 entities = [] diff --git a/src/projectile.js b/src/projectile.js index bd5d723..33273b6 100644 --- a/src/projectile.js +++ b/src/projectile.js @@ -4,11 +4,11 @@ import SATX from './satx.js' export default class Projectile { id = crypto.randomUUID() - after = null + after = null // TODO: hide from reports but keep public height = 50 - memory = {} - onCollide = null - owner = null + memory = {} // TODO: hide from reports but keep public + onCollide = null // TODO: hide from reports but keep public + owner = null // TODO: only keep an ID position = new Vector2() radius = 5 speed = 1000 diff --git a/src/template.js b/src/template.js index 50fdba9..a45d99d 100644 --- a/src/template.js +++ b/src/template.js @@ -22,7 +22,7 @@ export default class Template { abilities: { a: Ability.rangedAttack.id, q: Ability.straightShot.id, - w: Ability.shieldThrow.id, + w: Ability.expose.id, e: Ability.blink.id, }, height: 80,