add skillshots

This commit is contained in:
2025-01-12 00:11:00 +09:00
parent 957b09b878
commit 51b61ab449
7 changed files with 275 additions and 30 deletions
+20
View File
@@ -0,0 +1,20 @@
import { Vector2 } from 'three'
import Projectile from './projectile.js'
export default class Ability {
static skillshot({ range, radius, speed, onCollide, after }) {
return function(x, y) {
console.log(this)
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)
}
}
}
+10
View File
@@ -0,0 +1,10 @@
export default class Effect {
static damage({ despawn }) {
return function(projectile, entity) {
entity.health -= 10
if (despawn) {
projectile.despawn()
}
}
}
}
+20 -2
View File
@@ -2,6 +2,8 @@ import { Vector2 } from 'three'
import SAT from 'sat'
import SATX from './satx.js'
import Pathfind from './pathfind.js'
import Ability from './ability.js'
import Effect from './effect.js'
export default class Entity {
id = crypto.randomUUID()
@@ -9,6 +11,13 @@ export default class Entity {
radius = 0
health = 1
maxHealth = 1
abilities = [
() => {},
Ability.skillshot({ range: 800, radius: 5, speed: 3000, onCollide: Effect.damage({ despawn: true }) }),
() => {},
() => {},
() => {},
]
#position = new Vector2()
#dest = null
@@ -59,6 +68,10 @@ export default class Entity {
])
}
castAction(slot, x, y) {
this.abilities[slot].bind(this)(x, y)
}
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()
@@ -74,6 +87,10 @@ export default class Entity {
return entityColliders.concat(terrainColliders)
}
despawn() {
this.game?.despawn(this)
}
fixPosition() {
this.#position = SATX.fixCollisions(this.#position, this.collidables(), this.radius, this.game?.width, this.game?.height)
}
@@ -122,8 +139,9 @@ export default class Entity {
if (this.#path.length > 0) {
const destination = this.#path.at(0)
const distance = this.position.clone().sub(destination).length()
const direction = destination.clone().sub(this.position).normalize()
const difference = destination.clone().sub(this.position)
const distance = difference.length()
const direction = difference.clone().normalize()
const stepTaken = this.position.clone().add(direction.multiplyScalar(speed))
const position = distance <= speed ? destination : stepTaken
+39 -8
View File
@@ -1,4 +1,7 @@
import { EventEmitter } from 'node:events'
import Entity from './entity.js'
import Terrain from './terrain.js'
import Projectile from './projectile.js'
export default class Game {
tickRate = 30
@@ -8,11 +11,13 @@ export default class Game {
#entities = []
#eventEmitter = new EventEmitter()
#projectiles = []
#terrains = []
#tickBudget = Math.floor(1000 / this.tickRate)
get entities() { return this.#entities }
get eventEmitter() { return this.#eventEmitter }
get projectiles() { return this.#projectiles }
get terrains() { return this.#terrains }
get tickBudget() { return this.#tickBudget }
@@ -20,29 +25,54 @@ export default class Game {
return this.terrains.map((t) => t.unadjustedWaypoints).concat(this.entities.map((e) => e.unadjustedWaypoints)).flat()
}
spawn_entity(entity) {
this.#entities.push(entity)
entity.game = this
addTerrain(terrain) {
this.#terrains.push(terrain)
}
despawn(entity) {
despawn(object) {
if (object instanceof Entity) { this.despawnEntity(object) }
else if (object instanceof Terrain) { this.removeTerrain(object) }
else if (object instanceof Projectile) { this.despawnProjectile(object) }
else { console.error({ error: { reason: 'Can\'t despawn object', object } }) }
}
despawnEntity(entity) {
this.#entities = this.#entities.filter((e) => e.id != entity.id)
entity.game = null
}
add_terrain(terrain) {
this.#terrains.push(terrain)
despawnProjectile(projectile) {
this.#projectiles = this.#projectiles.filter((p) => p.id != projectile.id)
projectile.game = null
}
remove_terrain(terrain) {
removeTerrain(terrain) {
this.#terrains = this.#terrains.filter((t) => t.id != terrain.id)
}
spawn(object) {
if (object instanceof Entity) { this.spawnEntity(object) }
else if (object instanceof Terrain) { this.addTerrain(object) }
else if (object instanceof Projectile) { this.spawnProjectile(object) }
else { console.error({ error: { reason: 'Can\'t spawn object', object } }) }
}
spawnEntity(entity) {
this.#entities.push(entity)
entity.game = this
}
spawnProjectile(projectile) {
this.#projectiles.push(projectile)
projectile.game = this
}
state() {
return {
...this,
entities: this.#entities.map((e) => e.state()),
terrains: this.#terrains.map((t) => t.state()),
projectiles: this.#projectiles.map((p) => p.state()),
}
}
@@ -53,7 +83,8 @@ export default class Game {
}
update() {
this.#entities.map((e) => e.update())
this.#entities.forEach((e) => e.update())
this.#projectiles.forEach((p) => p.update())
this.currentTick++
this.eventEmitter.emit('tick')
}
+16 -11
View File
@@ -41,6 +41,10 @@ app.ws('/ws', async (req, res) => {
if (message.action == 'move') {
entity.moveAction(message.x, message.y)
}
if (message.action == 'cast') {
entity.castAction(message.slot, message.x, message.y)
}
})
})
@@ -51,7 +55,7 @@ function testScenario() {
entity1.radius = 50
entity1.maxHealth = 100
entity1.health = 80
game.spawn_entity(entity1)
game.spawnEntity(entity1)
const entity2 = new Entity()
entity2.id = '2'
@@ -59,7 +63,7 @@ function testScenario() {
entity2.radius = 50
entity2.maxHealth = 50
entity2.health = 50
game.spawn_entity(entity2)
game.spawnEntity(entity2)
const horseshoe = new Terrain([
{ x: 400, y: 200 },
@@ -73,7 +77,7 @@ function testScenario() {
{ x: 400, y: 300 },
])
horseshoe.id = 'horseshoe'
game.add_terrain(horseshoe)
game.addTerrain(horseshoe)
const stopsign = new Terrain([
{ x: 800, y: 800 },
@@ -87,7 +91,7 @@ function testScenario() {
{ x: 700, y: 800 },
])
stopsign.id = 'stopsign'
game.add_terrain(stopsign)
game.addTerrain(stopsign)
const box = new Terrain([
{ x: 1200, y: 700 },
@@ -96,7 +100,7 @@ function testScenario() {
{ x: 1300, y: 700 },
])
box.id = 'box'
game.add_terrain(box)
game.addTerrain(box)
const diamond = new Terrain([
{ x: 1000, y: 300 },
@@ -105,7 +109,7 @@ function testScenario() {
{ x: 900, y: 400 },
])
diamond.id = 'diamond'
game.add_terrain(diamond)
game.addTerrain(diamond)
const pole = new Terrain([
{ x: 400, y: 1000 },
@@ -114,7 +118,7 @@ function testScenario() {
{ x: 400, y: 1010 },
])
pole.id = 'pole'
game.add_terrain(pole)
game.addTerrain(pole)
}
function laneScenario() {
@@ -124,7 +128,7 @@ function laneScenario() {
entity1.radius = 50
entity1.maxHealth = 100
entity1.health = 100
game.spawn_entity(entity1)
game.spawnEntity(entity1)
const entity2 = new Entity()
entity2.id = '2'
@@ -132,7 +136,7 @@ function laneScenario() {
entity2.radius = 50
entity2.maxHealth = 100
entity2.health = 100
game.spawn_entity(entity2)
game.spawnEntity(entity2)
const midWallStart = new Vector2(400, 400)
const midWallEnd = new Vector2(1600, 1600)
@@ -151,19 +155,20 @@ function laneScenario() {
const midNorthWallPoints = midWallPoints.map((p) => p.clone().add(midNorthWallOffset))
const midNorthWall = new Terrain(midNorthWallPoints)
midNorthWall.id = 'midNorthWall'
game.add_terrain(midNorthWall)
game.addTerrain(midNorthWall)
const midSouthWallOffset = new Vector2(200, -200)
const midSouthWallPoints = midWallPoints.map((p) => p.clone().add(midSouthWallOffset))
const midSouthWall = new Terrain(midSouthWallPoints)
midSouthWall.id = 'midSouthWall'
game.add_terrain(midSouthWall)
game.addTerrain(midSouthWall)
}
app.listen(port, () => {
console.log(`Server started! Visit http://localhost:${port}`)
laneScenario()
game.entities[0].castAction(1, 2000, 2000)
game.start()
})
+86
View File
@@ -0,0 +1,86 @@
import SAT from 'sat'
import SATX from './satx.js'
import { Vector2 } from 'three'
export default class Projectile {
id = crypto.randomUUID()
after = null
speed = 1000
radius = 5
owner = null
onCollide = null
visualHeight = 50
#position = new Vector2()
#dest = null
#game = null
get collider() {
return new SAT.Circle(new SAT.Vector(this.x, this.y), this.radius)
}
constructor(...options) {
Object.entries(options).forEach((value, key) => this[key] = value)
}
get game() { return this.#game }
get position() { return this.#position }
get x() { return this.position.x }
get y() { return this.position.y }
set game(value) { this.#game = value }
set x(value) { this.position.x = value }
set y(value) { this.position.y = value }
set destination(value) { this.#dest = value }
checkCollisions() {
(this.game?.entities ?? []).filter((e) => e.id != this.id).forEach((e) => {
if (e.id == this.owner) { return }
if (SATX.collideObject(this.collider, e.collider)) {
this.onCollide(this, e)
}
})
}
checkIfArrived() {
if (!this.#position.equals(this.#dest)) { return }
if (this.after != null) {
this.after(this)
}
this.despawn()
}
despawn() {
this.game?.despawn(this)
}
state() {
return {
...this,
position: {
x: this.x,
y: this.y,
},
}
}
takeStep() {
const speed = (this.speed / (this.game?.tickBudget ?? 1000))
const destination = this.#dest
const difference = destination.clone().sub(this.position)
const distance = difference.length()
const direction = difference.clone().normalize()
const stepTaken = this.position.clone().add(direction.multiplyScalar(speed))
const position = distance <= speed ? destination : stepTaken
this.position.copy(position)
}
update() {
this.takeStep()
if (this.onCollide != null) { this.checkCollisions() }
this.checkIfArrived()
}
}