Skip to content

Commit 72ecfb6

Browse files
committed
Add support for "Safe Pass" skill
- Add UI controls for automatically using Safe Pass on fumbles - Added Action Wheel selector for choosing to use Safe Pass - Restrict it to only Pass actions, at least until a FAQ says otherwise
1 parent 8c4d831 commit 72ecfb6

File tree

20 files changed

+301
-41
lines changed

20 files changed

+301
-41
lines changed

docs/bb2025/rules-faq-bb2025.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,23 @@ round up.
189189
Technically, you are allowed to user other skills like Extra Arms, but since
190190
it would be pointless, Jervis just ignore these skills.
191191

192-
### Page 135 - Put the Boot In
192+
### Page 134 - Put the Boot In
193193
Technically, using the skill should be optional, but there doesn't seem to be
194194
any use case for this (even a bad one), so until such a use case surfaces, this
195195
skill is always used.
196196

197+
### Page 135 - Safe Pass
198+
The skill uses the wording "Passing Ability Test", which is the same wording
199+
used for rolling for the throw in both Pass and Throw Team-mate actions. But
200+
then the rest of the skill only describes what happens to the ball, indicating
201+
that maybe they just mean that it works for normal Pass actions.
202+
203+
If we allowed it to work for Throw Team-mate actions, it would open up a lot
204+
of unspecified scenarios with regard to how the thrown player be handled?
205+
206+
For this reason, Jervis assumes that Safe Pass is only applicable for normal
207+
Pass actions (and by extension, throwing bombs).
208+
197209
### Page 138 - Very Long Legs
198210
While this skill is optional during Interceptions, Jervis always enables it as
199211
the coach can just choose to not intercept instead. This lowers the complexity

