diff --git a/docs/todos.md b/docs/todos.md index 5db8075..e0941e0 100644 --- a/docs/todos.md +++ b/docs/todos.md @@ -11,16 +11,16 @@ List of things to implement following the "release" of the minimum viable produc ## Towers -- [ ] Extend projectile into seperate defintion + json file +- [x] Extend projectile into seperate defintion + json file - [ ] Make tower react with slotted gems - [ ] Alter damage based on attunement from slotted gems -- [ ] Tower info on click +- [x] Tower info on click - [x] Animate projectiles - [x] Better mouseover tracking when placing tower and showing radius ## Gems -- [ ] Create Gem definitions +- [x] Create Gem definitions - [ ] Make gems affect towers ## Other diff --git a/public/assets/json/Creeps.json b/public/assets/json/Creeps.json index 2a8eff8..0679b07 100644 --- a/public/assets/json/Creeps.json +++ b/public/assets/json/Creeps.json @@ -5,7 +5,7 @@ "textureArrayLength": 12, "stats": { "health": 5, - "speed": 2.4, + "speed": 6, "special": null, "resistance": { "physical": 0, diff --git a/public/assets/json/Gems.json b/public/assets/json/Gems.json index 094e1bc..1177329 100644 --- a/public/assets/json/Gems.json +++ b/public/assets/json/Gems.json @@ -1,11 +1,12 @@ [ { "name": "Fire Gem", + "description": "Forged from molten lava, the Fire Gem imbues your tower's attacks and adds 50% extra fire damage. It can be merged with any gem and is common.", "type": "Fire", "totalLevels": 2, "textures": [], "cantCombineWith": [], - "specialCombine": ["Yeti"], + "specialCombine": [], "genericImprovements": [ { "damageUp": 2, @@ -21,6 +22,22 @@ "timeToLiveUp": 0, "pierceUp": 1 } + ], + "gemResistanceModifications": [ + { + "physical": 0, + "divine": 0, + "fire": 0.5, + "ice": 0, + "frostfire": 0 + }, + { + "physical": 0, + "divine": 0, + "fire": 0.5, + "ice": 0, + "frostfire": 0 + } ] } ] diff --git a/src/classes/Assets.ts b/src/classes/Assets.ts index d4bb42a..9a92fd3 100644 --- a/src/classes/Assets.ts +++ b/src/classes/Assets.ts @@ -125,7 +125,7 @@ export default class GameAssets { const gem = this.Gems[idx]; for (let i = 1; i <= gem.totalLevels; i++) { const texture = await this.Load(`./assets/gems/${gem.type}/${i}.png`); - gem.textures[i] = texture; + gem.textures[i - 1] = texture; } } for (let i = 0; i < 7; i++) { diff --git a/src/classes/Definitions.ts b/src/classes/Definitions.ts index 4985101..4bee02e 100644 --- a/src/classes/Definitions.ts +++ b/src/classes/Definitions.ts @@ -72,12 +72,14 @@ export type TowerStatsDefinition = { export type GemDefinition = { name: string; + description: string; type: GemType; totalLevels: number; textures: PIXI.Texture[]; cantCombineWith: GemType[]; specialCombine: GemType[]; genericImprovements: GenericGemImprovement[]; + gemResistanceModifications: CreepResistancesDefinition[]; }; export type GenericGemImprovement = { diff --git a/src/classes/game/Gem.ts b/src/classes/game/Gem.ts index b3877ed..b9ccea7 100644 --- a/src/classes/game/Gem.ts +++ b/src/classes/game/Gem.ts @@ -1,11 +1,12 @@ import * as PIXI from 'pixi.js'; -import { GemType } from '../Definitions'; +import { GemType, GemDefinition } from '../Definitions'; import GameAssets from '../Assets'; export default class Gem { public texture: PIXI.Texture; - public type: GemType; public level: number = 1; + public gemDefinition: GemDefinition; constructor(gemType: GemType) { - this.texture = GameAssets.Gems[gemType].textures[0]; + this.gemDefinition = GameAssets.Gems[gemType]; + this.texture = this.gemDefinition.textures[0]; } } diff --git a/src/classes/game/NotificationManager.ts b/src/classes/game/NotificationManager.ts index 1aa5aeb..df86263 100644 --- a/src/classes/game/NotificationManager.ts +++ b/src/classes/game/NotificationManager.ts @@ -3,7 +3,7 @@ import GameObject from '../GameObject'; import * as PIXI from 'pixi.js'; import { FadeInOut } from './AnimationManager'; -export type NotificationType = 'info' | 'warn' | 'danger' | 'reward'; +export type NotificationType = 'info' | 'warn' | 'danger' | 'reward' | 'gemaward'; class Notification { public textObj: PIXI.Text; @@ -20,6 +20,8 @@ class Notification { fill = 0xfc0a0a; } else if (type == 'reward') { fill = 0xd65afc; + } else if (type == 'gemaward') { + fill = 0xffff00; } this.ticksToFadeAway = ticksToFadeAway; this.textObj = new PIXI.Text({ diff --git a/src/classes/game/Tower.ts b/src/classes/game/Tower.ts index 8e58ed0..dbe1237 100644 --- a/src/classes/game/Tower.ts +++ b/src/classes/game/Tower.ts @@ -42,7 +42,7 @@ export class Tower extends GameObject { texture: texture, height: Engine.GridCellSize, width: Engine.GridCellSize, - zIndex: 10, + zIndex: 130, }); this.container.addChild(this.sprite); this.parent.container.addChild(this.container); @@ -53,7 +53,8 @@ export class Tower extends GameObject { } private onParentCellEnter = (e) => { - if (!Engine.TowerManager.isPlacingTower) this.parent.showRangePreview(false, this.definition.stats.range); + if (!Engine.TowerManager.isPlacingTower && Engine.Grid.gridInteractionEnabled) + this.parent.showRangePreview(false, this.definition.stats.range); }; private onParentCellLeave = (e) => { diff --git a/src/classes/gui/Tooltip.ts b/src/classes/gui/Tooltip.ts index 93d6558..32eba83 100644 --- a/src/classes/gui/Tooltip.ts +++ b/src/classes/gui/Tooltip.ts @@ -2,6 +2,7 @@ import * as PIXI from 'pixi.js'; import GuiObject from '../GuiObject'; import GameAssets from '../Assets'; import { Engine } from '../Bastion'; +import Gem from '../game/Gem'; // ! TODO NEXT! @@ -14,12 +15,17 @@ export default class Tooltip extends GuiObject { private damageText: PIXI.Text; private gemAmount: PIXI.Text; private gemAmountSprite: PIXI.Sprite; + private title: PIXI.Sprite; + private costSprite: PIXI.Sprite; + private damageSprite: PIXI.Sprite; + private gemDescriptionText: PIXI.Text; constructor(bounds: PIXI.Rectangle) { super(false); this.bounds = bounds; this.container.x = -500; this.container.y = -500; + this.container.zIndex = 150; this.tooltipSprite = new PIXI.NineSliceSprite({ texture: GameAssets.Frame04Texture, leftWidth: 200, @@ -43,16 +49,16 @@ export default class Tooltip extends GuiObject { }), }); this.titleText.anchor.set(0.5, 0); - let title = new PIXI.Sprite({ + this.title = new PIXI.Sprite({ x: this.tooltipSprite.width / 2, y: -20, width: 250, height: 40, texture: GameAssets.TitleTexture, }); - title.anchor.set(0.5, 0); + this.title.anchor.set(0.5, 0); - const costSprite = new PIXI.Sprite({ + this.costSprite = new PIXI.Sprite({ texture: GameAssets.GoldTexture, x: 10, y: 20, @@ -88,7 +94,7 @@ export default class Tooltip extends GuiObject { }, }, }); - const damageSprite = new PIXI.Sprite({ + this.damageSprite = new PIXI.Sprite({ texture: GameAssets.SwordsTexture, x: 22, y: 70, @@ -117,26 +123,64 @@ export default class Tooltip extends GuiObject { }, }, }); + this.gemDescriptionText = new PIXI.Text({ + x: 10, + y: 20, + text: '', + style: { + fontSize: 18, + wordWrap: true, + wordWrapWidth: this.tooltipSprite.width - 30, + fill: 'white', + fontWeight: 'bold', + fontStyle: 'italic', + stroke: { + color: 0x000000, + width: 5, + }, + }, + }); this.container.addChild(this.tooltipSprite); - this.container.addChild(title); - this.container.addChild(costSprite); - this.container.addChild(damageSprite); + this.container.addChild(this.title); + this.container.addChild(this.costSprite); + this.container.addChild(this.damageSprite); this.container.addChild(this.gemAmountSprite); this.container.addChild(this.costText); this.container.addChild(this.titleText); this.container.addChild(this.damageText); this.container.addChild(this.gemAmount); - Engine.GameMaster.currentScene.stage.addChild(this.container); + this.container.addChild(this.gemDescriptionText); + Engine.app.stage.addChildAt(this.container, 0); } - public SetContent(title, damage: number, cost: number, gemSlotsAmount: number) { + public SetContentTower(title, damage: number, cost: number, gemSlotsAmount: number) { + this.costSprite.alpha = 1; + this.damageSprite.alpha = 1; + this.gemAmountSprite.alpha = 1; + this.costText.alpha = 1; + this.damageText.alpha = 1; + this.gemAmount.alpha = 1; + this.gemDescriptionText.alpha = 0; + this.titleText.text = title; this.gemAmount.text = `Has ${gemSlotsAmount} Gem slots.`; this.gemAmountSprite.texture = GameAssets.GemAmountIcons[gemSlotsAmount]; this.costText.text = `Costs ${cost} gold.`; this.damageText.text = `Deals ${damage} base damage.`; } + public SetContentGem(gem: Gem) { + this.costSprite.alpha = 0; + this.damageSprite.alpha = 0; + this.gemAmountSprite.alpha = 0; + this.costText.alpha = 0; + this.damageText.alpha = 0; + this.gemAmount.alpha = 0; + this.gemDescriptionText.alpha = 1; + + this.titleText.text = `Lv. ${gem.level} ` + gem.gemDefinition.name; + this.gemDescriptionText.text = gem.gemDefinition.description; + } public Show(x, y) { this.container.alpha = 1; if (x + this.container.width > Engine.app.canvas.width) { diff --git a/src/classes/gui/TowerPanel.ts b/src/classes/gui/TowerPanel.ts index 20ac007..3921902 100644 --- a/src/classes/gui/TowerPanel.ts +++ b/src/classes/gui/TowerPanel.ts @@ -7,33 +7,43 @@ import Button, { ButtonTexture } from './Button'; import { Tower } from '../game/Tower'; import Gem from '../game/Gem'; -class VisualGemSlot extends GuiObject { +export class VisualGemSlot extends GuiObject { public iconSprite: PIXI.Sprite; private background: PIXI.Sprite; + private gem: Gem; private i: number = 0; constructor(index: number, parent: PIXI.Container, gem: Gem | null) { super(true); let gtexture; - if (gem == null) { - gtexture = GameAssets.PlusIconTexture; - } else { - gtexture = gem.texture; - } this.container.x = 10; this.container.y = index * Engine.GridCellSize + 300; this.background = new PIXI.Sprite({ texture: GameAssets.FrameInventory, }); + this.gem = gem; + if (gem == null) { + gtexture = GameAssets.PlusIconTexture; + } else { + gtexture = gem.texture; + } this.iconSprite = new PIXI.Sprite({ texture: gtexture, + zIndex: 10, }); this.background.width = Engine.GridCellSize; this.background.height = Engine.GridCellSize; - this.iconSprite.x = Engine.GridCellSize / 2; - this.iconSprite.y = Engine.GridCellSize / 2; - this.iconSprite.width = Engine.GridCellSize / 2; - this.iconSprite.height = Engine.GridCellSize / 2; - this.iconSprite.anchor.set(0.5, 0.5); + if (gem == null) { + this.iconSprite.x = Engine.GridCellSize / 2; + this.iconSprite.y = Engine.GridCellSize / 2; + this.iconSprite.width = Engine.GridCellSize / 2; + this.iconSprite.height = Engine.GridCellSize / 2; + this.iconSprite.anchor.set(0.5, 0.5); + } else { + this.iconSprite.x = 4; + this.iconSprite.y = 4; + this.iconSprite.width = Engine.GridCellSize - 8; + this.iconSprite.height = Engine.GridCellSize - 8; + } this.container.addChild(this.background); this.container.addChild(this.iconSprite); parent.addChild(this.container); diff --git a/src/classes/gui/TowerTab.ts b/src/classes/gui/TowerTab.ts index 81c2056..2c131ff 100644 --- a/src/classes/gui/TowerTab.ts +++ b/src/classes/gui/TowerTab.ts @@ -47,7 +47,8 @@ class TowerButton extends GuiObject { Engine.GameScene.events.on(TowerEvents.TowerPlacedEvent, (name) => { this.resetTint(); }); - this.container.onmousemove = (e) => { + this.container.onpointermove = (e) => { + if (Engine.Grid.gridInteractionEnabled == false) return; if (Engine.TowerManager.isPlacingTower) return; this.ShowTooltip(); }; @@ -63,7 +64,7 @@ class TowerButton extends GuiObject { definition = item; } }); - Engine.GameScene.tooltip.SetContent( + Engine.GameScene.tooltip.SetContentTower( this.towerName, definition.stats.damage, definition.stats.cost, @@ -72,6 +73,7 @@ class TowerButton extends GuiObject { Engine.GameScene.tooltip.Show(Engine.MouseX, Engine.MouseY); } public onClick(e: PIXI.FederatedPointerEvent): void { + if (Engine.Grid.gridInteractionEnabled == false) return; if (Engine.TowerManager.isPlacingTower && Engine.TowerManager.selectedTower.name != this.towerName) { Engine.GameScene.sidebar.towerTab.resetTint(); Engine.TowerManager.ResetChooseTower(); diff --git a/src/scenes/Game.ts b/src/scenes/Game.ts index 7cf6f4e..f6a3cfb 100644 --- a/src/scenes/Game.ts +++ b/src/scenes/Game.ts @@ -13,11 +13,13 @@ import TowerManager from '../classes/game/TowerManager'; import { MissionPickerScene } from './MissionPicker'; import GameUIConstants from '../classes/GameUIConstants'; import Tooltip from '../classes/gui/Tooltip'; -import TowerPanel from '../classes/gui/TowerPanel'; +import TowerPanel, { VisualGemSlot } from '../classes/gui/TowerPanel'; +import Gem from '../classes/game/Gem'; enum RoundMode { Purchase = 0, Combat = 1, + OfferingGems = 2, } export class GameScene extends Scene { @@ -31,6 +33,13 @@ export class GameScene extends Scene { public sidebar: Sidebar; public tooltip: Tooltip; public towerPanel: TowerPanel; + public dimGraphics: PIXI.Graphics = new PIXI.Graphics({ + x: 0, + y: 0, + zIndex: 120, + }); + private offerGemsSprite: PIXI.NineSliceSprite; + private visualGems: VisualGemSlot[] = []; private currentRound: number = 0; private isWaveManagerFinished: boolean = false; private playerWon: boolean = false; @@ -47,13 +56,6 @@ export class GameScene extends Scene { }); } public init() { - this.ticker = new PIXI.Ticker(); - this.ticker.maxFPS = 60; - this.ticker.minFPS = 30; - this.ticker.add(() => { - if (this.update) this.update(this.ticker.elapsedMS); - }); - this.ticker.start(); new Grid(this.mission.gameMap, this.missionIndex); new TowerManager(); new WaveManager(this.mission.rounds, this.mission.gameMap.paths); @@ -76,10 +78,11 @@ export class GameScene extends Scene { }); this.towerPanel = new TowerPanel(GameUIConstants.SidebarRect); this.sidebar = new Sidebar(GameUIConstants.SidebarRect); - this.tooltip = new Tooltip(new PIXI.Rectangle(0, 0, 350, 160)); this.changeRoundButton = new Button(GameUIConstants.ChangeRoundButtonRect, '', ButtonTexture.Button01, true); this.changeRoundButton.container.removeFromParent(); this.sidebar.container.addChild(this.changeRoundButton.container); + Engine.GameMaster.currentScene.stage.addChildAt(this.dimGraphics, 0); + this.tooltip = new Tooltip(new PIXI.Rectangle(0, 0, 350, 160)); // Added custom button logic to still keep all the regular events for the button, just have an icon instead of text. // TODO: maybe make this better? add like a seperate class for icon buttons or smth this.changeRoundButton.CustomButtonLogic = () => { @@ -98,12 +101,19 @@ export class GameScene extends Scene { if (this.roundMode == RoundMode.Combat) return Engine.NotificationManager.Notify('Wave is already in progress.', 'warn'); if (this.isGameOver) return Engine.NotificationManager.Notify('No more waves.', 'danger'); + if (this.roundMode == RoundMode.OfferingGems) return; this.setRoundMode(RoundMode.Combat); this.changeRoundButton.buttonIcon.texture = GameAssets.ExclamationIconTexture; this.events.emit(WaveManagerEvents.NewWave, `${this.currentRound + 1}`); }; - this.MissionStats = new MissionStats(100, 200); + this.ticker = new PIXI.Ticker(); + this.ticker.maxFPS = 60; + this.ticker.minFPS = 30; + this.ticker.add(() => { + if (this.update) this.update(this.ticker.elapsedMS); + }); + this.ticker.start(); } public update(elapsedMS) { if (this.isGameOver) { @@ -116,6 +126,7 @@ export class GameScene extends Scene { Engine.WaveManager.update(elapsedMS); Engine.Grid.update(elapsedMS); Engine.TowerManager.update(elapsedMS); + // Means the round is finished. if (this.isWaveManagerFinished && Engine.Grid.creeps.length == 0) { this.isWaveManagerFinished = false; this.setRoundMode(RoundMode.Purchase); @@ -124,16 +135,13 @@ export class GameScene extends Scene { `Round ${this.currentRound + 1}/${this.mission.rounds.length} completed.`, 'info' ); - if (this.currentRound + 2 == this.mission.rounds.length) { - Engine.NotificationManager.Notify(`Final round.`, 'danger'); - } if (this.currentRound + 1 == this.mission.rounds.length) { Engine.NotificationManager.Notify(`Mission victory!!`, 'reward'); this.changeRoundButton.buttonIcon.texture = GameAssets.HomeIconTexture; this.playerWon = true; } else { - this.currentRound++; this.OfferPlayerGems(); + this.currentRound++; } } @@ -145,14 +153,59 @@ export class GameScene extends Scene { this.ShowScoreScreen(false); } } + public DarkenScreen() { + this.dimGraphics.rect(0, 0, Engine.app.canvas.width, Engine.app.canvas.height); + this.dimGraphics.fill({ color: 0x000000, alpha: 0.5 }); + } private OfferPlayerGems() { Engine.Grid.gridInteractionEnabled = false; - + Engine.GameScene.sidebar.towerTab.resetTint(); + Engine.TowerManager.ResetChooseTower(); + this.setRoundMode(RoundMode.OfferingGems); + let gemsToOffer = this.mission.rounds[this.currentRound].offeredGems; + this.DarkenScreen(); + this.offerGemsSprite = new PIXI.NineSliceSprite({ + width: 400, + height: 200, + texture: GameAssets.Frame01Texture, + leftWidth: 100, + topHeight: 100, + rightWidth: 100, + bottomHeight: 100, + zIndex: this.dimGraphics.zIndex + 1, + x: Engine.app.canvas.width / 2 - 200, + y: Engine.app.canvas.height / 2 - 100, + }); + Engine.GameMaster.currentScene.stage.addChildAt(this.offerGemsSprite, 0); + gemsToOffer.forEach((gType, index) => { + let _Gem = new Gem(gType); + let vGem = new VisualGemSlot(0, Engine.app.stage, _Gem); + this.visualGems.push(vGem); + vGem.container.x = this.offerGemsSprite.x + 69 * (index + 1); + vGem.container.y = this.offerGemsSprite.y + 50; + vGem.container.onpointermove = () => { + Engine.GameScene.tooltip.SetContentGem(_Gem); + Engine.GameScene.tooltip.Show(Engine.MouseX, Engine.MouseY); + }; + vGem.container.onpointerleave = () => { + Engine.GameScene.tooltip.Hide(); + }; + vGem.onClick = () => { + Engine.GameScene.tooltip.Hide(); + this.PlayerPickedGem(_Gem); + }; + }); + } + private PlayerPickedGem(gem: Gem) { + this.offerGemsSprite.destroy(); + this.dimGraphics.clear(); + this.visualGems.forEach((item) => item.destroy()); Engine.Grid.gridInteractionEnabled = true; + Engine.NotificationManager.Notify(gem.gemDefinition.name + ' added to your inventory.', 'gemaward'); } private ShowScoreScreen(lost) { - // TODO: show to player for real + // TODO: show to player for real (see how this.OfferPlayerGems() does it) if (lost) { console.log('LOSE!'); } else {