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)
+ }
})
})