add cast times and cooldowns
This commit is contained in:
+2
-2
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user