diff --git a/src/ability.js b/src/ability.js index 9a16d61..aafc75b 100644 --- a/src/ability.js +++ b/src/ability.js @@ -49,7 +49,7 @@ export default class Ability { 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.cooldown(ability.id) }, @@ -60,31 +60,22 @@ export default class Ability { name: 'Ranged Attack', castTime: 0.25, cooldown: 1.25, - damage: 5, + damage: 25, radius: 5, range: 500, speed: 600, effect: function rangedAttackEffect(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() - if (newDistance < distance) { - closest = e - distance = newDistance - } - }) - - if (closest == null) { return } + const target = caster.closestTargetTo(cursor, ability.range) + if (target == null) { return } const rangedAttackAfter = function rangedAttackAfter() { - closest.damage(ability.damage) + target.damage(ability.damage) } const projectile = new Projectile({ after: rangedAttackAfter, - homingTarget: closest, + homingTarget: target, owner: caster, position: caster.position.clone(), radius: ability.radius, @@ -106,19 +97,10 @@ export default class Ability { 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 - } - }) + const target = caster.closestTargetTo(cursor, ability.range) + if (target == null) { return } - if (closest == null) { return } - - closest.damage(ability.damage) + target.damage(ability.damage) caster.cooldown(ability.id) }, }) @@ -153,7 +135,7 @@ export default class Ability { 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.cooldown(ability.id) }, @@ -168,8 +150,9 @@ export default class Ability { effect: function blinkEffect(caster, cursor) { const ability = this const direction = cursor.clone().sub(caster.position) - if (direction.length() > ability.range) { - direction.normalize().multiplyScalar(ability.range) + const realRange = ability.range + caster.radius + if (direction.length() > realRange) { + direction.normalize().multiplyScalar(realRange) } const destination = caster.position.clone().add(direction) diff --git a/src/entity.js b/src/entity.js index 1715efe..5d3545b 100644 --- a/src/entity.js +++ b/src/entity.js @@ -159,6 +159,14 @@ export default class Entity { 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) { this.health = Math.min(Math.max(0, this.health - amount), this.maxHealth) } @@ -167,6 +175,10 @@ export default class Entity { this.game?.despawn(this) } + distanceTo(vector) { + return this.position.distanceTo(vector) + } + heal(amount) { 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.#attacking) { - const attackCursor = 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 target = targets.reduce((prev, e) => { - if (prev == null || e.position.clone().sub(attackCursor).length() > prev.position.clone().sub(attackCursor).length()) { - return e - } - 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 cursor = this.#dest ?? this.position + const basicAttack = this.abilities[0] + const target = this.closestTargetTo(cursor, basicAttack.range) + if (target != null && this.distanceTo(target.position) < basicAttack.range) { + const cooldown = this.game?.secToTick(basicAttack.cooldown) ?? 0 + const lastCast = this.cooldowns[basicAttack.id] const timestamp = this.game?.currentTick ?? 0 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 } } diff --git a/src/index.js b/src/index.js index 99fe2d2..de50c8b 100644 --- a/src/index.js +++ b/src/index.js @@ -66,25 +66,27 @@ function laneScenario() { } } - const entity1 = new Entity({ - id: '1', + const playerTemplate = { height: 80, 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), - radius: 50, team: Team.blue, }) game.spawnEntity(entity1) const entity2 = new Entity({ + ...playerTemplate, id: '2', - height: 80, - logic: playerLogic, - maxHealth: 100, spawnPosition: new Vector2(1600, 1800), - radius: 50, team: Team.red, }) @@ -115,39 +117,38 @@ function laneScenario() { midSouthWall.id = 'midSouthWall' game.addTerrain(midSouthWall) - const blueMinionLogic = function minionLogic() { - const entity = this - if (entity.dead) { entity.despawn() } + const minionLogic = (team) => { + const finalGoal = team == Team.blue ? new Vector2(1900, 1900) : new Vector2(100, 100) + 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) - if (entity.position.x < 800 || entity.position.y < 1100) { - goal = new Vector2(850, 1150) + return function builtMinionLogic() { + const entity = this + 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 = { - health: 20, - maxHealth: 20, - radius: 30, - speed: 300, + height: 40, + maxHealth: 300, + radius: 48, + speed: 325, + } + + const meleeMinionTemplate = { + ...minionTemplate, + height: 38, + radius: 46, + maxHealth: 450, } const gameLogic = function gameLogic() { @@ -155,14 +156,14 @@ function laneScenario() { const blueMinion = new Entity({ ...minionTemplate, - logic: blueMinionLogic, + logic: minionLogic(Team.blue), team: Team.blue, position: new Vector2(200, 200), }) const blueMeleeMinion = new Entity({ - ...minionTemplate, - logic: blueMinionLogic, + ...meleeMinionTemplate, + logic: minionLogic(Team.blue), team: Team.blue, position: new Vector2(200, 200), }) @@ -170,14 +171,14 @@ function laneScenario() { const redMinion = new Entity({ ...minionTemplate, - logic: redMinionLogic, + logic: minionLogic(Team.red), team: Team.red, position: new Vector2(1800, 1800), }) const redMeleeMinion = new Entity({ - ...minionTemplate, - logic: redMinionLogic, + ...meleeMinionTemplate, + logic: minionLogic(Team.red), team: Team.red, position: new Vector2(1800, 1800), }) diff --git a/src/projectile.js b/src/projectile.js index 6d0a14b..23132e0 100644 --- a/src/projectile.js +++ b/src/projectile.js @@ -5,65 +5,42 @@ import { Vector2 } from 'three' export default class Projectile { id = crypto.randomUUID() after = null - speed = 1000 - radius = 5 - owner = null - onCollide = null height = 50 + onCollide = null + owner = null + position = new Vector2() + radius = 5 + speed = 1000 - #position = new Vector2() #dest = null #homingTarget = null #game = null - - get collider() { - return new SAT.Circle(new SAT.Vector(this.x, this.y), this.radius) - } - get homing() { - return !!this.#homingTarget - } + get game() { return this.#game } + set game(value) { this.#game = value } + set destination(value) { this.#dest = value } + set homingTarget(value) { this.#homingTarget = value } get destination() { return this.#dest ?? this.#homingTarget?.position } - set homingTarget(value) { - this.#homingTarget = value - } - constructor(options = {}) { 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() { (this.game?.entities ?? []).filter((e) => e.id != this.id).forEach((e) => { 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) } }) } - - checkIfArrived() { - if (!this.#position.equals(this.destination)) { return } - - if (this.after != null) { - this.after(this, this.#homingTarget) - } - - this.despawn() + + collider() { + return new SAT.Circle(new SAT.Vector(this.x, this.y), this.radius) } despawn() { @@ -73,28 +50,32 @@ export default class Projectile { state() { return { ...this, - position: { - x: this.x, - y: this.y, - }, } } - takeStep() { - const speed = (this.speed / (this.game?.tickBudget ?? 1000)) - const destination = this.destination - 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.#move() + if (this.onCollide != null) { this.checkCollisions() } + this.#checkIfArrived() } - update() { - this.takeStep() - if (this.onCollide != null) { this.checkCollisions() } - this.checkIfArrived() + #checkIfArrived() { + if (!this.position.equals(this.destination)) { return } + + 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) } }