add cast times and cooldowns

This commit is contained in:
2025-01-12 03:30:52 +09:00
parent 2eb914a680
commit e0dd7dcaf3
7 changed files with 142 additions and 47 deletions
+2 -2
View File
@@ -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()
}
+2
View File
@@ -63,6 +63,8 @@
<p>Connection: <span id="connection"></span></p>
<pre id="state"></pre>
</div>
<!-- TODO: HUD -->
<!-- TODO: cast indicator -->
<script type="module" src="client.js"></script>
</body>
</html>
+36 -38
View File
@@ -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)
}),
})
}
}
+45 -1
View File
@@ -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)
}
}
}
+51 -4
View File
@@ -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()
}
+4
View File
@@ -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) }
+1 -1
View File
@@ -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()