diff --git a/.eslintignore b/.eslintignore index 729110e..6dd7ca1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,4 +4,5 @@ node_modules dist # don't lint nyc coverage output coverage -jest.config.js \ No newline at end of file +jest.config.js +tests \ No newline at end of file diff --git a/src/BlueForceControl.ts b/src/BlueForceControl.ts index 1eab8a4..43bd1a1 100644 --- a/src/BlueForceControl.ts +++ b/src/BlueForceControl.ts @@ -1,38 +1,40 @@ -import AAGunGameObject from "./AAGunGameObject" -import BarrackGameObject from "./BarrackGameObject" -import ChopperGameObject from "./ChopperGameObject" -import CivilianGameObject from "./CivilianGameObject" -import FactoryGameObject from "./FactoryGameObject" -import GameButton from "./GameButton" +import AAGunGameObject from "./static-objects/AAGunGameObject" +import BarrackGameObject from "./static-objects/BarrackGameObject" +import ChopperGameObject from "./mobile-objects/ChopperGameObject" +import CivilianGameObject from "./mobile-objects/CivilianGameObject" +import FactoryGameObject from "./static-objects/FactoryGameObject" +import ForceControl from "./ForceControl" +import GameButton from "./ui-objects/GameButton" import GameScene from "./GameScene" -import HealthBarGameObject from "./HealthBarGameObject" -import HelipadGameObject from "./HelipadGameObject" -import HomeBuildingGameObject from "./HomeBuildingGameObject" -import SoldierGameObject from "./SoldierGameObject" -import TankGameObject from "./TankGameObject" -import VillageGameObject from "./VillageGameObject" - -export default class BlueForceControl { - private scene: GameScene +import HealthBarGameObject from "./ui-objects/HealthBarGameObject" +import HelipadGameObject from "./static-objects/HelipadGameObject" +import HomeBuildingGameObject from "./static-objects/HomeBuildingGameObject" +import SoldierGameObject from "./mobile-objects/SoldierGameObject" +import TankGameObject from "./mobile-objects/TankGameObject" +import VillageGameObject from "./static-objects/VillageGameObject" + +export default class BlueForceControl extends ForceControl { private cash = 1000 private cashDelta = 100 private cashUpdateInterval = 5000 private lastCashUpdate = 0 private cashText: Phaser.GameObjects.Text - private factory: FactoryGameObject - private barrack: BarrackGameObject - private villages: VillageGameObject[] = [] - private aaGunObjects: AAGunGameObject[] = [] - private tankObjects: TankGameObject[] = [] - private soliderObjects: SoldierGameObject[] = [] + private liftableBodies: Phaser.Physics.Arcade.Group - private boardableBodies: Phaser.Physics.Arcade.Group + private villages = new Set() + private aaGunObjects= new Set() + + protected factory: FactoryGameObject + protected barrack: BarrackGameObject + protected tankObjects = new Set() + protected soliderObjects = new Set() + protected boardableBodies: Phaser.Physics.Arcade.Group private _chopper: ChopperGameObject get chopper(): ChopperGameObject { return this._chopper } constructor(scene: GameScene) { - this.scene = scene + super(scene, 1) const screenWidth = scene.sys.scale.gameSize.width @@ -66,7 +68,7 @@ export default class BlueForceControl { nextPos += 100 const aaGun = new AAGunGameObject(scene, 1, this.liftableBodies, nextPos + 16, 3 * Math.PI / 4) - this.aaGunObjects.push(aaGun) + this.aaGunObjects.add(aaGun) nextPos += 32 + 15 nextPos += 100 @@ -75,7 +77,7 @@ export default class BlueForceControl { this.cashDelta += 100 this.adjustCash(0) }) - this.villages.push(village) + this.villages.add(village) nextPos += 128 + 15 const healthBar = new HealthBarGameObject(scene, 10, 15, 100) @@ -103,6 +105,16 @@ export default class BlueForceControl { soldierOnBoardCountText.setText(`${humans.get(SoldierGameObject.TYPE)?.length ?? 0}`) civilianOnBoardCountText.setText(`${humans.get(CivilianGameObject.TYPE)?.length ?? 0}`) }) + this.scene.gameMap.add(this._chopper) + + const soldier = new SoldierGameObject(this.scene, this.owner, this.scene.worldWidth - 1000, this.boardableBodies) + soldier.move(10) + this.soliderObjects.add(soldier) + this.scene.gameMap.add(soldier) + soldier.destroyCallback = () => { + this.soliderObjects.delete(soldier) + this.scene.gameMap.remove(soldier) + } } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -110,27 +122,15 @@ export default class BlueForceControl { this._chopper.update(time, delta) this.villages.forEach(v => v.update(time, delta)) this.aaGunObjects.forEach(gun => gun.update(time)) - this.soliderObjects.forEach(soldier => soldier.update(time, delta)) if ((time - this.lastCashUpdate) >= this.cashUpdateInterval) { this.adjustCash(this.cashDelta) this.lastCashUpdate = time } + super.update(time, delta) } private adjustCash(amt: number) { this.cash += amt this.cashText.setText(`$${this.cash} +$${this.cashDelta}`) } - - private buildTank() { - const tank = new TankGameObject(this.scene, 1, this.factory.spawnX) - tank.move(50, false) - this.tankObjects.push(tank) - } - - private buildSoldier() { - const soldier = new SoldierGameObject(this.scene, 1, this.barrack.spawnX, this.boardableBodies) - soldier.move(10, false) - this.soliderObjects.push(soldier) - } } \ No newline at end of file diff --git a/src/ForceControl.ts b/src/ForceControl.ts new file mode 100644 index 0000000..bd70024 --- /dev/null +++ b/src/ForceControl.ts @@ -0,0 +1,43 @@ +import BarrackGameObject from "./static-objects/BarrackGameObject" +import FactoryGameObject from "./static-objects/FactoryGameObject" +import GameScene from "./GameScene" +import SoldierGameObject from "./mobile-objects/SoldierGameObject" +import TankGameObject from "./mobile-objects/TankGameObject" + +export default abstract class ForceControl { + protected abstract factory: FactoryGameObject + protected abstract barrack: BarrackGameObject + protected abstract boardableBodies: Phaser.Physics.Arcade.Group + + protected tankObjects = new Set() + protected soliderObjects = new Set() + + constructor(protected readonly scene: GameScene, protected readonly owner: number) {} + + update(time: number, delta: number): void { + this.soliderObjects.forEach(soldier => soldier.update(time, delta)) + this.tankObjects.forEach(tank => tank.update(time)) + } + + protected buildTank(): void { + const tank = new TankGameObject(this.scene, this.owner, this.factory.spawnX, 50) + tank.move(50 * this.owner, this.owner < 0) + this.tankObjects.add(tank) + this.scene.gameMap.add(tank) + tank.destroyCallback = () => { + this.tankObjects.delete(tank) + this.scene.gameMap.remove(tank) + } + } + + protected buildSoldier(): void { + const soldier = new SoldierGameObject(this.scene, this.owner, this.barrack.spawnX, this.boardableBodies) + soldier.move(10 * this.owner, this.owner < 0) + this.soliderObjects.add(soldier) + this.scene.gameMap.add(soldier) + soldier.destroyCallback = () => { + this.soliderObjects.delete(soldier) + this.scene.gameMap.remove(soldier) + } + } +} \ No newline at end of file diff --git a/src/GameMap.ts b/src/GameMap.ts index c4d3a2a..5a002e5 100644 --- a/src/GameMap.ts +++ b/src/GameMap.ts @@ -1,17 +1,18 @@ import GameObject from "./GameObject" export default class GameMap { - private static readonly updateInterval = 250 + private static readonly updateInterval = 32 + private readonly cellCount: number private lastUpdate = 0 private gameObjects = new Map[]>() private gameObjectCells = new Map() - + constructor(readonly mapSize: number, readonly cellWidth: number) { - const cells = Math.ceil(mapSize / cellWidth) - const redCells = new Array(cells) - const blueCells = new Array(cells) - for (let i = 0; i < cells; i++) { + this.cellCount = Math.ceil(mapSize / cellWidth) + const redCells = new Array(this.cellCount) + const blueCells = new Array(this.cellCount) + for (let i = 0; i < this.cellCount; i++) { redCells[i] = new Set() blueCells[i] = new Set() } @@ -23,7 +24,6 @@ export default class GameMap { const objs = this.gameObjects.get(obj.owner) if (objs) { const cells = [this.getCell(obj.x1), this.getCell(obj.x2)] - console.log(`Add object in ${cells[0]} - ${cells[1]}`) for (let i = cells[0]; i <= cells[1]; i++) objs[i].add(obj) this.gameObjectCells.set(obj, cells) } @@ -41,21 +41,35 @@ export default class GameMap { filter?: (obj: GameObject) => boolean): Set { const c1 = this.getCell(x1) const c2 = this.getCell(x2) - console.log(`get objects in ${c1} - ${c2}`) const result = new Set() const objs = this.gameObjects.get(owner) for (let c = c1; c <= c2; c++) { objs[c].forEach(o => { - if (this.overlaps(o.x1, o.x2, x1, x2) && (!filter || filter(o))) result.add(o) + if (this.overlaps(o.x1, o.x2, x1, x2) && (!filter || filter(o))) { + result.add(o) + } }) } return result } + getObjectsFrom(thisObj: GameObject, thatOwner: number, + startOffset: number, distance: number, + filter?: (obj: GameObject) => boolean): Set { + if (thisObj.owner > 0) { + return this.getObjectsWithin(thatOwner, thisObj.x1 + startOffset, + thisObj.x1 + startOffset + distance, filter) + } else { + return this.getObjectsWithin(thatOwner, thisObj.x1 - startOffset - distance, + thisObj.x1 - startOffset, filter) + } + } + private overlaps(a1: number, a2: number, b1: number, b2: number) { return !(b2 < a1 || b1 > a2) } + // eslint-disable-next-line @typescript-eslint/no-unused-vars update(time: number, delta: number): void { if ((time - this.lastUpdate) >= GameMap.updateInterval) { this.lastUpdate = time @@ -76,5 +90,7 @@ export default class GameMap { } } - private getCell(x: number) { return Math.floor(x / this.cellWidth) } + private getCell(x: number) { + return Math.min(this.cellCount - 1, Math.max(0, Math.floor(x / this.cellWidth))) + } } \ No newline at end of file diff --git a/src/GameObject.ts b/src/GameObject.ts index aea97f6..e4912f1 100644 --- a/src/GameObject.ts +++ b/src/GameObject.ts @@ -5,7 +5,8 @@ interface WrappedPhaserGameObject { } export default abstract class GameObject { - constructor(readonly scene: GameScene, public owner: number) {} + protected _destroyCallback: () => void + set destroyCallback(cb: () => void) { this._destroyCallback = cb } abstract readonly x1: number abstract readonly x2: number @@ -14,9 +15,15 @@ export default abstract class GameObject { abstract readonly width: number abstract readonly height: number + constructor(readonly scene: GameScene, public owner: number) {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars moveTo(x: number, y: number): void { throw "object cannot be moved" } + abstract remove(): void + + removed(): void { this._destroyCallback?.() } + addWrapperProperty(obj: Phaser.GameObjects.GameObject): void { (obj as unknown as WrappedPhaserGameObject).wrapper = this } diff --git a/src/GameScene.ts b/src/GameScene.ts index 0626a99..549f9bb 100644 --- a/src/GameScene.ts +++ b/src/GameScene.ts @@ -1,30 +1,30 @@ import * as Phaser from 'phaser' -import AAGunGameObject from './AAGunGameObject' -import BarrackGameObject from './BarrackGameObject' -import BulletGameObject from './BulletGameObject' -import ChopperGameObject from './ChopperGameObject' -import CivilianGameObject from './CivilianGameObject' +import AAGunGameObject from './static-objects/AAGunGameObject' +import BarrackGameObject from './static-objects/BarrackGameObject' +import BulletGameObject from './mobile-objects/BulletGameObject' +import ChopperGameObject from './mobile-objects/ChopperGameObject' +import CivilianGameObject from './mobile-objects/CivilianGameObject' import RedForceControl from './RedForceControl' -import FactoryGameObject from './FactoryGameObject' -import GroundGameObject from './GroundGameObject' -import HelipadGameObject from './HelipadGameObject' -import HomeBuildingGameObject from './HomeBuildingGameObject' +import FactoryGameObject from './static-objects/FactoryGameObject' +import GroundGameObject from './static-objects/GroundGameObject' +import HelipadGameObject from './static-objects/HelipadGameObject' +import HomeBuildingGameObject from './static-objects/HomeBuildingGameObject' import BlueForceControl from './BlueForceControl' -import SoldierGameObject from './SoldierGameObject' -import TankGameObject from './TankGameObject' -import TreeGameObject from './TreeGameObject' -import VillageGameObject from './VillageGameObject' -import GameObject from './GameObject' +import SoldierGameObject from './mobile-objects/SoldierGameObject' +import TankGameObject from './mobile-objects/TankGameObject' +import TreeGameObject from './static-objects/TreeGameObject' +import VillageGameObject from './static-objects/VillageGameObject' +import GameMap from './GameMap' export default class GameScene extends Phaser.Scene { private frameTime = 0 - private ground: GroundGameObject private treeObjects: TreeGameObject[] = [] private bulletObjects = new Map() private bulletBodies = new Map() - private playerControl: BlueForceControl - private enemeyControl: RedForceControl - private gameObjects = new Map>() + private blueControl: BlueForceControl + private redControl: RedForceControl + + readonly gameMap: GameMap private _platformBodies: Phaser.Physics.Arcade.StaticGroup get platforms(): Phaser.Physics.Arcade.StaticGroup { return this._platformBodies } @@ -35,7 +35,7 @@ export default class GameScene extends Phaser.Scene { private _groundPos: number get groundPos(): number { return this._groundPos } - get chopper(): ChopperGameObject { return this.playerControl.chopper } + get chopper(): ChopperGameObject { return this.blueControl.chopper } constructor(readonly worldWidth: number, readonly worldHeight: number) { super({ @@ -43,8 +43,7 @@ export default class GameScene extends Phaser.Scene { visible: false, key: 'Game', }) - this.gameObjects.set(-1, new Map()) - this.gameObjects.set(1, new Map()) + this.gameMap = new GameMap(worldWidth, 100) } init(): void { @@ -86,7 +85,7 @@ export default class GameScene extends Phaser.Scene { bg.setDisplaySize(this.worldWidth, this.worldHeight) this._platformBodies = this.physics.add.staticGroup() - this.ground = new GroundGameObject(this) + new GroundGameObject(this) this._groundPos = this.sys.game.scale.gameSize.height - 32 @@ -106,25 +105,25 @@ export default class GameScene extends Phaser.Scene { HelipadGameObject.createCommon(this) SoldierGameObject.createCommon(this) CivilianGameObject.createCommon(this) - this.playerControl = new BlueForceControl(this) - this.enemeyControl = new RedForceControl(this) + this.blueControl = new BlueForceControl(this) + this.redControl = new RedForceControl(this) this.cameras.main.setBounds(0, 0, this.worldWidth, this.worldHeight) - this.cameras.main.startFollow(this.playerControl.chopper.sprite) + this.cameras.main.startFollow(this.blueControl.chopper.sprite) } - createBullet(owner: number, x: number, y: number, velocityX: number, velocityY: number): BulletGameObject { - const bullet = new BulletGameObject(this, owner, this.bulletBodies.get(owner), x, y, velocityX, velocityY) + createBullet(owner: number, duration: number, + x: number, y: number, velocityX: number, velocityY: number, + scale?: number): BulletGameObject { + const bullet = new BulletGameObject(this, owner, this.bulletBodies.get(owner), + duration, x, y, velocityX, velocityY, scale) this.bulletObjects.set(bullet.sprite, bullet) + bullet.destroyCallback = () => this.bulletObjects.delete(bullet.sprite) return bullet } removeBullet(b: Phaser.GameObjects.GameObject): void { - const bullet = this.bulletObjects.get(b) - if (bullet) { - this.bulletObjects.delete(b) - bullet.remove() - } + this.bulletObjects.get(b)?.remove() } getBulletBodies(owner: number): Phaser.Physics.Arcade.Group { return this.bulletBodies.get(owner) } @@ -133,13 +132,11 @@ export default class GameScene extends Phaser.Scene { this.frameTime += delta if (this.frameTime >= 16.5) { this.frameTime = 0 - this.playerControl.update(time, delta) - this.enemeyControl.update(time, delta) + this.gameMap.update(time, delta) + this.blueControl.update(time, delta) + this.redControl.update(time, delta) this.treeObjects.forEach(tree => tree.update()) - this.bulletObjects.forEach(bullet => { - if (bullet.sprite.x < 0 || bullet.sprite.x > this.worldWidth || - bullet.sprite.y < 0 || bullet.sprite.y > this.worldHeight) this.removeBullet(bullet.sprite) - }) + this.bulletObjects.forEach(bullet => bullet.update(time)) } } diff --git a/src/HumanBoardable.ts b/src/HumanBoardable.ts index 1f7898c..bfb9f37 100644 --- a/src/HumanBoardable.ts +++ b/src/HumanBoardable.ts @@ -1,4 +1,4 @@ -import HumanGameObject from "./HumanGameObject" +import HumanGameObject from "./mobile-objects/HumanGameObject" export default interface HumanBoardable { boardableGameObject: Phaser.GameObjects.GameObject diff --git a/src/PhysicsBodyGameObject.ts b/src/PhysicsBodyGameObject.ts index 3c260b1..e56e7e1 100644 --- a/src/PhysicsBodyGameObject.ts +++ b/src/PhysicsBodyGameObject.ts @@ -1,21 +1,17 @@ import GameObject from "./GameObject" -export default class PhysicsBodyGameObject extends GameObject { - private _x2: number - private _y2: number +export default abstract class PhysicsBodyGameObject extends GameObject { protected _mainBody: Phaser.Physics.Arcade.Body protected get mainBody(): Phaser.Physics.Arcade.Body { return this._mainBody } protected set mainBody(b: Phaser.Physics.Arcade.Body) { this._mainBody = b - this._x2 = b.x + b.width - this._y2 = b.y + b.height } - get x1(): number { return this.mainBody.x } - get x2(): number { return this._x2 } - get y1(): number { return this.mainBody.y } - get y2(): number { return this._y2 } + get x1(): number { return this._mainBody.x } + get x2(): number { return this._mainBody.x + this._mainBody.width } + get y1(): number { return this._mainBody.y } + get y2(): number { return this._mainBody.y + this._mainBody.height } get width(): number { return this.mainBody.width } get height(): number { return this.mainBody.height } } \ No newline at end of file diff --git a/src/ReactGame.tsx b/src/ReactGame.tsx index c4a448e..6d3ea43 100644 --- a/src/ReactGame.tsx +++ b/src/ReactGame.tsx @@ -30,7 +30,7 @@ export class ReactGame extends React.Component { parent: 'game', backgroundColor: '#000000', - scene: new GameScene(1068 * 5, 600), + scene: new GameScene(1068 * 3, 600), }) } } \ No newline at end of file diff --git a/src/RedForceControl.ts b/src/RedForceControl.ts index 2164123..cd7a729 100644 --- a/src/RedForceControl.ts +++ b/src/RedForceControl.ts @@ -1,29 +1,31 @@ -import AAGunGameObject from "./AAGunGameObject" -import BarrackGameObject from "./BarrackGameObject" -import ChopperGameObject from "./ChopperGameObject" -import FactoryGameObject from "./FactoryGameObject" +import AAGunGameObject from "./static-objects/AAGunGameObject" +import BarrackGameObject from "./static-objects/BarrackGameObject" +import ChopperGameObject from "./mobile-objects/ChopperGameObject" +import FactoryGameObject from "./static-objects/FactoryGameObject" +import ForceControl from "./ForceControl" import GameScene from "./GameScene" -import HelipadGameObject from "./HelipadGameObject" -import SoldierGameObject from "./SoldierGameObject" -import TankGameObject from "./TankGameObject" - -export default class RedForceControl { - private scene: GameScene - private factory: FactoryGameObject - private barrack: BarrackGameObject +import HelipadGameObject from "./static-objects/HelipadGameObject" +import SoldierGameObject from "./mobile-objects/SoldierGameObject" +import TankGameObject from "./mobile-objects/TankGameObject" + +export default class RedForceControl extends ForceControl { private aaGunObjects: AAGunGameObject[] = [] - private tankObjects: TankGameObject[] = [] - private soliderObjects: SoldierGameObject[] = [] + + protected factory: FactoryGameObject + protected barrack: BarrackGameObject + protected tankObjects = new Set() + protected soliderObjects = new Set() + protected boardableBodies = undefined private _chopper: ChopperGameObject get chopper(): ChopperGameObject { return this._chopper } constructor(scene: GameScene) { - this.scene = scene + super(scene, -1) let nextPos = this.scene.worldWidth - 10 - 64 - const helipad = new HelipadGameObject(scene, -1, scene.platforms, nextPos, true) + new HelipadGameObject(scene, -1, scene.platforms, nextPos, true) nextPos -= 64 + 15 this.factory = new FactoryGameObject(scene, -1, nextPos - 128, true) @@ -45,17 +47,6 @@ export default class RedForceControl { // eslint-disable-next-line @typescript-eslint/no-unused-vars update(time: number, delta: number): void { this.aaGunObjects.forEach(gun => gun.update(time)) - } - - private buildTank() { - const tank = new TankGameObject(this.scene, -1, this.factory.spawnX) - tank.move(-50, true) - this.tankObjects.push(tank) - } - - private buildSoldier() { - const soldier = new SoldierGameObject(this.scene, -1, this.barrack.spawnX) - soldier.move(-10, true) - this.soliderObjects.push(soldier) + super.update(time, delta) } } \ No newline at end of file diff --git a/src/SpriteGameObject.ts b/src/SpriteGameObject.ts index 6716b01..5c5ea94 100644 --- a/src/SpriteGameObject.ts +++ b/src/SpriteGameObject.ts @@ -1,25 +1,26 @@ import GameObject from "./GameObject" export default class SpriteGameObject extends GameObject { - private _x1: number - private _x2: number - private _y1: number - private _y2: number private _mainSprite: Phaser.GameObjects.Sprite + private halfWidth: number + private halfHeight: number protected get mainSprite(): Phaser.GameObjects.Sprite { return this._mainSprite } protected set mainSprite(s: Phaser.GameObjects.Sprite) { this._mainSprite = s - this._x1 = s.x - s.width / 2 - this._x2 = s.x + s.width / 2 - this._y1 = s.y - s.height / 2 - this._y2 = s.y + s.height / 2 + this.halfWidth = s.width / 2 + this.halfHeight = s.height / 2 } - get x1(): number { return this._x1 } - get x2(): number { return this._x2 } - get y1(): number { return this._y1 } - get y2(): number { return this._y2 } + 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 } + get y2(): number { return this._mainSprite.y + this.halfHeight } get width(): number { return this.mainSprite.width } get height(): number { return this.mainSprite.height } + + remove(): void { + this._mainSprite.destroy() + this.removed() + } } \ No newline at end of file diff --git a/src/TankGameObject.ts b/src/TankGameObject.ts deleted file mode 100644 index 833d99d..0000000 --- a/src/TankGameObject.ts +++ /dev/null @@ -1,51 +0,0 @@ -import GameScene from "./GameScene" -import PhysicsBodyGameObject from "./PhysicsBodyGameObject" - -export default class TankGameObject extends PhysicsBodyGameObject { - private readonly sprite: Phaser.Physics.Arcade.Sprite - private faceLeft: boolean - - constructor(scene: GameScene, owner: number, x: 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 - body.setAllowGravity(false) - this.mainBody = body - } - - move(velocityX: number, faceLeft: boolean): void { - this.faceLeft = faceLeft - - const body = this.sprite.body as Phaser.Physics.Arcade.Body - body.setVelocityX(velocityX) - - if (velocityX > 0) this.sprite.setFlipX(true) - else this.sprite.setFlipX(!faceLeft) - } - - die(): void { - const body = this.sprite.body as Phaser.Physics.Arcade.Body - body.setVelocityX(0) - const explosion = this.scene.add.sprite(this.sprite.x + (this.faceLeft ? 16 : - 16), - this.sprite.y, "explode", 0) - explosion.setDepth(1) - explosion.anims.play("explode") - explosion.once("animationcomplete", () => { - this.sprite.destroy() - explosion.destroy() - }) - } - - static preload(scene: GameScene): void { - scene.load.image("tank", "/images/tank.png") - } - - static createIcon(scene: GameScene, x: number, y: number): Phaser.GameObjects.Sprite { - const icon = scene.add.sprite(x, y, "tank") - icon.setScale(0.35, 0.35) - return icon - } -} \ No newline at end of file diff --git a/src/behaviours/ScanStopShootBehaviour.ts b/src/behaviours/ScanStopShootBehaviour.ts new file mode 100644 index 0000000..ea0987b --- /dev/null +++ b/src/behaviours/ScanStopShootBehaviour.ts @@ -0,0 +1,36 @@ +import GameObject from "../GameObject" +import GameScene from "../GameScene" + +export interface ScanStopShootBehaviourCallback { + findTarget(): boolean + isMoving(): boolean + stop(): void + move(): void + createBullet(): void +} + +export class ScanStopShootBehaviour { + private lastFired = 0 + private lastScan = 0 + private haveTarget = false + + constructor(private subject: GameObject, private readonly scene: GameScene, + readonly scanInterval: number, readonly fireInterval: number, + readonly callback: ScanStopShootBehaviourCallback) {} + + update(time: number): void { + if ((time - this.lastScan) >= this.scanInterval) { + this.lastScan = time + this.haveTarget = this.callback.findTarget() + if (this.haveTarget) { + this.callback.move() + } else if (!this.callback.isMoving()) { + this.callback.move() + } + } + if (this.haveTarget && (time - this.lastFired) > this.fireInterval) { + this.lastFired = time + this.callback.createBullet() + } + } +} \ No newline at end of file diff --git a/src/BulletGameObject.ts b/src/mobile-objects/BulletGameObject.ts similarity index 58% rename from src/BulletGameObject.ts rename to src/mobile-objects/BulletGameObject.ts index 8f44829..bafbf61 100644 --- a/src/BulletGameObject.ts +++ b/src/mobile-objects/BulletGameObject.ts @@ -1,18 +1,22 @@ -import GameScene from "./GameScene" -import PhysicsBodyGameObject from "./PhysicsBodyGameObject" +import GameScene from "../GameScene" +import PhysicsBodyGameObject from "../PhysicsBodyGameObject" export default class BulletGameObject extends PhysicsBodyGameObject { 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 physicsGroup: Phaser.Physics.Arcade.Group, + readonly duration: number, x: number, y: number, - velocityX: number, velocityY: number) { + velocityX: number, velocityY: number, + scale?: number) { super(scene, owner) this._sprite = this.scene.add.sprite(x, y, this.owner > 0 ? "bullet-blue" : "bullet-red") this._sprite.setDepth(200) + if (scale) this._sprite.setScale(scale, scale) this.physicsGroup.add(this._sprite) const body = this._sprite.body as Phaser.Physics.Arcade.Body body.setAllowGravity(false) @@ -22,8 +26,17 @@ export default class BulletGameObject extends PhysicsBodyGameObject { } remove(): void { - this.physicsGroup.remove(this._sprite) this._sprite.destroy() + this.removed() + } + + update(time: number): void { + 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() + } } static preload(scene: GameScene): void { diff --git a/src/ChopperGameObject.ts b/src/mobile-objects/ChopperGameObject.ts similarity index 96% rename from src/ChopperGameObject.ts rename to src/mobile-objects/ChopperGameObject.ts index 617a933..b195340 100644 --- a/src/ChopperGameObject.ts +++ b/src/mobile-objects/ChopperGameObject.ts @@ -1,10 +1,10 @@ import CivilianGameObject from "./CivilianGameObject" -import GameObject from "./GameObject" -import GameScene from "./GameScene" -import HelipadGameObject from "./HelipadGameObject" -import HumanBoardable from "./HumanBoardable" +import GameObject from "../GameObject" +import GameScene from "../GameScene" +import HelipadGameObject from "../static-objects/HelipadGameObject" +import HumanBoardable from "../HumanBoardable" import HumanGameObject from "./HumanGameObject" -import PhysicsBodyGameObject from "./PhysicsBodyGameObject" +import PhysicsBodyGameObject from "../PhysicsBodyGameObject" import SoldierGameObject from "./SoldierGameObject" export default class ChopperGameObject extends PhysicsBodyGameObject implements HumanBoardable { @@ -92,6 +92,12 @@ export default class ChopperGameObject extends PhysicsBodyGameObject implements }) } + remove(): void { + this.bodySprite.destroy() + this.tailSprite.destroy() + this.removed() + } + get boardableGameObject(): Phaser.GameObjects.GameObject { return this.bodySprite } board(human: HumanGameObject): boolean { diff --git a/src/CivilianGameObject.ts b/src/mobile-objects/CivilianGameObject.ts similarity index 99% rename from src/CivilianGameObject.ts rename to src/mobile-objects/CivilianGameObject.ts index c1aed00..7760f70 100644 --- a/src/CivilianGameObject.ts +++ b/src/mobile-objects/CivilianGameObject.ts @@ -1,4 +1,4 @@ -import GameScene from "./GameScene" +import GameScene from "../GameScene" import HumanGameObject from "./HumanGameObject" enum CivilianState { diff --git a/src/HumanGameObject.ts b/src/mobile-objects/HumanGameObject.ts similarity index 93% rename from src/HumanGameObject.ts rename to src/mobile-objects/HumanGameObject.ts index 1779b8c..034f5f6 100644 --- a/src/HumanGameObject.ts +++ b/src/mobile-objects/HumanGameObject.ts @@ -1,6 +1,6 @@ -import GameScene from "./GameScene" -import HumanBoardable from "./HumanBoardable" -import PhysicsBodyGameObject from "./PhysicsBodyGameObject" +import GameScene from "../GameScene" +import HumanBoardable from "../HumanBoardable" +import PhysicsBodyGameObject from "../PhysicsBodyGameObject" export default class HumanGameObject extends PhysicsBodyGameObject { protected readonly sprite: Phaser.Physics.Arcade.Sprite @@ -48,6 +48,11 @@ export default class HumanGameObject extends PhysicsBodyGameObject { } } + remove(): void { + this.sprite.destroy() + this.removed() + } + get walkSpeed(): number { return 10 } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/SoldierGameObject.ts b/src/mobile-objects/SoldierGameObject.ts similarity index 97% rename from src/SoldierGameObject.ts rename to src/mobile-objects/SoldierGameObject.ts index 0d131a7..75fa449 100644 --- a/src/SoldierGameObject.ts +++ b/src/mobile-objects/SoldierGameObject.ts @@ -1,4 +1,4 @@ -import GameScene from "./GameScene" +import GameScene from "../GameScene" import HumanGameObject from "./HumanGameObject" export default class SoldierGameObject extends HumanGameObject { diff --git a/src/mobile-objects/TankGameObject.ts b/src/mobile-objects/TankGameObject.ts new file mode 100644 index 0000000..c0586a2 --- /dev/null +++ b/src/mobile-objects/TankGameObject.ts @@ -0,0 +1,84 @@ +import { ScanStopShootBehaviour } from "../behaviours/ScanStopShootBehaviour" +import GameScene from "../GameScene" +import PhysicsBodyGameObject from "../PhysicsBodyGameObject" + +export default class TankGameObject extends PhysicsBodyGameObject { + private static readonly bulletDuration = 5000 + + private readonly sprite: Phaser.Physics.Arcade.Sprite + private readonly behaviour: ScanStopShootBehaviour + private faceLeft: boolean + private health = 100 + + 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 + body.setAllowGravity(false) + this.mainBody = body + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const superThis = this + this.behaviour = new ScanStopShootBehaviour(this, this.scene, 2000, 2000, { + findTarget() { return superThis.scene.gameMap.getObjectsFrom(superThis, superThis.owner * -1, 92, 300, + (obj) => obj.y2 > superThis.scene.groundPos - superThis.height).size > 0 }, + 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) }, + createBullet() { superThis.scene.createBullet(superThis.owner, TankGameObject.bulletDuration, + superThis.sprite.x + 46 * superThis.owner, superThis.sprite.y, + 100 * superThis.owner, 0, 1.5) } + }) + + this.scene.physics.add.collider(this.sprite, this.scene.getBulletBodies(owner * -1), (me, bullet) => { + this.health -= 50 + this.scene.removeBullet(bullet) + if (this.health <= 0) this.die() + }) + } + + update(time: number): void { + this.behaviour.update(time) + } + + remove(): void { + this.sprite.destroy() + this.removed() + } + + move(velocityX: number, faceLeft: boolean): void { + this.faceLeft = faceLeft + + const body = this.sprite.body as Phaser.Physics.Arcade.Body + body.setVelocityX(velocityX) + + if (velocityX > 0) this.sprite.setFlipX(true) + else this.sprite.setFlipX(!faceLeft) + } + + die(): void { + const body = this.sprite.body as Phaser.Physics.Arcade.Body + body.setVelocityX(0) + const explosion = this.scene.add.sprite(this.sprite.x + (this.faceLeft ? 16 : - 16), + this.sprite.y, "explode", 0) + explosion.setDepth(1) + explosion.anims.play("explode") + explosion.once("animationcomplete", () => { + explosion.destroy() + this.remove() + }) + } + + static preload(scene: GameScene): void { + scene.load.image("tank", "/images/tank.png") + } + + static createIcon(scene: GameScene, x: number, y: number): Phaser.GameObjects.Sprite { + const icon = scene.add.sprite(x, y, "tank") + icon.setScale(0.35, 0.35) + return icon + } +} \ No newline at end of file diff --git a/src/AAGunGameObject.ts b/src/static-objects/AAGunGameObject.ts similarity index 89% rename from src/AAGunGameObject.ts rename to src/static-objects/AAGunGameObject.ts index edefc25..b450482 100644 --- a/src/AAGunGameObject.ts +++ b/src/static-objects/AAGunGameObject.ts @@ -1,5 +1,5 @@ -import GameScene from "./GameScene" -import PhysicsBodyGameObject from "./PhysicsBodyGameObject" +import GameScene from "../GameScene" +import PhysicsBodyGameObject from "../PhysicsBodyGameObject" export default class AAGunGameObject extends PhysicsBodyGameObject { private static chopperWaveDistanceSq = 500*500 @@ -40,6 +40,12 @@ export default class AAGunGameObject extends PhysicsBodyGameObject { this.bodySprite.setY(Math.min(y, this.maxY)) } + remove(): void { + this.gunSprite.destroy() + this.bodySprite.destroy() + this.removed() + } + update(time: number): void { if (this.faceLeft) { this.angle = Phaser.Math.Angle.BetweenPoints(this.scene.chopper.sprite, this.gunSprite) @@ -50,7 +56,7 @@ export default class AAGunGameObject extends PhysicsBodyGameObject { 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, this.gunSprite.x + dir.x * 16, + this.scene.createBullet(this.owner, 30000, this.gunSprite.x + dir.x * 16, this.gunSprite.y + dir.y * 16, dir.x * 100, dir.y * 100) } diff --git a/src/BarrackGameObject.ts b/src/static-objects/BarrackGameObject.ts similarity index 86% rename from src/BarrackGameObject.ts rename to src/static-objects/BarrackGameObject.ts index 8e22212..e2dc646 100644 --- a/src/BarrackGameObject.ts +++ b/src/static-objects/BarrackGameObject.ts @@ -1,5 +1,5 @@ -import GameScene from "./GameScene" -import SpriteGameObject from "./SpriteGameObject" +import GameScene from "../GameScene" +import SpriteGameObject from "../SpriteGameObject" export default class BarrackGameObject extends SpriteGameObject { private readonly _spawnX: number diff --git a/src/FactoryGameObject.ts b/src/static-objects/FactoryGameObject.ts similarity index 86% rename from src/FactoryGameObject.ts rename to src/static-objects/FactoryGameObject.ts index 26f5df2..6efc53d 100644 --- a/src/FactoryGameObject.ts +++ b/src/static-objects/FactoryGameObject.ts @@ -1,5 +1,5 @@ -import GameScene from "./GameScene" -import SpriteGameObject from "./SpriteGameObject" +import GameScene from "../GameScene" +import SpriteGameObject from "../SpriteGameObject" export default class FactoryGameObject extends SpriteGameObject { private readonly _spawnX: number diff --git a/src/GroundGameObject.ts b/src/static-objects/GroundGameObject.ts similarity index 82% rename from src/GroundGameObject.ts rename to src/static-objects/GroundGameObject.ts index 6fb224f..a120f9b 100644 --- a/src/GroundGameObject.ts +++ b/src/static-objects/GroundGameObject.ts @@ -1,5 +1,5 @@ -import GameObject from "./GameObject" -import GameScene from "./GameScene" +import GameObject from "../GameObject" +import GameScene from "../GameScene" export default class GroundGameObject extends GameObject { readonly x1: number @@ -23,4 +23,6 @@ export default class GroundGameObject extends GameObject { this.width = this.scene.worldWidth this.height = sprite.height } + + remove(): void { throw "GoundGameObject cannot be removed" } } \ No newline at end of file diff --git a/src/HelipadGameObject.ts b/src/static-objects/HelipadGameObject.ts similarity index 90% rename from src/HelipadGameObject.ts rename to src/static-objects/HelipadGameObject.ts index f1928d1..8c6b539 100644 --- a/src/HelipadGameObject.ts +++ b/src/static-objects/HelipadGameObject.ts @@ -1,5 +1,5 @@ -import GameScene from "./GameScene" -import PhysicsBodyGameObject from "./PhysicsBodyGameObject" +import GameScene from "../GameScene" +import PhysicsBodyGameObject from "../PhysicsBodyGameObject" export default class HelipadGameObject extends PhysicsBodyGameObject { private readonly sprite: Phaser.GameObjects.Sprite @@ -26,6 +26,8 @@ export default class HelipadGameObject extends PhysicsBodyGameObject { this._center = new Phaser.Math.Vector2(x, this.scene.groundPos - 4) } + remove(): void { throw "HelipadGameObject cannot be removed" } + chopperOnPad(onPad: boolean): void { if (onPad) this.sprite.anims.stop() else this.sprite.anims.play("helipad") diff --git a/src/HomeBuildingGameObject.ts b/src/static-objects/HomeBuildingGameObject.ts similarity index 83% rename from src/HomeBuildingGameObject.ts rename to src/static-objects/HomeBuildingGameObject.ts index 78ba830..724ce6a 100644 --- a/src/HomeBuildingGameObject.ts +++ b/src/static-objects/HomeBuildingGameObject.ts @@ -1,5 +1,5 @@ -import GameScene from "./GameScene" -import SpriteGameObject from "./SpriteGameObject" +import GameScene from "../GameScene" +import SpriteGameObject from "../SpriteGameObject" export default class HomeBuildingGameObject extends SpriteGameObject { private readonly _entryX: number diff --git a/src/TreeGameObject.ts b/src/static-objects/TreeGameObject.ts similarity index 88% rename from src/TreeGameObject.ts rename to src/static-objects/TreeGameObject.ts index dbf7151..bcfdab9 100644 --- a/src/TreeGameObject.ts +++ b/src/static-objects/TreeGameObject.ts @@ -1,5 +1,5 @@ -import GameScene from "./GameScene" -import PhysicsBodyGameObject from "./PhysicsBodyGameObject" +import GameScene from "../GameScene" +import PhysicsBodyGameObject from "../PhysicsBodyGameObject" export default class TreeGameObject extends PhysicsBodyGameObject { private readonly sprites: Phaser.GameObjects.Sprite[] = [] @@ -32,6 +32,8 @@ export default class TreeGameObject extends PhysicsBodyGameObject { this.sprites.push(top) } + remove(): void { throw "TreeGameObject cannot be removed" } + update(): void { const x = this.mainBody.position.x + 16 let y = this.mainBody.position.y + this.mainBody.height - 16 diff --git a/src/VillageGameObject.ts b/src/static-objects/VillageGameObject.ts similarity index 90% rename from src/VillageGameObject.ts rename to src/static-objects/VillageGameObject.ts index 7f38665..c803612 100644 --- a/src/VillageGameObject.ts +++ b/src/static-objects/VillageGameObject.ts @@ -1,6 +1,6 @@ -import CivilianGameObject from "./CivilianGameObject" -import GameScene from "./GameScene" -import SpriteGameObject from "./SpriteGameObject" +import CivilianGameObject from "../mobile-objects/CivilianGameObject" +import GameScene from "../GameScene" +import SpriteGameObject from "../SpriteGameObject" export default class VillageGameObject extends SpriteGameObject { private villagers: CivilianGameObject[] = [] diff --git a/src/GameButton.ts b/src/ui-objects/GameButton.ts similarity index 93% rename from src/GameButton.ts rename to src/ui-objects/GameButton.ts index d84c87b..45f0e17 100644 --- a/src/GameButton.ts +++ b/src/ui-objects/GameButton.ts @@ -1,4 +1,4 @@ -import GameScene from "./GameScene" +import GameScene from "../GameScene" export default class GameButton { private pointerDown = false diff --git a/src/HealthBarGameObject.ts b/src/ui-objects/HealthBarGameObject.ts similarity index 89% rename from src/HealthBarGameObject.ts rename to src/ui-objects/HealthBarGameObject.ts index 7b77bda..7727313 100644 --- a/src/HealthBarGameObject.ts +++ b/src/ui-objects/HealthBarGameObject.ts @@ -1,5 +1,5 @@ -import GameObject from "./GameObject" -import GameScene from "./GameScene" +import GameObject from "../GameObject" +import GameScene from "../GameScene" export default class HealthBarGameObject extends GameObject { readonly x1: number @@ -33,6 +33,8 @@ export default class HealthBarGameObject extends GameObject { this.y2 = this.y + this._height } + remove(): void { throw "HealthBarGameObject cannot be removed" } + set health(h: number) { if (h != this._health) { this._health = h diff --git a/tests/GameMap.test.ts b/tests/GameMap.test.ts index 0212777..2e8647a 100644 --- a/tests/GameMap.test.ts +++ b/tests/GameMap.test.ts @@ -2,10 +2,10 @@ import GameMap from "../src/GameMap" import GameObject from "../src/GameObject" class MockGameObject extends GameObject { - constructor(readonly owner: number, - readonly x1: number, readonly x2: number, - readonly y1: number, readonly y2: number, - readonly width: number, readonly height: number) { + constructor(public owner: number, + public x1: number, public x2: number, + public y1: number, public y2: number, + public width: number, public height: number) { super(undefined, owner) } } @@ -40,4 +40,31 @@ test("getObjectsWithin", () => { expect(result.size).toBe(2) expect(result.has(obj2)).toBeTruthy expect(result.has(obj3)).toBeTruthy +}) + +test("update", () => { + const map = new GameMap(100, 10) + const obj1 = new MockGameObject(1, 8, 12, 0, 5, 4, 5) + map.add(obj1) + const obj2 = new MockGameObject(1, 10, 15, 0, 5, 5, 5) + map.add(obj2) + const obj3 = new MockGameObject(1, 45, 48, 0, 5, 3, 5) + map.add(obj3) + + let result = map.getObjectsWithin(1, 1, 9) + expect(result.size).toBe(1) + expect(result.has(obj1)).toBeTruthy + + obj2.x1 = 5 + obj2.x2 = 10 + + obj3.x1 = 8 + obj3.x2 = 11 + + map.update(100000, 0) + result = map.getObjectsWithin(1, 1, 9) + expect(result.size).toBe(3) + expect(result.has(obj1)).toBeTruthy + expect(result.has(obj2)).toBeTruthy + expect(result.has(obj3)).toBeTruthy }) \ No newline at end of file