docs/bb2025/todo-skills-bb2025.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,11 @@ a test class in `modules/jervis-engine/src/commonTest/kotlin/dk/ilios/jervis/bb2
179179
- [ ] Works for Bombs
180180
- [x] Works for normal passes
181181
- [x] Works for hail mary passes
182-
- [ ] Quick Pass
183-
- [ ] Rush
184-
- [ ] Sneaky Pass
185182
- [ ] Punt
186183
- [ ] Safe Pass
184+
- [x] Works on Pass
185+
- [x] Works on Hail Mary Pass
186+
- [ ] Works on throwing a bomb
187187

188188
## Strength Skills
189189

docs/bugs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
This file is just a temporary way to capture any bugs seen that I didn't have time to investigate yet.
44

55
## Known bugs
6+
- Move counter on field squares does not reset when a players action ends immediately (like when using Safe Pass)
67
- Action Wheel animation isn't correct when animating D6 rererolls when automatically selecting the reroll type.
78
It looks like two animations are running over each other or that some positions are being reused.
89
- Action Wheel for Catch rerolls jump back to the thrower when undoing actions

modules/fumbbl-net/src/commonMain/kotlin/com/jervisffb/fumbbl/net/adapter/impl/AbortActionMapper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ object AbortActionMapper: CommandActionMapper {
5252
when ((command.reportList.first() as PlayerActionReport).playerAction) {
5353
PlayerAction.MOVE -> {
5454
val movingPlayerId = command.modelChangeList.filterIsInstance<ActingPlayerSetPlayerId>().first().value!!
55-
val movingPlayer = jervisGame.getPlayerById(PlayerId(movingPlayerId.id))!!
55+
val movingPlayer = jervisGame.getPlayerById(PlayerId(movingPlayerId.id))
5656
newActions.add(PlayerSelected(movingPlayer.id), TeamTurn.SelectPlayerOrEndTurn)
5757
newActions.add(
5858
{ state, rules -> PlayerActionSelected(rules.teamActions.move.type) },

modules/fumbbl-net/src/commonMain/kotlin/com/jervisffb/fumbbl/net/adapter/impl/setup/SetupPlayerMapper.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ object SetupPlayerMapper: CommandActionMapper {
4242
jervisCommands: List<JervisActionHolder>,
4343
newActions: MutableList<JervisActionHolder>
4444
) {
45-
val playerId = (command.modelChangeList.first() as FieldModelSetPlayerState).key!!
45+
val playerId = (command.modelChangeList.first() as FieldModelSetPlayerState).key
4646
var coordinates = (command.modelChangeList[1] as FieldModelSetPlayerCoordinate).value!!
47-
val selectedPlayer = jervisGame.getPlayerById(PlayerId(playerId))!!
47+
val selectedPlayer = jervisGame.getPlayerById(PlayerId(playerId))
4848
newActions.add(PlayerSelected(selectedPlayer), SetupTeam.SelectPlayerOrEndSetup)
4949
if (coordinates.x < 0 || coordinates.y > 25) {
5050
newActions.add(DogoutSelected, SetupTeam.PlacePlayer)

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/rules/bb2025/BB2025SkillSettings.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.jervisffb.engine.rules.bb2025.skills.QuickFoul
3939
import com.jervisffb.engine.rules.bb2025.skills.ReallyStupid
4040
import com.jervisffb.engine.rules.bb2025.skills.Regeneration
4141
import com.jervisffb.engine.rules.bb2025.skills.RightStuff
42+
import com.jervisffb.engine.rules.bb2025.skills.SafePass
4243
import com.jervisffb.engine.rules.bb2025.skills.Shadowing
4344
import com.jervisffb.engine.rules.bb2025.skills.Sidestep
4445
import com.jervisffb.engine.rules.bb2025.skills.Sprint
@@ -392,9 +393,9 @@ class BB2025SkillSettings: SkillSettings() {
392393
// }
393394
}
394395
SkillType.SAFE_PASS -> {
395-
// addEntry(type, SkillCategory.PASSING) { player, category, _ , expiresAt ->
396-
// TODO()
397-
// }
396+
addNoValueEntry("Safe Pass", type, SkillCategory.PASSING) { player, category,expiresAt ->
397+
SafePass(player, category, expiresAt)
398+
}
398399
}
399400

400401
//

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/rules/bb2025/procedures/actions/pass/HailMaryPassStep.kt

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package com.jervisffb.engine.rules.bb2025.procedures.actions.pass
22

33
import com.jervisffb.engine.actions.Cancel
44
import com.jervisffb.engine.actions.CancelWhenReady
5+
import com.jervisffb.engine.actions.Confirm
6+
import com.jervisffb.engine.actions.ConfirmWhenReady
7+
import com.jervisffb.engine.actions.ContinueWhenReady
58
import com.jervisffb.engine.actions.FieldSquareSelected
69
import com.jervisffb.engine.actions.GameAction
710
import com.jervisffb.engine.actions.GameActionDescriptor
@@ -17,6 +20,7 @@ import com.jervisffb.engine.commands.context.RemoveContext
1720
import com.jervisffb.engine.commands.context.SetContext
1821
import com.jervisffb.engine.commands.fsm.ExitProcedure
1922
import com.jervisffb.engine.commands.fsm.GotoNode
23+
import com.jervisffb.engine.ext.d6
2024
import com.jervisffb.engine.fsm.ActionNode
2125
import com.jervisffb.engine.fsm.Node
2226
import com.jervisffb.engine.fsm.ParentNode
@@ -28,6 +32,8 @@ import com.jervisffb.engine.model.Team
2832
import com.jervisffb.engine.model.TurnOver
2933
import com.jervisffb.engine.model.context.assertContext
3034
import com.jervisffb.engine.model.context.getContext
35+
import com.jervisffb.engine.model.isSkillAvailable
36+
import com.jervisffb.engine.reports.ReportSkillUsed
3137
import com.jervisffb.engine.reports.ReportStartingPass
3238
import com.jervisffb.engine.rules.Rules
3339
import com.jervisffb.engine.rules.common.procedures.Bounce
@@ -39,14 +45,15 @@ import com.jervisffb.engine.rules.common.procedures.ThrowInContext
3945
import com.jervisffb.engine.rules.common.procedures.actions.pass.PassAction
4046
import com.jervisffb.engine.rules.common.procedures.actions.pass.PassContext
4147
import com.jervisffb.engine.rules.common.procedures.actions.pass.PassingType
48+
import com.jervisffb.engine.rules.common.skills.SkillType
4249
import com.jervisffb.engine.rules.common.tables.Range
4350
import com.jervisffb.engine.utils.INVALID_GAME_STATE
4451

4552
/**
4653
* Procedure for handling the passing part of a [PassAction] when using [HailMary].
4754
*
4855
* See page 70 in the BB2025 rulebook.
49-
* See page XXX in the BB2025 rulebook.
56+
* See page 129 in the BB2025 rulebook.
5057
*/
5158
object HailMaryPassStep: Procedure() {
5259
override val initialNode: Node = DeclareTargetSquare
@@ -107,7 +114,7 @@ object HailMaryPassStep: Procedure() {
107114
val context = state.getContext<PassContext>()
108115
return when (val result = context.passingResult) {
109116
PassingType.INACCURATE -> GotoNode(ResolveInaccuratePass)
110-
PassingType.FUMBLED -> GotoNode(ResolveFumbledPass)
117+
PassingType.FUMBLED -> GotoNode(ChooseToUseSafePass)
111118
else -> INVALID_GAME_STATE("Unsupported passing result: $result")
112119
}
113120
}
@@ -158,6 +165,42 @@ object HailMaryPassStep: Procedure() {
158165
}
159166
}
160167

168+
object ChooseToUseSafePass: ActionNode() {
169+
override fun actionOwner(state: Game, rules: Rules): Team {
170+
return state.getContext<PassContext>().thrower.team
171+
}
172+
173+
override fun getAvailableActions(state: Game, rules: Rules): List<GameActionDescriptor> {
174+
val context = state.getContext<PassContext>()
175+
val player = context.thrower
176+
val hasSafePass = player.isSkillAvailable(SkillType.SAFE_PASS)
177+
// Safe Pass requires a natural 1 on the dice. It doesn't work on a modified 1 and below.
178+
val isSafePassEligible = (context.passingRoll?.result == 1.d6)
179+
val isFumble = (context.passingResult == PassingType.FUMBLED)
180+
return when (hasSafePass && isSafePassEligible && isFumble) {
181+
true -> listOf(ConfirmWhenReady, CancelWhenReady)
182+
false -> listOf(ContinueWhenReady)
183+
}
184+
}
185+
186+
override fun applyAction(action: GameAction, state: Game, rules: Rules): Command {
187+
val context = state.getContext<PassContext>()
188+
val ball = state.currentBall()
189+
val useSafePass = (action == Confirm)
190+
191+
return if (useSafePass) {
192+
compositeCommandOf(
193+
ReportSkillUsed(context.thrower, SkillType.SAFE_PASS),
194+
SetBallState.carried(ball, context.thrower),
195+
SetContext(context.copy(useSafePass = true)),
196+
ExitProcedure()
197+
)
198+
} else {
199+
GotoNode(ResolveFumbledPass)
200+
}
201+
}
202+
}
203+
161204
/**
162205
* If the pass is fumbled, the ball will bounce from the thrower's location
163206
* and a turnover happens. Regardless of who, if any, catches the ball.

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/rules/bb2025/procedures/actions/pass/PassStep.kt

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package com.jervisffb.engine.rules.bb2025.procedures.actions.pass
22

33
import com.jervisffb.engine.actions.Cancel
44
import com.jervisffb.engine.actions.CancelWhenReady
5+
import com.jervisffb.engine.actions.Confirm
6+
import com.jervisffb.engine.actions.ConfirmWhenReady
7+
import com.jervisffb.engine.actions.ContinueWhenReady
58
import com.jervisffb.engine.actions.FieldSquareSelected
69
import com.jervisffb.engine.actions.GameAction
710
import com.jervisffb.engine.actions.GameActionDescriptor
@@ -17,6 +20,7 @@ import com.jervisffb.engine.commands.context.RemoveContext
1720
import com.jervisffb.engine.commands.context.SetContext
1821
import com.jervisffb.engine.commands.fsm.ExitProcedure
1922
import com.jervisffb.engine.commands.fsm.GotoNode
23+
import com.jervisffb.engine.ext.d6
2024
import com.jervisffb.engine.fsm.ActionNode
2125
import com.jervisffb.engine.fsm.ComputationNode
2226
import com.jervisffb.engine.fsm.Node
@@ -29,6 +33,8 @@ import com.jervisffb.engine.model.Team
2933
import com.jervisffb.engine.model.TurnOver
3034
import com.jervisffb.engine.model.context.assertContext
3135
import com.jervisffb.engine.model.context.getContext
36+
import com.jervisffb.engine.model.isSkillAvailable
37+
import com.jervisffb.engine.reports.ReportSkillUsed
3238
import com.jervisffb.engine.reports.ReportStartingPass
3339
import com.jervisffb.engine.rules.Rules
3440
import com.jervisffb.engine.rules.common.procedures.Bounce
@@ -40,6 +46,7 @@ import com.jervisffb.engine.rules.common.procedures.ThrowInContext
4046
import com.jervisffb.engine.rules.common.procedures.actions.pass.PassAction
4147
import com.jervisffb.engine.rules.common.procedures.actions.pass.PassContext
4248
import com.jervisffb.engine.rules.common.procedures.actions.pass.PassingType
49+
import com.jervisffb.engine.rules.common.skills.SkillType
4350
import com.jervisffb.engine.rules.common.tables.Range
4451
import com.jervisffb.engine.rules.common.tables.Weather
4552
import com.jervisffb.engine.utils.INVALID_GAME_STATE
@@ -98,7 +105,7 @@ object PassStep: Procedure() {
98105
range = distance
99106
)
100107
),
101-
SetBallState.Companion.accurateThrow(ball), // Until proven otherwise. Should we invent a new type?
108+
SetBallState.accurateThrow(ball), // Until proven otherwise. Should we invent a new type?
102109
SetBallLocation(ball, newLocation),
103110
GotoNode(TestForAccuracy)
104111
)
@@ -115,7 +122,7 @@ object PassStep: Procedure() {
115122
return when (val result = context.passingResult) {
116123
PassingType.ACCURATE -> GotoNode(ResolveAccuratePass)
117124
PassingType.INACCURATE -> GotoNode(ResolveInaccuratePass)
118-
PassingType.FUMBLED -> GotoNode(ResolveFumbledPass)
125+
PassingType.FUMBLED -> GotoNode(ChooseToUseSafePass)
119126
else -> INVALID_GAME_STATE("Unsupported passing result: $result")
120127
}
121128
}
@@ -162,7 +169,7 @@ object PassStep: Procedure() {
162169
val ball = state.currentBall()
163170
return if (context.outOfBoundsAt != null) {
164171
compositeCommandOf(
165-
SetBallState.Companion.outOfBounds(ball, context.outOfBoundsAt),
172+
SetBallState.outOfBounds(ball, context.outOfBoundsAt),
166173
SetBallLocation(ball, context.landsAt!!),
167174
SetContext(passContext.copy(target = context.landsAt)),
168175
RemoveContext<ScatterRollContext>(),
@@ -180,6 +187,41 @@ object PassStep: Procedure() {
180187
}
181188
}
182189

190+
object ChooseToUseSafePass: ActionNode() {
191+
override fun actionOwner(state: Game, rules: Rules): Team {
192+
return state.getContext<PassContext>().thrower.team
193+
}
194+
195+
override fun getAvailableActions(state: Game, rules: Rules): List<GameActionDescriptor> {
196+
val context = state.getContext<PassContext>()
197+
val player = context.thrower
198+
val hasSafePass = player.isSkillAvailable(SkillType.SAFE_PASS)
199+
val isSafePassEligible = (context.passingRoll?.result == 1.d6)
200+
val isFumble = (context.passingResult == PassingType.FUMBLED)
201+
return when (hasSafePass && isSafePassEligible && isFumble) {
202+
true -> listOf(ConfirmWhenReady, CancelWhenReady)
203+
false -> listOf(ContinueWhenReady)
204+
}
205+
}
206+
207+
override fun applyAction(action: GameAction, state: Game, rules: Rules): Command {
208+
val context = state.getContext<PassContext>()
209+
val ball = state.currentBall()
210+
val useSafePass = (action == Confirm)
211+
212+
return if (useSafePass) {
213+
compositeCommandOf(
214+
ReportSkillUsed(context.thrower, SkillType.SAFE_PASS),
215+
SetBallState.carried(ball, context.thrower),
216+
SetContext(context.copy(useSafePass = true)),
217+
ExitProcedure()
218+
)
219+
} else {
220+
GotoNode(ResolveFumbledPass)
221+
}
222+
}
223+
}
224+
183225
/**
184226
* If the pass is fumbled, the ball will bounce from the thrower's location
185227
* and a turnover happens. Regardless of who, if any, catches the ball.

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/rules/bb2025/skills/DirtyPlayer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import com.jervisffb.engine.rules.common.skills.SkillType
1010
/**
1111
* Represents the "Dirty Player" skill.
1212
*
13-
* See page XXX in the BB2025 rulebook.
13+
* See page 127 in the BB2025 rulebook.
1414
*/
1515
class DirtyPlayer(
1616
override val player: Player,

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/rules/bb2025/skills/HailMaryPass.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import com.jervisffb.engine.rules.common.skills.SkillType
1010
/**
1111
* Representation of the Hail Mary Pass (Active) skill.
1212
*
13-
* See page XXX in the BB2025 rulebook.
13+
* See page 129 in the BB2025 rulebook.
1414
*/
1515
class HailMaryPass(
1616
override val player: Player,

0 commit comments

Comments
 (0)