fix pathfinding issues
This commit is contained in:
@@ -6,11 +6,14 @@ const scene = new THREE.Scene()
|
|||||||
const raycaster = new THREE.Raycaster()
|
const raycaster = new THREE.Raycaster()
|
||||||
const camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.1, 1000)
|
const camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||||
const renderer = new THREE.WebGLRenderer()
|
const renderer = new THREE.WebGLRenderer()
|
||||||
|
const backgroundColor = new THREE.Color().setHex(0x112233)
|
||||||
|
scene.background = backgroundColor
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
renderer.setAnimationLoop(render)
|
renderer.setAnimationLoop(render)
|
||||||
camera.position.set(5, -12, 10)
|
camera.position.set(5, -12, 10)
|
||||||
camera.rotation.set((60 / 180) * Math.PI, 0, 0)
|
camera.rotation.set((60 / 180) * Math.PI, 0, 0)
|
||||||
camera.layers.enable(1)
|
camera.layers.enable(1)
|
||||||
|
camera.layers.enable(2)
|
||||||
|
|
||||||
const entityMaterial = new THREE.MeshToonMaterial({ color: 0xffffff })
|
const entityMaterial = new THREE.MeshToonMaterial({ color: 0xffffff })
|
||||||
const projectileMaterial = new THREE.MeshToonMaterial({ color: 0xff00ff })
|
const projectileMaterial = new THREE.MeshToonMaterial({ color: 0xff00ff })
|
||||||
@@ -159,6 +162,10 @@ function connectWebSocket() {
|
|||||||
ground.position.set(state.width / 200, state.height / 200, 0)
|
ground.position.set(state.width / 200, state.height / 200, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const e of Object.values(entities)) {
|
||||||
|
e.userData.flaggedForRemoval = true
|
||||||
|
}
|
||||||
|
|
||||||
for (const e of state.entities ?? []) {
|
for (const e of state.entities ?? []) {
|
||||||
let entity
|
let entity
|
||||||
if (e.id in entities) {
|
if (e.id in entities) {
|
||||||
@@ -190,6 +197,7 @@ function connectWebSocket() {
|
|||||||
entities[e.id] = entity
|
entities[e.id] = entity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity.userData.flaggedForRemoval = false
|
||||||
positionTweens[entity.id] = new Tween(entity.position).to({ x: e.position.x / 100, y: e.position.y / 100, z: e.radius / 100 }, tweenDuration).start()
|
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)
|
const hp = entity.children.at(0).children.at(0)
|
||||||
@@ -198,6 +206,14 @@ function connectWebSocket() {
|
|||||||
hp.position.x = -(1 - percentageHp) / 2
|
hp.position.x = -(1 - percentageHp) / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const e of Object.values(entities)) {
|
||||||
|
if (e.userData.flaggedForRemoval) {
|
||||||
|
scene.remove(e)
|
||||||
|
delete entities[e.userData.id]
|
||||||
|
delete positionTweens[e.userData.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const p of Object.values(projectiles)) {
|
for (const p of Object.values(projectiles)) {
|
||||||
p.userData.flaggedForRemoval = true
|
p.userData.flaggedForRemoval = true
|
||||||
}
|
}
|
||||||
@@ -212,6 +228,7 @@ function connectWebSocket() {
|
|||||||
projectile.userData.type = 'projectile'
|
projectile.userData.type = 'projectile'
|
||||||
projectile.userData.id = p.id
|
projectile.userData.id = p.id
|
||||||
projectile.position.set(p.position.x / 100, p.position.y / 100, p.visualHeight / 100)
|
projectile.position.set(p.position.x / 100, p.position.y / 100, p.visualHeight / 100)
|
||||||
|
projectile.layers.set(2)
|
||||||
scene.add(projectile)
|
scene.add(projectile)
|
||||||
|
|
||||||
projectiles[p.id] = projectile
|
projectiles[p.id] = projectile
|
||||||
|
|||||||
+67
-12
@@ -28,7 +28,7 @@ export default class Ability {
|
|||||||
id: 'straight_shot',
|
id: 'straight_shot',
|
||||||
name: 'Straight Shot',
|
name: 'Straight Shot',
|
||||||
castTime: 0.1,
|
castTime: 0.1,
|
||||||
cooldown: 7,
|
cooldown: 1,
|
||||||
damage: 10,
|
damage: 10,
|
||||||
radius: 7,
|
radius: 7,
|
||||||
range: 800,
|
range: 800,
|
||||||
@@ -54,20 +54,20 @@ export default class Ability {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
static basicAttack = new Ability({
|
static rangedAttack = new Ability({
|
||||||
id: 'basic_attack',
|
id: 'ranged_attack',
|
||||||
name: 'Basic Attack',
|
name: 'Ranged Attack',
|
||||||
castTime: 0.25,
|
castTime: 0.25,
|
||||||
cooldown: 1.25,
|
cooldown: 1.25,
|
||||||
damage: 5,
|
damage: 5,
|
||||||
radius: 5,
|
radius: 5,
|
||||||
range: 600,
|
range: 500,
|
||||||
speed: 600,
|
speed: 600,
|
||||||
effect: function basicAttackEffect(caster, cursor) {
|
effect: function rangedAttackEffect(caster, cursor) {
|
||||||
const ability = this
|
const ability = this
|
||||||
let closest = null
|
let closest = null
|
||||||
let distance = Infinity
|
let distance = Infinity
|
||||||
caster.game?.entities.filter((e) => e.id != caster.id && e.position.clone().sub(caster.position).length() < ability.range).forEach((e) => {
|
caster.game?.entities.filter((e) => e.team != caster.team && e.position.clone().sub(caster.position).length() < ability.range).forEach((e) => {
|
||||||
const newDistance = e.position.clone().sub(cursor).length() < distance
|
const newDistance = e.position.clone().sub(cursor).length() < distance
|
||||||
if (newDistance < distance) {
|
if (newDistance < distance) {
|
||||||
closest = e
|
closest = e
|
||||||
@@ -77,12 +77,12 @@ export default class Ability {
|
|||||||
|
|
||||||
if (closest == null) { return }
|
if (closest == null) { return }
|
||||||
|
|
||||||
const basicAttackAfter = function basicAttackAfter() {
|
const rangedAttackAfter = function rangedAttackAfter() {
|
||||||
closest.damage(ability.damage)
|
closest.damage(ability.damage)
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectile = new Projectile({
|
const projectile = new Projectile({
|
||||||
after: basicAttackAfter,
|
after: rangedAttackAfter,
|
||||||
homingTarget: closest,
|
homingTarget: closest,
|
||||||
owner: caster,
|
owner: caster,
|
||||||
position: caster.position.clone(),
|
position: caster.position.clone(),
|
||||||
@@ -95,12 +95,67 @@ export default class Ability {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
static meleeAttack = new Ability({
|
||||||
|
id: 'melee_attack',
|
||||||
|
name: 'Melee Attack',
|
||||||
|
castTime: 0.1,
|
||||||
|
cooldown: 1.75,
|
||||||
|
damage: 10,
|
||||||
|
radius: 5,
|
||||||
|
range: 100,
|
||||||
|
effect: function meleeAttackEffect(caster, cursor) {
|
||||||
|
const ability = this
|
||||||
|
let closest = null
|
||||||
|
let distance = Infinity
|
||||||
|
caster.game?.entities.filter((e) => e.team != caster.team && e.position.clone().sub(caster.position).length() < ability.range).forEach((e) => {
|
||||||
|
const newDistance = e.position.clone().sub(cursor).length() < distance
|
||||||
|
if (newDistance < distance) {
|
||||||
|
closest = e
|
||||||
|
distance = newDistance
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (closest == null) { return }
|
||||||
|
|
||||||
|
closest.damage(ability.damage)
|
||||||
|
caster.cooldown(ability.id)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
static shieldThrow = new Ability({
|
static shieldThrow = new Ability({
|
||||||
id: 'shield_throw',
|
id: 'shield_throw',
|
||||||
name: 'Shield Throw',
|
name: 'Shield Throw',
|
||||||
castTime: 0.1,
|
castTime: 0.15,
|
||||||
cooldown: 7,
|
cooldown: 5,
|
||||||
effect: function shieldThrowEffect(caster, cursor) { },
|
radius: 20,
|
||||||
|
range: 1000,
|
||||||
|
speed: 2000,
|
||||||
|
effect: function shieldThrowEffect(caster, cursor) {
|
||||||
|
const ability = this
|
||||||
|
const shieldThrowReturn = function shieldThrowReturn(projectile, homingTarget) {
|
||||||
|
const returnProjectile = new Projectile({
|
||||||
|
owner: caster,
|
||||||
|
position: projectile.position.clone(),
|
||||||
|
radius: ability.radius,
|
||||||
|
speed: ability.speed,
|
||||||
|
homingTarget: caster,
|
||||||
|
})
|
||||||
|
|
||||||
|
caster.game?.spawnProjectile(returnProjectile)
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectile = new Projectile({
|
||||||
|
after: shieldThrowReturn,
|
||||||
|
owner: caster,
|
||||||
|
position: caster.position.clone(),
|
||||||
|
radius: ability.radius,
|
||||||
|
speed: ability.speed,
|
||||||
|
})
|
||||||
|
|
||||||
|
projectile.destination = caster.position.clone().add(cursor.clone().sub(caster.position).normalize().multiplyScalar(ability.range))
|
||||||
|
caster.game?.spawnProjectile(projectile)
|
||||||
|
caster.cooldown(ability.id)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
static blink = new Ability({
|
static blink = new Ability({
|
||||||
|
|||||||
+74
-67
@@ -3,6 +3,7 @@ import SAT from 'sat'
|
|||||||
import SATX from './satx.js'
|
import SATX from './satx.js'
|
||||||
import Pathfind from './pathfind.js'
|
import Pathfind from './pathfind.js'
|
||||||
import Ability from './ability.js'
|
import Ability from './ability.js'
|
||||||
|
import Team from './team.js'
|
||||||
|
|
||||||
export default class Entity {
|
export default class Entity {
|
||||||
id = crypto.randomUUID()
|
id = crypto.randomUUID()
|
||||||
@@ -11,21 +12,23 @@ export default class Entity {
|
|||||||
health = 1
|
health = 1
|
||||||
maxHealth = 1
|
maxHealth = 1
|
||||||
abilities = [
|
abilities = [
|
||||||
Ability.basicAttack,
|
Ability.rangedAttack,
|
||||||
Ability.straightShot,
|
Ability.straightShot,
|
||||||
Ability.shieldThrow,
|
Ability.shieldThrow,
|
||||||
Ability.blink,
|
Ability.blink,
|
||||||
]
|
]
|
||||||
casting = null
|
casting = null
|
||||||
// TODO: teams
|
team = Team.neutral
|
||||||
|
|
||||||
cooldowns = {}
|
cooldowns = {}
|
||||||
|
|
||||||
#attack = false
|
#attack = false
|
||||||
#position = new Vector2()
|
|
||||||
#dest = null
|
#dest = null
|
||||||
#game = null
|
#game = null
|
||||||
|
#logic = null
|
||||||
#path = []
|
#path = []
|
||||||
|
#position = new Vector2()
|
||||||
|
#scheduledPathfinding = null
|
||||||
|
|
||||||
static collider(x, y, radius) {
|
static collider(x, y, radius) {
|
||||||
return new SAT.Circle(new SAT.Vector(x, y), radius)
|
return new SAT.Circle(new SAT.Vector(x, y), radius)
|
||||||
@@ -35,11 +38,19 @@ export default class Entity {
|
|||||||
Object.entries(options).forEach(([key, value]) => this[key] = value)
|
Object.entries(options).forEach(([key, value]) => this[key] = value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get destination() { return this.#dest }
|
||||||
|
get logic() { return this.#logic }
|
||||||
get game() { return this.#game }
|
get game() { return this.#game }
|
||||||
get position() { return this.#position }
|
get position() { return this.#position }
|
||||||
|
get scheduledPathfinding() { return this.#scheduledPathfinding }
|
||||||
get x() { return this.position.x }
|
get x() { return this.position.x }
|
||||||
get y() { return this.position.y }
|
get y() { return this.position.y }
|
||||||
|
|
||||||
|
set destination(value) { this.#dest = value }
|
||||||
|
set logic(value) { this.#logic = value }
|
||||||
set game(value) { this.#game = value }
|
set game(value) { this.#game = value }
|
||||||
|
set position(value) { this.#position = value }
|
||||||
|
set scheduledPathfinding(value) { this.#scheduledPathfinding = value }
|
||||||
set x(value) { this.position.x = value }
|
set x(value) { this.position.x = value }
|
||||||
set y(value) { this.position.y = value }
|
set y(value) { this.position.y = value }
|
||||||
|
|
||||||
@@ -123,20 +134,6 @@ export default class Entity {
|
|||||||
this.#attack = false
|
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() {
|
cast() {
|
||||||
if (this.casting == null) {
|
if (this.casting == null) {
|
||||||
return false
|
return false
|
||||||
@@ -209,50 +206,71 @@ export default class Entity {
|
|||||||
this.fixPosition()
|
this.fixPosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: unset destination on teleports, etc.
|
|
||||||
// TODO: recalculate path on obstructions (currently next waypoint is used)
|
|
||||||
takeStep(distanceTraveled = 0) {
|
takeStep(distanceTraveled = 0) {
|
||||||
if (this.casting != null) { return false }
|
if (this.casting != null) { return false }
|
||||||
|
|
||||||
const speed = (this.speed / (this.game?.tickBudget ?? 1000)) - distanceTraveled
|
if (this.#attack && this.game?.entities.some((e) => e.team != this.team && e.position.clone().sub(this.position).length() < this.abilities[0].range)) {
|
||||||
const collidables = this.collidables()
|
const cooldown = this.game?.secToTick(this.abilities[0].cooldown) ?? 0
|
||||||
if (this.#dest != null) {
|
const lastCast = this.cooldowns[this.abilities[0].id]
|
||||||
const fixedDest = SATX.clamp(SATX.fixCollisions(this.#dest, collidables, this.radius), this.game?.width, this.game?.height, this.radius)
|
const timestamp = this.game?.currentTick ?? 0
|
||||||
|
if (lastCast != null && lastCast + cooldown > timestamp) { return false }
|
||||||
|
|
||||||
if (this.#path.length < 1 || !this.#path.at(-1).equals(fixedDest)) {
|
const target = this.#dest ?? this.position
|
||||||
// console.time('pathfinding')
|
this.castAction(0, target.x, target.y, false)
|
||||||
const start = SATX.vectorToFloat32Array(this.position)
|
return true
|
||||||
const goal = SATX.vectorToFloat32Array(fixedDest)
|
}
|
||||||
const nonUniqueWaypoints = this.waypoints().map((w) => SATX.vectorToFloat32Array(w)).concat([start, goal])
|
|
||||||
const waypoints = Pathfind.uniqueWaypoints(nonUniqueWaypoints)
|
if (this.#dest == null) { return false }
|
||||||
const graph = Pathfind.buildGraph(waypoints, collidables, this.radius)
|
|
||||||
this.#path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1]))
|
const collidables = this.collidables()
|
||||||
// console.timeEnd('pathfinding')
|
const fixedDest = SATX.clamp(SATX.fixCollisions(this.#dest, collidables, this.radius), this.game?.width, this.game?.height, this.radius)
|
||||||
|
const tunnel = SATX.entityTunnel(this.#position.x, this.#position.y, fixedDest.x, fixedDest.y, this.radius)
|
||||||
|
const destinationInLineOfSight = !SATX.collideObjects(tunnel, collidables)
|
||||||
|
|
||||||
|
if (this.#path.length > 0) {
|
||||||
|
if (!destinationInLineOfSight) {
|
||||||
|
this.#path = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#path.length < 1 || !this.#path.at(-1).equals(fixedDest)) {
|
||||||
|
if (destinationInLineOfSight) {
|
||||||
|
this.#path = [fixedDest]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.#path.length < 1 || !this.#path.at(-1).equals(fixedDest)) && (!this.#scheduledPathfinding || this.game?.currentTick % this.game?.tickRate == this.#scheduledPathfinding)) {
|
||||||
|
const start = SATX.vectorToFloat32Array(this.position)
|
||||||
|
const goal = SATX.vectorToFloat32Array(fixedDest)
|
||||||
|
const nonUniqueWaypoints = this.waypoints().map((w) => SATX.vectorToFloat32Array(w)).concat([start, goal])
|
||||||
|
const waypoints = Pathfind.uniqueWaypoints(nonUniqueWaypoints)
|
||||||
|
const graph = Pathfind.buildGraph(waypoints, collidables, this.radius)
|
||||||
|
this.#path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#path.length > 0) {
|
||||||
|
const speed = (this.speed / (this.game?.tickBudget ?? 1000)) - distanceTraveled
|
||||||
|
const destination = this.#path.at(0)
|
||||||
|
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
|
||||||
|
|
||||||
|
const collider = Entity.collider(position.x, position.y, this.radius)
|
||||||
|
const isColliding = SATX.collideObjects(collider, this.collidables())
|
||||||
|
|
||||||
|
if (!isColliding) {
|
||||||
|
this.position.copy(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#path.length > 0) {
|
if (this.position.equals(destination)) {
|
||||||
const destination = this.#path.at(0)
|
this.#path = this.#path.slice(1)
|
||||||
const difference = destination.clone().sub(this.position)
|
if (this.#path.length > 0) {
|
||||||
const distance = difference.length()
|
this.takeStep(distance)
|
||||||
const direction = difference.clone().normalize()
|
|
||||||
const stepTaken = this.position.clone().add(direction.multiplyScalar(speed))
|
|
||||||
const position = distance <= speed ? destination : stepTaken
|
|
||||||
|
|
||||||
const collider = Entity.collider(position.x, position.y, this.radius)
|
|
||||||
const isColliding = SATX.collideObjects(collider, this.collidables())
|
|
||||||
|
|
||||||
if (!isColliding) {
|
|
||||||
this.position.copy(position)
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
if (this.position.equals(destination)) {
|
this.#dest = null
|
||||||
this.#path = this.#path.slice(1)
|
|
||||||
if (this.#path.length > 0) {
|
|
||||||
this.takeStep(distance)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.#dest = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,19 +280,8 @@ export default class Entity {
|
|||||||
this.cast()
|
this.cast()
|
||||||
this.takeStep()
|
this.takeStep()
|
||||||
this.fixPosition()
|
this.fixPosition()
|
||||||
this.autoAttack()
|
if (this.#logic != null) {
|
||||||
|
this.#logic()
|
||||||
// TODO: proper death and respawn
|
|
||||||
if (this.health <= 0) {
|
|
||||||
if (this.id == '1' || this.id == '2') {
|
|
||||||
this.health = this.maxHealth
|
|
||||||
if (this.id == '1') {
|
|
||||||
this.teleport(new Vector2(200, 200))
|
|
||||||
}
|
|
||||||
if (this.id == '2') {
|
|
||||||
this.teleport(new Vector2(1800, 1800))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,17 +11,20 @@ export default class Game {
|
|||||||
running = false
|
running = false
|
||||||
nextTick = 0
|
nextTick = 0
|
||||||
|
|
||||||
|
#logic = null
|
||||||
#entities = []
|
#entities = []
|
||||||
#eventEmitter = new EventEmitter()
|
#eventEmitter = new EventEmitter()
|
||||||
#projectiles = []
|
#projectiles = []
|
||||||
#terrains = []
|
#terrains = []
|
||||||
#tickBudget = 1000 / this.tickRate
|
#tickBudget = 1000 / this.tickRate
|
||||||
|
|
||||||
|
get logic() { return this.#logic }
|
||||||
get entities() { return this.#entities }
|
get entities() { return this.#entities }
|
||||||
get eventEmitter() { return this.#eventEmitter }
|
get eventEmitter() { return this.#eventEmitter }
|
||||||
get projectiles() { return this.#projectiles }
|
get projectiles() { return this.#projectiles }
|
||||||
get terrains() { return this.#terrains }
|
get terrains() { return this.#terrains }
|
||||||
get tickBudget() { return this.#tickBudget }
|
get tickBudget() { return this.#tickBudget }
|
||||||
|
set logic(value) { this.#logic = value }
|
||||||
|
|
||||||
get unadjustedWaypoints() {
|
get unadjustedWaypoints() {
|
||||||
return this.terrains.map((t) => t.unadjustedWaypoints).concat(this.entities.map((e) => e.unadjustedWaypoints)).flat()
|
return this.terrains.map((t) => t.unadjustedWaypoints).concat(this.entities.map((e) => e.unadjustedWaypoints)).flat()
|
||||||
@@ -104,6 +107,10 @@ export default class Game {
|
|||||||
update() {
|
update() {
|
||||||
this.#entities.forEach((e) => e.update())
|
this.#entities.forEach((e) => e.update())
|
||||||
this.#projectiles.forEach((p) => p.update())
|
this.#projectiles.forEach((p) => p.update())
|
||||||
|
if (this.#logic != null) {
|
||||||
|
this.#logic()
|
||||||
|
}
|
||||||
|
|
||||||
this.currentTick++
|
this.currentTick++
|
||||||
this.eventEmitter.emit('tick')
|
this.eventEmitter.emit('tick')
|
||||||
}
|
}
|
||||||
|
|||||||
+215
-79
@@ -4,6 +4,8 @@ import Game from './game.js'
|
|||||||
import Entity from './entity.js'
|
import Entity from './entity.js'
|
||||||
import Terrain from './terrain.js'
|
import Terrain from './terrain.js'
|
||||||
import { Vector2 } from 'three'
|
import { Vector2 } from 'three'
|
||||||
|
import Team from './team.js'
|
||||||
|
import Ability from './ability.js'
|
||||||
|
|
||||||
const app = new WebSocketExpress()
|
const app = new WebSocketExpress()
|
||||||
const port = 1280
|
const port = 1280
|
||||||
@@ -60,94 +62,100 @@ app.ws('/ws', async (req, res) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function testScenario() {
|
// function testScenario() {
|
||||||
const entity1 = new Entity()
|
// const entity1 = new Entity()
|
||||||
entity1.id = '1'
|
// entity1.id = '1'
|
||||||
entity1.teleport(new Vector2(200, 500))
|
// entity1.teleport(new Vector2(200, 500))
|
||||||
entity1.radius = 50
|
// entity1.radius = 50
|
||||||
entity1.maxHealth = 100
|
// entity1.maxHealth = 100
|
||||||
entity1.health = 80
|
// entity1.health = 80
|
||||||
game.spawnEntity(entity1)
|
// game.spawnEntity(entity1)
|
||||||
|
|
||||||
const entity2 = new Entity()
|
// const entity2 = new Entity()
|
||||||
entity2.id = '2'
|
// entity2.id = '2'
|
||||||
entity2.teleport(new Vector2(110, 110))
|
// entity2.teleport(new Vector2(110, 110))
|
||||||
entity2.radius = 50
|
// entity2.radius = 50
|
||||||
entity2.maxHealth = 50
|
// entity2.maxHealth = 50
|
||||||
entity2.health = 50
|
// entity2.health = 50
|
||||||
game.spawnEntity(entity2)
|
// game.spawnEntity(entity2)
|
||||||
|
|
||||||
const horseshoe = new Terrain([
|
// const horseshoe = new Terrain([
|
||||||
{ x: 400, y: 200 },
|
// { x: 400, y: 200 },
|
||||||
{ x: 600, y: 200 },
|
// { x: 600, y: 200 },
|
||||||
{ x: 700, y: 300 },
|
// { x: 700, y: 300 },
|
||||||
{ x: 650, y: 600 },
|
// { x: 650, y: 600 },
|
||||||
{ x: 400, y: 600 },
|
// { x: 400, y: 600 },
|
||||||
{ x: 400, y: 450 },
|
// { x: 400, y: 450 },
|
||||||
{ x: 600, y: 500 },
|
// { x: 600, y: 500 },
|
||||||
{ x: 600, y: 300 },
|
// { x: 600, y: 300 },
|
||||||
{ x: 400, y: 300 },
|
// { x: 400, y: 300 },
|
||||||
])
|
// ])
|
||||||
horseshoe.id = 'horseshoe'
|
// horseshoe.id = 'horseshoe'
|
||||||
game.addTerrain(horseshoe)
|
// game.addTerrain(horseshoe)
|
||||||
|
|
||||||
const stopsign = new Terrain([
|
// const stopsign = new Terrain([
|
||||||
{ x: 800, y: 800 },
|
// { x: 800, y: 800 },
|
||||||
{ x: 900, y: 900 },
|
// { x: 900, y: 900 },
|
||||||
{ x: 900, y: 1000 },
|
// { x: 900, y: 1000 },
|
||||||
{ x: 800, y: 1100 },
|
// { x: 800, y: 1100 },
|
||||||
{ x: 800, y: 1100 },
|
// { x: 800, y: 1100 },
|
||||||
{ x: 700, y: 1100 },
|
// { x: 700, y: 1100 },
|
||||||
{ x: 600, y: 1000 },
|
// { x: 600, y: 1000 },
|
||||||
{ x: 600, y: 900 },
|
// { x: 600, y: 900 },
|
||||||
{ x: 700, y: 800 },
|
// { x: 700, y: 800 },
|
||||||
])
|
// ])
|
||||||
stopsign.id = 'stopsign'
|
// stopsign.id = 'stopsign'
|
||||||
game.addTerrain(stopsign)
|
// game.addTerrain(stopsign)
|
||||||
|
|
||||||
const box = new Terrain([
|
// const box = new Terrain([
|
||||||
{ x: 1200, y: 700 },
|
// { x: 1200, y: 700 },
|
||||||
{ x: 1200, y: 800 },
|
// { x: 1200, y: 800 },
|
||||||
{ x: 1300, y: 800 },
|
// { x: 1300, y: 800 },
|
||||||
{ x: 1300, y: 700 },
|
// { x: 1300, y: 700 },
|
||||||
])
|
// ])
|
||||||
box.id = 'box'
|
// box.id = 'box'
|
||||||
game.addTerrain(box)
|
// game.addTerrain(box)
|
||||||
|
|
||||||
const diamond = new Terrain([
|
// const diamond = new Terrain([
|
||||||
{ x: 1000, y: 300 },
|
// { x: 1000, y: 300 },
|
||||||
{ x: 1100, y: 400 },
|
// { x: 1100, y: 400 },
|
||||||
{ x: 1000, y: 500 },
|
// { x: 1000, y: 500 },
|
||||||
{ x: 900, y: 400 },
|
// { x: 900, y: 400 },
|
||||||
])
|
// ])
|
||||||
diamond.id = 'diamond'
|
// diamond.id = 'diamond'
|
||||||
game.addTerrain(diamond)
|
// game.addTerrain(diamond)
|
||||||
|
|
||||||
const pole = new Terrain([
|
// const pole = new Terrain([
|
||||||
{ x: 400, y: 1000 },
|
// { x: 400, y: 1000 },
|
||||||
{ x: 410, y: 1000 },
|
// { x: 410, y: 1000 },
|
||||||
{ x: 410, y: 1010 },
|
// { x: 410, y: 1010 },
|
||||||
{ x: 400, y: 1010 },
|
// { x: 400, y: 1010 },
|
||||||
])
|
// ])
|
||||||
pole.id = 'pole'
|
// pole.id = 'pole'
|
||||||
game.addTerrain(pole)
|
// game.addTerrain(pole)
|
||||||
}
|
// }
|
||||||
|
|
||||||
function laneScenario() {
|
function laneScenario() {
|
||||||
const entity1 = new Entity()
|
const entity1 = new Entity({
|
||||||
entity1.id = '1'
|
id: '1',
|
||||||
entity1.teleport(new Vector2(200, 200))
|
health: 100,
|
||||||
entity1.radius = 50
|
maxHealth: 100,
|
||||||
entity1.maxHealth = 100
|
position: new Vector2(500, 150),
|
||||||
entity1.health = 100
|
radius: 50,
|
||||||
|
team: Team.blue,
|
||||||
|
})
|
||||||
|
|
||||||
game.spawnEntity(entity1)
|
game.spawnEntity(entity1)
|
||||||
|
|
||||||
const entity2 = new Entity()
|
const entity2 = new Entity({
|
||||||
entity2.id = '2'
|
id: '2',
|
||||||
entity2.teleport(new Vector2(1800, 1800))
|
health: 100,
|
||||||
entity2.radius = 50
|
maxHealth: 100,
|
||||||
entity2.maxHealth = 100
|
position: new Vector2(1600, 1800),
|
||||||
entity2.health = 100
|
radius: 50,
|
||||||
|
team: Team.red,
|
||||||
|
})
|
||||||
|
|
||||||
game.spawnEntity(entity2)
|
game.spawnEntity(entity2)
|
||||||
|
|
||||||
const midWallStart = new Vector2(400, 400)
|
const midWallStart = new Vector2(400, 400)
|
||||||
@@ -174,13 +182,141 @@ function laneScenario() {
|
|||||||
const midSouthWall = new Terrain(midSouthWallPoints)
|
const midSouthWall = new Terrain(midSouthWallPoints)
|
||||||
midSouthWall.id = 'midSouthWall'
|
midSouthWall.id = 'midSouthWall'
|
||||||
game.addTerrain(midSouthWall)
|
game.addTerrain(midSouthWall)
|
||||||
|
|
||||||
|
// TODO: proper death and respawn
|
||||||
|
const playerLogic = function playerLogic() {
|
||||||
|
const entity = this
|
||||||
|
if (entity.health <= 0) {
|
||||||
|
if (entity.id == '1' || entity.id == '2') {
|
||||||
|
entity.health = entity.maxHealth
|
||||||
|
if (entity.id == '1') {
|
||||||
|
entity.teleport(new Vector2(500, 150))
|
||||||
|
}
|
||||||
|
if (entity.id == '2') {
|
||||||
|
entity.teleport(new Vector2(1600, 1800))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entity1.logic = playerLogic
|
||||||
|
entity2.logic = playerLogic
|
||||||
|
|
||||||
|
const blueMinionLogic = function minionLogic() {
|
||||||
|
const entity = this
|
||||||
|
let goal = new Vector2(1900, 1900)
|
||||||
|
if (entity.position.x < 800 || entity.position.y < 1100) {
|
||||||
|
goal = new Vector2(850, 1150)
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(75)
|
||||||
|
const subGoal = entity.position.clone().add(direction)
|
||||||
|
// console.log(subGoal)
|
||||||
|
entity.attackAction(subGoal.x, subGoal.y)
|
||||||
|
|
||||||
|
if (entity.health <= 0) {
|
||||||
|
entity.despawn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const redMinionLogic = function minionLogic() {
|
||||||
|
const entity = this
|
||||||
|
let goal = new Vector2(100, 100)
|
||||||
|
if (entity.position.x > 900 || entity.position.y > 1200) {
|
||||||
|
goal = new Vector2(850, 1150)
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(75)
|
||||||
|
const subGoal = entity.position.clone().add(direction)
|
||||||
|
entity.attackAction(subGoal.x, subGoal.y)
|
||||||
|
|
||||||
|
if (entity.health <= 0) {
|
||||||
|
entity.despawn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const minionTemplate = {
|
||||||
|
health: 20,
|
||||||
|
maxHealth: 20,
|
||||||
|
radius: 30,
|
||||||
|
speed: 300,
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameLogic = function gameLogic() {
|
||||||
|
const game = this
|
||||||
|
|
||||||
|
const blueMinion = new Entity({
|
||||||
|
...minionTemplate,
|
||||||
|
logic: blueMinionLogic,
|
||||||
|
team: Team.blue,
|
||||||
|
position: new Vector2(200, 200),
|
||||||
|
})
|
||||||
|
// blueMinion.scheduledPathfinding = game.entities.length % game.tickRate
|
||||||
|
|
||||||
|
const blueMeleeMinion = new Entity({
|
||||||
|
...minionTemplate,
|
||||||
|
logic: blueMinionLogic,
|
||||||
|
team: Team.blue,
|
||||||
|
position: new Vector2(200, 200),
|
||||||
|
})
|
||||||
|
blueMeleeMinion.abilities[0] = Ability.meleeAttack
|
||||||
|
|
||||||
|
const redMinion = new Entity({
|
||||||
|
...minionTemplate,
|
||||||
|
logic: redMinionLogic,
|
||||||
|
team: Team.red,
|
||||||
|
position: new Vector2(1800, 1800),
|
||||||
|
})
|
||||||
|
// redMinion.scheduledPathfinding = game.entities.length % game.tickRate
|
||||||
|
|
||||||
|
const redMeleeMinion = new Entity({
|
||||||
|
...minionTemplate,
|
||||||
|
logic: redMinionLogic,
|
||||||
|
team: Team.red,
|
||||||
|
position: new Vector2(1800, 1800),
|
||||||
|
})
|
||||||
|
redMeleeMinion.abilities[0] = Ability.meleeAttack
|
||||||
|
|
||||||
|
if (game.currentTick % (30 * game.tickRate) == (0 * game.tickRate)) {
|
||||||
|
game.spawnEntity(blueMeleeMinion)
|
||||||
|
game.spawnEntity(redMeleeMinion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.currentTick % (30 * game.tickRate) == (1 * game.tickRate)) {
|
||||||
|
game.spawnEntity(blueMeleeMinion)
|
||||||
|
game.spawnEntity(redMeleeMinion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.currentTick % (30 * game.tickRate) == (2 * game.tickRate)) {
|
||||||
|
game.spawnEntity(blueMeleeMinion)
|
||||||
|
game.spawnEntity(redMeleeMinion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.currentTick % (30 * game.tickRate) == (3 * game.tickRate)) {
|
||||||
|
game.spawnEntity(blueMinion)
|
||||||
|
game.spawnEntity(redMinion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.currentTick % (30 * game.tickRate) == (4 * game.tickRate)) {
|
||||||
|
game.spawnEntity(blueMinion)
|
||||||
|
game.spawnEntity(redMinion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.currentTick % (30 * game.tickRate) == (5 * game.tickRate)) {
|
||||||
|
game.spawnEntity(blueMinion)
|
||||||
|
game.spawnEntity(redMinion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entity2.attackAction(1600, 1800)
|
||||||
|
entity1.attackAction(500, 150)
|
||||||
|
game.logic = gameLogic
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server started! Visit http://localhost:${port}`)
|
console.log(`Server started! Visit http://localhost:${port}`)
|
||||||
|
|
||||||
laneScenario()
|
laneScenario()
|
||||||
game.entities[0].castAction(3, 2000, 2000)
|
|
||||||
|
|
||||||
game.start()
|
game.start()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default class Team {
|
||||||
|
static neutral = 'neutral'
|
||||||
|
static blue = 'blue'
|
||||||
|
static red = 'red'
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user