diff --git a/roguelike-shared/src/main/scala/roguelike/GameEvent.scala b/roguelike-shared/src/main/scala/roguelike/GameEvent.scala index 10741cd..ac0aa45 100644 --- a/roguelike-shared/src/main/scala/roguelike/GameEvent.scala +++ b/roguelike-shared/src/main/scala/roguelike/GameEvent.scala @@ -52,6 +52,11 @@ enum GameEvent extends GlobalEvent: // ViewModel events case CameraSnapToPlayer + // Stats + case ModifyPower(amount: Int) + case ModifyMaxHp(amount: Int) + case ModifyDefense(amount: Int) + enum HostileEvent: case HostileMeleeAttack(attackerName: String, power: Int) case HostileGiveXP(amount: Int) diff --git a/roguelike/src/main/scala/roguelike/components/windows/LevelUp.scala b/roguelike/src/main/scala/roguelike/components/windows/LevelUp.scala deleted file mode 100644 index c88aa38..0000000 --- a/roguelike/src/main/scala/roguelike/components/windows/LevelUp.scala +++ /dev/null @@ -1,106 +0,0 @@ -package roguelike.components.windows - -import indigo.* -import indigo.scenes.SceneContext -import roguelike.ColorScheme -import roguelike.GameEvent -import roguelike.assets.GameAssets -import roguelike.components.GameComponent -import roguelike.model.Message -import roguelike.model.Model -import roguelike.model.entity.Player -import roguelike.viewmodel.GameViewModel -import roguelike.windows.WindowManagerCommand -import roguelikestarterkit.* - -object LevelUp extends GameComponent[Size, Model, GameViewModel]: - type Command = HandleInput - type ComponentModel = Player - type ComponentViewModel = Size - - def modelLens: Lens[Model, Player] = - Lens( - _.player, - (m, p) => m.copy(player = p) - ) - - def viewModelLens: Lens[GameViewModel, Size] = Lens.readOnly(_.viewportSize) - - def nextModel( - context: SceneContext[Size], - player: Player - ): HandleInput => Outcome[Player] = - // Constitution - case HandleInput(Key.KEY_1) => - player - .increaseMaxHp(20) - .addGlobalEvents( - GameEvent.WindowEvent(WindowManagerCommand.CloseAll) - ) - - // Strength - case HandleInput(Key.KEY_2) => - player - .increasePower(1) - .addGlobalEvents( - GameEvent.WindowEvent(WindowManagerCommand.CloseAll) - ) - - // Agility - case HandleInput(Key.KEY_3) => - player - .increaseDefense(1) - .addGlobalEvents( - GameEvent.WindowEvent(WindowManagerCommand.CloseAll) - ) - - case HandleInput(_) => - Outcome(player).addGlobalEvents( - GameEvent.Log( - Message("Invalid, please press 1, 2, or 3.", ColorScheme.invalid) - ) - ) - - def nextViewModel( - context: SceneContext[Size], - model: Player, - viewModel: Size - ): HandleInput => Outcome[Size] = - _ => Outcome(viewModel) - - def view( - context: SceneContext[Size], - player: Player, - viewportSize: Size - ): Outcome[Batch[SceneNode]] = - val text = - s"""Level Up! - | - |Congratulations! You level up! - |Select an attribute to increase. - |[1] Constitution (+20 HP, from ${player.fighter.maxHp}) - |[2] Strength (+1 attack, from ${player.fighter.power}) - |[3] Agility (+1 defense, from ${player.fighter.defense}) - |""".stripMargin - - val windowSize = Size(350, 100) - - Outcome( - Batch( - Group( - Shape.Box( - Rectangle(Point.zero, windowSize), - Fill.Color(RGBA.Black), - Stroke(2, RGBA.Magenta) - ), - Text( - text, - RoguelikeTiles.Size10x10.Fonts.fontKey, - TerminalText(GameAssets.assets.init.AnikkiSquare10x10, RGBA.White, RGBA.Zero) - ) - .moveTo(5, 5) - ).moveTo(((viewportSize - windowSize) / 2).toPoint) - ) - ) - - final case class HandleInput(key: Key) diff --git a/roguelike/src/main/scala/roguelike/model/Model.scala b/roguelike/src/main/scala/roguelike/model/Model.scala index a6d4b7a..d5eab98 100644 --- a/roguelike/src/main/scala/roguelike/model/Model.scala +++ b/roguelike/src/main/scala/roguelike/model/Model.scala @@ -151,6 +151,15 @@ final case class Model( } def update(context: SceneContext[Size]): GameEvent => Outcome[Model] = + case GameEvent.ModifyDefense(amount) => + player.increaseDefense(amount).map(p => this.copy(player = p)) + + case GameEvent.ModifyMaxHp(amount) => + player.increaseMaxHp(amount).map(p => this.copy(player = p)) + + case GameEvent.ModifyPower(amount) => + player.increasePower(amount).map(p => this.copy(player = p)) + case GameEvent.GenerateLevel => Outcome(this) diff --git a/roguelike/src/main/scala/roguelike/scenes/GameScene.scala b/roguelike/src/main/scala/roguelike/scenes/GameScene.scala index f456ef1..f0c4587 100644 --- a/roguelike/src/main/scala/roguelike/scenes/GameScene.scala +++ b/roguelike/src/main/scala/roguelike/scenes/GameScene.scala @@ -72,6 +72,12 @@ object GameScene extends Scene[Size, Model, ViewModel]: GameWindows.defaultCharSheet ) ) + .register( + LevelUpWindow.window( + Dimensions(1280, 720) / 10, + GameWindows.defaultCharSheet + ) + ) .open( InfoPanel.windowId, MenuWindow.windowId diff --git a/roguelike/src/main/scala/roguelike/viewmodel/ViewModel.scala b/roguelike/src/main/scala/roguelike/viewmodel/ViewModel.scala index b78d3c4..9220d24 100644 --- a/roguelike/src/main/scala/roguelike/viewmodel/ViewModel.scala +++ b/roguelike/src/main/scala/roguelike/viewmodel/ViewModel.scala @@ -105,6 +105,15 @@ final case class GameViewModel( context: SceneContext[Size], model: Model ): GameEvent => Outcome[GameViewModel] = + case GameEvent.ModifyDefense(_) => + Outcome(this) + + case GameEvent.ModifyMaxHp(_) => + Outcome(this) + + case GameEvent.ModifyPower(_) => + Outcome(this) + case GameEvent.GenerateLevel => Outcome(this) diff --git a/roguelike/src/main/scala/roguelike/windows/ControlsWindow.scala b/roguelike/src/main/scala/roguelike/windows/ControlsWindow.scala index 5ce721d..b7e2bd1 100644 --- a/roguelike/src/main/scala/roguelike/windows/ControlsWindow.scala +++ b/roguelike/src/main/scala/roguelike/windows/ControlsWindow.scala @@ -49,6 +49,9 @@ object ControlsWindow: def cascade(model: Unit, newBounds: Bounds): Unit = model + def refresh(model: Unit): Unit = + model + private val separator = " | " private val commandsAndValues = KeyMapping.helpText.map(p => (p._1, p._2.mkString(separator))) private val longest = KeyMapping.longestMappings + separator.length diff --git a/roguelike/src/main/scala/roguelike/windows/HistoryWindow.scala b/roguelike/src/main/scala/roguelike/windows/HistoryWindow.scala index a88a0fd..5595a83 100644 --- a/roguelike/src/main/scala/roguelike/windows/HistoryWindow.scala +++ b/roguelike/src/main/scala/roguelike/windows/HistoryWindow.scala @@ -71,3 +71,6 @@ object HistoryWindow: def cascade(model: TerminalClones, newBounds: Bounds): TerminalClones = model + + def refresh(model: TerminalClones): TerminalClones = + model diff --git a/roguelike/src/main/scala/roguelike/windows/InfoPanel.scala b/roguelike/src/main/scala/roguelike/windows/InfoPanel.scala index 9217862..3b91f86 100644 --- a/roguelike/src/main/scala/roguelike/windows/InfoPanel.scala +++ b/roguelike/src/main/scala/roguelike/windows/InfoPanel.scala @@ -63,6 +63,9 @@ object InfoPanel: def cascade(model: Unit, newBounds: Bounds): Unit = model + def refresh(model: Unit): Unit = + model + def renderBar(charSheet: CharSheet, player: Player, totalWidth: Int, position: Point): Group = val height = charSheet.size.height + 4 val width = charSheet.size.width * totalWidth diff --git a/roguelike/src/main/scala/roguelike/windows/LevelUpWindow.scala b/roguelike/src/main/scala/roguelike/windows/LevelUpWindow.scala new file mode 100644 index 0000000..07f2856 --- /dev/null +++ b/roguelike/src/main/scala/roguelike/windows/LevelUpWindow.scala @@ -0,0 +1,120 @@ +package roguelike.windows + +import indigo.* +import roguelike.GameEvent +import roguelike.model.GameWindowContext +import roguelikestarterkit.* + +object LevelUpWindow: + + val windowId: WindowId = WindowId("Level up") + + private val windowSize = Dimensions(40, 10) + + def window( + screenSize: Dimensions, + charSheet: CharSheet + ): WindowModel[LevelUpMenu, GameWindowContext] = + WindowModel( + windowId, + charSheet, + LevelUpMenu.initial(charSheet) + ) + .withTitle("Level Up!") + .moveTo(((screenSize - windowSize) / 2).toCoords) + .resizeTo(windowSize) + +final case class LevelUpMenu(components: ComponentGroup[GameWindowContext]) +object LevelUpMenu: + + def initial(charSheet: CharSheet): LevelUpMenu = + val buttonTheme = + Button.Theme( + charSheet, + RGBA.Silver -> RGBA.Black, + RGBA.White -> RGBA.Black, + RGBA.Black -> RGBA.White + ) + + LevelUpMenu( + ComponentGroup(Bounds(0, 0, 16, 1)) + .withLayout(ComponentLayout.Vertical()) + .add( + Label("Congratulations! You level up!", Label.Theme(charSheet)), + Label("Select an attribute to increase.", Label.Theme(charSheet)), + Label("", Label.Theme(charSheet)) + ) + .add( + Button( + (ctx: GameWindowContext) => + s"[1] Constitution (+20 HP, from ${ctx.player.fighter.maxHp})", + buttonTheme + ).onClick( + GameEvent.ModifyMaxHp(20), + GameEvent.WindowEvent(WindowManagerCommand.CloseAll) + ), + Button( + (ctx: GameWindowContext) => + s"[2] Strength (+1 attack, from ${ctx.player.fighter.power})", + buttonTheme + ).onClick( + GameEvent.ModifyPower(1), + GameEvent.WindowEvent(WindowManagerCommand.CloseAll) + ), + Button( + (ctx: GameWindowContext) => + s"[3] Agility (+1 defense, from ${ctx.player.fighter.defense})", + buttonTheme + ).onClick( + GameEvent.ModifyDefense(1), + GameEvent.WindowEvent(WindowManagerCommand.CloseAll) + ) + ) + ) + + given (using + cg: WindowContent[ComponentGroup[GameWindowContext], GameWindowContext] + ): WindowContent[LevelUpMenu, GameWindowContext] with + + def updateModel( + context: UiContext[GameWindowContext], + model: LevelUpMenu + ): GlobalEvent => Outcome[LevelUpMenu] = + + // Constitution + case KeyboardEvent.KeyUp(Key.KEY_1) => + Outcome( + model, + Batch(GameEvent.ModifyMaxHp(20), GameEvent.WindowEvent(WindowManagerCommand.CloseAll)) + ) + + // Strength + case KeyboardEvent.KeyUp(Key.KEY_2) => + Outcome( + model, + Batch(GameEvent.ModifyPower(1), GameEvent.WindowEvent(WindowManagerCommand.CloseAll)) + ) + + // Agility + case KeyboardEvent.KeyUp(Key.KEY_3) => + Outcome( + model, + Batch(GameEvent.ModifyDefense(1), GameEvent.WindowEvent(WindowManagerCommand.CloseAll)) + ) + + case e => + cg.updateModel(context, model.components)(e).map { c => + model.copy(components = c) + } + + def present( + context: UiContext[GameWindowContext], + model: LevelUpMenu + ): Outcome[Layer] = + cg.present(context, model.components) + + def cascade(model: LevelUpMenu, newBounds: Bounds): LevelUpMenu = + model.copy(components = cg.cascade(model.components, newBounds)) + + def refresh(model: LevelUpMenu): LevelUpMenu = + model.copy(components = cg.refresh(model.components)) diff --git a/roguelike/src/main/scala/roguelike/windows/MenuWindow.scala b/roguelike/src/main/scala/roguelike/windows/MenuWindow.scala index 3e5dcc1..74a875f 100644 --- a/roguelike/src/main/scala/roguelike/windows/MenuWindow.scala +++ b/roguelike/src/main/scala/roguelike/windows/MenuWindow.scala @@ -24,7 +24,7 @@ object MenuWindow: ) .isStatic -final case class MainMenu(components: ComponentGroup) +final case class MainMenu(components: ComponentGroup[GameWindowContext]) object MainMenu: def initial(charSheet: CharSheet): MainMenu = @@ -81,3 +81,6 @@ object MainMenu: def cascade(model: MainMenu, newBounds: Bounds): MainMenu = model.copy(components = model.components.cascade(newBounds)) + + def refresh(model: MainMenu): MainMenu = + model.copy(components = model.components.reflow) diff --git a/roguelike/src/main/scala/roguelike/windows/QuitSaveWindow.scala b/roguelike/src/main/scala/roguelike/windows/QuitSaveWindow.scala index a1f8fd9..355bb02 100644 --- a/roguelike/src/main/scala/roguelike/windows/QuitSaveWindow.scala +++ b/roguelike/src/main/scala/roguelike/windows/QuitSaveWindow.scala @@ -26,7 +26,7 @@ object QuitSaveWindow: .moveTo(((screenSize - windowSize) / 2).toCoords) .resizeTo(windowSize) -final case class QuitMenu(components: ComponentGroup) +final case class QuitMenu(components: ComponentGroup[GameWindowContext]) object QuitMenu: def initial(charSheet: CharSheet): QuitMenu = @@ -96,3 +96,6 @@ object QuitMenu: def cascade(model: QuitMenu, newBounds: Bounds): QuitMenu = model.copy(components = model.components.cascade(newBounds)) + + def refresh(model: QuitMenu): QuitMenu = + model.copy(components = model.components.reflow) diff --git a/roguelike/src/main/scala/roguelike/windows/WindowStateManager.scala b/roguelike/src/main/scala/roguelike/windows/WindowStateManager.scala index 04ebad3..779f1d0 100644 --- a/roguelike/src/main/scala/roguelike/windows/WindowStateManager.scala +++ b/roguelike/src/main/scala/roguelike/windows/WindowStateManager.scala @@ -24,6 +24,7 @@ object WindowStateManager: case WindowManagerCommand.ShowLevelUp => Outcome(updateActive.set(model.pauseForWindow, ActiveWindow.LevelUp)) + .addGlobalEvents(WindowEvent.Open(LevelUpWindow.windowId)) case WindowManagerCommand.ShowDropMenu => Outcome(updateActive.set(model.pauseForWindow, ActiveWindow.DropMenu)) @@ -49,7 +50,7 @@ object WindowStateManager: model.activeWindow match case ActiveWindow.None => Batch.empty case ActiveWindow.Quit => Batch(WindowEvent.Close(QuitSaveWindow.windowId)) - case ActiveWindow.LevelUp => Batch() + case ActiveWindow.LevelUp => Batch(WindowEvent.Close(LevelUpWindow.windowId)) case ActiveWindow.DropMenu => Batch() case ActiveWindow.EquipMenu => Batch() case ActiveWindow.History => Batch(WindowEvent.Close(HistoryWindow.windowId))