From 6ff950640c24269ca109e460a60b6bbf7ba70669 Mon Sep 17 00:00:00 2001 From: Thayol Date: Sun, 12 Jan 2025 17:03:42 +0900 Subject: [PATCH] extend moveset with attack, halt, stop --- public/client.js | 47 ++++++++++++++++++++--- public/index.html | 35 +++++++++++++++++ src/entity.js | 98 ++++++++++++++++++++++++++++++++++++----------- src/index.js | 17 +++++++- 4 files changed, 167 insertions(+), 30 deletions(-) diff --git a/public/client.js b/public/client.js index 05b596a..0262264 100644 --- a/public/client.js +++ b/public/client.js @@ -271,10 +271,33 @@ function connectWebSocket() { } } - document.getElementById(`ability-${abilityIndex}-cooldown`).style.clipPath = `polygon(0 ${cssPercentage}, 100% ${cssPercentage}, 100% 100%, 0 100%)` + if (player.casting?.ability?.id == ability.id) { + document.getElementById(`ability-${abilityIndex}-cooldown`).style.clipPath = `polygon(50% 0%, 0% 100%, 100% 100%)` // triangle + } + else { + document.getElementById(`ability-${abilityIndex}-cooldown`).style.clipPath = `polygon(0 ${cssPercentage}, 100% ${cssPercentage}, 100% 100%, 0 100%)` + } + document.getElementById(`ability-${abilityIndex}-cooldown-text`).innerHTML = text } } + + let castIndicatorDisplay = 'none' + if (player.casting != null) { + castIndicatorDisplay = 'block' + const castDuration = (player.casting.ability.castTime * state.tickRate) ?? 0 + const remainingCastTime = (player.casting.timestamp + castDuration) - state.currentTick + let cssPercentage = '100%' + if (remainingCastTime > 0) { + const castPercentage = 1 - (remainingCastTime / castDuration) + cssPercentage = `${Math.round(100 * castPercentage)}%` + } + + document.getElementById('cast_indicator_progress').style.clipPath = `polygon(0 0, ${cssPercentage} 0, ${cssPercentage} 100%, 0% 100%)` + document.getElementById('cast_indicator_name').innerHTML = player.casting.ability?.name ?? '' + } + + document.getElementById('cast_indicator').style.display = castIndicatorDisplay } } @@ -294,12 +317,12 @@ window.addEventListener('load', () => { const canvas = renderer.domElement canvas.classList.add('canvas') - canvas.addEventListener('mousedown', (event) => { + window.addEventListener('mousedown', (event) => { const intersect = raycastToGround() if (intersect != null) { const { x, y } = intersect if (event.button == 0) { - websocket.send(JSON.stringify({ action: 'cast', slot: 0, id: playerId, x, y })) + websocket.send(JSON.stringify({ action: 'attack', id: playerId, x, y })) } if (event.button == 2) { @@ -311,6 +334,20 @@ window.addEventListener('load', () => { const intersect = raycastToGround() if (intersect != null) { const { x, y } = intersect + if (event.code == 'KeyA') { + websocket.send(JSON.stringify({ action: 'attack', id: playerId, x, y })) + } + if (event.code == 'KeyX') { + websocket.send(JSON.stringify({ action: 'cast', slot: 0, id: playerId, x, y })) + } + + if (event.code == 'KeyS') { + websocket.send(JSON.stringify({ action: 'stop', id: playerId })) + } + if (event.code == 'KeyH') { + websocket.send(JSON.stringify({ action: 'halt', id: playerId })) + } + if (event.code == 'KeyQ') { websocket.send(JSON.stringify({ action: 'cast', slot: 1, id: playerId, x, y })) } @@ -330,7 +367,7 @@ window.addEventListener('load', () => { } }) - document.addEventListener('wheel', (event) => { + window.addEventListener('wheel', (event) => { if (event.deltaY < 0) { camera.zoom += 0.2 if (camera.zoom > 3) { @@ -353,7 +390,7 @@ window.addEventListener('load', () => { renderer.setSize(window.innerWidth, window.innerHeight) }) - document.addEventListener('contextmenu', (event) => event.preventDefault()) + window.addEventListener('contextmenu', (event) => event.preventDefault()) window.addEventListener('keydown', (event) => keysDown[event.code] = true) window.addEventListener('keyup', (event) => keysDown[event.code] = false) window.addEventListener('keydown', (event) => { diff --git a/public/index.html b/public/index.html index 164ffad..c3ccf3d 100644 --- a/public/index.html +++ b/public/index.html @@ -103,6 +103,35 @@ color: white; font-family: monospace; } + + .cast-indicator-wrapper { + display: none; + position: fixed; + inset: auto 0 30%; + width: 400px; + margin: auto; + } + + .cast-indicator-progress { + position: absolute; + background-color: #edd9ff; + width: calc(100% - 4px); + height: calc(100% - 4px); + } + + .cast-indicator-name { + text-align: center; + color: white; + text-shadow: 1px 1px 2px black, 0 0 1em dimgray, 0 0 0.2em dimgray; + } + + .cast-indicator-bar { + position: relative; + background-color: dimgray; + width: 100%; + height: 20px; + padding: 2px; + } @@ -110,6 +139,12 @@

Connection:


     
+    
+
+
+
+
+
A diff --git a/src/entity.js b/src/entity.js index 0873945..672f89d 100644 --- a/src/entity.js +++ b/src/entity.js @@ -8,7 +8,7 @@ export default class Entity { id = crypto.randomUUID() speed = 400 radius = 0 - health = 1 // TODO: health can go into negatives and can go over maxHealth + health = 1 maxHealth = 1 abilities = [ Ability.basicAttack, @@ -21,6 +21,7 @@ export default class Entity { cooldowns = {} + #attack = false #position = new Vector2() #dest = null #game = null @@ -70,6 +71,72 @@ export default class Entity { ]) } + attackAction(x, y) { + this.moveAction(x, y, true) + } + + castAction(slot, x, y, clearDestination = true) { + const ability = this.abilities[slot] + + if (this.casting != null) { + const abilityBeingCasted = this.casting.ability + if (abilityBeingCasted.id == ability.id) { + return false + } + + return false + } + + if (clearDestination) { + this.#dest = null + } + + 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.casting = { ability, cursor, timestamp } + + return true + } + + haltAction() { + this.#dest = null + } + + moveAction(x, y, attack = false) { + this.#attack = attack + if (this.casting != null && (!this.#attack || this.casting.ability.id != this.abilities[0].id)) { + this.casting = null + } + + this.#dest = SATX.fixCollisions(new Vector2(x, y), this.collidables(), this.radius, this.game?.width, this.game?.height) + } + + stopAction() { + this.casting = null + this.#dest = null + this.#attack = false + } + + autoAttack() { + if (!this.#attack) { return false } + + if (this.game?.entities.some((e) => e.id != this.id && e.position.clone().sub(this.position).length() < this.abilities[0].range)) { + const cooldown = this.game?.secToTick(this.abilities[0].cooldown) ?? 0 + const lastCast = this.cooldowns[this.abilities[0].id] + const timestamp = this.game?.currentTick ?? 0 + if (lastCast != null && lastCast + cooldown > timestamp) { return false } + + const target = this.#dest ?? this.position + this.castAction(0, target.x, target.y, false) + } + } + cast() { if (this.casting == null) { return false @@ -88,22 +155,6 @@ export default class Entity { return true } - castAction(slot, 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() { 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() @@ -123,7 +174,7 @@ export default class Entity { this.cooldowns[id] = this.game?.currentTick ?? 0 } - damage(amount, source = null) { + damage(amount) { this.health = Math.min(Math.max(0, this.health - amount), this.maxHealth) } @@ -131,7 +182,7 @@ export default class Entity { this.game?.despawn(this) } - heal(amount, source = null) { + heal(amount) { this.health = Math.min(Math.max(0, this.health + amount), this.maxHealth) } @@ -143,10 +194,6 @@ export default class Entity { return SATX.collideObjects(this.collider, colliders) } - moveAction(x, y) { - this.#dest = SATX.fixCollisions(new Vector2(x, y), this.collidables(), this.radius, this.game?.width, this.game?.height) - } - state() { return { ...this, @@ -165,6 +212,8 @@ export default class Entity { // TODO: unset destination on teleports, etc. // TODO: recalculate path on obstructions (currently next waypoint is used) takeStep(distanceTraveled = 0) { + if (this.casting != null) { return false } + const speed = (this.speed / (this.game?.tickBudget ?? 1000)) - distanceTraveled const collidables = this.collidables() if (this.#dest != null) { @@ -213,6 +262,9 @@ export default class Entity { this.cast() this.takeStep() this.fixPosition() + this.autoAttack() + + // TODO: proper death and respawn if (this.health <= 0) { if (this.id == '1' || this.id == '2') { this.health = this.maxHealth diff --git a/src/index.js b/src/index.js index 021d412..3d80af4 100644 --- a/src/index.js +++ b/src/index.js @@ -33,17 +33,30 @@ app.ws('/ws', async (req, res) => { } console.log(message) + // DEPRECATED: teleporting is now directly possible via Ability... if (message.action == 'teleport') { entity.teleport(new Vector2(message.x, message.y)) } - if (message.action == 'move') { - entity.moveAction(message.x, message.y) + if (message.action == 'attack') { + entity.attackAction(message.x, message.y) } if (message.action == 'cast') { entity.castAction(message.slot, message.x, message.y) } + + if (message.action == 'halt') { + entity.haltAction() + } + + if (message.action == 'stop') { + entity.stopAction() + } + + if (message.action == 'move') { + entity.moveAction(message.x, message.y) + } }) })