diff --git a/res/SpiderSheet.aseprite b/res/SpiderSheet.aseprite new file mode 100644 index 0000000..fc0fac9 Binary files /dev/null and b/res/SpiderSheet.aseprite differ diff --git a/res/slime.aseprite b/res/slime.aseprite new file mode 100644 index 0000000..8a825be Binary files /dev/null and b/res/slime.aseprite differ diff --git a/res/slime.png b/res/slime.png new file mode 100644 index 0000000..9277a08 Binary files /dev/null and b/res/slime.png differ diff --git a/src/config.ts b/src/config.ts index 8faff18..2f3aa64 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ import * as ex from "excalibur"; -import { KnightIdle, SpiderIdle } from "./resources"; +import { KnightIdle, SlimeIdle, SpiderIdle } from "./resources"; const seed = Date.now(); console.log("Random seed:", seed); export const RANDOM = new ex.Random(seed); @@ -9,7 +9,7 @@ export const SCALE = ex.vec(3, 3); export const BOARD_OFFSET = ex.vec(32 * 3, 32 * 4); export const ENEMY_SPEED = 200; -export type UnitType = "Knight" | "Spider"; +export type UnitType = "Knight" | "Spider" | "Slime"; export interface UnitConfig { graphics: { offset: ex.Vector, @@ -27,7 +27,7 @@ export const UNIT_CONFIG: Record = { idle: KnightIdle }, health: 5, - movement: 4, + movement: 3, attack: 2, range: 1 }, @@ -40,6 +40,16 @@ export const UNIT_CONFIG: Record = { movement: 3, attack: 1, range: 1 + }, + Slime: { + graphics: { + offset: ex.vec(0, 8 * SCALE.y), + idle: SlimeIdle + }, + health: 2, + movement: 1, + attack: 4, + range: 1 } } as const; diff --git a/src/levels/level-base.ts b/src/levels/level-base.ts index 2bd02dd..90ac4b3 100644 --- a/src/levels/level-base.ts +++ b/src/levels/level-base.ts @@ -44,6 +44,12 @@ export interface LevelData { // ] // } +export const CharToUnit = { + K: 'Knight', + S: 'Spider', + M: 'Slime' +} as const; + export class LevelBase extends ex.Scene { board!: Board; @@ -80,13 +86,15 @@ export class LevelBase extends ex.Scene { this.camera.pos = this.board.getCenter(); } + private _subscriptions: ex.Subscription[] = []; override onActivate(): void { this.resetAndLoad(); this.turnManager.start(); Resources.LevelMusic2.loop = true; Resources.LevelMusic2.play(); - this.engine.input.keyboard.on('press', (evt) => { + this._subscriptions.push( + this.engine.input.keyboard.once('press', (evt) => { // DELETEME for debugging if (evt.key === ex.Keys.W) { (this.players[1] as ComputerPlayer).lose(); @@ -94,7 +102,7 @@ export class LevelBase extends ex.Scene { if (evt.key === ex.Keys.L) { (this.players[0] as HumanPlayer).lose(); } - }); + })); } @@ -102,6 +110,7 @@ export class LevelBase extends ex.Scene { // TODO deactivate event handlers on types that have them!! Resources.LevelMusic2.instances.forEach(i => i.stop()); Resources.LevelMusic2.stop(); + this._subscriptions.forEach(s => s.close()); } parse(levelData: LevelData): Board { @@ -123,7 +132,7 @@ export class LevelBase extends ex.Scene { const terrain = data.charAt(0) as Terrain; let unit: Unit | null = null; if (data.length === 3) { - const unitType: UnitType = data.charAt(1) === 'K' ? 'Knight' : 'Spider'; + const unitType: UnitType = CharToUnit[data.charAt(1) as 'K' | 'S' | 'M'] const playerIndex = (+data.charAt(2)) - 1; unit = new Unit(x, y, unitType, board, this.players[playerIndex]); diff --git a/src/levels/start-screen.ts b/src/levels/start-screen.ts index 8920277..c88d5c0 100644 --- a/src/levels/start-screen.ts +++ b/src/levels/start-screen.ts @@ -56,19 +56,24 @@ export class StartScreen extends ex.Scene { this.add(this.instructions); } + _subscriptions: ex.Subscription[] = []; onActivate(): void { + console.log('activate start screen') Resources.TitleMusic.loop = true; Resources.TitleMusic.play(); - this.engine.input.pointers.primary.once('down', () => { + this._subscriptions.push( + this.engine.input.pointers.primary.once('down', () => { + this.engine.goToScene('tutorial'); + })); + this._subscriptions.push( + this.engine.input.keyboard.once('press', () => { this.engine.goToScene('tutorial'); - }); - this.engine.input.keyboard.once('press', () => { - this.engine.goToScene('tutorial'); - }); + })); } onDeactivate(): void { Resources.TitleMusic.stop(); + this._subscriptions.forEach(h => h.close()); } } \ No newline at end of file diff --git a/src/levels/tutorial.ts b/src/levels/tutorial.ts index 282ab2d..0d9e1ef 100644 --- a/src/levels/tutorial.ts +++ b/src/levels/tutorial.ts @@ -8,7 +8,7 @@ import { HumanPlayer } from '../human-player'; export const TutorialData: LevelData = { name: 'tutorial', displayName: 'Gentle Plains', - nextLevel: 'level', + nextLevel: 'level1', width: 6, height: 3, maxTurns: 10, @@ -21,7 +21,9 @@ export const TutorialData: LevelData = { } export class Tutorial extends LevelBase { focus!: ex.Actor; - + tutorialDirections!: ex.Actor; + private bottomScreen = ex.vec(400, 2000); + private centerScreen = ex.vec(400, 700); constructor() { super(TutorialData, 'tutorial'); } @@ -42,8 +44,44 @@ export class Tutorial extends LevelBase { this.focus.graphics.opacity = 0; this.engine.add(this.focus); } + const screenWidth = engine.screen.resolution.width; + const tutorialDirections = new ex.Text({ + text: `S or Tap to Skip!`, + font: new ex.Font({ + family: 'notjamslab14', + size: 32 * SCALE.x, + unit: ex.FontUnit.Px, + color: ex.Color.White, + baseAlign: ex.BaseAlign.Top, + quality: 4 + }), + }); - + this.tutorialDirections = new ex.Actor({ + name: 'directions', + pos: this.bottomScreen, + coordPlane: ex.CoordPlane.Screen, + color: new ex.Color(50, 240, 50, .4), + width: screenWidth, + height: 100, + z: 10 + }); + this.tutorialDirections.graphics.opacity = 0; + this.tutorialDirections.graphics.add('text', tutorialDirections); + this.tutorialDirections.graphics.show('text') + engine.add(this.tutorialDirections); + } + + async showSkip() { + const transitionTime = 1200; + await this.tutorialDirections.actions.runAction( + new ex.ParallelActions([ + new ex.ActionSequence(this.tutorialDirections, ctx => + ctx.easeTo(this.centerScreen, transitionTime, ex.EasingFunctions.EaseInOutCubic)), + new ex.ActionSequence(this.tutorialDirections, ctx => + ctx.fade(1, transitionTime)) + ]) + ).toPromise(); } async moveToUnit1() { @@ -122,12 +160,20 @@ export class Tutorial extends LevelBase { await this.focus.actions.fade(0, 200).toPromise(); } + private _subs: ex.Subscription[] = []; async onActivate() { - this.engine.input.keyboard.once('press', evt => { - if (evt.key === ex.Keys.Esc) { + this.showSkip(); + console.log('activate tutorial'); + this._subs.push( + this.engine.input.keyboard.on('press', evt => { + if (evt.key === ex.Keys.S) { + this.engine.goToScene('level1'); + } + })); + this._subs.push( + this.engine.input.pointers.primary.on('down', evt => { this.engine.goToScene('level1'); - } - }) + })); Resources.LevelMusic2.loop = true; Resources.LevelMusic2.play(); @@ -177,7 +223,7 @@ export class Tutorial extends LevelBase { await this.hideText(); await this.showText(7); - await this.focus.actions.delay(7000); + await this.focus.actions.delay(3000); await this.hideText(); this.camera.zoomOverTime(1, 1000, ex.EasingFunctions.EaseInOutCubic); @@ -187,6 +233,7 @@ export class Tutorial extends LevelBase { onDeactivate(): void { Resources.LevelMusic2.stop(); + this._subs.forEach(s => s.close()); } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index ee45427..b7972ba 100644 --- a/src/main.ts +++ b/src/main.ts @@ -53,12 +53,12 @@ export const Level2Data: LevelData = { maxTurns: 100, players: ['human', 'computer'], data: [ - 'GK1', 'G', 'GS2', 'G', 'G', 'GS2', + 'GK1', 'G', 'GM2', 'G', 'G', 'GS2', 'GK1', 'G', 'G', 'G', 'G', 'GS2', 'GK1', 'G', 'W', 'W', 'G', 'G', 'G', 'G', 'W', 'W', 'G', 'G', 'G', 'G', 'W', 'W', 'G', 'G', - 'G', 'GS2', 'W', 'W', 'GS2', 'GS2', + 'GM2', 'GM2', 'W', 'W', 'GS2', 'GS2', ] } diff --git a/src/resources.ts b/src/resources.ts index 7ec8b03..e9fea0d 100644 --- a/src/resources.ts +++ b/src/resources.ts @@ -4,6 +4,7 @@ import * as ex from 'excalibur'; import TitleImagePath from '../res/title.png'; import KnightSpriteSheetPath from '../res/KnightSheet.png'; import SpiderSheetPath from '../res/SpiderSheet.png'; +import SlimeSheetPath from '../res/slime.png'; import HeartSheetPath from '../res/HeartSheet.png'; import UISheetPath from '../res/UISheet.png'; import TerrainSheetPath from '../res/TerrainSheet.png'; @@ -26,6 +27,7 @@ export const Resources = { TitleImage: new ex.ImageSource(TitleImagePath), KnightSpriteSheet: new ex.ImageSource(KnightSpriteSheetPath), SpiderSheet: new ex.ImageSource(SpiderSheetPath), + SlimeSheet: new ex.ImageSource(SlimeSheetPath), HeartSheet: new ex.ImageSource(HeartSheetPath), UISheet: new ex.ImageSource(UISheetPath), TerrainSheet: new ex.ImageSource(TerrainSheetPath), @@ -142,6 +144,27 @@ export const SpiderIdle = ex.Animation.fromSpriteSheetCoordinates({ ] }); +export const SlimeSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: Resources.SlimeSheet, + grid: { + rows: 1, + columns: 4, + spriteHeight: 32, + spriteWidth: 32 + } +}); + +export const SlimeIdle = ex.Animation.fromSpriteSheetCoordinates({ + spriteSheet: SlimeSpriteSheet, + strategy: ex.AnimationStrategy.Loop, + frameCoordinates: [ + {x: 0, y: 0, duration: 200}, + {x: 1, y: 0, duration: 200}, + {x: 2, y: 0, duration: 200}, + {x: 3, y: 0, duration: 200} + ] +}); + export const KnightSpriteSheet = ex.SpriteSheet.fromImageSource({ image: Resources.KnightSpriteSheet, grid: { diff --git a/src/turn-manager.ts b/src/turn-manager.ts index 5c00bd1..147d48a 100644 --- a/src/turn-manager.ts +++ b/src/turn-manager.ts @@ -23,6 +23,7 @@ export class TurnManager { private centerScreen = ex.vec(400, 400); private bottomScreen = ex.vec(400, 2000); private victory: ex.Actor; + private victoryDirections: ex.Actor; private failure: ex.Actor; constructor(public engine: ex.Engine, public level: LevelBase, public players: Player[], selectionManager: SelectionManager, public maxTurns: number) { @@ -84,6 +85,32 @@ export class TurnManager { this.victory.graphics.show('text') engine.add(this.victory); + const victoryDirections = new ex.Text({ + text: `Click to proceed!`, + font: new ex.Font({ + family: 'notjamslab14', + size: 32 * SCALE.x, + unit: ex.FontUnit.Px, + color: ex.Color.White, + baseAlign: ex.BaseAlign.Top, + quality: 4 + }), + }); + + this.victoryDirections = new ex.Actor({ + name: 'directions', + pos: this.topScreen, + coordPlane: ex.CoordPlane.Screen, + color: new ex.Color(50, 240, 50, .4), + width: screenWidth, + height: 100, + z: 10 + }); + this.victoryDirections.graphics.opacity = 0; + this.victoryDirections.graphics.add('text', victoryDirections); + this.victoryDirections.graphics.show('text') + engine.add(this.victoryDirections); + const failureText1 = new ex.Text({ text: `Failure!`, font: new ex.Font({ @@ -166,6 +193,15 @@ export class TurnManager { ctx.fade(1, transitionTime)) ]) ).toPromise(); + + await this.victoryDirections.actions.runAction( + new ex.ParallelActions([ + new ex.ActionSequence(this.victoryDirections, ctx => + ctx.easeTo(this.centerScreen.add(ex.vec(0, 150)), transitionTime, ex.EasingFunctions.EaseInOutCubic)), + new ex.ActionSequence(this.victoryDirections, ctx => + ctx.fade(1, transitionTime)) + ]) + ).toPromise(); } async start() { @@ -185,7 +221,9 @@ export class TurnManager { if (this.currentPlayer instanceof ComputerPlayer) { await this.showVictory(); this.engine.input.pointers.once('down', () => { - this.engine.goToScene(this.level.levelData.nextLevel); + setTimeout(() => { + this.engine.goToScene(this.level.levelData.nextLevel); + }); }); return; } diff --git a/todo.md b/todo.md index 2f56dbf..d3914f8 100644 --- a/todo.md +++ b/todo.md @@ -1,13 +1,15 @@ ## Bugs * [ ] Sometimes the tutorial crashes and there is no way to proceed +* [ ] Re-entering the tutorial is in a weird state ## UI * [x] Add social card! ## Gameplay * [x] Start Screen -* [ ] Skip tutorial option -* [ ] Add instructions to the win screen +* [ ] Disable cheats +* [x] Skip tutorial option +* [x] Add instructions to the win screen * [ ] Add a stats win screen * [ ] Units kills/units lost * [ ] Wait for death anim before popping ui?