Skip to content

Commit 3cc90d8

Browse files
committed
wolf mechanic (half tested) and psychic (tested)
1 parent 8f4067e commit 3cc90d8

21 files changed

Lines changed: 1174 additions & 30 deletions

File tree

src/dashboard/src/constants/gameData.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,20 @@ export const GAME_ROLES: Record<string, GameEntityConfig> = {
169169
translationKey: 'roles.labels.BLOOD_MOON',
170170
camp: 'WEREWOLF',
171171
},
172-
ROBOT_WOLF: {
173-
id: 'ROBOT_WOLF',
172+
WOLF_MECHANIC: {
173+
id: 'WOLF_MECHANIC',
174174
color: '#dc2626', // Red-600
175175
icon: Zap,
176-
translationKey: 'roles.labels.ROBOT_WOLF',
176+
translationKey: 'roles.labels.WOLF_MECHANIC',
177177
camp: 'WEREWOLF',
178178
},
179+
PSYCHIC: {
180+
id: 'PSYCHIC',
181+
color: '#06b6d4', // Cyan-500
182+
icon: Eye,
183+
translationKey: 'roles.labels.PSYCHIC',
184+
camp: 'GOD',
185+
},
179186
CLONE: {
180187
id: 'CLONE',
181188
color: '#8b5cf6', // Violet-500
@@ -237,6 +244,12 @@ export const GAME_ACTIONS: Record<string, GameEntityConfig> = {
237244
icon: Search,
238245
translationKey: 'roles.actions.SEER_CHECK',
239246
},
247+
PSYCHIC_CHECK: {
248+
id: 'PSYCHIC_CHECK',
249+
color: '#06b6d4',
250+
icon: Search,
251+
translationKey: 'roles.actions.PSYCHIC_CHECK',
252+
},
240253
GUARD_PROTECT: {
241254
id: 'GUARD_PROTECT',
242255
color: '#f59e0b',
@@ -303,6 +316,42 @@ export const GAME_ACTIONS: Record<string, GameEntityConfig> = {
303316
icon: Ghost,
304317
translationKey: 'roles.actions.NIGHTMARE_FEAR',
305318
},
319+
WOLF_MECHANIC_LEARN: {
320+
id: 'WOLF_MECHANIC_LEARN',
321+
color: '#dc2626',
322+
icon: Zap,
323+
translationKey: 'roles.actions.WOLF_MECHANIC_LEARN',
324+
},
325+
WOLF_MECHANIC_SEER_CHECK: {
326+
id: 'WOLF_MECHANIC_SEER_CHECK',
327+
color: '#06b6d4',
328+
icon: Search,
329+
translationKey: 'roles.actions.WOLF_MECHANIC_SEER_CHECK',
330+
},
331+
WOLF_MECHANIC_POISON: {
332+
id: 'WOLF_MECHANIC_POISON',
333+
color: '#a855f7',
334+
icon: Pill,
335+
translationKey: 'roles.actions.WOLF_MECHANIC_POISON',
336+
},
337+
WOLF_MECHANIC_GUARD_PROTECT: {
338+
id: 'WOLF_MECHANIC_GUARD_PROTECT',
339+
color: '#f59e0b',
340+
icon: Shield,
341+
translationKey: 'roles.actions.WOLF_MECHANIC_GUARD_PROTECT',
342+
},
343+
WOLF_MECHANIC_HUNTER_REVENGE: {
344+
id: 'WOLF_MECHANIC_HUNTER_REVENGE',
345+
color: '#ec4899',
346+
icon: Crosshair,
347+
translationKey: 'roles.actions.WOLF_MECHANIC_HUNTER_REVENGE',
348+
},
349+
WOLF_MECHANIC_EXTRA_KILL: {
350+
id: 'WOLF_MECHANIC_EXTRA_KILL',
351+
color: '#f87171',
352+
icon: Flame,
353+
translationKey: 'roles.actions.WOLF_MECHANIC_EXTRA_KILL',
354+
},
306355
DEATH: {
307356
id: 'DEATH',
308357
color: '#6b7280',
@@ -325,8 +374,9 @@ export const getRoleConfig = (roleName: string): GameEntityConfig => {
325374
if (roleName.includes('石像鬼')) return GAME_ROLES.GARGOYLE;
326375
if (roleName.includes('惡靈騎士')) return GAME_ROLES.NIGHT_KNIGHT;
327376
if (roleName.includes('血月使者')) return GAME_ROLES.BLOOD_MOON;
328-
if (roleName.includes('機械狼')) return GAME_ROLES.ROBOT_WOLF;
377+
if (roleName.includes('機械狼')) return GAME_ROLES.WOLF_MECHANIC;
329378
if (roleName.includes('狼')) return GAME_ROLES.WEREWOLF;
379+
if (roleName.includes('通靈師')) return GAME_ROLES.PSYCHIC;
330380
if (roleName.includes('預言')) return GAME_ROLES.SEER;
331381
if (roleName.includes('女巫')) return GAME_ROLES.WITCH;
332382
if (roleName.includes('守衛')) return GAME_ROLES.GUARD;

src/dashboard/src/features/game/components/NightStatus.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface NightStatusData {
2626
| 'WEREWOLF_VOTING'
2727
| 'ROLE_ACTIONS'
2828
| 'WOLF_YOUNGER_BROTHER_ACTION'
29+
| 'WOLF_MECHANIC_ACTION'
2930
| 'NIGHTMARE_ACTION'
3031
| 'MAGICIAN_ACTION';
3132
startTime: number;
@@ -52,7 +53,7 @@ export const NightStatus: React.FC<NightStatusProps> = ({
5253
}) => {
5354
const { t } = useTranslation();
5455
const [activeTab, setActiveTab] = useState<
55-
'werewolves' | 'actions' | 'nightmare' | 'wolf_brother' | 'magician'
56+
'werewolves' | 'actions' | 'nightmare' | 'wolf_brother' | 'wolf_mechanic' | 'magician'
5657
>('werewolves');
5758
const messageScrollContainerRef = useRef<HTMLDivElement>(null);
5859

@@ -128,6 +129,8 @@ export const NightStatus: React.FC<NightStatusProps> = ({
128129
setActiveTab('nightmare');
129130
} else if (type === 'WOLF_YOUNGER_BROTHER_ACTION') {
130131
setActiveTab('wolf_brother');
132+
} else if (type === 'WOLF_MECHANIC_ACTION') {
133+
setActiveTab('wolf_mechanic');
131134
} else if (type === 'MAGICIAN_ACTION') {
132135
setActiveTab('magician');
133136
} else {
@@ -262,6 +265,18 @@ export const NightStatus: React.FC<NightStatusProps> = ({
262265
return nightStatus.actionStatuses.find((a) => a.actorRole.includes('狼弟'));
263266
}, [nightStatus.actionStatuses]);
264267

268+
const hasWolfMechanic = useMemo(() => {
269+
return players.some((p) =>
270+
p.roles?.some((r) => r.includes('機械狼') || r.includes('WOLF_MECHANIC'))
271+
);
272+
}, [players]);
273+
274+
const wolfMechanicAction = useMemo(() => {
275+
return nightStatus.actionStatuses.find(
276+
(a) => a.actorRole.includes('機械狼') || a.actorRole.includes('WOLF_MECHANIC')
277+
);
278+
}, [nightStatus.actionStatuses]);
279+
265280
const hasMagician = useMemo(() => {
266281
return players.some((p) =>
267282
p.roles?.some((r) => r.includes('魔術師') || r.includes('MAGICIAN'))
@@ -310,6 +325,15 @@ export const NightStatus: React.FC<NightStatusProps> = ({
310325
</button>
311326
)}
312327

328+
{hasWolfMechanic && (
329+
<button
330+
onClick={() => setActiveTab('wolf_mechanic')}
331+
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors whitespace-nowrap ${activeTab === 'wolf_mechanic' ? 'bg-white dark:bg-slate-700 text-red-600 dark:text-red-300 shadow-sm' : 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-white'}`}
332+
>
333+
{t('roles.labels.WOLF_MECHANIC')}
334+
</button>
335+
)}
336+
313337
<button
314338
onClick={() => setActiveTab('werewolves')}
315339
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors whitespace-nowrap ${activeTab === 'werewolves' ? 'bg-white dark:bg-slate-700 text-indigo-600 dark:text-white shadow-sm' : 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-white'}`}
@@ -629,6 +653,16 @@ export const NightStatus: React.FC<NightStatusProps> = ({
629653
/>
630654
)}
631655

656+
{activeTab === 'wolf_mechanic' && wolfMechanicAction && (
657+
<StandardActionCard
658+
status={wolfMechanicAction}
659+
roleId="WOLF_MECHANIC"
660+
players={players}
661+
guildId={guildId}
662+
variant="large"
663+
/>
664+
)}
665+
632666
{activeTab === 'actions' && (
633667
/* Role Actions Screen (Redesigned) */
634668
<div className="space-y-8 pb-32 animate-in fade-in duration-500">

src/dashboard/src/locales/zh-TW.json

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@
4949
"HUNTER_REVENGE": "獵人開槍",
5050
"WOLF_KING_REVENGE": "狼王復仇",
5151
"WOLF_YOUNGER_BROTHER_EXTRA_KILL": "狼弟復仇",
52+
"WOLF_MECHANIC_LEARN": "機械狼學習",
53+
"WOLF_MECHANIC_SEER_CHECK": "機械狼查驗",
54+
"WOLF_MECHANIC_POISON": "機械狼毒藥",
55+
"WOLF_MECHANIC_GUARD_PROTECT": "機械狼守護",
56+
"WOLF_MECHANIC_HUNTER_REVENGE": "機械狼開槍",
57+
"WOLF_MECHANIC_EXTRA_KILL": "機械狼復仇",
58+
"PSYCHIC_CHECK": "通靈師查驗",
5259
"MERCHANT_SEER_CHECK": "查驗 (商人贈予)",
5360
"MERCHANT_POISON": "毒藥 (商人贈予)",
5461
"MERCHANT_GUN": "獵槍 (商人贈予)",
@@ -229,7 +236,8 @@
229236
"GARGOYLE": "石像鬼",
230237
"NIGHT_KNIGHT": "惡靈騎士",
231238
"BLOOD_MOON": "血月使者",
232-
"ROBOT_WOLF": "機械狼",
239+
"WOLF_MECHANIC": "機械狼",
240+
"PSYCHIC": "通靈師",
233241
"IDIOT": "白痴",
234242
"KNIGHT": "騎士",
235243
"GRAVE_KEEPER": "守墓人",
@@ -248,6 +256,13 @@
248256
"GUARD_PROTECT": "守衛守護",
249257
"HUNTER_REVENGE": "獵人開槍",
250258
"WOLF_KING_REVENGE": "狼王開槍",
259+
"WOLF_MECHANIC_LEARN": "機械狼學習",
260+
"WOLF_MECHANIC_SEER_CHECK": "機械狼查驗 (通靈師)",
261+
"WOLF_MECHANIC_POISON": "機械狼毒藥",
262+
"WOLF_MECHANIC_GUARD_PROTECT": "機械狼守護",
263+
"WOLF_MECHANIC_HUNTER_REVENGE": "機械狼開槍",
264+
"WOLF_MECHANIC_EXTRA_KILL": "機械狼復仇刀",
265+
"PSYCHIC_CHECK": "通靈師查驗",
251266
"DARK_MERCHANT_TRADE": "黑市商人交易",
252267
"MIRACLE_MERCHANT_TRADE_GUARD": "奇蹟商人交易 (給予守護)",
253268
"MERCHANT_SEER_CHECK": "商人技能:查驗",
@@ -569,7 +584,9 @@
569584
"ROLE_ACTIONS": "角色行動",
570585
"NIGHTMARE_ACTION": "夢魘行動",
571586
"WOLF_YOUNGER_BROTHER_ACTION": "狼弟行動",
572-
"MAGICIAN_ACTION": "魔術師行動"
587+
"WOLF_MECHANIC_ACTION": "機械狼行動",
588+
"MAGICIAN_ACTION": "魔術師行動",
589+
"WEREWOLF_ACTION": "狼人行動"
573590
}
574591
},
575592
"replays": {

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,33 @@ data class Session(
283283
return false
284284
}
285285

286+
fun isWolfMechanicInherited(): Boolean {
287+
// Mechanic inherits the extra kill phase when all original wolves are dead.
288+
val mainWolves = players.values.filter {
289+
it.wolf &&
290+
!it.roles.contains("機械狼") &&
291+
!(it.roles.contains("狼弟") && isCharacterAlive("狼兄"))
292+
}
293+
val aliveMainWolves = mainWolves.any { it.alive }
294+
return mainWolves.isNotEmpty() && !aliveMainWolves
295+
}
296+
297+
/**
298+
* Resolves the string role name learned by the Wolf Mechanic.
299+
* Maps mechanic playerId -> learned role name
300+
*/
301+
val wolfMechanicLearnedRole: Map<Int, String>
302+
get() {
303+
val result = mutableMapOf<Int, String>()
304+
stateData.wolfMechanicLearnedPlayerId.forEach { (actor, targetId) ->
305+
val target = getPlayer(targetId)
306+
val targetRole =
307+
target?.roles?.firstOrNull { it !in target.deadRoles } ?: target?.roles?.firstOrNull() ?: "平民"
308+
result[actor] = targetRole
309+
}
310+
return result
311+
}
312+
286313
companion object {
287314
val DASHBOARD_BASE_URL: String
288315
get() = System.getenv("DASHBOARD_URL")?.removeSuffix("/") ?: "http://localhost:5173"

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,21 @@ enum class ActionDefinitionId(val actionName: String) {
1111
WOLF_YOUNGER_BROTHER_EXTRA_KILL("狼弟復仇刀"),
1212
WOLF_DETONATE("自爆"),
1313

14+
// Wolf Mechanic Actions
15+
WOLF_MECHANIC_LEARN("學習"),
16+
WOLF_MECHANIC_SEER_CHECK("查驗 (機械狼)"),
17+
WOLF_MECHANIC_POISON("毒藥 (機械狼)"),
18+
WOLF_MECHANIC_GUARD_PROTECT("守護 (機械狼)"),
19+
WOLF_MECHANIC_HUNTER_REVENGE("開槍 (機械狼)"),
20+
WOLF_MECHANIC_EXTRA_KILL("機械狼復仇刀"),
21+
1422
// Witch Actions
1523
WITCH_ANTIDOTE("解藥"),
1624
WITCH_POISON("毒藥"),
1725

18-
// Seer Actions
26+
// Seer & Psychic Actions
1927
SEER_CHECK("查驗"),
28+
PSYCHIC_CHECK("查驗 (通靈師)"),
2029

2130
// Guard Actions
2231
GUARD_PROTECT("守護"),

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import org.springframework.data.annotation.Transient
1212
enum class NightPhase(val defaultDurationMs: Long) {
1313
NIGHTMARE_ACTION(60_000L),
1414
MAGICIAN_ACTION(60_000L),
15+
WOLF_MECHANIC_ACTION(60_000L),
1516
WOLF_YOUNGER_BROTHER_ACTION(60_000L),
1617
WEREWOLF_VOTING(90_000L),
1718
ROLE_ACTIONS(60_000L)
@@ -144,6 +145,7 @@ data class GameStateData(
144145

145146
@Schema(description = "Player ID of the Wolf Younger Brother if he is awakened this night")
146147
var wolfBrotherAwakenedPlayerId: Int? = null,
148+
147149
@Schema(description = "Start time of the current game step")
148150
var stepStartTime: Long = 0,
149151

@@ -313,6 +315,33 @@ data class GameStateData(
313315

314316
return result
315317
}
318+
319+
@get:BsonIgnore
320+
val wolfMechanicLearnDay: Map<Int, Int>
321+
get() {
322+
val result = mutableMapOf<Int, Int>()
323+
executedActions.forEach { (day, actions) ->
324+
actions.filter { it.actionDefinitionId == ActionDefinitionId.WOLF_MECHANIC_LEARN }
325+
.forEach { result[it.actor] = day }
326+
}
327+
submittedActions.filter { it.actionDefinitionId == ActionDefinitionId.WOLF_MECHANIC_LEARN && it.status.executed }
328+
.forEach {
329+
val currentDay = (executedActions.keys.maxOrNull() ?: 0) + 1
330+
result[it.actor] = currentDay
331+
}
332+
return result
333+
}
334+
335+
@get:BsonIgnore
336+
val wolfMechanicLearnedPlayerId: Map<Int, Int>
337+
get() {
338+
val result = mutableMapOf<Int, Int>()
339+
executedActions.values.flatten().filter { it.actionDefinitionId == ActionDefinitionId.WOLF_MECHANIC_LEARN }
340+
.forEach { if (it.targets.isNotEmpty()) result[it.actor] = it.targets.first() }
341+
submittedActions.filter { it.actionDefinitionId == ActionDefinitionId.WOLF_MECHANIC_LEARN && it.status.executed }
342+
.forEach { if (it.targets.isNotEmpty()) result[it.actor] = it.targets.first() }
343+
return result
344+
}
316345
}
317346

318347
/**

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

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,21 +96,14 @@ fun Session.isActionAvailable(
9696
}
9797
}
9898

99-
// Check usage limit
100-
if (action.usageLimit == -1) {
101-
return true
99+
// Delegate to action's isAvailable method which may contain additional logic (like limits or role-specific checks)
100+
if (!action.isAvailable(this, playerId)) {
101+
return false
102102
}
103103

104-
val usage = getActionUsageCount(playerId, actionDefinitionId, roleRegistry)
105-
if (usage >= action.usageLimit) return false
106-
107-
// Wolf Younger Brother extra kill logic
108104
val currentPlayer = getPlayer(playerId)
109-
if (actionDefinitionId == ActionDefinitionId.WOLF_YOUNGER_BROTHER_EXTRA_KILL) {
110-
// Delegate to action's isAvailable method which now contains the logic
111-
return action.isAvailable(this, playerId)
112-
}
113105

106+
// Wolf Younger Brother default kill logic
114107
if (actionDefinitionId == ActionDefinitionId.WEREWOLF_KILL && currentPlayer?.roles?.contains("狼弟") == true) {
115108
val isWolfBrotherAlive = alivePlayers().values.any { it.roles.contains("狼兄") }
116109
// Younger Brother only gets to kill if Brother is dead

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ object PredefinedRoles {
77
const val WITCH_ANTIDOTE_PRIORITY = 200
88
const val WITCH_POISON_PRIORITY = 210
99
const val SEER_PRIORITY = 300
10+
const val PSYCHIC_PRIORITY = 300 // Same as Seer
1011
const val GUARD_PRIORITY = 150
1112
const val HUNTER_PRIORITY = 250
1213
const val POLICE_PRIORITY = 400
1314
const val DARK_MERCHANT_PRIORITY = 50
15+
const val NIGHTMARE_PRIORITY = 0 // First thing at night
16+
const val WOLF_MECHANIC_PRIORITY = 50
1417
const val DREAM_WEAVER_PRIORITY = 60 // Before wolves
1518
const val MAGICIAN_PRIORITY = 40 // Before Dream Weaver and Wolves
16-
const val NIGHTMARE_PRIORITY = 0 // First thing at night
1719

1820
// Action IDs
1921

@@ -22,6 +24,7 @@ object PredefinedRoles {
2224
const val WITCH_ANTIDOTE = "WITCH_ANTIDOTE"
2325
const val WITCH_POISON = "WITCH_POISON"
2426
const val SEER_CHECK = "SEER_CHECK"
27+
const val PSYCHIC_CHECK = "PSYCHIC_CHECK"
2528
const val GUARD_PROTECT = "GUARD_PROTECT"
2629
const val HUNTER_REVENGE = "HUNTER_REVENGE"
2730
const val WOLF_KING_REVENGE = "WOLF_KING_REVENGE"
@@ -39,6 +42,14 @@ object PredefinedRoles {
3942
const val DREAM_WEAVER_LINK = "DREAM_WEAVER_LINK"
4043
const val NIGHTMARE_FEAR = "NIGHTMARE_FEAR"
4144

45+
// Wolf Mechanic
46+
const val WOLF_MECHANIC_LEARN = "WOLF_MECHANIC_LEARN"
47+
const val WOLF_MECHANIC_SEER_CHECK = "WOLF_MECHANIC_SEER_CHECK"
48+
const val WOLF_MECHANIC_POISON = "WOLF_MECHANIC_POISON"
49+
const val WOLF_MECHANIC_GUARD_PROTECT = "WOLF_MECHANIC_GUARD_PROTECT"
50+
const val WOLF_MECHANIC_HUNTER_REVENGE = "WOLF_MECHANIC_HUNTER_REVENGE"
51+
const val WOLF_MECHANIC_EXTRA_KILL = "WOLF_MECHANIC_EXTRA_KILL"
52+
4253
// Special death causes (legacy)
4354
const val DOUBLE_PROTECTION = "DOUBLE_PROTECTION"
4455

0 commit comments

Comments
 (0)