Skip to content

Commit 5ec29de

Browse files
committed
ghost rider (half tested)
1 parent 3c9f935 commit 5ec29de

12 files changed

Lines changed: 414 additions & 40 deletions

File tree

src/main/kotlin/dev/robothanzo/werewolf/database/documents/Player.kt

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -194,25 +194,6 @@ data class Player(
194194
}
195195
session.courtTextChannel?.sendMessageEmbeds(embed.build())?.queue()
196196
}
197-
198-
val result = session.hasEnded(killedRole)
199-
if (result != Session.Result.NOT_ENDED) {
200-
val judgePing = "<@&" + (session.judgeRole?.idLong ?: 0) + "> "
201-
val message = if (result == Session.Result.WOLVES_DIED) {
202-
judgePing + "遊戲結束,**好**人獲勝,原因:" + result.reason
203-
} else {
204-
judgePing + "遊戲結束,**狼**人獲勝,原因:" + result.reason
205-
}
206-
session.spectatorTextChannel?.sendMessage(message)?.queue()
207-
// Trigger Replay Generation
208-
Replay.upsertFromSession(session, WerewolfApplication.replayRepository)
209-
210-
// Trigger Judge Decision
211-
session.stateData.pendingNextStep = session.currentState
212-
session.stateData.gameEndReason = result.reason
213-
WerewolfApplication.gameStateService.startStep(session, "JUDGE_DECISION")
214-
}
215-
216197
if (alive) {
217198
val remainingRoles = roles.toMutableList()
218199
deadRoles.forEach { deadRole ->

src/main/kotlin/dev/robothanzo/werewolf/game/model/ActionDefinitionId.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ enum class ActionDefinitionId(val actionName: String) {
4646
// Nightmare Actions
4747
NIGHTMARE_FEAR("恐懼"),
4848

49+
// Ghost Rider Actions
50+
GHOST_RIDER_REFLECT("反傷"),
51+
4952
// System Actions
5053
DEATH_RESOLUTION("結算"),
5154
DREAM_DEATH("夢亡"),

src/main/kotlin/dev/robothanzo/werewolf/game/model/DeathCause.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ enum class DeathCause(val logMessage: String) {
1212
DOUBLE_PROTECTION("同時受到女巫解藥與守衛守護而死亡"),
1313
EXPEL("被放逐"),
1414
TRADED_WITH_WOLF("與狼人黑市交易技能而亡"),
15+
REFLECT("因攻擊惡靈騎士遭到反噬而死"),
1516
DREAM_WEAVER("死於夢中"),
1617
UNKNOWN("死亡原因未知")
1718
}

src/main/kotlin/dev/robothanzo/werewolf/game/model/GameStateData.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@ data class GameStateData(
206206

207207
// For frontend display after death announcement only, shows who died last night
208208
var deadPlayers: List<Int> = emptyList()
209+
210+
/**
211+
* Whether the Ghost Rider's reflection ability has been triggered.
212+
* Derived from executed actions.
213+
*/
214+
val ghostRiderReflected: Boolean
215+
get() = executedActions.values.flatten().any { it.actionDefinitionId == ActionDefinitionId.GHOST_RIDER_REFLECT }
209216
}
210217

211218
/**

src/main/kotlin/dev/robothanzo/werewolf/game/model/SessionExtensions.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,27 @@ fun Session.resolveNightActions(
216216
it.status.executed
217217
}
218218

219+
val finalDeaths = executionResult.deaths.toMutableMap()
220+
// Add Ghost Rider reflection deaths from history.
221+
// Reflections triggered by immediate actions (like Seer) occur before the day increment at sunrise,
222+
// while batch actions occur after. We check both current and previous day to be sure.
223+
val reflectActions = mutableListOf<RoleActionInstance>()
224+
stateData.executedActions[currentDay]?.let { actions -> reflectActions.addAll(actions.filter { it.actionDefinitionId == ActionDefinitionId.GHOST_RIDER_REFLECT }) }
225+
if (currentDay > 0) {
226+
stateData.executedActions[currentDay - 1]?.let { actions -> reflectActions.addAll(actions.filter { it.actionDefinitionId == ActionDefinitionId.GHOST_RIDER_REFLECT }) }
227+
}
228+
229+
for (reflectAction in reflectActions) {
230+
val reflectDeaths = finalDeaths.getOrPut(DeathCause.REFLECT) { mutableListOf() }
231+
for (target in reflectAction.targets) {
232+
if (!reflectDeaths.contains(target)) {
233+
reflectDeaths.add(target)
234+
}
235+
}
236+
}
237+
219238
return NightResolutionResult(
220-
deaths = executionResult.deaths,
239+
deaths = finalDeaths,
221240
saved = executionResult.saved
222241
)
223242
}
@@ -304,7 +323,9 @@ fun Session.validateAndSubmitAction(
304323
val executionResult = roleActionExecutor.executeActionInstance(this, action)
305324

306325
// Handle immediate deaths if any (e.g. Hunter revenge)
326+
// Defer Ghost Rider reflection deaths to death announcement
307327
for ((cause, deaths) in executionResult.deaths) {
328+
if (cause == DeathCause.REFLECT) continue
308329
for (userId in deaths) {
309330
val player = this.getPlayer(userId) ?: continue
310331
// Handle immediate deaths if any (e.g. Hunter revenge)

src/main/kotlin/dev/robothanzo/werewolf/game/roles/Roles.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ class Nightmare(
7171
override fun getActions(): List<RoleAction> = listOf(fearAction, killAction)
7272
}
7373

74+
@Component
75+
class GhostRider(@Transient private val killAction: WerewolfKillAction) : BaseRole("惡靈騎士", Camp.WEREWOLF) {
76+
override fun getActions(): List<RoleAction> = listOf(killAction)
77+
}
78+
7479
@Component
7580
class Villager : BaseRole("平民", Camp.VILLAGER)
7681

src/main/kotlin/dev/robothanzo/werewolf/game/roles/actions/Actions.kt

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,25 +44,41 @@ class WerewolfKillAction : BaseRoleAction(
4444
alivePlayers: List<Int>,
4545
accumulatedState: ActionExecutionResult
4646
): List<Int> {
47-
val targets = super.eligibleTargets(session, actor, alivePlayers, accumulatedState)
48-
if (session.settings.allowWolfSelfKill) return targets
47+
val baseTargets = super.eligibleTargets(session, actor, alivePlayers, accumulatedState)
4948

50-
return targets.filter { targetId ->
51-
val targetPlayer = session.getPlayer(targetId)
52-
targetPlayer?.wolf == false
49+
return baseTargets.filter { targetId ->
50+
val targetPlayer = session.getPlayer(targetId) ?: return@filter false
51+
52+
// Ghost Rider and Nightmare can NEVER be targeted by Wolf Kill
53+
if (targetPlayer.roles.contains("惡靈騎士") || targetPlayer.roles.contains("夢魘")) {
54+
return@filter false
55+
}
56+
57+
// Other wolves depend on the setting
58+
if (!session.settings.allowWolfSelfKill && targetPlayer.wolf) {
59+
return@filter false
60+
}
61+
62+
true
5363
}
5464
}
5565

5666
override fun validate(session: Session, actor: Int, targets: List<Int>): String? {
5767
val baseError = super.validate(session, actor, targets)
5868
if (baseError != null) return baseError
5969

60-
if (!session.settings.allowWolfSelfKill) {
61-
for (targetId in targets) {
62-
val targetPlayer = session.getPlayer(targetId)
63-
if (targetPlayer?.wolf == true) {
64-
return "不允許狼人自殺"
65-
}
70+
for (targetId in targets) {
71+
val targetPlayer = session.getPlayer(targetId) ?: continue
72+
// Ghost Rider cannot be targeted by Wolf Kill (cannot self-kill)
73+
if (targetPlayer.roles.contains("惡靈騎士")) {
74+
return "惡靈騎士不能自刀"
75+
}
76+
if (targetPlayer.roles.contains("夢魘")) {
77+
return "夢魘不能自刀"
78+
}
79+
80+
if (!session.settings.allowWolfSelfKill && targetPlayer.wolf) {
81+
return "不允許狼人自殺"
6682
}
6783
}
6884
return null

src/main/kotlin/dev/robothanzo/werewolf/game/roles/actions/RoleActionExecutor.kt

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class RoleActionExecutor(private val roleRegistry: RoleRegistry) {
1515

1616
/**
1717
* Execute all pending actions in priority order
18-
*
18+
*
1919
* @param session The current game session
2020
* @param pendingActions The list of pending actions to execute
2121
* @return The accumulated result of all action executions
@@ -64,6 +64,35 @@ class RoleActionExecutor(private val roleRegistry: RoleRegistry) {
6464
status = dev.robothanzo.werewolf.game.model.ActionStatus.SUBMITTED
6565
)
6666
result = deathResolution.execute(session, dummyAction, result)
67+
68+
// --- Ghost Rider Immunity Logic (Post-processing) ---
69+
val ghostRiderPlayerIds = session.players.values.filter { it.roles.contains("惡靈騎士") }.map { it.id }.toSet()
70+
// "Ghost Rider cannot die at night"
71+
// Remove Ghost Rider from deaths caused by: WEREWOLF, POISON, HUNTER_REVENGE, WOLF_KING_REVENGE
72+
// (He is NOT immune to REFLECT? Well he can't be reflected by himself.
73+
// He IS immune to Witch poison, Hunter shot.
74+
// Is he immune to DREAM_WEAVER? "Good camp players use skills... will die".
75+
// If Dream Weaver links him, usually Dream Weaver dies. Does Ghost Rider die?
76+
// Rule says "Immune to skills". So probably immune to Dream death too if it's considered night death.
77+
// Let's stick to explicit "Cannot die at night".
78+
79+
if (ghostRiderPlayerIds.isNotEmpty()) {
80+
result.deaths.forEach { (cause, playerList) ->
81+
// Check if this cause is a night death cause (usually all except EXILE/SUICIDE?)
82+
// Explicitly: Wolf, Poison, Hunter, Wolf King, Scheme/Dream?
83+
if (cause == dev.robothanzo.werewolf.game.model.DeathCause.WEREWOLF ||
84+
cause == dev.robothanzo.werewolf.game.model.DeathCause.POISON ||
85+
cause == dev.robothanzo.werewolf.game.model.DeathCause.HUNTER_REVENGE ||
86+
cause == dev.robothanzo.werewolf.game.model.DeathCause.WOLF_KING_REVENGE ||
87+
cause == dev.robothanzo.werewolf.game.model.DeathCause.DREAM_WEAVER
88+
) { // Assuming Dream death is night death
89+
90+
playerList.removeAll(ghostRiderPlayerIds)
91+
}
92+
}
93+
// Clean up empty lists
94+
result.deaths.entries.removeIf { it.value.isEmpty() }
95+
}
6796
println("RoleActionExecutor: Final Result - Deaths: ${result.deaths}, Saved: ${result.saved}, Protected: ${result.protectedPlayers}")
6897

6998
return result
@@ -77,6 +106,47 @@ class RoleActionExecutor(private val roleRegistry: RoleRegistry) {
77106
action: RoleActionInstance,
78107
accumulatedState: ActionExecutionResult = ActionExecutionResult()
79108
): ActionExecutionResult {
109+
// --- Ghost Rider Reflection Logic ---
110+
val ghostRiderPlayerIds = session.players.values.filter { it.roles.contains("惡靈騎士") }.map { it.id }.toSet()
111+
112+
if (ghostRiderPlayerIds.isNotEmpty() && !session.stateData.ghostRiderReflected) {
113+
val targetsGhostRider = action.targets.any { it in ghostRiderPlayerIds }
114+
115+
if (targetsGhostRider) {
116+
// Check Actor Camp
117+
val actorRoleName = action.actorRole
118+
val actorRole = roleRegistry.getRole(actorRoleName)
119+
val isGoodCamp = actorRole?.camp == dev.robothanzo.werewolf.game.model.Camp.GOD ||
120+
actorRole?.camp == dev.robothanzo.werewolf.game.model.Camp.VILLAGER
121+
// Exclude Guard (Guard Protect)
122+
val isGuard = action.actionDefinitionId == ActionDefinitionId.GUARD_PROTECT
123+
124+
if (isGoodCamp && !isGuard) {
125+
val victimId = action.actor
126+
println("RoleActionExecutor: Ghost Rider Reflection Triggered (Singular)! ${action.actionDefinitionId} from Actor $victimId reflected.")
127+
128+
// Record reflection action to history
129+
val ghostRiderActorId = ghostRiderPlayerIds.first()
130+
val reflectAction = RoleActionInstance(
131+
actor = ghostRiderActorId,
132+
actorRole = "惡靈騎士",
133+
actionDefinitionId = ActionDefinitionId.GHOST_RIDER_REFLECT,
134+
targets = mutableListOf(victimId),
135+
submittedBy = ActionSubmissionSource.SYSTEM,
136+
status = dev.robothanzo.werewolf.game.model.ActionStatus.PROCESSED
137+
)
138+
session.stateData.executedActions.getOrPut(session.day) { mutableListOf() }.add(reflectAction)
139+
140+
// Kill the actor
141+
accumulatedState.deaths.getOrPut(dev.robothanzo.werewolf.game.model.DeathCause.REFLECT) { mutableListOf() }
142+
.add(victimId)
143+
144+
// Skip original action
145+
return accumulatedState
146+
}
147+
}
148+
}
149+
80150
val executor = roleRegistry.getAction(action.actionDefinitionId)
81151
return if (executor != null) {
82152
executor.execute(session, action, accumulatedState)

src/main/kotlin/dev/robothanzo/werewolf/game/steps/JudgeDecisionStep.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dev.robothanzo.werewolf.game.steps
22

33
import dev.robothanzo.werewolf.WerewolfApplication
44
import dev.robothanzo.werewolf.database.documents.LogType
5+
import dev.robothanzo.werewolf.database.documents.Replay
56
import dev.robothanzo.werewolf.database.documents.Session
67
import dev.robothanzo.werewolf.game.GameStep
78
import dev.robothanzo.werewolf.service.GameStateService
@@ -79,6 +80,19 @@ class JudgeDecisionStep : GameStep {
7980
session.addLog(LogType.SYSTEM, "法官確認結束遊戲。")
8081
session.courtTextChannel?.sendMessage("✅ 遊戲結束,正在開放權限...")?.queue()
8182

83+
val result = session.hasEnded(null)
84+
if (result != Session.Result.NOT_ENDED) {
85+
val judgePing = "<@&" + (session.judgeRole?.idLong ?: 0) + "> "
86+
val message = if (result == Session.Result.WOLVES_DIED) {
87+
judgePing + "遊戲結束,**好**人獲勝,原因:" + result.reason
88+
} else {
89+
judgePing + "遊戲結束,**狼**人獲勝,原因:" + result.reason
90+
}
91+
session.spectatorTextChannel?.sendMessage(message)?.queue()
92+
// Trigger Replay Generation
93+
Replay.upsertFromSession(session, WerewolfApplication.replayRepository)
94+
}
95+
8296
val guild = session.guild ?: return
8397
val courtVoice = session.courtVoiceChannel
8498
val courtText = session.courtTextChannel

src/main/kotlin/dev/robothanzo/werewolf/service/impl/GameStateServiceImpl.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,16 @@ class GameStateServiceImpl(
103103
}
104104

105105
// Check for Game End
106-
// We only check if we are NOT already in JUDGE_DECISION (to avoid loops if we came from there and chose continue)
107-
108-
val endResult = session.hasEnded(null)
109-
if (endResult != Session.Result.NOT_ENDED) {
110-
session.stateData.pendingNextStep = nextId
111-
session.stateData.gameEndReason = endResult.reason
112-
startStep(session, "JUDGE_DECISION")
113-
return
106+
// We only check if we are NOT already in JUDGE_DECISION
107+
// Defer game end check if we are heading into DEATH_ANNOUNCEMENT to allow resolution results to be shown.
108+
if (nextId != "DEATH_ANNOUNCEMENT") {
109+
val endResult = session.hasEnded(null)
110+
if (endResult != Session.Result.NOT_ENDED) {
111+
session.stateData.pendingNextStep = nextId
112+
session.stateData.gameEndReason = endResult.reason
113+
startStep(session, "JUDGE_DECISION")
114+
return
115+
}
114116
}
115117

116118
startStep(session, nextId)

0 commit comments

Comments
 (0)