diff --git a/src/BlueForceControl.ts b/src/BlueForceControl.ts index ce6dc2d..ffe0451 100644 --- a/src/BlueForceControl.ts +++ b/src/BlueForceControl.ts @@ -62,8 +62,12 @@ export default class BlueForceControl extends ForceControl { nextPos += 64 + 15 this.liftableBodies = this.scene.physics.add.group() - + nextPos += 100 + this.buildMissileBase(nextPos + 16, this.liftableBodies) + nextPos += 32 + 15 + + nextPos += 50 this.buildAAGun(nextPos + 16, this.liftableBodies) nextPos += 32 + 15 diff --git a/src/ForceControl.ts b/src/ForceControl.ts index e775b4a..9aa1b65 100644 --- a/src/ForceControl.ts +++ b/src/ForceControl.ts @@ -5,6 +5,8 @@ import SoldierGameObject from "./mobile-objects/SoldierGameObject" import TankGameObject from "./mobile-objects/TankGameObject" import AAGunGameObject from "./static-objects/AAGunGameObject" import BunkerGameObject from "./static-objects/BunkerGameObject" +import MissileLauncherGameObject from "./mobile-objects/MissileLauncherGameObject" +import MissileBaseGameObject from "./static-objects/MissileBaseGameObject" export default abstract class ForceControl { protected abstract factory: FactoryGameObject @@ -14,6 +16,8 @@ export default abstract class ForceControl { protected tankObjects = new Set() protected soliderObjects = new Set() protected aaGunObjects = new Set() + protected missileLauncherObjects = new Set() + protected missileBaseObjects = new Set() protected bunkerObjects = new Set() constructor(protected readonly scene: GameScene, protected readonly owner: number) {} @@ -21,6 +25,8 @@ export default abstract class ForceControl { update(time: number, delta: number): void { this.soliderObjects.forEach(soldier => soldier.update(time, delta)) this.tankObjects.forEach(tank => tank.update(time, delta)) + this.missileLauncherObjects.forEach(m => m.update(time, delta)) + this.missileBaseObjects.forEach(m => m.update(time, delta)) this.aaGunObjects.forEach(gun => gun.update(time, delta)) this.bunkerObjects.forEach(b => b.update(time, delta)) } @@ -46,6 +52,29 @@ export default abstract class ForceControl { } } + protected buildMissileLauncher(): void { + const obj = new MissileLauncherGameObject(this.scene, this.owner, this.factory.spawnX, 50) + obj.showHealthBar(50) + obj.move(50 * this.owner, this.owner < 0) + this.missileLauncherObjects.add(obj) + this.scene.gameMap.add(obj) + obj.destroyCallback = () => { + this.missileLauncherObjects.delete(obj) + this.scene.gameMap.remove(obj) + } + } + + protected buildMissileBase(x: number, group: Phaser.Physics.Arcade.Group): void { + const obj = new MissileBaseGameObject(this.scene, this.owner, x, group) + obj.showHealthBar(50) + this.missileBaseObjects.add(obj) + this.scene.gameMap.add(obj) + obj.destroyCallback = () => { + this.missileBaseObjects.delete(obj) + this.scene.gameMap.remove(obj) + } + } + protected buildTank(): void { const obj = new TankGameObject(this.scene, this.owner, this.factory.spawnX, 50) obj.showHealthBar(50) diff --git a/src/GameMap.ts b/src/GameMap.ts index 5a002e5..5f551ac 100644 --- a/src/GameMap.ts +++ b/src/GameMap.ts @@ -1,10 +1,12 @@ import GameObject from "./GameObject" +import ChopperGameObject from "./mobile-objects/ChopperGameObject" export default class GameMap { private static readonly updateInterval = 32 private readonly cellCount: number private lastUpdate = 0 + private allGameObjects = new Map>() private gameObjects = new Map[]>() private gameObjectCells = new Map() @@ -18,6 +20,8 @@ export default class GameMap { } this.gameObjects.set(-1, redCells) this.gameObjects.set(1, blueCells) + this.allGameObjects.set(-1, new Set()) + this.allGameObjects.set(1, new Set()) } add(obj: GameObject): void { @@ -26,6 +30,7 @@ export default class GameMap { const cells = [this.getCell(obj.x1), this.getCell(obj.x2)] for (let i = cells[0]; i <= cells[1]; i++) objs[i].add(obj) this.gameObjectCells.set(obj, cells) + this.allGameObjects.get(obj.owner).add(obj) } } @@ -34,9 +39,28 @@ export default class GameMap { if (objs) { const cells = this.gameObjectCells.get(obj) for (let i = cells[0]; i <= cells[1]; i++) objs[i].delete(obj) + this.allGameObjects.get(obj.owner).delete(obj) } } + findObject(owner: number, predicate: (obj: GameObject) => boolean): GameObject { + const objs = this.allGameObjects.get(owner).values() + let obj = objs.next() + while (!obj.done) { + if (predicate(obj.value)) return obj.value as GameObject + obj = objs.next() + } + return undefined + } + + findChopper(subject: GameObject, distanceSq: number): ChopperGameObject { + return this.findObject(-subject.owner, (obj) => { + if (obj instanceof ChopperGameObject) { + return Phaser.Math.Distance.BetweenPointsSquared(subject, obj) < distanceSq + } else return false + }) as ChopperGameObject + } + getObjectsWithin(owner: number, x1: number, x2: number, filter?: (obj: GameObject) => boolean): Set { const c1 = this.getCell(x1) diff --git a/src/GameObject.ts b/src/GameObject.ts index a4729f9..06b729d 100644 --- a/src/GameObject.ts +++ b/src/GameObject.ts @@ -1,6 +1,7 @@ import Behaviour from "./Behaviour" import GameScene from "./GameScene" import { BulletGameObject, BulletType } from "./mobile-objects/BulletGameObject" +import { MissileGameObject } from "./mobile-objects/MissileGameObject" import HealthBarGraphics from "./ui-objects/HealthBarGraphics" interface WrappedPhaserGameObject { @@ -11,6 +12,8 @@ export default abstract class GameObject { protected _destroyCallback: () => void set destroyCallback(cb: () => void) { this._destroyCallback = cb } + abstract readonly x: number + abstract readonly y: number abstract readonly x1: number abstract readonly x2: number abstract readonly y1: number @@ -39,13 +42,15 @@ export default abstract class GameObject { // eslint-disable-next-line @typescript-eslint/no-unused-vars update(time: number, delta: number): void { - this.healthBar?.draw(this.x1 + (this.width - this.healthBar.width) / 2, this.y1 - 5, this._health) + this.healthBar?.draw(this.x - this.healthBar.width / 2, this.y1 - 5, this._health) if (this.behaviourActive) this.behaviour?.update(time) } // eslint-disable-next-line @typescript-eslint/no-unused-vars moveTo(x: number, y: number): void { throw "object cannot be moved" } + abstract setVisible(visible: boolean): void + abstract remove(): void removed(): void { @@ -73,14 +78,26 @@ export default abstract class GameObject { addBulletResponse(subject: Phaser.GameObjects.GameObject, damageMap: Map, defaultDamage = 0): void { - this.scene.physics.add.overlap(subject, this.scene.getBulletBodies(this.owner * -1), (me, bullet) => { + this.scene.physics.add.overlap(subject, this.scene.getBulletBodies(this.owner * -1), (me, b) => { if (!this.dying) { - const bulletType = (this.getWrapper(bullet) as BulletGameObject).bulletType - this._health -= damageMap.get(bulletType) ?? defaultDamage + const bullet = this.getWrapper(b) as BulletGameObject + this._health -= damageMap.get(bullet.bulletType) ?? defaultDamage this.healthCallback?.(this._health) this.scene.removeBullet(bullet) if (this._health <= 0) this.die() } }) } + + addMissileResponse(subject: Phaser.GameObjects.GameObject, damage: number): void { + this.scene.physics.add.overlap(subject, this.scene.getMissileBodies(this.owner * -1), (me, m) => { + if (!this.dying) { + const missile = this.getWrapper(m) as MissileGameObject + this._health -= damage + this.healthCallback?.(this._health) + this.scene.removeMissile(missile) + if (this._health <= 0) this.die() + } + }) + } } diff --git a/src/GameScene.ts b/src/GameScene.ts index 444779e..5cc39f1 100644 --- a/src/GameScene.ts +++ b/src/GameScene.ts @@ -16,12 +16,18 @@ import TreeGameObject from './static-objects/TreeGameObject' import VillageGameObject from './static-objects/VillageGameObject' import GameMap from './GameMap' import BunkerGameObject from './static-objects/BunkerGameObject' +import { MissileGameObject } from './mobile-objects/MissileGameObject' +import GameObject from './GameObject' +import MissileLauncherGameObject from './mobile-objects/MissileLauncherGameObject' +import MissileBaseGameObject from './static-objects/MissileBaseGameObject' export default class GameScene extends Phaser.Scene { private frameTime = 0 private treeObjects: TreeGameObject[] = [] private bulletObjects = new Map() private bulletBodies = new Map() + private missileObjects = new Map() + private missileBodies = new Map() private blueControl: BlueForceControl private redControl: RedForceControl @@ -59,6 +65,7 @@ export default class GameScene extends Phaser.Scene { HomeBuildingGameObject.preload(this) VillageGameObject.preload(this) BulletGameObject.preload(this) + MissileGameObject.preload(this) ChopperGameObject.preload(this) AAGunGameObject.preload(this) SoldierGameObject.preload(this) @@ -68,6 +75,8 @@ export default class GameScene extends Phaser.Scene { BarrackGameObject.preload(this) BunkerGameObject.preload(this) TankGameObject.preload(this) + MissileLauncherGameObject.preload(this) + MissileBaseGameObject.preload(this) } private createAnims(): void { @@ -103,6 +112,8 @@ export default class GameScene extends Phaser.Scene { this.bulletBodies.set(-1, this.physics.add.group()) this.bulletBodies.set(1, this.physics.add.group()) + this.missileBodies.set(-1, this.physics.add.group()) + this.missileBodies.set(1, this.physics.add.group()) HelipadGameObject.createCommon(this) SoldierGameObject.createCommon(this) @@ -114,6 +125,8 @@ export default class GameScene extends Phaser.Scene { this.cameras.main.startFollow(this.blueControl.chopper.sprite) } + getBulletBodies(owner: number): Phaser.Physics.Arcade.Group { return this.bulletBodies.get(owner) } + createBullet(owner: number, duration: number, x: number, y: number, velocityX: number, velocityY: number, type: BulletType): BulletGameObject { @@ -124,11 +137,26 @@ export default class GameScene extends Phaser.Scene { return bullet } - removeBullet(b: Phaser.GameObjects.GameObject): void { - this.bulletObjects.get(b)?.remove() + removeBullet(b: BulletGameObject): void { + this.bulletObjects.get(b.sprite)?.remove() } - getBulletBodies(owner: number): Phaser.Physics.Arcade.Group { return this.bulletBodies.get(owner) } + removeMissile(m: MissileGameObject): void { + m.die() + } + + getMissileBodies(owner: number): Phaser.Physics.Arcade.Group { return this.missileBodies.get(owner) } + + createMissile(owner: number, + duration: number, x: number, y: number, + velocityX: number, velocityY: number, + maxAcceleration: number, target: GameObject): MissileGameObject { + const missile = new MissileGameObject(this, owner, this.missileBodies.get(owner), duration, + x, y, velocityX, velocityY, maxAcceleration, target) + this.missileObjects.set(missile.sprite, missile) + missile.destroyCallback = () => this.missileObjects.delete(missile.sprite) + return missile + } update(time: number, delta: number): void { this.frameTime += delta @@ -139,6 +167,7 @@ export default class GameScene extends Phaser.Scene { this.redControl.update(time, delta) this.treeObjects.forEach(tree => tree.update()) this.bulletObjects.forEach(bullet => bullet.update(time, delta)) + this.missileObjects.forEach(m => m.update(time, delta)) } } diff --git a/src/PhysicsBodyGameObject.ts b/src/PhysicsBodyGameObject.ts index 141b207..2fb971d 100644 --- a/src/PhysicsBodyGameObject.ts +++ b/src/PhysicsBodyGameObject.ts @@ -8,6 +8,8 @@ export default abstract class PhysicsBodyGameObject extends GameObject { this._mainBody = b } + get x(): number { return this._mainBody.x + this._mainBody.width / 2 } + get y(): number { return this._mainBody.y + this._mainBody.height / 2 } get x1(): number { return this._mainBody.x } get x2(): number { return this._mainBody.x + this._mainBody.width } get y1(): number { return this._mainBody.y } @@ -15,17 +17,13 @@ export default abstract class PhysicsBodyGameObject extends GameObject { get width(): number { return this._mainBody.width } get height(): number { return this._mainBody.height } - protected abstract setVisible(visible: boolean): void - die(): void { super.beforeDie() this.setVisible(false) this._mainBody.setVelocityX(0) this._mainBody.setEnable(false) - const explosion = this.scene.add.sprite(this._mainBody.x + this._mainBody.width / 2, - this._mainBody.y + this._mainBody.height / 2, - "explode", 0) + const explosion = this.scene.add.sprite(this.x, this.y, "explode", 0) explosion.setDepth(1) explosion.anims.play("explode") explosion.once("animationcomplete", () => { diff --git a/src/RedForceControl.ts b/src/RedForceControl.ts index 403a09c..bba0133 100644 --- a/src/RedForceControl.ts +++ b/src/RedForceControl.ts @@ -30,14 +30,18 @@ export default class RedForceControl extends ForceControl { this.barrack = new BarrackGameObject(scene, -1, nextPos - 32) nextPos -= 64 + 15 - + nextPos -= 100 const gunBodies = scene.physics.add.group() + this.buildMissileBase(nextPos - 16, gunBodies) + nextPos -= 32 + 15 + + nextPos -= 50 this.buildAAGun(nextPos - 16, gunBodies) nextPos -= 32 + 15 setTimeout(() => this.buildSoldier(), 2000) - setTimeout(() => this.buildTank(), 5000) + setTimeout(() => this.buildMissileLauncher(), 5000) } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/SpriteGameObject.ts b/src/SpriteGameObject.ts index cc81f00..720954f 100644 --- a/src/SpriteGameObject.ts +++ b/src/SpriteGameObject.ts @@ -12,6 +12,10 @@ export default class SpriteGameObject extends GameObject { this.halfHeight = s.height / 2 } + get sprite(): Phaser.GameObjects.Sprite { return this._mainSprite } + + get x(): number { return this._mainSprite.x } + get y(): number { return this._mainSprite.y } get x1(): number { return this._mainSprite.x - this.halfWidth } get x2(): number { return this._mainSprite.x + this.halfWidth } get y1(): number { return this._mainSprite.y - this.halfHeight } @@ -24,6 +28,10 @@ export default class SpriteGameObject extends GameObject { this.mainSprite.setY(Math.min(this.scene.groundPos - this.halfHeight, y)) } + setVisible(visible: boolean): void { + this.mainSprite.setVisible(visible) + } + remove(): void { this._mainSprite.destroy() this.removed() diff --git a/src/behaviours/ScanStopShootBehaviour.ts b/src/behaviours/ScanStopShootBehaviour.ts index 9bf4fad..59d29fd 100644 --- a/src/behaviours/ScanStopShootBehaviour.ts +++ b/src/behaviours/ScanStopShootBehaviour.ts @@ -1,17 +1,17 @@ import GameObject from "../GameObject" export interface ScanStopShootBehaviourCallback { - findTarget(): boolean + findTarget(): Set isMoving(): boolean stop(): void move(): void - createBullet(): void + createBullet(targets: Set): void } export class ScanStopShootBehaviour { private lastFired = 0 private lastScan = 0 - private haveTarget = false + private targets = new Set() constructor(private readonly subject: GameObject, private readonly scanInterval: number, private readonly fireInterval: number, private readonly callback: ScanStopShootBehaviourCallback) {} @@ -20,16 +20,16 @@ export class ScanStopShootBehaviour { if (this.subject.dying != true) { if ((time - this.lastScan) >= this.scanInterval) { this.lastScan = time - this.haveTarget = this.callback.findTarget() - if (this.haveTarget) { + this.targets = this.callback.findTarget() + if (this.targets.size > 0) { this.callback.stop() } else if (!this.callback.isMoving()) { this.callback.move() } } - if (this.haveTarget && (time - this.lastFired) > this.fireInterval) { + if (this.targets.size > 0 && (this.lastFired == 0 || (time - this.lastFired) > this.fireInterval)) { this.lastFired = time - this.callback.createBullet() + this.callback.createBullet(this.targets) } } } diff --git a/src/mobile-objects/BulletGameObject.ts b/src/mobile-objects/BulletGameObject.ts index 68ce709..b0ec53f 100644 --- a/src/mobile-objects/BulletGameObject.ts +++ b/src/mobile-objects/BulletGameObject.ts @@ -1,20 +1,17 @@ import GameScene from "../GameScene" -import PhysicsBodyGameObject from "../PhysicsBodyGameObject" +import SpriteGameObject from "../SpriteGameObject" export enum BulletType { Chopper, AA, Bunker, Tank, Rifle } -export class BulletGameObject extends PhysicsBodyGameObject { +export class BulletGameObject extends SpriteGameObject { private static typeToScale = new Map([ [BulletType.Chopper, 0.75], [BulletType.AA, 1], [BulletType.Bunker, 1.5], [BulletType.Tank, 1.5], [BulletType.Rifle, 0.5]]) - private readonly _sprite: Phaser.GameObjects.Sprite private startTime = 0 - get sprite(): Phaser.GameObjects.Sprite { return this._sprite } - constructor(scene: GameScene, owner: number, readonly physicsGroup: Phaser.Physics.Arcade.Group, readonly duration: number, @@ -22,23 +19,15 @@ export class BulletGameObject extends PhysicsBodyGameObject { velocityX: number, velocityY: number, readonly bulletType: BulletType) { super(scene, owner) - this._sprite = this.scene.add.sprite(x, y, this.owner > 0 ? "bullet-blue" : "bullet-red") - this._sprite.setDepth(200) - this.addWrapperProperty(this._sprite) + this.mainSprite = this.scene.add.sprite(x, y, this.owner > 0 ? "bullet-blue" : "bullet-red") + this.mainSprite.setDepth(200) + this.addWrapperProperty(this.mainSprite) const scale = BulletGameObject.typeToScale.get(bulletType) - this._sprite.setScale(scale, scale) - this.physicsGroup.add(this._sprite) - const body = this._sprite.body as Phaser.Physics.Arcade.Body + this.mainSprite.setScale(scale, scale) + this.physicsGroup.add(this.mainSprite) + const body = this.mainSprite.body as Phaser.Physics.Arcade.Body body.setAllowGravity(false) body.setVelocity(velocityX, velocityY) - body.mass = 0.01 - - this.mainBody = body - } - - remove(): void { - this._sprite.destroy() - this.removed() } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -47,15 +36,11 @@ export class BulletGameObject extends PhysicsBodyGameObject { if (this.startTime == 0) this.startTime = time else if ((time - this.startTime) > this.duration) this.remove() else { - if (this._sprite.x < 0 || this._sprite.x > this.scene.worldWidth || - this._sprite.y < 0 || this._sprite.y > this.scene.worldHeight) this.remove() + if (this.mainSprite.x < 0 || this.mainSprite.x > this.scene.worldWidth || + this.mainSprite.y < 0 || this.mainSprite.y > this.scene.worldHeight) this.remove() } } - protected setVisible(visible: boolean): void { - this._sprite.setVisible(visible) - } - static preload(scene: GameScene): void { scene.load.image("bullet-red", "/images/bullet-red.png") scene.load.image("bullet-blue", "/images/bullet-blue.png") diff --git a/src/mobile-objects/ChopperGameObject.ts b/src/mobile-objects/ChopperGameObject.ts index 88462e2..c8345af 100644 --- a/src/mobile-objects/ChopperGameObject.ts +++ b/src/mobile-objects/ChopperGameObject.ts @@ -76,6 +76,7 @@ export default class ChopperGameObject extends PhysicsBodyGameObject implements this.healthCallback = healthCallback this.addBulletResponse(this.bodySprite, ChopperGameObject.bulletDamageMap) + this.addMissileResponse(this.bodySprite, 80) this.ropeObject = this.scene.add.rectangle(x, y, 1, 1, 0xFFFFFF) this.ropeObject.setDepth(5) @@ -95,10 +96,11 @@ export default class ChopperGameObject extends PhysicsBodyGameObject implements remove(): void { this.bodySprite.destroy() this.tailSprite.destroy() + this.scene.gameMap.remove(this) this.removed() } - protected setVisible(visible: boolean): void { + setVisible(visible: boolean): void { this.bodySprite.setVisible(visible) this.tailSprite.setVisible(visible) } diff --git a/src/mobile-objects/CivilianGameObject.ts b/src/mobile-objects/CivilianGameObject.ts index 78a2239..855c01d 100644 --- a/src/mobile-objects/CivilianGameObject.ts +++ b/src/mobile-objects/CivilianGameObject.ts @@ -49,7 +49,7 @@ export default class CivilianGameObject extends HumanGameObject { if (this.state != CivilianState.GoingHome) { if ((time - this.lastLookForChopper) > CivilianGameObject.chopperLookInterval) { this.lastLookForChopper = time - if (!this.scene.chopper.onGround) { + if (!this.scene.chopper.onGround && !this.scene.chopper.dying) { const distSq = Phaser.Math.Distance.BetweenPointsSquared(body.position, this.scene.chopper.sprite) if (distSq <= CivilianGameObject.chopperWaveDistanceSq) { if (this.state != CivilianState.Wave && @@ -60,13 +60,15 @@ export default class CivilianGameObject extends HumanGameObject { (time - this.lastStateChange) > CivilianGameObject.minStateDuration) { this.wander(time) } - } else { + } else if (!this.scene.chopper.dying) { const d = body.x - this.scene.chopper.sprite.x if (Math.abs(d) <= CivilianGameObject.minBoardDistance) { this.lastStateChange = time this.state = CivilianState.Run this.move(CivilianGameObject.runSpeed * -Math.sign(d)) } + } else if (this.state != CivilianState.Move) { + this.wander(time) } } diff --git a/src/mobile-objects/HumanGameObject.ts b/src/mobile-objects/HumanGameObject.ts index 04354db..046b69d 100644 --- a/src/mobile-objects/HumanGameObject.ts +++ b/src/mobile-objects/HumanGameObject.ts @@ -53,7 +53,7 @@ export default class HumanGameObject extends PhysicsBodyGameObject { this.removed() } - protected setVisible(visible: boolean): void { + setVisible(visible: boolean): void { this.sprite.setVisible(visible) } diff --git a/src/mobile-objects/MissileGameObject.ts b/src/mobile-objects/MissileGameObject.ts new file mode 100644 index 0000000..3fc5385 --- /dev/null +++ b/src/mobile-objects/MissileGameObject.ts @@ -0,0 +1,69 @@ +import GameObject from "../GameObject" +import GameScene from "../GameScene" +import SpriteGameObject from "../SpriteGameObject" + +export class MissileGameObject extends SpriteGameObject { + private static trackInterval = 50 + + private readonly speed: number + + private startTime = 0 + private lastTrack = 0 + + constructor(scene: GameScene, owner: number, + readonly physicsGroup: Phaser.Physics.Arcade.Group, + readonly duration: number, + x: number, y: number, + velocityX: number, velocityY: number, + private readonly maxAcceleration: number, + private readonly target: GameObject) { + super(scene, owner) + this.mainSprite = this.scene.add.sprite(x, y, "missile") + this.mainSprite.setDepth(200) + this.addWrapperProperty(this.mainSprite) + this.physicsGroup.add(this.mainSprite) + const body = this.mainSprite.body as Phaser.Physics.Arcade.Body + body.setAllowGravity(false) + body.setVelocity(velocityX, velocityY) + this.speed = body.velocity.length() + body.useDamping = true + body.setDrag(0.9, 0.9) + this.updateRotation() + } + + private updateRotation() { + const body = this.mainSprite.body as Phaser.Physics.Arcade.Body + const angle = body.acceleration.lengthSq() > 0.01 ? + Phaser.Math.Angle.BetweenPoints(Phaser.Math.Vector2.ZERO, body.acceleration) : + (body.velocity.lengthSq() > 0.01 ? + Phaser.Math.Angle.BetweenPoints(Phaser.Math.Vector2.ZERO, body.velocity) : + this.mainSprite.rotation) + this.mainSprite.rotation = angle + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + update(time: number, delta: number): void { + super.update(time, delta) + + if (!this.dying) { + if (this.startTime == 0) this.startTime = time + else if ((time - this.startTime) > this.duration) this.die() + else if ((time - this.lastTrack) > MissileGameObject.trackInterval) { + this.lastTrack = time + const body = this.mainSprite.body as Phaser.Physics.Arcade.Body + const reqVelocity = new Phaser.Math.Vector2(this.target.x - this.x, this.target.y - this.y) + .normalize().scale(this.speed) + let acc = reqVelocity.subtract(body.velocity) + if (acc.lengthSq() > this.maxAcceleration * this.maxAcceleration) { + acc = acc.normalize().scale(this.maxAcceleration) + } + body.setAcceleration(acc.x, acc.y) + } + this.updateRotation() + } + } + + static preload(scene: GameScene): void { + scene.load.image("missile", "/images/missile.png") + } +} \ No newline at end of file diff --git a/src/mobile-objects/MissileLauncherGameObject.ts b/src/mobile-objects/MissileLauncherGameObject.ts new file mode 100644 index 0000000..29bba31 --- /dev/null +++ b/src/mobile-objects/MissileLauncherGameObject.ts @@ -0,0 +1,54 @@ +import { ScanStopShootBehaviour } from "../behaviours/ScanStopShootBehaviour" +import GameObject from "../GameObject" +import GameScene from "../GameScene" +import SpriteGameObject from "../SpriteGameObject" +import { BulletType } from "./BulletGameObject" + +export default class MissileLauncherGameObject extends SpriteGameObject { + private static readonly scanRangeSq = 1000 * 1000 + private static readonly missileDuration = 10000 + private static readonly bulletDamageMap = new Map([ + [BulletType.Chopper, 10], [BulletType.Tank, 50], [BulletType.Bunker, 50], [BulletType.Rifle, 1]]) + + constructor(scene: GameScene, owner: number, x: number, private readonly speed: number) { + super(scene, owner) + + this.mainSprite = this.scene.physics.add.sprite(x, this.scene.groundPos - 32, "missile-launcher") + this.mainSprite.setDepth(1) + const body = this.mainSprite.body as Phaser.Physics.Arcade.Body + body.setAllowGravity(false) + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const superThis = this + this.behaviour = new ScanStopShootBehaviour(this, 2000, MissileLauncherGameObject.missileDuration + 5000, { + findTarget() { + const target = superThis.scene.gameMap.findChopper(superThis, MissileLauncherGameObject.scanRangeSq) + if (target) return new Set([target]) + else return new Set() + }, + isMoving() { return (superThis.mainSprite.body as Phaser.Physics.Arcade.Body).velocity.x != 0 }, + stop() { superThis.move(0, superThis.owner < 0) }, + move() { superThis.move(superThis.speed * superThis.owner, superThis.owner < 0) }, + createBullet(targets: Set) { + const target = targets.values().next() + scene.createMissile(superThis.owner, MissileLauncherGameObject.missileDuration, + superThis.x1, superThis.y1 - 8, + 300 * superThis.owner, -300, 450, target.value) + } + }) + + this.addBulletResponse(this.sprite, MissileLauncherGameObject.bulletDamageMap) + } + + move(velocityX: number, faceLeft: boolean): void { + const body = this.mainSprite.body as Phaser.Physics.Arcade.Body + body.setVelocityX(velocityX) + + if (velocityX > 0) this.mainSprite.setFlipX(true) + else this.mainSprite.setFlipX(!faceLeft) + } + + static preload(scene: GameScene): void { + scene.load.image("missile-launcher", "/images/missile-launcher.png") + } +} \ No newline at end of file diff --git a/src/mobile-objects/SoldierGameObject.ts b/src/mobile-objects/SoldierGameObject.ts index dad0a75..0cf0ee9 100644 --- a/src/mobile-objects/SoldierGameObject.ts +++ b/src/mobile-objects/SoldierGameObject.ts @@ -31,7 +31,7 @@ export default class SoldierGameObject extends HumanGameObject { this.behaviour = new ScanStopShootBehaviour(this, 2000, 2000, { findTarget() { return superThis.scene.gameMap.getObjectsFrom( superThis, superThis.owner * -1, 92, SoldierGameObject.scanRange, - (obj) => obj instanceof SoldierGameObject).size > 0 }, + (obj) => obj instanceof SoldierGameObject) }, isMoving() { return (superThis.sprite.body as Phaser.Physics.Arcade.Body).velocity.x != 0 }, stop() { superThis.move(0, superThis.owner < 0) }, move() { superThis.move(superThis.speed * superThis.owner, superThis.owner < 0) }, diff --git a/src/mobile-objects/TankGameObject.ts b/src/mobile-objects/TankGameObject.ts index 184fc05..48a8ef8 100644 --- a/src/mobile-objects/TankGameObject.ts +++ b/src/mobile-objects/TankGameObject.ts @@ -1,59 +1,47 @@ import { ScanStopShootBehaviour } from "../behaviours/ScanStopShootBehaviour" import GameScene from "../GameScene" -import PhysicsBodyGameObject from "../PhysicsBodyGameObject" +import SpriteGameObject from "../SpriteGameObject" import { BulletType } from "./BulletGameObject" -export default class TankGameObject extends PhysicsBodyGameObject { +export default class TankGameObject extends SpriteGameObject { private static readonly scanRange = 300 private static readonly bulletDuration = 5000 private static readonly bulletDamageMap = new Map([ [BulletType.Chopper, 10], [BulletType.Tank, 50], [BulletType.Bunker, 50], [BulletType.Rifle, 1]]) - private readonly sprite: Phaser.Physics.Arcade.Sprite - constructor(scene: GameScene, owner: number, x: number, private readonly speed: number) { super(scene, owner) - this.sprite = this.scene.physics.add.sprite(x, this.scene.groundPos - 20, "tank") - this.sprite.setScale(0.5, 0.5) - this.sprite.setDepth(1) - const body = this.sprite.body as Phaser.Physics.Arcade.Body + this.mainSprite = this.scene.physics.add.sprite(x, this.scene.groundPos - 20, "tank") + this.mainSprite.setScale(0.5, 0.5) + this.mainSprite.setDepth(1) + const body = this.mainSprite.body as Phaser.Physics.Arcade.Body body.setAllowGravity(false) - this.mainBody = body // eslint-disable-next-line @typescript-eslint/no-this-alias const superThis = this this.behaviour = new ScanStopShootBehaviour(this, 2000, 2000, { findTarget() { return superThis.scene.gameMap.getObjectsFrom( superThis, superThis.owner * -1, 92, TankGameObject.scanRange, - (obj) => obj.y2 > superThis.scene.groundPos - superThis.height).size > 0 }, - isMoving() { return (superThis.sprite.body as Phaser.Physics.Arcade.Body).velocity.x != 0 }, + (obj) => obj.y2 > superThis.scene.groundPos - superThis.height) }, + isMoving() { return (superThis.mainSprite.body as Phaser.Physics.Arcade.Body).velocity.x != 0 }, stop() { superThis.move(0, superThis.owner < 0) }, move() { superThis.move(superThis.speed * superThis.owner, superThis.owner < 0) }, createBullet() { superThis.scene.createBullet(superThis.owner, TankGameObject.bulletDuration, - superThis.sprite.x + 46 * superThis.owner, superThis.sprite.y, + superThis.mainSprite.x + 46 * superThis.owner, superThis.mainSprite.y, 100 * superThis.owner, 0, BulletType.Tank) } }) - this.addBulletResponse(this.sprite, TankGameObject.bulletDamageMap) - } - - remove(): void { - this.sprite.destroy() - this.removed() - } - - protected setVisible(visible: boolean): void { - this.sprite.setVisible(visible) + this.addBulletResponse(this.mainSprite, TankGameObject.bulletDamageMap) } move(velocityX: number, faceLeft: boolean): void { - const body = this.sprite.body as Phaser.Physics.Arcade.Body + const body = this.mainSprite.body as Phaser.Physics.Arcade.Body body.setVelocityX(velocityX) - if (velocityX > 0) this.sprite.setFlipX(true) - else this.sprite.setFlipX(!faceLeft) + if (velocityX > 0) this.mainSprite.setFlipX(true) + else this.mainSprite.setFlipX(!faceLeft) } static preload(scene: GameScene): void { diff --git a/src/static-objects/AAGunGameObject.ts b/src/static-objects/AAGunGameObject.ts index 959507a..f8fd36b 100644 --- a/src/static-objects/AAGunGameObject.ts +++ b/src/static-objects/AAGunGameObject.ts @@ -53,7 +53,7 @@ export default class AAGunGameObject extends PhysicsBodyGameObject { this.removed() } - protected setVisible(visible: boolean): void { + setVisible(visible: boolean): void { this.gunSprite.setVisible(visible) this.bodySprite.setVisible(visible) } @@ -62,19 +62,17 @@ export default class AAGunGameObject extends PhysicsBodyGameObject { update(time: number, delta: number): void { super.update(time, delta) - if (this.faceLeft) { - this.angle = Phaser.Math.Angle.BetweenPoints(this.scene.chopper.sprite, this.gunSprite) - const distSq = Phaser.Math.Distance.BetweenPointsSquared(this.gunSprite, this.scene.chopper.sprite) - if (distSq < AAGunGameObject.chopperTrackDistanceSq) { - if ((time - this.lastFired) > 1000) { - this.lastFired = time - const dir = new Phaser.Math.Vector2( - this.scene.chopper.sprite.x - this.gunSprite.x, - this.scene.chopper.sprite.y - this.gunSprite.y).normalize() - this.scene.createBullet(this.owner, 30000, this.gunSprite.x + dir.x * 16, - this.gunSprite.y + dir.y * 16, - dir.x * 100, dir.y * 100, BulletType.AA) - } + if (this.faceLeft && (time - this.lastFired > 500)) { + const target = this.scene.gameMap.findChopper(this, AAGunGameObject.chopperTrackDistanceSq) + if (target) { + this.lastFired = time + this.angle = Phaser.Math.Angle.BetweenPoints(target, this.gunSprite) + const dir = new Phaser.Math.Vector2( + target.x - this.gunSprite.x, + target.y - this.gunSprite.y).normalize() + this.scene.createBullet(this.owner, 30000, this.gunSprite.x + dir.x * 16, + this.gunSprite.y + dir.y * 16, + dir.x * 200, dir.y * 200, BulletType.AA) } } } diff --git a/src/static-objects/BunkerGameObject.ts b/src/static-objects/BunkerGameObject.ts index 8d628af..920b63e 100644 --- a/src/static-objects/BunkerGameObject.ts +++ b/src/static-objects/BunkerGameObject.ts @@ -26,7 +26,7 @@ export default class BunkerGameObject extends SpriteGameObject { this.behaviour = new ScanStopShootBehaviour(this, 2000, 2000, { findTarget() { return superThis.scene.gameMap.getObjectsFrom( superThis, superThis.owner * -1, 16, BunkerGameObject.scanRange, - (obj) => obj.y2 > superThis.scene.groundPos - superThis.height).size > 0 }, + (obj) => obj.y2 > superThis.scene.groundPos - superThis.height) }, isMoving() { return (superThis.mainSprite.body as Phaser.Physics.Arcade.Body).velocity.x != 0 }, stop() { /* do nothing */ }, move() { /* do nothing */ }, diff --git a/src/static-objects/GroundGameObject.ts b/src/static-objects/GroundGameObject.ts index a120f9b..a7f9dca 100644 --- a/src/static-objects/GroundGameObject.ts +++ b/src/static-objects/GroundGameObject.ts @@ -1,28 +1,9 @@ -import GameObject from "../GameObject" import GameScene from "../GameScene" -export default class GroundGameObject extends GameObject { - readonly x1: number - readonly x2: number - readonly y1: number - readonly y2: number - readonly width: number - readonly height: number - - constructor(readonly scene: GameScene) { - super(scene, 0) - - const sprite = this.scene.add.tileSprite(0, this.scene.worldHeight - 16, - this.scene.worldWidth, 16, "nature", 1).setScale(2) - this.scene.platforms.add(sprite) - - this.x1 = 0 - this.x2 = this.scene.worldWidth - this.y1 = sprite.y - sprite.height / 2 - this.y2 = sprite.y + sprite.height / 2 - this.width = this.scene.worldWidth - this.height = sprite.height +export default class GroundGameObject { + constructor(scene: GameScene) { + const sprite = scene.add.tileSprite(0, scene.worldHeight - 16, + scene.worldWidth, 16, "nature", 1).setScale(2) + scene.platforms.add(sprite) } - - remove(): void { throw "GoundGameObject cannot be removed" } } \ No newline at end of file diff --git a/src/static-objects/HelipadGameObject.ts b/src/static-objects/HelipadGameObject.ts index d321939..95fdf28 100644 --- a/src/static-objects/HelipadGameObject.ts +++ b/src/static-objects/HelipadGameObject.ts @@ -28,7 +28,7 @@ export default class HelipadGameObject extends PhysicsBodyGameObject { remove(): void { throw "HelipadGameObject cannot be removed" } - protected setVisible(visible: boolean): void { + setVisible(visible: boolean): void { this.sprite.setVisible(visible) } diff --git a/src/static-objects/MissileBaseGameObject.ts b/src/static-objects/MissileBaseGameObject.ts new file mode 100644 index 0000000..7774798 --- /dev/null +++ b/src/static-objects/MissileBaseGameObject.ts @@ -0,0 +1,51 @@ +import { ScanStopShootBehaviour } from "../behaviours/ScanStopShootBehaviour" +import GameObject from "../GameObject" +import GameScene from "../GameScene" +import SpriteGameObject from "../SpriteGameObject" +import { BulletType } from "../mobile-objects/BulletGameObject" + +export default class MissileBaseGameObject extends SpriteGameObject { + private static readonly scanRangeSq = 1000 * 1000 + private static readonly missileDuration = 10000 + private static readonly bulletDamageMap = new Map([ + [BulletType.Chopper, 10], [BulletType.Tank, 20], [BulletType.Bunker, 10], [BulletType.Rifle, 1]]) + + constructor(scene: GameScene, owner: number, x: number, physicsGroup: Phaser.Physics.Arcade.Group) { + super(scene, owner) + + this.mainSprite = this.scene.physics.add.sprite(x, this.scene.groundPos - 32, "missile-base") + this.addWrapperProperty(this.mainSprite) + physicsGroup.add(this.mainSprite) + + if (owner > 0) this.mainSprite.setFlipX(true) + this.mainSprite.setDepth(1) + + const body = this.mainSprite.body as Phaser.Physics.Arcade.Body + body.setAllowGravity(false) + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const superThis = this + this.behaviour = new ScanStopShootBehaviour(this, 2000, MissileBaseGameObject.missileDuration + 5000, { + findTarget() { + const target = superThis.scene.gameMap.findChopper(superThis, MissileBaseGameObject.scanRangeSq) + if (target) return new Set([target]) + else return new Set() + }, + isMoving() { return (superThis.mainSprite.body as Phaser.Physics.Arcade.Body).velocity.x != 0 }, + stop() { /* do nothing */ }, + move() { /* do nothing */ }, + createBullet(targets: Set) { + const target = targets.values().next() + scene.createMissile(superThis.owner, MissileBaseGameObject.missileDuration, + superThis.x1, superThis.y1 - 8, + 300 * superThis.owner, -300, 450, target.value) + } + }) + + this.addBulletResponse(this.sprite, MissileBaseGameObject.bulletDamageMap) + } + + static preload(scene: GameScene): void { + scene.load.image("missile-base", "/images/missile-base.png") + } +} \ No newline at end of file diff --git a/src/static-objects/TreeGameObject.ts b/src/static-objects/TreeGameObject.ts index 1ded6e4..a51eda8 100644 --- a/src/static-objects/TreeGameObject.ts +++ b/src/static-objects/TreeGameObject.ts @@ -34,7 +34,7 @@ export default class TreeGameObject extends PhysicsBodyGameObject { remove(): void { throw "TreeGameObject cannot be removed" } - protected setVisible(visible: boolean): void { + setVisible(visible: boolean): void { this.sprites.forEach(s => s.setVisible(visible)) } diff --git a/src/ui-objects/HealthBarGameObject.ts b/src/ui-objects/HealthBarGameObject.ts index a04fe9a..3fb79ec 100644 --- a/src/ui-objects/HealthBarGameObject.ts +++ b/src/ui-objects/HealthBarGameObject.ts @@ -3,9 +3,9 @@ import GameScene from "../GameScene" import HealthBarGraphics from "./HealthBarGraphics" export default class HealthBarGameObject extends GameObject { - readonly x1: number + readonly x: number readonly x2: number - readonly y1: number + readonly y: number readonly y2: number private readonly graphics: HealthBarGraphics @@ -13,24 +13,26 @@ export default class HealthBarGameObject extends GameObject { get width(): number { return this.graphics.width } get height(): number { return this.graphics.height } - constructor(scene: GameScene, private readonly x: number, private readonly y: number) { + constructor(scene: GameScene, readonly x1: number, readonly y1: number) { super(scene, 0) - this.x = x - this.y = y this.graphics = new HealthBarGraphics(scene, 100, 10) this.graphics.graphics.setScrollFactor(0, 0) - this.graphics.draw(x, y, this._health) + this.graphics.draw(x1, y1, this._health) - this.x1 = this.x - this.x2 = this.x + this.width - this.y1 = this.y - this.y2 = this.y + this.height + this.x2 = this.x1 + this.width + this.x = this.x1 + this.width / 2 + this.y2 = this.y1 + this.height + this.y = this.y1 + this.height / 2 } set health(h: number) { this._health = h - this.graphics.draw(this.x, this.y, h) + this.graphics.draw(this.x1, this.y1, h) + } + + setVisible(visible: boolean): void { + this.graphics.graphics.setVisible(visible) } remove(): void { throw "HealthBarGameObject cannot be removed" } diff --git a/static/images/missile-base.png b/static/images/missile-base.png new file mode 100644 index 0000000..ec62ca9 Binary files /dev/null and b/static/images/missile-base.png differ diff --git a/static/images/missile-launcher.png b/static/images/missile-launcher.png new file mode 100644 index 0000000..9420cab Binary files /dev/null and b/static/images/missile-launcher.png differ diff --git a/static/images/missile.png b/static/images/missile.png new file mode 100644 index 0000000..ec2b980 Binary files /dev/null and b/static/images/missile.png differ