inflate ranges by entity radii

This commit is contained in:
2025-01-13 16:17:34 +09:00
parent 16429a6e1b
commit ffbc4d9803
4 changed files with 110 additions and 141 deletions
+13 -30
View File
@@ -49,7 +49,7 @@ export default class Ability {
speed: ability.speed, speed: ability.speed,
}) })
projectile.destination = caster.position.clone().add(cursor.clone().sub(caster.position).normalize().multiplyScalar(ability.range)) projectile.destination = caster.position.clone().add(cursor.clone().sub(caster.position).normalize().multiplyScalar(ability.range + caster.radius))
caster.game?.spawnProjectile(projectile) caster.game?.spawnProjectile(projectile)
caster.cooldown(ability.id) caster.cooldown(ability.id)
}, },
@@ -60,31 +60,22 @@ export default class Ability {
name: 'Ranged Attack', name: 'Ranged Attack',
castTime: 0.25, castTime: 0.25,
cooldown: 1.25, cooldown: 1.25,
damage: 5, damage: 25,
radius: 5, radius: 5,
range: 500, range: 500,
speed: 600, speed: 600,
effect: function rangedAttackEffect(caster, cursor) { effect: function rangedAttackEffect(caster, cursor) {
const ability = this const ability = this
let closest = null const target = caster.closestTargetTo(cursor, ability.range)
let distance = Infinity if (target == null) { return }
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()
if (newDistance < distance) {
closest = e
distance = newDistance
}
})
if (closest == null) { return }
const rangedAttackAfter = function rangedAttackAfter() { const rangedAttackAfter = function rangedAttackAfter() {
closest.damage(ability.damage) target.damage(ability.damage)
} }
const projectile = new Projectile({ const projectile = new Projectile({
after: rangedAttackAfter, after: rangedAttackAfter,
homingTarget: closest, homingTarget: target,
owner: caster, owner: caster,
position: caster.position.clone(), position: caster.position.clone(),
radius: ability.radius, radius: ability.radius,
@@ -106,19 +97,10 @@ export default class Ability {
range: 100, range: 100,
effect: function meleeAttackEffect(caster, cursor) { effect: function meleeAttackEffect(caster, cursor) {
const ability = this const ability = this
let closest = null const target = caster.closestTargetTo(cursor, ability.range)
let distance = Infinity if (target == null) { return }
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 } target.damage(ability.damage)
closest.damage(ability.damage)
caster.cooldown(ability.id) caster.cooldown(ability.id)
}, },
}) })
@@ -153,7 +135,7 @@ export default class Ability {
speed: ability.speed, speed: ability.speed,
}) })
projectile.destination = caster.position.clone().add(cursor.clone().sub(caster.position).normalize().multiplyScalar(ability.range)) projectile.destination = caster.position.clone().add(cursor.clone().sub(caster.position).normalize().multiplyScalar(ability.range + caster.radius))
caster.game?.spawnProjectile(projectile) caster.game?.spawnProjectile(projectile)
caster.cooldown(ability.id) caster.cooldown(ability.id)
}, },
@@ -168,8 +150,9 @@ export default class Ability {
effect: function blinkEffect(caster, cursor) { effect: function blinkEffect(caster, cursor) {
const ability = this const ability = this
const direction = cursor.clone().sub(caster.position) const direction = cursor.clone().sub(caster.position)
if (direction.length() > ability.range) { const realRange = ability.range + caster.radius
direction.normalize().multiplyScalar(ability.range) if (direction.length() > realRange) {
direction.normalize().multiplyScalar(realRange)
} }
const destination = caster.position.clone().add(direction) const destination = caster.position.clone().add(direction)
+19 -15
View File
@@ -159,6 +159,14 @@ export default class Entity {
this.cooldowns[id] = this.game?.currentTick ?? 0 this.cooldowns[id] = this.game?.currentTick ?? 0
} }
closestTargetTo(cursor, range) {
return this
.game
?.entities
.filter((e) => this.team != e.team && e.distanceTo(cursor) <= range + this.radius + e.radius)
.reduce((e1, e2) => (e1?.distanceTo(cursor) ?? Infinity) < e2.distanceTo(cursor) ? e1 : e2, null)
}
damage(amount) { damage(amount) {
this.health = Math.min(Math.max(0, this.health - amount), this.maxHealth) this.health = Math.min(Math.max(0, this.health - amount), this.maxHealth)
} }
@@ -167,6 +175,10 @@ export default class Entity {
this.game?.despawn(this) this.game?.despawn(this)
} }
distanceTo(vector) {
return this.position.distanceTo(vector)
}
heal(amount) { heal(amount) {
this.health = Math.min(Math.max(0, this.health + amount), this.maxHealth) this.health = Math.min(Math.max(0, this.health + amount), this.maxHealth)
} }
@@ -260,24 +272,16 @@ export default class Entity {
if (this.casting != null) { return false } if (this.casting != null) { return false }
if (this.#attacking) { if (this.#attacking) {
const attackCursor = this.#dest ?? this.position const cursor = this.#dest ?? this.position
const targets = this.game?.entities.filter((e) => e.team != this.team && e.position.clone().sub(attackCursor).length() < this.abilities[0].range) const basicAttack = this.abilities[0]
const target = targets.reduce((prev, e) => { const target = this.closestTargetTo(cursor, basicAttack.range)
if (prev == null || e.position.clone().sub(attackCursor).length() > prev.position.clone().sub(attackCursor).length()) { if (target != null && this.distanceTo(target.position) < basicAttack.range) {
return e const cooldown = this.game?.secToTick(basicAttack.cooldown) ?? 0
} const lastCast = this.cooldowns[basicAttack.id]
else {
return prev
}
}, null)
if (target != null && target.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 const timestamp = this.game?.currentTick ?? 0
if (lastCast != null && lastCast + cooldown > timestamp) { return false } if (lastCast != null && lastCast + cooldown > timestamp) { return false }
this.castAction(0, attackCursor.x, attackCursor.y, false) this.castAction(0, cursor.x, cursor.y, false)
return true return true
} }
} }
+43 -42
View File
@@ -66,25 +66,27 @@ function laneScenario() {
} }
} }
const entity1 = new Entity({ const playerTemplate = {
id: '1',
height: 80, height: 80,
logic: playerLogic, logic: playerLogic,
maxHealth: 100, maxHealth: 600,
spawnPosition: new Vector2(500, 150),
radius: 65,
}
const entity1 = new Entity({
...playerTemplate,
id: '1',
spawnPosition: new Vector2(500, 150), spawnPosition: new Vector2(500, 150),
radius: 50,
team: Team.blue, team: Team.blue,
}) })
game.spawnEntity(entity1) game.spawnEntity(entity1)
const entity2 = new Entity({ const entity2 = new Entity({
...playerTemplate,
id: '2', id: '2',
height: 80,
logic: playerLogic,
maxHealth: 100,
spawnPosition: new Vector2(1600, 1800), spawnPosition: new Vector2(1600, 1800),
radius: 50,
team: Team.red, team: Team.red,
}) })
@@ -115,39 +117,38 @@ function laneScenario() {
midSouthWall.id = 'midSouthWall' midSouthWall.id = 'midSouthWall'
game.addTerrain(midSouthWall) game.addTerrain(midSouthWall)
const blueMinionLogic = function minionLogic() { const minionLogic = (team) => {
const entity = this const finalGoal = team == Team.blue ? new Vector2(1900, 1900) : new Vector2(100, 100)
if (entity.dead) { entity.despawn() } const subGoal = new Vector2(850, 1150)
const subGoalCheck = team == Team.blue ? ((entity) => entity.position.x < 800 || entity.position.y < 1100) : ((entity) => entity.position.x > 900 || entity.position.y > 1200)
let goal = new Vector2(1900, 1900) return function builtMinionLogic() {
if (entity.position.x < 800 || entity.position.y < 1100) { const entity = this
goal = new Vector2(850, 1150) if (entity.dead) { entity.despawn() }
let goal = finalGoal
if (subGoalCheck(entity)) {
goal = subGoal
}
const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(100)
const fakeDestination = entity.position.clone().add(direction)
entity.attackAction(fakeDestination.x, fakeDestination.y)
} }
const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(75)
const subGoal = entity.position.clone().add(direction)
entity.attackAction(subGoal.x, subGoal.y)
}
const redMinionLogic = function minionLogic() {
const entity = this
if (entity.dead) { entity.despawn() }
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)
} }
const minionTemplate = { const minionTemplate = {
health: 20, height: 40,
maxHealth: 20, maxHealth: 300,
radius: 30, radius: 48,
speed: 300, speed: 325,
}
const meleeMinionTemplate = {
...minionTemplate,
height: 38,
radius: 46,
maxHealth: 450,
} }
const gameLogic = function gameLogic() { const gameLogic = function gameLogic() {
@@ -155,14 +156,14 @@ function laneScenario() {
const blueMinion = new Entity({ const blueMinion = new Entity({
...minionTemplate, ...minionTemplate,
logic: blueMinionLogic, logic: minionLogic(Team.blue),
team: Team.blue, team: Team.blue,
position: new Vector2(200, 200), position: new Vector2(200, 200),
}) })
const blueMeleeMinion = new Entity({ const blueMeleeMinion = new Entity({
...minionTemplate, ...meleeMinionTemplate,
logic: blueMinionLogic, logic: minionLogic(Team.blue),
team: Team.blue, team: Team.blue,
position: new Vector2(200, 200), position: new Vector2(200, 200),
}) })
@@ -170,14 +171,14 @@ function laneScenario() {
const redMinion = new Entity({ const redMinion = new Entity({
...minionTemplate, ...minionTemplate,
logic: redMinionLogic, logic: minionLogic(Team.red),
team: Team.red, team: Team.red,
position: new Vector2(1800, 1800), position: new Vector2(1800, 1800),
}) })
const redMeleeMinion = new Entity({ const redMeleeMinion = new Entity({
...minionTemplate, ...meleeMinionTemplate,
logic: redMinionLogic, logic: minionLogic(Team.red),
team: Team.red, team: Team.red,
position: new Vector2(1800, 1800), position: new Vector2(1800, 1800),
}) })
+34 -53
View File
@@ -5,65 +5,42 @@ import { Vector2 } from 'three'
export default class Projectile { export default class Projectile {
id = crypto.randomUUID() id = crypto.randomUUID()
after = null after = null
speed = 1000
radius = 5
owner = null
onCollide = null
height = 50 height = 50
onCollide = null
owner = null
position = new Vector2()
radius = 5
speed = 1000
#position = new Vector2()
#dest = null #dest = null
#homingTarget = null #homingTarget = null
#game = null #game = null
get collider() { get game() { return this.#game }
return new SAT.Circle(new SAT.Vector(this.x, this.y), this.radius) set game(value) { this.#game = value }
} set destination(value) { this.#dest = value }
set homingTarget(value) { this.#homingTarget = value }
get homing() {
return !!this.#homingTarget
}
get destination() { get destination() {
return this.#dest ?? this.#homingTarget?.position return this.#dest ?? this.#homingTarget?.position
} }
set homingTarget(value) {
this.#homingTarget = value
}
constructor(options = {}) { constructor(options = {}) {
Object.entries(options).forEach(([key, value]) => this[key] = value) Object.entries(options).forEach(([key, value]) => 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 }
set position(value) { this.#position = value }
checkCollisions() { checkCollisions() {
(this.game?.entities ?? []).filter((e) => e.id != this.id).forEach((e) => { (this.game?.entities ?? []).filter((e) => e.id != this.id).forEach((e) => {
if (e.id == this.owner?.id) { return } if (e.id == this.owner?.id) { return }
if (SATX.collideObject(this.collider, e.collider)) { if (SATX.collideObject(this.collider(), e.collider)) {
this.onCollide(this, e) this.onCollide(this, e)
} }
}) })
} }
checkIfArrived() { collider() {
if (!this.#position.equals(this.destination)) { return } return new SAT.Circle(new SAT.Vector(this.x, this.y), this.radius)
if (this.after != null) {
this.after(this, this.#homingTarget)
}
this.despawn()
} }
despawn() { despawn() {
@@ -73,28 +50,32 @@ export default class Projectile {
state() { state() {
return { return {
...this, ...this,
position: {
x: this.x,
y: this.y,
},
} }
} }
takeStep() { update() {
const speed = (this.speed / (this.game?.tickBudget ?? 1000)) this.#move()
const destination = this.destination if (this.onCollide != null) { this.checkCollisions() }
const difference = destination.clone().sub(this.position) this.#checkIfArrived()
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() { #checkIfArrived() {
this.takeStep() if (!this.position.equals(this.destination)) { return }
if (this.onCollide != null) { this.checkCollisions() }
this.checkIfArrived() if (this.after != null) {
this.after(this, this.#homingTarget)
}
this.despawn()
}
#move() {
const speed = (this.speed / (this.game?.tickBudget ?? 1000))
if (this.position.distanceTo(this.destination) < speed) {
this.position.copy(this.destination)
}
const step = this.destination.clone().sub(this.position).normalize().multiplyScalar(speed)
this.position.add(step)
} }
} }