Skip to content

Commit 5e0e719

Browse files
committed
Added Dev Mode GameAction API
This commit moves the "Player Edit" screen from just local edits into proper GameActions (all under the `DevModeGameAction` interface), this allows us to store them inside the a game save file, making it possible to save/load games that used these to change the game state. For now, the only actions available are modifying player stats, but others will probably follow. These Dev Actions are not hooked into the normal game loop, but can be applied in the middle of another action. This means that there is a chance the UI gets a little out of date, at least until the next "real" action is triggered. This should be fixed at some point, but will require a bigger refactor and isn't super critical.
1 parent 541bd10 commit 5e0e719

File tree

18 files changed

+407
-104
lines changed

18 files changed

+407
-104
lines changed

docs/todo-ui.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ things as they come up.
2020
- [ ] UI for pushing players into the crowd is not working (and have been disabled for now).
2121
- [ ] Add a "right-click" menu for players. We should combine "Edit Player" and "Forego Activation" in there.
2222
- [ ] Add a better error message when reporting invalid setup. Possibly adopt the BB3 approach where you are shown all the invariants during setup.
23-
- [ ] Add a "dev-mode" protocol for networked game. Should be used for e.g. "Allow Player Edits". Need to figure out
24-
how to refresh UI on the other client.
23+
- [ ] The current "dev-mode" protocol works around the UI-loop in a way that can lead to UI inconsistencies.
2524
- [ ] Make a keyboard shortcut for showing "Player range" and "Tackle Zones"
2625
Unclear which keys makes sense. Probably need to make it configurable
2726
- [ ] Add UI indicator signaling limit of opponent players move (see BB3).
@@ -49,8 +48,6 @@ things as they come up.
4948
- [ ] Add scrollbar indicator to game log components.
5049
- [ ] Could we add 3d dice rolling across the board? Probably difficult in pure Compose, but maybe using Lottie?
5150
- [ ] Add AFK Limit to timer settings + AFK button in the UI
52-
- [ ] Add a "Player Editor" in Dev Mode that makes it possible to add skills, change stats, and states.
53-
Need to figure out exactly how this should work.
5451
- [ ] When Undo'ing block dice it messes up the Action Wheel logic so it shows the dice roll that is skipped when going
5552
forward. That isn't a bug per see, but how the design works. However it feels jarring in that case. Not 100% sure
5653
what the best approach is. We can either combine Nodes in the engine, or find some ways to hack the UI flow.

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/ActionRequest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.jervisffb.engine.actions.D6Result
2121
import com.jervisffb.engine.actions.D8Result
2222
import com.jervisffb.engine.actions.DBlockResult
2323
import com.jervisffb.engine.actions.DeselectPlayer
24+
import com.jervisffb.engine.actions.DevModeGameAction
2425
import com.jervisffb.engine.actions.Dice
2526
import com.jervisffb.engine.actions.DicePoolResultsSelected
2627
import com.jervisffb.engine.actions.DiceRollResults
@@ -220,6 +221,8 @@ data class ActionRequest(
220221
it.players.containsAll(action.players)
221222
} ?: false
222223
}
224+
225+
is DevModeGameAction -> false // Dev Actions should never be handled here
223226
}
224227
}
225228

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/GameEngineController.kt

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package com.jervisffb.engine
22

3+
import com.jervisffb.engine.actions.AddPlayerKeyword
4+
import com.jervisffb.engine.actions.AddPlayerSkill
5+
import com.jervisffb.engine.actions.ChangePlayerBaseStat
36
import com.jervisffb.engine.actions.CompositeGameAction
47
import com.jervisffb.engine.actions.Continue
58
import com.jervisffb.engine.actions.ContinueWhenReady
9+
import com.jervisffb.engine.actions.DevModeGameAction
610
import com.jervisffb.engine.actions.GameAction
711
import com.jervisffb.engine.actions.GameActionId
12+
import com.jervisffb.engine.actions.RemovePlayerKeyword
13+
import com.jervisffb.engine.actions.RemovePlayerSkill
814
import com.jervisffb.engine.actions.Revert
915
import com.jervisffb.engine.actions.Undo
1016
import com.jervisffb.engine.commands.Command
1117
import com.jervisffb.engine.commands.EnterProcedure
18+
import com.jervisffb.engine.commands.ModifyPlayerBaseStat
1219
import com.jervisffb.engine.commands.compositeCommandOf
1320
import com.jervisffb.engine.fsm.ActionNode
1421
import com.jervisffb.engine.fsm.ComputationNode
@@ -27,6 +34,7 @@ import com.jervisffb.engine.reports.SimpleLogEntry
2734
import com.jervisffb.engine.rules.Rules
2835
import com.jervisffb.engine.rules.builder.UndoActionBehavior
2936
import com.jervisffb.engine.rules.common.procedures.FullGame
37+
import com.jervisffb.engine.rules.common.skills.Duration
3038
import com.jervisffb.engine.serialize.JervisSerialization
3139
import com.jervisffb.engine.utils.INVALID_ACTION
3240
import com.jervisffb.engine.utils.InvalidActionException
@@ -175,6 +183,7 @@ class GameEngineController(
175183
// and just apply it without restrictions.
176184
undoLastAction(revertActionId = true)
177185
}
186+
is DevModeGameAction -> processDevAction(action)
178187
else -> processForwardAction(action)
179188
}
180189
} catch (ex: InvalidActionException) {
@@ -317,6 +326,24 @@ class GameEngineController(
317326
}
318327
}
319328

