diff --git a/public/client.js b/public/client.js index 456c4cc..029f18b 100644 --- a/public/client.js +++ b/public/client.js @@ -172,6 +172,8 @@ function connectWebSocket() { entity.position.set(e.position.x / 100, e.position.y / 100, e.radius / 100) scene.add(entity) + // TODO: player model out of basic geometries + const hpMargin = 0.4 const maxHp = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0xd03333 })) maxHp.position.set(0, (e.radius / 100) + hpMargin, 0) @@ -188,7 +190,6 @@ function connectWebSocket() { entities[e.id] = entity } - // entity.position.set(e.position.x / 100, e.position.y / 100, e.radius / 100) positionTweens[entity.id] = new Tween(entity.position).to({ x: e.position.x / 100, y: e.position.y / 100, z: e.radius / 100 }, tweenDuration).start() const hp = entity.children.at(0).children.at(0) @@ -217,7 +218,6 @@ function connectWebSocket() { } projectile.userData.flaggedForRemoval = false - // projectile.position.set(p.position.x / 100, p.position.y / 100, p.visualHeight / 100) positionTweens[projectile.id] = new Tween(projectile.position).to({ x: p.position.x / 100, y: p.position.y / 100, z: p.visualHeight / 100 }, tweenDuration).start() } diff --git a/public/index.html b/public/index.html index 3333640..2585e80 100644 --- a/public/index.html +++ b/public/index.html @@ -63,6 +63,8 @@

Connection:


     
+    
+    
     
   
 
diff --git a/src/ability.js b/src/ability.js
index 9dad9ed..9ca8c1f 100644
--- a/src/ability.js
+++ b/src/ability.js
@@ -1,46 +1,44 @@
-import { Vector2 } from 'three'
-import Projectile from './projectile.js'
+import Effect from './effect.js'
 
 export default class Ability {
-  static skillshot({ range, radius, speed, onCollide, after }) {
-    return function(x, y) {
-      const projectile = new Projectile()
-      const destination = this.position.clone().add(new Vector2(x, y).sub(this.position).normalize().multiplyScalar(range))
-      projectile.owner = this.id
-      projectile.position.copy(this.position)
-      projectile.destination = destination
-      projectile.radius = radius
-      projectile.speed = speed
-      projectile.after = after
-      projectile.onCollide = onCollide
-      this.game?.spawnProjectile(projectile)
-    }
+  id = crypto.randomUUID()
+  name = 'Ability'
+  cooldown = 0
+  effect = () => {}
+  castTime = 0
+
+  constructor(options) {
+    if (options.name != null) { this.name = options.name }
+    if (options.effect != null) { this.effect = options.effect }
+    if (options.cooldown != null) { this.cooldown = options.cooldown }
+    if (options.castTime != null) { this.castTime = options.castTime }
   }
 
-  static homingProjectile({ range, radius, speed, onCollide, after }) {
-    return function(x, y) {
-      const cursor = new Vector2(x, y)
-      let closest = null
-      let distance = Infinity
-      this.game?.entities.filter((e) => e.id != this.id && e.position.clone().sub(this.position).length() < range).forEach((e) => {
-        const newDistance = e.position.clone().sub(cursor).length() < distance
-        if (newDistance < distance) {
-          closest = e
-          distance = newDistance
-        }
-      })
+  static basicAttack(range, radius, speed) {
+    return new this({
+      name: 'Basic Attack',
+      castTime: 0.25,
+      cooldown: 1.25,
+      effect: Effect.homingProjectile({
+        range,
+        radius,
+        speed,
+        after: Effect.damage({ despawn: true }).bind(this),
+      }),
+    })
+  }
 
-      if (closest == null) { return } // TODO: refund
-
-      const projectile = new Projectile()
-      projectile.owner = this.id
-      projectile.position.copy(this.position)
-      projectile.homingTarget = closest
-      projectile.radius = radius
-      projectile.speed = speed
-      projectile.after = after
-      projectile.onCollide = onCollide
-      this.game?.spawnProjectile(projectile)
-    }
+  static straightShot(range, radius, speed) {
+    return new this({
+      name: 'Straight Shot',
+      castTime: 0.1,
+      cooldown: 7,
+      effect: Effect.skillshot({
+        range,
+        radius,
+        speed,
+        onCollide: Effect.damage({ despawn: true }).bind(this)
+      }),
+    })
   }
 }
diff --git a/src/effect.js b/src/effect.js
index 9a85da8..52335e1 100644
--- a/src/effect.js
+++ b/src/effect.js
@@ -1,10 +1,54 @@
+import { Vector2 } from 'three'
+import Projectile from './projectile.js'
+
 export default class Effect {
   static damage({ despawn }) {
     return function(projectile, entity) {
-      entity.health -= 10
+      entity.damage(10, this)
       if (despawn) {
         projectile.despawn()
       }
     }
   }
+
+  static skillshot({ range, radius, speed, onCollide, after }) {
+    return function(cursor) {
+      const projectile = new Projectile()
+      const destination = this.position.clone().add(cursor.clone().sub(this.position).normalize().multiplyScalar(range))
+      projectile.owner = this.id
+      projectile.position.copy(this.position)
+      projectile.destination = destination
+      projectile.radius = radius
+      projectile.speed = speed
+      projectile.after = after
+      projectile.onCollide = onCollide
+      this.game?.spawnProjectile(projectile)
+    }
+  }
+
+  static homingProjectile({ range, radius, speed, onCollide, after }) {
+    return function(cursor) {
+      let closest = null
+      let distance = Infinity
+      this.game?.entities.filter((e) => e.id != this.id && e.position.clone().sub(this.position).length() < range).forEach((e) => {
+        const newDistance = e.position.clone().sub(cursor).length() < distance
+        if (newDistance < distance) {
+          closest = e
+          distance = newDistance
+        }
+      })
+
+      if (closest == null) { return } // TODO: refund
+
+      const projectile = new Projectile()
+      projectile.owner = this.id
+      projectile.position.copy(this.position)
+      projectile.homingTarget = closest
+      projectile.radius = radius
+      projectile.speed = speed
+      projectile.after = after
+      projectile.onCollide = onCollide
+      this.game?.spawnProjectile(projectile)
+    }
+  }
 }
\ No newline at end of file
diff --git a/src/entity.js b/src/entity.js
index e266bde..1330aeb 100644
--- a/src/entity.js
+++ b/src/entity.js
@@ -9,15 +9,19 @@ export default class Entity {
   id = crypto.randomUUID()
   speed = 400
   radius = 0
-  health = 1
+  health = 1 // TODO: health can go into negatives and can go over maxHealth
   maxHealth = 1
   abilities = [
-    Ability.homingProjectile({ range: 600, radius: 3, speed: 500, onCollide: Effect.damage({ despawn: true }) }),
-    Ability.skillshot({ range: 800, radius: 5, speed: 3000, onCollide: Effect.damage({ despawn: true }) }),
+    Ability.basicAttack(600, 5, 600),
+    Ability.straightShot(800, 7, 3000),
     () => {},
     () => {},
     () => {},
   ]
+  casting = null
+  // TODO: teams
+
+  cooldowns = {}
 
   #position = new Vector2()
   #dest = null
@@ -68,8 +72,42 @@ export default class Entity {
     ])
   }
 
+  cast() {
+    if (this.casting == null) {
+      return false
+    }
+
+    const castTime = this.game?.secToTick(this.casting.ability.castTime) ?? 0
+    const castStart = this.casting.timestamp
+    const timestamp = this.game?.currentTick ?? 0
+    if (castStart + castTime < timestamp) {
+      return false
+    }
+
+    this.casting.ability.effect.bind(this)(this.casting.cursor)
+
+    if (this.casting.ability.cooldown != null) {
+      this.cooldowns[this.casting.ability.id] = timestamp
+    }
+
+    this.casting = null
+    return true
+  }
+
   castAction(slot, x, y) {
-    this.abilities[slot].bind(this)(x, y)
+    const ability = this.abilities[slot]
+    const cursor = new Vector2(x, y)
+    const cooldown = this.game?.secToTick(ability.cooldown) ?? 0
+    const lastCast = this.cooldowns[ability.id]
+    const timestamp = this.game?.currentTick ?? 0
+    if (lastCast != null && lastCast + cooldown > timestamp) {
+      return false
+    }
+
+    this.#dest = null
+    this.casting = { ability, cursor, timestamp }
+
+    return true
   }
 
   collidables() {
@@ -87,10 +125,18 @@ export default class Entity {
     return entityColliders.concat(terrainColliders)
   }
 
+  damage(amount, source = null) {
+    this.health = Math.min(Math.max(0, this.health - amount), this.maxHealth)
+  }
+
   despawn() {
     this.game?.despawn(this)
   }
 
+  heal(amount, source = null) {
+    this.health = Math.min(Math.max(0, this.health + amount), this.maxHealth)
+  }
+
   fixPosition() {
     this.#position = SATX.fixCollisions(this.#position, this.collidables(), this.radius, this.game?.width, this.game?.height)
   }
@@ -166,6 +212,7 @@ export default class Entity {
   }
 
   update() {
+    this.cast()
     this.takeStep()
     this.fixPosition()
   }
diff --git a/src/game.js b/src/game.js
index bc01fd0..a823adf 100644
--- a/src/game.js
+++ b/src/game.js
@@ -50,6 +50,10 @@ export default class Game {
     this.#terrains = this.#terrains.filter((t) => t.id != terrain.id)
   }
 
+  secToTick(sec) {
+    return Math.floor(this.tickRate * sec)
+  }
+
   spawn(object) {
     if (object instanceof Entity) { this.spawnEntity(object) }
     else if (object instanceof Terrain) { this.addTerrain(object) }
diff --git a/src/projectile.js b/src/projectile.js
index 4adcc24..dc12a3f 100644
--- a/src/projectile.js
+++ b/src/projectile.js
@@ -59,7 +59,7 @@ export default class Projectile {
     if (!this.#position.equals(this.destination)) { return }
 
     if (this.after != null) {
-      this.after(this)
+      this.after(this, this.#homingTarget)
     }
 
     this.despawn()