From 8de96f037d929c3a02e4ef701c4e1aa13a82499e Mon Sep 17 00:00:00 2001 From: pr0mming Date: Tue, 18 Jun 2024 15:41:54 -0500 Subject: [PATCH] Add some changes: - GitHub button - Fix bug with stopwatch - Refactor logic of labels in new class - Add some comments --- index.html | 12 ++ public/styles/global.css | 4 + src/game/controls/label/Label.ts | 17 +++ src/game/controls/label/LabelGroup.ts | 91 +++++++++++++++ src/game/objects/disk/Disk.ts | 24 +++- src/game/objects/disk/DiskGroup.ts | 10 +- src/game/objects/tower/Tower.ts | 5 + src/game/objects/tower/TowerGroup.ts | 8 +- src/game/scenes/Game.ts | 159 ++++++++++---------------- 9 files changed, 219 insertions(+), 111 deletions(-) create mode 100644 src/game/controls/label/Label.ts create mode 100644 src/game/controls/label/LabelGroup.ts diff --git a/index.html b/index.html index 2ad3094..915771b 100644 --- a/index.html +++ b/index.html @@ -20,6 +20,18 @@
+ +
+ Star +
diff --git a/public/styles/global.css b/public/styles/global.css index 93ff985..ddf099a 100644 --- a/public/styles/global.css +++ b/public/styles/global.css @@ -23,3 +23,7 @@ main { justify-content: center; align-items: center; } + +#game-controls { + margin: 0 auto; +} diff --git a/src/game/controls/label/Label.ts b/src/game/controls/label/Label.ts new file mode 100644 index 0000000..e709566 --- /dev/null +++ b/src/game/controls/label/Label.ts @@ -0,0 +1,17 @@ +import { GameObjects, Scene } from 'phaser'; + +interface ILabelProps { + scene: Scene; + x: number; + y: number; + text: string; + nameKey: string; +} + +export class Label extends GameObjects.Text { + constructor({ scene, x, y, text, nameKey }: ILabelProps) { + super(scene, x, y, text, {}); + + this.name = nameKey; + } +} diff --git a/src/game/controls/label/LabelGroup.ts b/src/game/controls/label/LabelGroup.ts new file mode 100644 index 0000000..7376cf8 --- /dev/null +++ b/src/game/controls/label/LabelGroup.ts @@ -0,0 +1,91 @@ +import { GameObjects, Scene } from 'phaser'; + +// Interfaces +import { IGameInitialData } from '@src/game/common/interfaces/IGameInitialData'; + +// Controls +import { Label } from '@src/game/controls/label/Label'; + +interface ILabelGroupProps { + scene: Scene; + gameData: IGameInitialData; +} + +export class LabelGroup extends GameObjects.Group { + private _gameData: IGameInitialData; + + constructor({ scene, gameData }: ILabelGroupProps) { + super(scene); + + this._gameData = gameData; + + this.classType = Label; + + this._setUp(); + } + + private _setUp() { + const style = { + font: '15px BitBold', + fill: 'white', + stroke: 'black', + strokeThickness: 2.5, + }; + + const attempsLabel = new Label({ + scene: this.scene, + x: 50, + y: 22, + text: `MOVEMENTS: ${this._gameData.attemps}`, + nameKey: 'ATTEMPS', + }).setStyle(style); + + this.add(attempsLabel, true); + + const timeLabel = new Label({ + scene: this.scene, + x: 230, + y: 22, + text: 'TIME: 00:00:00', + nameKey: 'TIME', + }).setStyle(style); + + this.add(timeLabel, true); + } + + showEndGameLabel(text: string) { + const stWin = new Label({ + scene: this.scene, + x: this.scene.cameras.main.centerX, + y: 120, + text: text, + nameKey: 'END_TEXT', + }) + .setFontFamily('"BitBold", "Tahoma"') + .setFontSize(20) + .setColor('white') + .setStroke('black', 2.5) + .setAlpha(0) + .setOrigin(0.5); + + this.add(stWin, true); + + this.scene.tweens.add({ + targets: stWin, + props: { + alpha: 1, + }, + ease: 'Sine.easeInOut', + yoyo: true, + repeat: -1, + }); + } + + setTextByKey(keyName: string, value: string) { + const _label = this.getMatching('name', keyName)[0]; + + if (_label) { + (_label as GameObjects.Text).setText(value); + } + } +} diff --git a/src/game/objects/disk/Disk.ts b/src/game/objects/disk/Disk.ts index 13d22d9..efc7f63 100644 --- a/src/game/objects/disk/Disk.ts +++ b/src/game/objects/disk/Disk.ts @@ -12,9 +12,12 @@ interface IDiskProps { tint: number; diskType: number; towerOwner: number; - onDragLeave: (disk: Disk) => void; + onDragEnd: (disk: Disk) => void; } +/** + * This method represents a disk to put over a tower + */ export class Disk extends Physics.Arcade.Image { private readonly _diskType: number; private _towerOwner: number; @@ -30,7 +33,7 @@ export class Disk extends Physics.Arcade.Image { tint, diskType, towerOwner, - onDragLeave, + onDragEnd, }: IDiskProps) { super(scene, x, y, textureKey); @@ -41,16 +44,21 @@ export class Disk extends Physics.Arcade.Image { this._currentPosition = { x, y }; - this.setTint(tint); + this.setTint(tint); // Little hack to have different colors of disks (because there is limited colors) this.setScale(scaleX, 0.6); this.setBodySize(this.width - 60, this.height); - this._setUpEvents(onDragLeave); + this._setUpEvents(onDragEnd); } - private _setUpEvents(onDragLeave: (disk: Disk) => void) { + /** + * This method prepares the events to allow use the pointer to move a disk + * @param onDragEnd callback of drag end event + */ + private _setUpEvents(onDragEnd: (disk: Disk) => void) { this.enableInteraction(); + // Update sprite position using the pointer this.on( Phaser.Input.Events.DRAG, (_: unknown, dragX: number, dragY: number) => { @@ -59,11 +67,15 @@ export class Disk extends Physics.Arcade.Image { } ); + // Is necessary a callback to process large logic of collisions this.on(Phaser.Input.Events.DRAG_END, () => { - onDragLeave(this); + onDragEnd(this); }); } + /** + * This method enable the interaction for the sprite + */ enableInteraction() { this.setInteractive({ useHandCursor: true, diff --git a/src/game/objects/disk/DiskGroup.ts b/src/game/objects/disk/DiskGroup.ts index 6c361a9..7289df4 100644 --- a/src/game/objects/disk/DiskGroup.ts +++ b/src/game/objects/disk/DiskGroup.ts @@ -7,7 +7,7 @@ interface IDiskGroupProps { scene: Scene; world: Physics.Arcade.World; diskNumber: number; - onDragLeave: (disk: Disk) => void; + onDragEnd: (disk: Disk) => void; } export class DiskGroup extends Physics.Arcade.Group { @@ -19,7 +19,7 @@ export class DiskGroup extends Physics.Arcade.Group { private readonly _DISK_TEXTURES: string[]; - constructor({ scene, world, diskNumber, onDragLeave }: IDiskGroupProps) { + constructor({ scene, world, diskNumber, onDragEnd }: IDiskGroupProps) { super(world, scene); this.classType = Disk; @@ -39,10 +39,10 @@ export class DiskGroup extends Physics.Arcade.Group { 'pieceBrown', ]; - this._setUp(onDragLeave); + this._setUp(onDragEnd); } - private _setUp(onDragLeave: (disk: Disk) => void) { + private _setUp(onDragEnd: (disk: Disk) => void) { for ( let i = 0, scaleX = this._INITIAL_DISK_X_AXIS_SCALE, @@ -61,7 +61,7 @@ export class DiskGroup extends Physics.Arcade.Group { tint, diskType: i, towerOwner: 0, - onDragLeave, + onDragEnd, }); this.add(newDisk, true); diff --git a/src/game/objects/tower/Tower.ts b/src/game/objects/tower/Tower.ts index 3c3bc2b..723ab64 100644 --- a/src/game/objects/tower/Tower.ts +++ b/src/game/objects/tower/Tower.ts @@ -8,6 +8,9 @@ interface ITowerProps { disks: number[]; } +/** + * This class represents a tower of the game + */ export class Tower extends Physics.Arcade.Image { private readonly _towerType: number; @@ -22,6 +25,8 @@ export class Tower extends Physics.Arcade.Image { this._disks = pieces; this.setScale(0.6, 0.6); + + // Adjust the physics body to detect better the collisions this.setBodySize(this.width - 60, this.height - 20); } diff --git a/src/game/objects/tower/TowerGroup.ts b/src/game/objects/tower/TowerGroup.ts index 885ce4e..c0c0d17 100644 --- a/src/game/objects/tower/TowerGroup.ts +++ b/src/game/objects/tower/TowerGroup.ts @@ -10,11 +10,14 @@ interface ITowerGroupProps { towersNumber: number; } +/** + * This method represents the array of the towers + */ export class TowerGroup extends Physics.Arcade.Group { private readonly _INITIAL_X_AXIS_POSIION: number; private readonly _INITIAL_Y_AXIS_POSIION: number; - private readonly _INTERVAL_X_AXIS_OFFSET: number; + private readonly _INTERVAL_X_AXIS_OFFSET: number; // Used to increment horizontally private readonly _TOWERS_NUMBER: number; constructor({ scene, world, diskNumber, towersNumber }: ITowerGroupProps) { @@ -32,6 +35,7 @@ export class TowerGroup extends Physics.Arcade.Group { } private _setUp(diskNumber: number) { + // Loop to place each tower for ( let i = 0, x = this._INITIAL_X_AXIS_POSIION; i < this._TOWERS_NUMBER; @@ -42,7 +46,7 @@ export class TowerGroup extends Physics.Arcade.Group { x, y: this._INITIAL_Y_AXIS_POSIION, towerType: i, - disks: i === 0 ? Array.from(Array(diskNumber).keys()) : [], + disks: i === 0 ? Array.from(Array(diskNumber).keys()) : [], // The first tower has always all the disks }); this.add(newTower, true); diff --git a/src/game/scenes/Game.ts b/src/game/scenes/Game.ts index b949960..64f8dc0 100644 --- a/src/game/scenes/Game.ts +++ b/src/game/scenes/Game.ts @@ -1,4 +1,4 @@ -import { GameObjects, Scene, Time } from 'phaser'; +import { Scene, Time } from 'phaser'; // Interfaces import { IGameInitialData } from '@game/common/interfaces/IGameInitialData'; @@ -7,6 +7,7 @@ import { IGameInstruction } from '@game/common/interfaces/IGameInstruction'; // Controls import { ButtonGroup } from '@game/controls/button/ButtonGroup'; import { RegulationButtonGroup } from '@game/controls/regulation-button/RegulationButton'; +import { LabelGroup } from '@game/controls/label/LabelGroup'; // Objects import { Disk } from '@game/objects/disk/Disk'; @@ -25,29 +26,27 @@ export class Game extends Scene { private _gameRulesManager!: GameRulesManager; + private _labelGroup!: LabelGroup; private _buttonsGroup!: ButtonGroup; private _towerGroup!: TowerGroup; private _diskGroup!: DiskGroup; - private _labels!: GameObjects.Group; - - private _elapsedSeconds: number; // It's used to keep the seconds elapsed (Idk if there is a better approach) + private _elapsedSeconds: number = 0; // It's used to keep the seconds elapsed (Idk if there is a better approach) private _stopWatch?: Time.TimerEvent; constructor() { super('Game'); - - this._elapsedSeconds = 0; } init(gameData: IGameInitialData) { this._gameData = gameData; + this._elapsedSeconds = 0; } create() { this.cameras.main.setBounds(0, 0, 900, 600); - //Crear botones + // Create control buttons this._buttonsGroup = new ButtonGroup({ scene: this, @@ -70,13 +69,18 @@ export class Game extends Scene { name: 'RESTART', onPointerDownEvent: () => { const gameData = getInitialGameData(); - this.scene.start('Game', gameData); + this.scene.restart(gameData); }, }); - //Crear controles de juego + // Create labels + + this._labelGroup = new LabelGroup({ + scene: this, + gameData: this._gameData, + }); - this.setUpLabels(); + // Create regulation buttons const regulationBtnGroup = new RegulationButtonGroup({ scene: this, @@ -108,17 +112,13 @@ export class Game extends Scene { // Game objects - /* - disk 3 -> -- - disk 2 -> ---- - disk 1 -> ------ - disk 0 -> -------- - */ + // disk 3 -> -- + // disk 2 -> ---- + // disk 1 -> ------ + // disk 0 -> -------- - /* - | | | - tower 0 tower 1 tower 2 - */ + // | | | + // tower 0 tower 1 tower 2 this._towerGroup = new TowerGroup({ scene: this, @@ -131,11 +131,12 @@ export class Game extends Scene { scene: this, world: this.physics.world, diskNumber: this._gameData.disksAmmount, - onDragLeave: (disk: Disk) => { + onDragEnd: (disk: Disk) => { this.validateHanoi(disk); }, }); + // Add game rules logic this._gameRulesManager = new GameRulesManager({ scene: this, gameData: this._gameData, @@ -144,20 +145,27 @@ export class Game extends Scene { }); this.input.on(Phaser.Input.Events.DRAG_START, () => { - if (this._stopWatch === undefined || this._stopWatch?.paused) { - this._setUpTimer(); + if (this._elapsedSeconds === 0) { + this.restartStopwatch(); + + this.input.removeListener(Phaser.Input.Events.DRAG_START); } }); - } - private _setUpTimer() { - this._elapsedSeconds = 0; + // Prepare timer to reuse + this.setUpTimer(); + } - this._stopWatch = this.time.addEvent({ + /** + * This method to prepares the stopwatch instance + */ + setUpTimer() { + this._stopWatch = new Phaser.Time.TimerEvent({ delay: 1000, callback: () => { const elapsedSeconds = this._elapsedSeconds; + // Show time format const hours = Math.floor(elapsedSeconds / 3600); const minutes = Math.floor((elapsedSeconds % 3600) / 60); const seconds = elapsedSeconds % 60; @@ -169,7 +177,7 @@ export class Game extends Scene { ':' + String(seconds).padStart(2, '0'); - this._setLabelTextByKey('TIME', `TIME: ${timeFormat}`); + this._labelGroup.setTextByKey('TIME', `TIME: ${timeFormat}`); this._elapsedSeconds++; }, @@ -178,13 +186,17 @@ export class Game extends Scene { }); } + restartStopwatch() { + if (this._stopWatch) this.time.addEvent(this._stopWatch); + } + validateHanoi(disk: Disk) { if ( !this.physics.overlap( disk, this._towerGroup, (_, tower) => { - // Ubicar pieza en la torre (visualmente) + // Calculate x, y position and set the sprite visually const { x, y } = this._gameRulesManager.computeDiskPosition( disk, @@ -196,12 +208,12 @@ export class Game extends Scene { this._gameData.attemps++; - this._setLabelTextByKey( + this._labelGroup.setTextByKey( 'ATTEMPS', 'MOVEMENTS: ' + this._gameData.attemps ); - // Win? + // Check if user has won if (this._gameRulesManager.hasFinished()) { this._buttonsGroup.getByName('SOLVE').disableInteractive(); @@ -209,7 +221,7 @@ export class Game extends Scene { if (this._stopWatch) this._stopWatch.paused = true; - this._showEndGameLabel('WIN!'); + this._labelGroup.showEndGameLabel('YOU WON!'); } }, (_, tower) => { @@ -218,46 +230,30 @@ export class Game extends Scene { this ) ) { - //Volver a la posiciĆ³n anterior en caso de no poner la ficha en una torre + // Put back the sprite where it was (invalid movement case) disk.x = disk.currentPosition.x; disk.y = disk.currentPosition.y; } } - private _showEndGameLabel(text: string) { - if (this._stopWatch) this._stopWatch.paused = true; - - const stWin = this.add - .text(this.cameras.main.centerX, 120, text) - .setFontFamily('"BitBold", "Tahoma"') - .setFontSize(20) - .setColor('white') - .setStroke('black', 2.5) - .setScrollFactor(0, 0) - .setAlpha(0) - .setOrigin(0.5); - - this.tweens.add({ - targets: stWin, - props: { - alpha: 1, - }, - ease: 'Sine.easeInOut', - yoyo: true, - repeat: -1, - }); - } - + /** + * This method dispatch the logic to solve the game from any position + */ handleSolveClick() { + // Disable buttons while is solving this._diskGroup.setInteractive(false); this._buttonsGroup.setInteractive(false); + // Get set of instructions array const instructions: IGameInstruction[] = []; this._gameRulesManager.getSolutionInstructions(0, 0, 2, 1, instructions); - this._setUpTimer(); + if (this._elapsedSeconds === 0) { + this.restartStopwatch(); + } + // Put sprites visually this.processGameInstruction(instructions, 0); } @@ -279,58 +275,25 @@ export class Game extends Scene { }, ease: 'Sine.easeInOut', onComplete: () => { + // Recall again for the next instruction ... this.processGameInstruction(instructions, index + 1); }, }); this._gameData.attemps++; - this._setLabelTextByKey( - 'MOVEMENTS', + this._labelGroup.setTextByKey( + 'ATTEMPS', 'MOVEMENTS: ' + this._gameData.attemps ); } else { - this._buttonsGroup.getByName('RESTART').enableInteraction(); - - this._showEndGameLabel('FINISHED!'); - } - } - - setUpLabels() { - const style = { - font: '15px BitBold', - fill: 'white', - stroke: 'black', - strokeThickness: 2.5, - }; + // Has finished - this._labels = this.add.group(); - - const attempsLabel = this.add.text( - 50, - 22, - 'MOVEMENTS: ' + this._gameData.attemps, - style - ); - - attempsLabel.name = 'ATTEMPS'; - attempsLabel.setScrollFactor(0, 0); - - this._labels.add(attempsLabel, true); - - const timeLabel = this.add.text(230, 22, 'TIME: 00:00:00', style); - - timeLabel.name = 'TIME'; - timeLabel.setScrollFactor(0, 0); - - this._labels.add(timeLabel, true); - } + this._buttonsGroup.getByName('RESTART').enableInteraction(); - private _setLabelTextByKey(keyName: string, value: string) { - const _label = this._labels.getMatching('name', keyName)[0]; + if (this._stopWatch) this._stopWatch.paused = true; - if (_label) { - (_label as GameObjects.Text).setText(value); + this._labelGroup.showEndGameLabel('FINISHED!'); } } }