329+
private fun processDevAction(action: DevModeGameAction) {
330+
// For now, the Dev Actions only allow changes to players, revisit these
331+
// checks and errors if it changes.
332+
// For now, we also allow both coaches to emit Dev Commands, this should
333+
// probably also change.
334+
if (!rules.allowPlayerEditsDuringGame) {
335+
error("Player edits are not allowed during this game.")
336+
}
337+
lastActionIfUndo = null
338+
val newDeltaId = (lastGameActionId + 1)
339+
deltaBuilder = DeltaBuilder(newDeltaId, null)
340+
processSingleDevAction(deltaBuilder, action)
341+
val delta = deltaBuilder.build()
342+
_history.add(delta)
343+
lastGameActionId = newDeltaId
344+
}
345+
346+
// Any change here might need to be replicated in [processDevAction]
320347
private fun processForwardAction(userAction: GameAction) {
321348
lastActionIfUndo = null
322349
val actionOwner = currentNode().let { node ->
@@ -360,6 +387,45 @@ class GameEngineController(
360387
deltaBuilder.endAction()
361388
}
362389

390+
// This is a reduced version of [processSingleAction]
391+
private fun processSingleDevAction(deltaBuilder: DeltaBuilder, action: DevModeGameAction) {
392+
val currentProcedure = stack.peepOrNull()!!
393+
deltaBuilder.beginAction(
394+
action,
395+
currentProcedure.procedure,
396+
currentProcedure.currentNode())
397+
logInternalEvent(ReportHandleAction(action))
398+
val command = when (action) {
399+
is AddPlayerSkill -> {
400+
val player = action.getPlayer(state)
401+
val skill = rules.createSkill(player, action.skill, Duration.PERMANENT)
402+
com.jervisffb.engine.commands.AddPlayerSkill(player, skill)
403+
}
404+
is RemovePlayerSkill -> {
405+
val player = action.getPlayer(state)
406+
val skill = player.getSkill(action.skill.type)
407+
com.jervisffb.engine.commands.RemovePlayerSkill(player, skill)
408+
}
409+
is ChangePlayerBaseStat -> {
410+
val player = action.getPlayer(state)
411+
ModifyPlayerBaseStat(player, action.type, action.modifier)
412+
}
413+
is AddPlayerKeyword -> {
414+
val player = action.getPlayer(state)
415+
com.jervisffb.engine.commands.AddPlayerKeyword(player, action.keyword)
416+
}
417+
is RemovePlayerKeyword -> {
418+
val player = action.getPlayer(state)
419+
com.jervisffb.engine.commands.RemovePlayerKeyword(player, action.keyword)
420+
}
421+
}
422+
executeCommand(command)
423+
if (logAvailableActions) {
424+
logInternalEvent(ReportAvailableActions(getAvailableActions()))
425+
}
426+
deltaBuilder.endAction()
427+
}
428+
363429
/**
364430
* Check if an action will be accepted by [ActionNode.applyAction]. If not
365431
* an [InvalidActionException] will be thrown.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.jervisffb.engine.actions
2+
3+
import com.jervisffb.engine.model.Game
4+
import com.jervisffb.engine.model.Player
5+
import com.jervisffb.engine.model.PlayerId
6+
import com.jervisffb.engine.model.PlayerKeyword
7+
import com.jervisffb.engine.model.SkillId
8+
import com.jervisffb.engine.model.modifiers.StatModifier
9+
import kotlinx.serialization.Serializable
10+
11+
/**
12+
* Interface for game actions that can modify the game state "out-of-order",
13+
* i.e., they can do things that you would normally not allow. This can be
14+
* useful during development or as an admin tool.
15+
*
16+
* The [com.jervisffb.engine.GameEngineController] should accept these in
17+
* [com.jervisffb.engine.GameEngineController.handleAction] and leave access
18+
* control to other layers.
19+
*/
20+
sealed interface DevModeGameAction: GameAction
21+
22+
@Serializable
23+
data class ChangePlayerBaseStat(
24+
val playerId: PlayerId,
25+
val type: StatModifier.Type,
26+
val modifier: Int
27+
): DevModeGameAction {
28+
fun getPlayer(state: Game): Player = state.getPlayerById(playerId)
29+
}
30+
31+
@Serializable
32+
data class AddPlayerSkill(
33+
val playerId: PlayerId,
34+
val skill: SkillId
35+
): DevModeGameAction {
36+
fun getPlayer(state: Game): Player = state.getPlayerById(playerId)
37+
}
38+
39+
@Serializable
40+
data class RemovePlayerSkill(
41+
val playerId: PlayerId,
42+
val skill: SkillId
43+
): DevModeGameAction {
44+
fun getPlayer(state: Game): Player = state.getPlayerById(playerId)
45+
}
46+
47+
@Serializable
48+
data class AddPlayerKeyword(
49+
val playerId: PlayerId,
50+
val keyword: PlayerKeyword
51+
): DevModeGameAction {
52+
fun getPlayer(state: Game): Player = state.getPlayerById(playerId)
53+
}
54+
55+
@Serializable
56+
data class RemovePlayerKeyword(
57+
val playerId: PlayerId,
58+
val keyword: PlayerKeyword
59+
): DevModeGameAction {
60+
fun getPlayer(state: Game): Player = state.getPlayerById(playerId)
61+
}

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/actions/GameAction.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class CalculatedAction(private val action: GameEngineController.(Game, Rules) ->
4545
* Group multiple actions together as one.
4646
* The rule engine will this action as an atomic action. This means that when you
4747
* Undo this action, all "sub-actions" will all be undone as one.
48+
*
49+
* It is not allowed to put [DevModeGameAction] inside of this action.
4850
*/
4951
@Serializable
5052
data class CompositeGameAction(val actionList: List<GameAction>): GameAction {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.jervisffb.engine.commands
2+
3+
import com.jervisffb.engine.model.Game
4+
import com.jervisffb.engine.model.Player
5+
import com.jervisffb.engine.model.PlayerKeyword
6+
7+
class AddPlayerKeyword(private val player: Player, val keyword: PlayerKeyword) : Command {
8+
override fun execute(state: Game) {
9+
player.keywords.add(keyword)
10+
}
11+
12+
override fun undo(state: Game) {
13+
player.keywords.remove(keyword)
14+
}
15+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.jervisffb.engine.commands
2+
3+
import com.jervisffb.engine.model.Game
4+
import com.jervisffb.engine.model.Player
5+
import com.jervisffb.engine.model.modifiers.StatModifier
6+
7+
/**
8+
* Modify a Players Base Stat.
9+
* Note, we allow this command to modify most base stat outside legal values as
10+
* this allows us more flexibility when changing stats.
11+
*
12+
* The total (like [Player.move]) will still be clamped to the valid ranges.
13+
*
14+
* Passing will be treated differently as going below 1, removes the ability to
15+
* pass.
16+
*/
17+
class ModifyPlayerBaseStat(private val player: Player, val stat: StatModifier.Type, val change: Int) : Command {
18+
val rules = player.team.game.rules
19+
var originalBaseStat: Int? = 0
20+
override fun execute(state: Game) {
21+
player.apply {
22+
when (stat) {
23+
StatModifier.Type.AV -> {
24+
originalBaseStat = baseArmorValue
25+
baseArmorValue += change
26+
}
27+
StatModifier.Type.MA -> {
28+
originalBaseStat = baseMove
29+
baseMove += change
30+
}
31+
StatModifier.Type.PA -> {
32+
originalBaseStat = basePassing
33+
basePassing = if (basePassing == null) {
34+
change
35+
} else {
36+
basePassing!! + change
37+
}
38+
basePassing?.let { pa ->
39+
if (pa <= 0) {
40+
basePassing = null
41+
}
42+
}
43+
}
44+
StatModifier.Type.AG -> {
45+
originalBaseStat = baseAgility
46+
baseAgility += change
47+
}
48+
StatModifier.Type.ST -> {
49+
originalBaseStat = baseStrength
50+
baseStrength += change
51+
}
52+
}
53+
rules.updatePlayerStat(player, stat)
54+
}
55+
}
56+
57+
override fun undo(state: Game) {
58+
player.apply {
59+
when (stat) {
60+
StatModifier.Type.AV -> {
61+
baseArmorValue = originalBaseStat!!
62+
}
63+
StatModifier.Type.MA -> {
64+
baseMove = originalBaseStat!!
65+
}
66+
StatModifier.Type.PA -> {
67+
basePassing = originalBaseStat
68+
}
69+
StatModifier.Type.AG -> {
70+
baseAgility = originalBaseStat!!
71+
}
72+
StatModifier.Type.ST -> {
73+
baseStrength = originalBaseStat!!
74+
}
75+
}
76+
rules.updatePlayerStat(player, stat)
77+
}
78+
}
79+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.jervisffb.engine.commands
2+
3+
import com.jervisffb.engine.model.Game
4+
import com.jervisffb.engine.model.Player
5+
import com.jervisffb.engine.model.PlayerKeyword
6+
7+
class RemovePlayerKeyword(private val player: Player, val keyword: PlayerKeyword) : Command {
8+
override fun execute(state: Game) {
9+
player.keywords.remove(keyword)
10+
}
11+
12+
override fun undo(state: Game) {
13+
player.keywords.add(keyword)
14+
}
15+
}

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/model/modifiers/StatModifier.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ interface StatModifier {
1919
val expiresAt: Duration
2020
}
2121

22+
// Fake StatModifier constructor for generic stat modifiers
23+
fun StatModifier(
24+
type: Type,
25+
modifier: Int,
26+
description: String,
27+
expiresAt: Duration = Duration.PERMANENT
28+
): StatModifier =
29+
object : StatModifier {
30+
override val type: Type = type
31+
override val modifier: Int = modifier
32+
override val description: String = description
33+
override val expiresAt: Duration = expiresAt
34+
}
35+
2236
enum class SkillStatModifier(
2337
override val description: String,
2438
override val modifier: Int,

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/serialize/generatedSerializer.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ val generatedJervisSerializerModule = SerializersModule {
3030
subclass(com.jervisffb.engine.rules.common.procedures.D6DieRoll::class)
3131
}
3232
polymorphic(com.jervisffb.engine.actions.GameAction::class) {
33+
subclass(com.jervisffb.engine.actions.AddPlayerKeyword::class)
34+
subclass(com.jervisffb.engine.actions.AddPlayerSkill::class)
3335
subclass(com.jervisffb.engine.actions.BlockTypeSelected::class)
3436
subclass(com.jervisffb.engine.actions.Cancel::class)
37+
subclass(com.jervisffb.engine.actions.ChangePlayerBaseStat::class)
3538
subclass(com.jervisffb.engine.actions.CoinSideSelected::class)
3639
subclass(com.jervisffb.engine.actions.CoinTossResult::class)
3740
subclass(com.jervisffb.engine.actions.CompositeGameAction::class)
@@ -64,10 +67,19 @@ val generatedJervisSerializerModule = SerializersModule {
6467
subclass(com.jervisffb.engine.actions.PlayerSelected::class)
6568
subclass(com.jervisffb.engine.actions.PlayersSelected::class)
6669
subclass(com.jervisffb.engine.actions.RandomPlayersSelected::class)
70+
subclass(com.jervisffb.engine.actions.RemovePlayerKeyword::class)
71+
subclass(com.jervisffb.engine.actions.RemovePlayerSkill::class)
6772
subclass(com.jervisffb.engine.actions.RerollOptionSelected::class)
6873
subclass(com.jervisffb.engine.actions.Revert::class)
6974
subclass(com.jervisffb.engine.actions.SkillSelected::class)
7075
subclass(com.jervisffb.engine.actions.Undo::class)
76+
polymorphic(com.jervisffb.engine.actions.DevModeGameAction::class) {
77+
subclass(com.jervisffb.engine.actions.AddPlayerKeyword::class)
78+
subclass(com.jervisffb.engine.actions.AddPlayerSkill::class)
79+
subclass(com.jervisffb.engine.actions.ChangePlayerBaseStat::class)
80+
subclass(com.jervisffb.engine.actions.RemovePlayerKeyword::class)
81+
subclass(com.jervisffb.engine.actions.RemovePlayerSkill::class)
82+
}
7183
polymorphic(com.jervisffb.engine.actions.DieResult::class) {
7284
subclass(com.jervisffb.engine.actions.D12Result::class)
7385
subclass(com.jervisffb.engine.actions.D16Result::class)

0 commit comments

Comments
 (0)