kaggle-environments 1.20.1__py3-none-any.whl → 1.21.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kaggle-environments might be problematic. Click here for more details.
- kaggle_environments/__init__.py +2 -2
- kaggle_environments/envs/cabt/cabt.js +8 -8
- kaggle_environments/envs/cabt/cg/cg.dll +0 -0
- kaggle_environments/envs/cabt/cg/libcg.so +0 -0
- kaggle_environments/envs/{open_spiel → open_spiel_env}/games/universal_poker/universal_poker.js +52 -28
- kaggle_environments/envs/{open_spiel/open_spiel.py → open_spiel_env/open_spiel_env.py} +37 -1
- kaggle_environments/envs/{open_spiel/test_open_spiel.py → open_spiel_env/test_open_spiel_env.py} +65 -1
- kaggle_environments/envs/werewolf/GAME_RULE.md +75 -0
- kaggle_environments/envs/werewolf/__init__.py +0 -0
- kaggle_environments/envs/werewolf/game/__init__.py +0 -0
- kaggle_environments/envs/werewolf/game/actions.py +268 -0
- kaggle_environments/envs/werewolf/game/base.py +115 -0
- kaggle_environments/envs/werewolf/game/consts.py +156 -0
- kaggle_environments/envs/werewolf/game/engine.py +580 -0
- kaggle_environments/envs/werewolf/game/night_elimination_manager.py +101 -0
- kaggle_environments/envs/werewolf/game/protocols/__init__.py +4 -0
- kaggle_environments/envs/werewolf/game/protocols/base.py +242 -0
- kaggle_environments/envs/werewolf/game/protocols/bid.py +248 -0
- kaggle_environments/envs/werewolf/game/protocols/chat.py +467 -0
- kaggle_environments/envs/werewolf/game/protocols/factory.py +59 -0
- kaggle_environments/envs/werewolf/game/protocols/vote.py +471 -0
- kaggle_environments/envs/werewolf/game/records.py +334 -0
- kaggle_environments/envs/werewolf/game/roles.py +326 -0
- kaggle_environments/envs/werewolf/game/states.py +214 -0
- kaggle_environments/envs/werewolf/game/test_actions.py +45 -0
- kaggle_environments/envs/werewolf/test_werewolf.py +161 -0
- kaggle_environments/envs/werewolf/test_werewolf_deterministic.py +211 -0
- kaggle_environments/envs/werewolf/werewolf.js +4377 -0
- kaggle_environments/envs/werewolf/werewolf.json +286 -0
- kaggle_environments/envs/werewolf/werewolf.py +602 -0
- kaggle_environments/static/player.html +19 -1
- kaggle_environments-1.21.0.dist-info/METADATA +30 -0
- {kaggle_environments-1.20.1.dist-info → kaggle_environments-1.21.0.dist-info}/RECORD +55 -36
- kaggle_environments/envs/chess/chess.js +0 -4289
- kaggle_environments/envs/chess/chess.json +0 -60
- kaggle_environments/envs/chess/chess.py +0 -4241
- kaggle_environments/envs/chess/test_chess.py +0 -60
- kaggle_environments-1.20.1.dist-info/METADATA +0 -315
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/chess/chess.js +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/chess/image_config.jsonl +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/chess/openings.jsonl +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/connect_four/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/connect_four/connect_four.js +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/connect_four/connect_four_proxy.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/go/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/go/go.js +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/go/go_proxy.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/tic_tac_toe/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/tic_tac_toe/tic_tac_toe.js +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/tic_tac_toe/tic_tac_toe_proxy.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/universal_poker/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/universal_poker/universal_poker_proxy.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/html_playthrough_generator.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/observation.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/proxy.py +0 -0
- {kaggle_environments-1.20.1.dist-info → kaggle_environments-1.21.0.dist-info}/WHEEL +0 -0
- {kaggle_environments-1.20.1.dist-info → kaggle_environments-1.21.0.dist-info}/entry_points.txt +0 -0
- {kaggle_environments-1.20.1.dist-info → kaggle_environments-1.21.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,4377 @@
|
|
|
1
|
+
function renderer(context) {
|
|
2
|
+
const {
|
|
3
|
+
environment,
|
|
4
|
+
step,
|
|
5
|
+
parent,
|
|
6
|
+
height = 1000,
|
|
7
|
+
width = 1500
|
|
8
|
+
} = context;
|
|
9
|
+
|
|
10
|
+
if (!parent.id) {
|
|
11
|
+
parent.id = 'werewolf-renderer-parent-' + Math.random().toString(36).substring(2, 9);
|
|
12
|
+
}
|
|
13
|
+
const parentId = parent.id;
|
|
14
|
+
|
|
15
|
+
const systemEntryTypeSet = new Set([
|
|
16
|
+
'moderator_announcement',
|
|
17
|
+
'elimination',
|
|
18
|
+
'vote_request',
|
|
19
|
+
'heal_request',
|
|
20
|
+
'heal_result',
|
|
21
|
+
'inspect_request',
|
|
22
|
+
'inspect_result',
|
|
23
|
+
'bidding_info',
|
|
24
|
+
'bid_result',
|
|
25
|
+
'day_start',
|
|
26
|
+
'night_start'
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
if (!window.werewolfGamePlayer) {
|
|
30
|
+
window.werewolfGamePlayer = {
|
|
31
|
+
initialized: false,
|
|
32
|
+
allEvents: [],
|
|
33
|
+
displayEvents: [],
|
|
34
|
+
eventToKaggleStep: [],
|
|
35
|
+
displayStepToAllEventsIndex: [],
|
|
36
|
+
allEventsIndexToDisplayStep: [],
|
|
37
|
+
originalSteps: environment.steps,
|
|
38
|
+
reasoningCounter: 0,
|
|
39
|
+
};
|
|
40
|
+
const player = window.werewolfGamePlayer;
|
|
41
|
+
|
|
42
|
+
const visibleEventDataTypes = new Set([
|
|
43
|
+
'ChatDataEntry',
|
|
44
|
+
'DayExileVoteDataEntry',
|
|
45
|
+
'WerewolfNightVoteDataEntry',
|
|
46
|
+
'DoctorHealActionDataEntry',
|
|
47
|
+
'SeerInspectActionDataEntry',
|
|
48
|
+
'DayExileElectedDataEntry',
|
|
49
|
+
'WerewolfNightEliminationDataEntry',
|
|
50
|
+
'SeerInspectResultDataEntry',
|
|
51
|
+
'DoctorSaveDataEntry',
|
|
52
|
+
'GameEndResultsDataEntry',
|
|
53
|
+
'PhaseDividerDataEntry',
|
|
54
|
+
'DiscussionOrderDataEntry'
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
let allEventsIndex = 0;
|
|
58
|
+
let currentDisplayStep = 0;
|
|
59
|
+
const processedPhaseEvents = new Set(); // This will track events within a single phase.
|
|
60
|
+
(environment.info?.MODERATOR_OBSERVATION || []).forEach((stepEvents, kaggleStep) => {
|
|
61
|
+
(stepEvents || []).flat().forEach(dataEntry => {
|
|
62
|
+
const event = JSON.parse(dataEntry.json_str);
|
|
63
|
+
const dataType = dataEntry.data_type;
|
|
64
|
+
const visibleInUI = event.visible_in_ui ?? true;
|
|
65
|
+
|
|
66
|
+
console.debug(`[RAW SEEN] Kaggle Step: ${kaggleStep}`, { dataType: dataType, event: event });
|
|
67
|
+
|
|
68
|
+
if (!visibleInUI) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 1. Reset our memory on key phase-changing events.
|
|
73
|
+
if (event.event_name === 'day_start' || event.event_name === 'night_start' || event.description?.includes('Voting phase begins')) {
|
|
74
|
+
processedPhaseEvents.clear();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Generate a unique "fingerprint" based on essential event content.
|
|
78
|
+
let eventFingerprint = event.description;
|
|
79
|
+
const eventData = event.data;
|
|
80
|
+
|
|
81
|
+
// 3. Check against the new fingerprint.
|
|
82
|
+
if (processedPhaseEvents.has(eventFingerprint)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
processedPhaseEvents.add(eventFingerprint);
|
|
86
|
+
|
|
87
|
+
const isVisibleDataType = visibleEventDataTypes.has(dataType);
|
|
88
|
+
const isVisibleEntryType = systemEntryTypeSet.has(event.event_name);
|
|
89
|
+
|
|
90
|
+
if (!isVisibleDataType && !isVisibleEntryType) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
event.kaggleStep = kaggleStep;
|
|
95
|
+
event.dataType = dataType;
|
|
96
|
+
player.allEvents.push(event);
|
|
97
|
+
player.eventToKaggleStep.push(kaggleStep);
|
|
98
|
+
|
|
99
|
+
if (dataType !== 'PhaseDividerDataEntry') {
|
|
100
|
+
player.displayEvents.push(event);
|
|
101
|
+
player.displayStepToAllEventsIndex.push(allEventsIndex);
|
|
102
|
+
player.allEventsIndexToDisplayStep[allEventsIndex] = currentDisplayStep;
|
|
103
|
+
currentDisplayStep++;
|
|
104
|
+
}
|
|
105
|
+
allEventsIndex++;
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
console.debug(`[FINAL STEP LIST]`, player.displayEvents);
|
|
110
|
+
|
|
111
|
+
const newSteps = player.displayEvents.map((event) => {
|
|
112
|
+
return player.originalSteps[event.kaggleStep];
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
if (window.kaggle) {
|
|
117
|
+
window.kaggle.environment.steps = newSteps;
|
|
118
|
+
}
|
|
119
|
+
window.postMessage({ setSteps: newSteps }, "*");
|
|
120
|
+
}, 100); // A small delay to ensure player is ready
|
|
121
|
+
player.initialized = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// We patch the functions on the 'context' object directly.
|
|
125
|
+
if (context.__mainContext && !window.customPlayerControlsInjected) {
|
|
126
|
+
const mainContext = context.__mainContext;
|
|
127
|
+
|
|
128
|
+
if (!window.wwOriginals) {
|
|
129
|
+
console.debug("DEBUG: Storing original controls for the first time.");
|
|
130
|
+
window.wwOriginals = {
|
|
131
|
+
setStep: mainContext.setStep,
|
|
132
|
+
play: mainContext.play,
|
|
133
|
+
pause: mainContext.pause,
|
|
134
|
+
setPlaying: mainContext.setPlaying
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Patch setStep ---
|
|
139
|
+
if (mainContext.setSetStep) {
|
|
140
|
+
mainContext.setSetStep(() => (newStep) => {
|
|
141
|
+
console.debug(`DEBUG: [setStep] User manually set step to ${newStep}. Stopping audio.`);
|
|
142
|
+
stopAndClearAudio();
|
|
143
|
+
audioState.isPaused = true;
|
|
144
|
+
window.wwOriginals.setStep(newStep);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Patch Play ---
|
|
149
|
+
mainContext.setPlay(() => (continuing) => {
|
|
150
|
+
console.debug(`DEBUG: [setPlay] Play button clicked. Continuing: ${continuing}`);
|
|
151
|
+
|
|
152
|
+
if (audioState.isAudioEnabled) {
|
|
153
|
+
// --- AUDIO-DRIVEN PLAYBACK ---
|
|
154
|
+
console.debug("DEBUG: [setPlay] Audio is ON. Using audio-driven playback.");
|
|
155
|
+
window.wwOriginals.setPlaying(true);
|
|
156
|
+
let currentDisplayStep = context.step;
|
|
157
|
+
|
|
158
|
+
if (!continuing && !audioState.isPaused && currentDisplayStep === newSteps.length - 1) {
|
|
159
|
+
currentDisplayStep = 0;
|
|
160
|
+
window.wwOriginals.setStep(0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const allEventsIndex = window.werewolfGamePlayer.displayStepToAllEventsIndex[currentDisplayStep];
|
|
164
|
+
if (allEventsIndex === undefined) {
|
|
165
|
+
window.wwOriginals.setPlaying(false);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
playAudioFrom(allEventsIndex, true);
|
|
170
|
+
|
|
171
|
+
} else {
|
|
172
|
+
// --- TIMER-DRIVEN PLAYBACK (When audio is off) ---
|
|
173
|
+
console.debug("DEBUG: [setPlay] Audio is OFF. Using original Kaggle timer-based playback.");
|
|
174
|
+
// This call uses the original player's setTimeout logic.
|
|
175
|
+
window.wwOriginals.play(continuing);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// --- Patch Pause ---
|
|
180
|
+
mainContext.setPause(() => () => {
|
|
181
|
+
console.debug("DEBUG: [setPause] Pause button clicked. Stopping audio.");
|
|
182
|
+
|
|
183
|
+
// Stop the timer-based playback if it's running.
|
|
184
|
+
window.wwOriginals.pause();
|
|
185
|
+
|
|
186
|
+
window.wwOriginals.setPlaying(false);
|
|
187
|
+
audioState.isPaused = true;
|
|
188
|
+
if (audioState.isAudioPlaying) {
|
|
189
|
+
audioState.audioPlayer.pause();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
mainContext.patchesApplied = true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- THREE.js Scene Setup (Singleton Pattern) ---
|
|
197
|
+
if (!window.werewolfThreeJs) {
|
|
198
|
+
window.werewolfThreeJs = {
|
|
199
|
+
initialized: false,
|
|
200
|
+
demo: null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const threeState = window.werewolfThreeJs;
|
|
204
|
+
|
|
205
|
+
function playAudioFrom(startIndex, isContinuous = true) {
|
|
206
|
+
console.debug(`DEBUG: [playAudioFrom] Called with startIndex: ${startIndex}, isContinuous: ${isContinuous}`);
|
|
207
|
+
if (!audioState.isAudioEnabled) {
|
|
208
|
+
console.error("DEBUG: [playAudioFrom] FAILED: Audio is not enabled.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// This tells the main Kaggle player that playback is active.
|
|
213
|
+
if (window.wwOriginals && window.wwOriginals.setPlaying) {
|
|
214
|
+
window.wwOriginals.setPlaying(true);
|
|
215
|
+
}
|
|
216
|
+
// This updates your custom audio panel's button.
|
|
217
|
+
|
|
218
|
+
stopAndClearAudio();
|
|
219
|
+
console.debug("DEBUG: [playAudioFrom] Audio stopped and cleared.");
|
|
220
|
+
|
|
221
|
+
if (audioState.isPaused) {
|
|
222
|
+
console.debug("DEBUG: [playAudioFrom] Audio state was paused.");
|
|
223
|
+
audioState.isPaused = false; // Un-pause regardless.
|
|
224
|
+
|
|
225
|
+
// If we're at a *new* index (e.g., user clicked slider),
|
|
226
|
+
// we must NOT resume. We must reload the queue.
|
|
227
|
+
if (startIndex !== audioState.lastStartedIndex) {
|
|
228
|
+
console.debug(`DEBUG: [playAudioFrom] New start index. Loading queue from: ${startIndex}`);
|
|
229
|
+
audioState.lastStartedIndex = startIndex;
|
|
230
|
+
loadQueueFrom(startIndex);
|
|
231
|
+
playNextInQueue(isContinuous);
|
|
232
|
+
return; // We are done.
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// If we are at the *same* index, just resume from the (now empty) queue.
|
|
236
|
+
// The queue will be re-filled by loadQueueFrom.
|
|
237
|
+
console.debug("DEBUG: [playAudioFrom] Paused, resuming from same start index (or undefined).");
|
|
238
|
+
// Fall through to load and play.
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
audioState.isPaused = false;
|
|
242
|
+
audioState.lastStartedIndex = startIndex;
|
|
243
|
+
loadQueueFrom(startIndex);
|
|
244
|
+
playNextInQueue(isContinuous);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function loadQueueFrom(startIndex) {
|
|
248
|
+
console.debug(`DEBUG: [loadQueueFrom] Loading queue from index: ${startIndex}`);
|
|
249
|
+
if (!window.werewolfGamePlayer || !window.werewolfGamePlayer.allEvents) {
|
|
250
|
+
console.error("DEBUG: [loadQueueFrom] CRITICAL: allEvents not found.");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const allEvents = window.werewolfGamePlayer.allEvents;
|
|
254
|
+
const eventsToPlay = allEvents.slice(startIndex);
|
|
255
|
+
console.debug(`DEBUG: [loadQueueFrom] Found ${eventsToPlay.length} potential events.`);
|
|
256
|
+
|
|
257
|
+
audioState.audioQueue = []; // Clear previous queue
|
|
258
|
+
|
|
259
|
+
if (eventsToPlay.length > 0) {
|
|
260
|
+
eventsToPlay.forEach((entry, i) => {
|
|
261
|
+
const allEventsIndex = startIndex + i;
|
|
262
|
+
|
|
263
|
+
let audioEventDetails = null;
|
|
264
|
+
const data = entry.data || {};
|
|
265
|
+
const event_name = entry.event_name;
|
|
266
|
+
const description = entry.description || '';
|
|
267
|
+
const day_count = entry.day;
|
|
268
|
+
|
|
269
|
+
// This logic is to identify if an event should have audio
|
|
270
|
+
// and what the audio content is.
|
|
271
|
+
switch (entry.dataType) {
|
|
272
|
+
case 'ChatDataEntry':
|
|
273
|
+
if (data.actor_id && data.actor_id !== 'moderator' && data.message) {
|
|
274
|
+
audioEventDetails = { message: data.message, speaker: data.actor_id };
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
case 'DayExileVoteDataEntry':
|
|
278
|
+
if (data.actor_id && data.target_id) {
|
|
279
|
+
audioEventDetails = { message: `${data.actor_id} votes to exile ${data.target_id}.`, speaker: 'moderator' };
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
case 'WerewolfNightVoteDataEntry':
|
|
283
|
+
if (data.actor_id && data.target_id) {
|
|
284
|
+
audioEventDetails = { message: `${data.actor_id} votes to eliminate ${data.target_id}.`, speaker: 'moderator' };
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
case 'SeerInspectActionDataEntry':
|
|
288
|
+
if (data.actor_id && data.target_id) {
|
|
289
|
+
audioEventDetails = { message: `${data.actor_id} inspects ${data.target_id}.`, speaker: 'moderator' };
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
case 'DoctorHealActionDataEntry':
|
|
293
|
+
if (data.actor_id && data.target_id) {
|
|
294
|
+
audioEventDetails = { message: `${data.actor_id} heals ${data.target_id}.`, speaker: 'moderator' };
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
case 'DayExileElectedDataEntry':
|
|
298
|
+
if (data.elected_player_id && data.elected_player_role_name) {
|
|
299
|
+
audioEventDetails = { message: `${data.elected_player_id} was exiled by vote. Their role was a ${data.elected_player_role_name}.`, speaker: 'moderator' };
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
case 'WerewolfNightEliminationDataEntry':
|
|
303
|
+
if (data.eliminated_player_id && data.eliminated_player_role_name) {
|
|
304
|
+
audioEventDetails = { message: `${data.eliminated_player_id} was eliminated. Their role was a ${data.eliminated_player_role_name}.`, speaker: 'moderator' };
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
case 'DoctorSaveDataEntry':
|
|
308
|
+
if (data.saved_player_id) {
|
|
309
|
+
audioEventDetails = { message: `${data.saved_player_id} was attacked but saved by a Doctor!`, speaker: 'moderator' };
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
case 'GameEndResultsDataEntry':
|
|
313
|
+
if (data.winner_team) {
|
|
314
|
+
audioEventDetails = { message: `The game is over. The ${data.winner_team} team has won!`, speaker: 'moderator' };
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
case 'WerewolfNightEliminationElectedDataEntry':
|
|
318
|
+
if (data.elected_target_player_id) {
|
|
319
|
+
audioEventDetails = { message: `The werewolves have chosen to eliminate ${data.elected_target_player_id}.`, speaker: 'moderator' };
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
case 'SeerInspectResultDataEntry':
|
|
323
|
+
if (data.role) {
|
|
324
|
+
audioEventDetails = { message: `${data.actor_id} saw ${data.target_id}'s role is ${data.role}.`, speaker: 'moderator'};
|
|
325
|
+
} else if (data.team) {
|
|
326
|
+
audioEventDetails = { message: `${data.actor_id} saw ${data.target_id}'s team is ${data.team}.`, speaker: 'moderator'};
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
case 'DiscussionOrderDataEntry':
|
|
330
|
+
audioEventDetails = { message: description, speaker: 'moderator' };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!audioEventDetails && event_name === 'moderator_announcement') {
|
|
334
|
+
if (description.includes('discussion rule is')) {
|
|
335
|
+
audioEventDetails = { message: 'Discussion begins!', speaker: 'moderator' };
|
|
336
|
+
} else if (description.includes('Voting phase begins')) {
|
|
337
|
+
audioEventDetails = { message: 'Exile voting begins!', speaker: 'moderator' };
|
|
338
|
+
} else {
|
|
339
|
+
audioEventDetails = { message: entry.description, speaker: 'moderator' };
|
|
340
|
+
}
|
|
341
|
+
} else if (!audioEventDetails && event_name === 'day_start') {
|
|
342
|
+
audioEventDetails = { message: `Day ${day_count} begins!`, speaker: 'moderator' };
|
|
343
|
+
} else if (!audioEventDetails && event_name === 'night_start') {
|
|
344
|
+
audioEventDetails = { message: `Night ${day_count} begins!`, speaker: 'moderator' };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Every event goes into the queue.
|
|
348
|
+
audioState.audioQueue.push({
|
|
349
|
+
allEventsIndex: allEventsIndex,
|
|
350
|
+
audioEvent: audioEventDetails, // This will be null for events without audio
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
console.debug(`DEBUG: [loadQueueFrom] Loaded ${audioState.audioQueue.length} events into queue.`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function playNextInQueue(isContinuous = true) {
|
|
358
|
+
const currentParent = document.getElementById(parentId);
|
|
359
|
+
if (!currentParent) {
|
|
360
|
+
console.error("Werewolf renderer parent container not found in DOM, stopping playback.");
|
|
361
|
+
stopAndClearAudio();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.debug(`DEBUG: [playNextInQueue] Called. Queue length: ${audioState.audioQueue.length}. isPaused: ${audioState.isPaused}. isAudioPlaying: ${audioState.isAudioPlaying}.`);
|
|
366
|
+
|
|
367
|
+
// 1. Clear any previously highlighted element
|
|
368
|
+
const currentlyPlaying = currentParent.querySelector('#chat-log .now-playing');
|
|
369
|
+
if (currentlyPlaying) {
|
|
370
|
+
currentlyPlaying.classList.remove('now-playing');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (audioState.isPaused || audioState.isAudioPlaying || audioState.audioQueue.length === 0 || !audioState.isAudioEnabled) {
|
|
374
|
+
console.warn(`DEBUG: [playNextInQueue] Exiting early. Paused: ${audioState.isPaused}, Playing: ${audioState.isAudioPlaying}, Queue: ${audioState.audioQueue.length}, Enabled: ${audioState.isAudioEnabled}`);
|
|
375
|
+
if (audioState.audioQueue.length === 0 && !audioState.isAudioPlaying) {
|
|
376
|
+
console.debug("DEBUG: [playNextInQueue] Playback finished. Setting player to 'paused' state.");
|
|
377
|
+
window.wwOriginals.setPlaying(false); // Stop the parent player
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
audioState.isAudioPlaying = true;
|
|
383
|
+
const event = audioState.audioQueue.shift();
|
|
384
|
+
|
|
385
|
+
// This is the slider logic, it should always run
|
|
386
|
+
if (event.allEventsIndex !== undefined) {
|
|
387
|
+
const displayStep = window.werewolfGamePlayer.allEventsIndexToDisplayStep[event.allEventsIndex];
|
|
388
|
+
console.debug(`DEBUG: [playNextInQueue] Found displayStep: ${displayStep} for event index ${event.allEventsIndex}`);
|
|
389
|
+
|
|
390
|
+
if (displayStep !== undefined && window.wwOriginals && window.wwOriginals.setStep) {
|
|
391
|
+
console.debug(`DEBUG: [playNextInQueue] ### ADVANCING SLIDER TO ${displayStep} ###`);
|
|
392
|
+
window.wwOriginals.setStep(displayStep);
|
|
393
|
+
|
|
394
|
+
// Use a short timeout to allow the DOM to update after the step change
|
|
395
|
+
setTimeout(() => {
|
|
396
|
+
const freshParent = document.getElementById(parentId);
|
|
397
|
+
if (!freshParent) {
|
|
398
|
+
console.error(`DEBUG: Parent element not found after timeout for event index ${event.allEventsIndex}.`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const liToHighlight = freshParent.querySelector(`#chat-log li[data-all-events-index="${event.allEventsIndex}"]`);
|
|
402
|
+
console.debug(`DEBUG: [Timeout] Attempting to highlight element for index ${event.allEventsIndex}`, liToHighlight);
|
|
403
|
+
if (liToHighlight) {
|
|
404
|
+
liToHighlight.classList.add('now-playing');
|
|
405
|
+
console.debug(`DEBUG: [Timeout] Successfully added .now-playing to element for index ${event.allEventsIndex}`);
|
|
406
|
+
} else {
|
|
407
|
+
console.error(`DEBUG: [Timeout] FAILED to find element to highlight for index ${event.allEventsIndex}`);
|
|
408
|
+
}
|
|
409
|
+
}, 50); // A small delay to ensure the re-render completes
|
|
410
|
+
|
|
411
|
+
} else {
|
|
412
|
+
console.error(`DEBUG: [playNextInQueue] CRITICAL: FAILED to advance slider. displayStep: ${displayStep}, wwOriginals: ${!!window.wwOriginals}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let audioPath = null;
|
|
417
|
+
let audioKey = null;
|
|
418
|
+
if (event.audioEvent) {
|
|
419
|
+
audioKey = event.audioEvent.speaker === 'moderator' ? `moderator:${event.audioEvent.message}` : `${event.audioEvent.speaker}:${event.audioEvent.message}`;
|
|
420
|
+
audioPath = audioMap[audioKey];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (audioPath) {
|
|
424
|
+
console.debug(`DEBUG: [playNextInQueue] Popped event for index: ${event.allEventsIndex}. Audio key: "${audioKey}"`);
|
|
425
|
+
console.debug(`DEBUG: [playNextInQueue] Playing audio: ${audioPath}`);
|
|
426
|
+
audioState.audioPlayer.src = audioPath;
|
|
427
|
+
audioState.audioPlayer.playbackRate = audioState.playbackRate;
|
|
428
|
+
audioState.audioPlayer.onended = () => {
|
|
429
|
+
console.debug(`DEBUG: [onended] Audio for index ${event.allEventsIndex} finished.`);
|
|
430
|
+
audioState.isAudioPlaying = false;
|
|
431
|
+
if (!audioState.isPaused && isContinuous) {
|
|
432
|
+
console.debug("DEBUG: [onended] Calling playNextInQueue recursively.");
|
|
433
|
+
playNextInQueue(isContinuous);
|
|
434
|
+
} else {
|
|
435
|
+
console.debug("DEBUG: [onended] Loop stopped. isPaused or !isContinuous.");
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
audioState.audioPlayer.onerror = () => {
|
|
439
|
+
console.error(`DEBUG: [onerror] Audio failed to play for key: "${audioKey}"`);
|
|
440
|
+
audioState.isAudioPlaying = false;
|
|
441
|
+
playNextInQueue(isContinuous);
|
|
442
|
+
};
|
|
443
|
+
audioState.audioPlayer.play().catch(e => {
|
|
444
|
+
console.error(`DEBUG: [play.catch] Audio failed to play:`, e);
|
|
445
|
+
audioState.isAudioPlaying = false;
|
|
446
|
+
playNextInQueue(isContinuous);
|
|
447
|
+
});
|
|
448
|
+
} else {
|
|
449
|
+
console.warn(`DEBUG: [playNextInQueue] No audio for event index: ${event.allEventsIndex}. Using setTimeout.`);
|
|
450
|
+
setTimeout(() => {
|
|
451
|
+
audioState.isAudioPlaying = false;
|
|
452
|
+
if (!audioState.isPaused && isContinuous) {
|
|
453
|
+
playNextInQueue(isContinuous);
|
|
454
|
+
}
|
|
455
|
+
}, context.speed);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function stopAndClearAudio() {
|
|
460
|
+
if (audioState.isAudioPlaying) {
|
|
461
|
+
audioState.audioPlayer.pause();
|
|
462
|
+
audioState.isAudioPlaying = false;
|
|
463
|
+
}
|
|
464
|
+
audioState.audioQueue = [];
|
|
465
|
+
audioState.currentlyPlayingIndex = -1;
|
|
466
|
+
|
|
467
|
+
const currentParent = document.getElementById(parentId);
|
|
468
|
+
if (currentParent) {
|
|
469
|
+
// Clear any "now-playing" highlights
|
|
470
|
+
const nowPlayingElement = currentParent.querySelector('#chat-log .now-playing');
|
|
471
|
+
if (nowPlayingElement) {
|
|
472
|
+
nowPlayingElement.classList.remove('now-playing');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function initThreeJs() {
|
|
478
|
+
if (threeState.initialized) {
|
|
479
|
+
if (threeState.demo && threeState.demo._parent && !parent.contains(threeState.demo._parent)) {
|
|
480
|
+
parent.appendChild(threeState.demo._threejs.domElement);
|
|
481
|
+
parent.appendChild(threeState.demo._labelRenderer.domElement);
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const loadAndSetup = async () => {
|
|
487
|
+
try {
|
|
488
|
+
const THREE = await import('https://cdn.jsdelivr.net/npm/three@0.118/build/three.module.js');
|
|
489
|
+
const { OrbitControls } = await import('https://cdn.jsdelivr.net/npm/three@0.118/examples/jsm/controls/OrbitControls.js');
|
|
490
|
+
const { FBXLoader } = await import('https://cdn.jsdelivr.net/npm/three@0.118.1/examples/jsm/loaders/FBXLoader.js');
|
|
491
|
+
const { SkeletonUtils } = await import('https://cdn.jsdelivr.net/npm/three@0.118.1/examples/jsm/utils/SkeletonUtils.js');
|
|
492
|
+
const { CSS2DRenderer, CSS2DObject } = await import('https://cdn.jsdelivr.net/npm/three@0.118.1/examples/jsm/renderers/CSS2DRenderer.js');
|
|
493
|
+
const { EffectComposer } = await import('https://cdn.jsdelivr.net/npm/three@0.118.1/examples/jsm/postprocessing/EffectComposer.js');
|
|
494
|
+
const { RenderPass } = await import('https://cdn.jsdelivr.net/npm/three@0.118.1/examples/jsm/postprocessing/RenderPass.js');
|
|
495
|
+
const { UnrealBloomPass } = await import('https://cdn.jsdelivr.net/npm/three@0.118.1/examples/jsm/postprocessing/UnrealBloomPass.js');
|
|
496
|
+
const { ShaderPass } = await import('https://cdn.jsdelivr.net/npm/three@0.118.1/examples/jsm/postprocessing/ShaderPass.js');
|
|
497
|
+
const { FilmPass } = await import('https://cdn.jsdelivr.net/npm/three@0.118.1/examples/jsm/postprocessing/FilmPass.js');
|
|
498
|
+
|
|
499
|
+
class BasicWorldDemo {
|
|
500
|
+
constructor(options) {
|
|
501
|
+
this._Initialize(options, THREE, OrbitControls, FBXLoader, SkeletonUtils, CSS2DRenderer, CSS2DObject, EffectComposer, RenderPass, UnrealBloomPass, ShaderPass, FilmPass);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
_Initialize(options, THREE, OrbitControls, FBXLoader, SkeletonUtils, CSS2DRenderer, CSS2DObject, EffectComposer, RenderPass, UnrealBloomPass, ShaderPass, FilmPass) {
|
|
505
|
+
this._parent = options.parent;
|
|
506
|
+
this._width = options.width;
|
|
507
|
+
this._height = options.height;
|
|
508
|
+
|
|
509
|
+
// WebGL Renderer with enhanced settings
|
|
510
|
+
this._threejs = new THREE.WebGLRenderer({
|
|
511
|
+
antialias: true,
|
|
512
|
+
alpha: true,
|
|
513
|
+
powerPreference: "high-performance"
|
|
514
|
+
});
|
|
515
|
+
this._threejs.shadowMap.enabled = true;
|
|
516
|
+
this._threejs.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
517
|
+
this._threejs.shadowMap.autoUpdate = true;
|
|
518
|
+
this._threejs.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
519
|
+
this._threejs.setSize(this._width, this._height);
|
|
520
|
+
this._threejs.outputEncoding = THREE.sRGBEncoding;
|
|
521
|
+
this._threejs.toneMapping = THREE.ACESFilmicToneMapping;
|
|
522
|
+
this._threejs.toneMappingExposure = 1.2;
|
|
523
|
+
this._threejs.domElement.style.position = 'absolute';
|
|
524
|
+
this._threejs.domElement.style.top = '0';
|
|
525
|
+
this._threejs.domElement.style.left = '0';
|
|
526
|
+
this._threejs.domElement.style.zIndex = '0';
|
|
527
|
+
this._parent.appendChild(this._threejs.domElement);
|
|
528
|
+
|
|
529
|
+
// CSS2D Renderer
|
|
530
|
+
this._labelRenderer = new CSS2DRenderer();
|
|
531
|
+
this._labelRenderer.setSize(this._width, this._height);
|
|
532
|
+
this._labelRenderer.domElement.style.position = 'absolute';
|
|
533
|
+
this._labelRenderer.domElement.style.top = '0px';
|
|
534
|
+
this._labelRenderer.domElement.style.left = '0px';
|
|
535
|
+
this._labelRenderer.domElement.style.zIndex = '1'; // On top of 3D, behind UI
|
|
536
|
+
this._labelRenderer.domElement.style.pointerEvents = 'none';
|
|
537
|
+
this._parent.appendChild(this._labelRenderer.domElement);
|
|
538
|
+
|
|
539
|
+
const fov = 60;
|
|
540
|
+
const aspect = this._width / this._height;
|
|
541
|
+
const near = 1.0;
|
|
542
|
+
const far = 100000.0;
|
|
543
|
+
this._camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
|
|
544
|
+
this._camera.position.set(0, 0, 50);
|
|
545
|
+
|
|
546
|
+
this._scene = new THREE.Scene();
|
|
547
|
+
this._scene.fog = new THREE.FogExp2(0x2a2a4a, 0.01); // Start with day fog color
|
|
548
|
+
|
|
549
|
+
this._createSkybox(THREE);
|
|
550
|
+
this._createAdvancedLighting(THREE);
|
|
551
|
+
this._setupPostProcessing(THREE, EffectComposer, RenderPass, UnrealBloomPass, ShaderPass, FilmPass);
|
|
552
|
+
|
|
553
|
+
this._controls = new OrbitControls(this._camera, this._threejs.domElement);
|
|
554
|
+
this._controls.target.set(0, 0, 0);
|
|
555
|
+
this._controls.enableKeys = false;
|
|
556
|
+
this._controls.update();
|
|
557
|
+
|
|
558
|
+
this._votingArcsGroup = new THREE.Group();
|
|
559
|
+
this._votingArcsGroup.name = 'votingArcs';
|
|
560
|
+
this._scene.add(this._votingArcsGroup);
|
|
561
|
+
|
|
562
|
+
this._targetRingsGroup = new THREE.Group();
|
|
563
|
+
this._targetRingsGroup.name = 'targetRings';
|
|
564
|
+
this._scene.add(this._targetRingsGroup);
|
|
565
|
+
|
|
566
|
+
this._activeVoteArcs = new Map();
|
|
567
|
+
this._activeTargetRings = new Map();
|
|
568
|
+
|
|
569
|
+
this._speakingAnimations = [];
|
|
570
|
+
|
|
571
|
+
this._LoadModels(THREE, FBXLoader, SkeletonUtils, CSS2DObject);
|
|
572
|
+
this._RAF();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
_createSkybox(THREE) {
|
|
576
|
+
// Store THREE reference first
|
|
577
|
+
this._THREE = THREE;
|
|
578
|
+
|
|
579
|
+
const skyboxSize = 1000;
|
|
580
|
+
const skyboxGeo = new THREE.BoxGeometry(skyboxSize, skyboxSize, skyboxSize);
|
|
581
|
+
|
|
582
|
+
// Create materials for each face with initial day colors
|
|
583
|
+
this._skyboxMaterials = [];
|
|
584
|
+
for (let i = 0; i < 6; i++) {
|
|
585
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
586
|
+
color: new THREE.Color(0x87ceeb), // Start with day color
|
|
587
|
+
side: THREE.BackSide
|
|
588
|
+
});
|
|
589
|
+
this._skyboxMaterials.push(mat);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const skybox = new THREE.Mesh(skyboxGeo, this._skyboxMaterials);
|
|
593
|
+
this._skybox = skybox;
|
|
594
|
+
this._scene.add(skybox);
|
|
595
|
+
|
|
596
|
+
// Create dynamic sky canvas for the back panel (where moon/sun appears)
|
|
597
|
+
const backCanvas = document.createElement('canvas');
|
|
598
|
+
const canvasSize = 2048;
|
|
599
|
+
backCanvas.width = canvasSize;
|
|
600
|
+
backCanvas.height = canvasSize;
|
|
601
|
+
this._skyCanvas = backCanvas;
|
|
602
|
+
this._skyContext = backCanvas.getContext('2d');
|
|
603
|
+
|
|
604
|
+
// Store celestial body properties
|
|
605
|
+
this._celestialBody = {
|
|
606
|
+
x: canvasSize / 2,
|
|
607
|
+
y: canvasSize / 3,
|
|
608
|
+
size: 250,
|
|
609
|
+
phase: 0.0 // Start with day (0 = day, 1 = night)
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// Create texture from canvas
|
|
613
|
+
this._skyTexture = new THREE.CanvasTexture(backCanvas);
|
|
614
|
+
this._skyboxMaterials[4].map = this._skyTexture;
|
|
615
|
+
|
|
616
|
+
// Load moon image
|
|
617
|
+
const moonImage = new Image();
|
|
618
|
+
moonImage.crossOrigin = 'Anonymous';
|
|
619
|
+
moonImage.onload = () => {
|
|
620
|
+
this._moonImage = moonImage;
|
|
621
|
+
this._updateSkybox(0.0); // Start with day
|
|
622
|
+
};
|
|
623
|
+
moonImage.onerror = () => {
|
|
624
|
+
console.error("Failed to load moon texture for skybox.");
|
|
625
|
+
this._updateSkybox(0.0); // Start with day
|
|
626
|
+
};
|
|
627
|
+
moonImage.src = 'assets/moon4.png';
|
|
628
|
+
|
|
629
|
+
// Create stars for night sky
|
|
630
|
+
this._createStars(THREE);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
_createStars(THREE) {
|
|
634
|
+
const starsGeometry = new THREE.BufferGeometry();
|
|
635
|
+
const starCount = 2000;
|
|
636
|
+
const positions = new Float32Array(starCount * 3);
|
|
637
|
+
const colors = new Float32Array(starCount * 3);
|
|
638
|
+
const sizes = new Float32Array(starCount);
|
|
639
|
+
|
|
640
|
+
for (let i = 0; i < starCount; i++) {
|
|
641
|
+
const i3 = i * 3;
|
|
642
|
+
|
|
643
|
+
// Random position on a large sphere
|
|
644
|
+
const theta = Math.random() * Math.PI * 2;
|
|
645
|
+
const phi = Math.acos(2 * Math.random() - 1);
|
|
646
|
+
const radius = 400 + Math.random() * 100;
|
|
647
|
+
|
|
648
|
+
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
|
|
649
|
+
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
|
|
650
|
+
positions[i3 + 2] = radius * Math.cos(phi);
|
|
651
|
+
|
|
652
|
+
// Star colors (white to slightly blue/yellow)
|
|
653
|
+
const starColor = new THREE.Color();
|
|
654
|
+
const colorChoice = Math.random();
|
|
655
|
+
if (colorChoice < 0.3) {
|
|
656
|
+
starColor.setHSL(0.6, 0.1, 0.9); // Bluish
|
|
657
|
+
} else if (colorChoice < 0.6) {
|
|
658
|
+
starColor.setHSL(0.1, 0.1, 0.95); // Yellowish
|
|
659
|
+
} else {
|
|
660
|
+
starColor.setHSL(0, 0, 1); // Pure white
|
|
661
|
+
}
|
|
662
|
+
colors[i3] = starColor.r;
|
|
663
|
+
colors[i3 + 1] = starColor.g;
|
|
664
|
+
colors[i3 + 2] = starColor.b;
|
|
665
|
+
|
|
666
|
+
sizes[i] = Math.random() * 2 + 0.5;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
starsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
670
|
+
starsGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
671
|
+
starsGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
|
672
|
+
|
|
673
|
+
const starsMaterial = new THREE.ShaderMaterial({
|
|
674
|
+
uniforms: {
|
|
675
|
+
phase: { value: 0.0 } // Start with day (0 = day, 1 = night)
|
|
676
|
+
},
|
|
677
|
+
vertexShader: `
|
|
678
|
+
attribute float size;
|
|
679
|
+
attribute vec3 color;
|
|
680
|
+
varying vec3 vColor;
|
|
681
|
+
uniform float phase;
|
|
682
|
+
|
|
683
|
+
void main() {
|
|
684
|
+
vColor = color;
|
|
685
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
686
|
+
gl_PointSize = size * (300.0 / -mvPosition.z) * phase;
|
|
687
|
+
gl_Position = projectionMatrix * mvPosition;
|
|
688
|
+
}
|
|
689
|
+
`,
|
|
690
|
+
fragmentShader: `
|
|
691
|
+
varying vec3 vColor;
|
|
692
|
+
uniform float phase;
|
|
693
|
+
|
|
694
|
+
void main() {
|
|
695
|
+
float dist = distance(gl_PointCoord, vec2(0.5));
|
|
696
|
+
if (dist > 0.5) discard;
|
|
697
|
+
|
|
698
|
+
float alpha = (1.0 - dist * 2.0) * phase * 0.8;
|
|
699
|
+
gl_FragColor = vec4(vColor, alpha);
|
|
700
|
+
}
|
|
701
|
+
`,
|
|
702
|
+
transparent: true,
|
|
703
|
+
blending: THREE.AdditiveBlending,
|
|
704
|
+
depthWrite: false
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
this._stars = new THREE.Points(starsGeometry, starsMaterial);
|
|
708
|
+
this._starsMaterial = starsMaterial;
|
|
709
|
+
this._scene.add(this._stars);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
_updateSkybox(phase) {
|
|
713
|
+
if (!this._skyContext || !this._skyCanvas) {
|
|
714
|
+
console.debug('Skybox context not ready');
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const ctx = this._skyContext;
|
|
719
|
+
const canvas = this._skyCanvas;
|
|
720
|
+
const celestial = this._celestialBody;
|
|
721
|
+
|
|
722
|
+
// Clear canvas with a transparent background
|
|
723
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
724
|
+
|
|
725
|
+
// Create gradient overlay
|
|
726
|
+
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
|
727
|
+
|
|
728
|
+
if (phase > 0.5) {
|
|
729
|
+
// Night sky
|
|
730
|
+
const nightIntensity = (phase - 0.5) * 2;
|
|
731
|
+
gradient.addColorStop(0, `rgba(10, 10, 40, ${nightIntensity})`);
|
|
732
|
+
gradient.addColorStop(0.3, `rgba(20, 20, 60, ${nightIntensity})`);
|
|
733
|
+
gradient.addColorStop(1, `rgba(5, 5, 20, ${nightIntensity})`);
|
|
734
|
+
|
|
735
|
+
// Fill with gradient
|
|
736
|
+
ctx.fillStyle = gradient;
|
|
737
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
738
|
+
|
|
739
|
+
// Draw stars manually
|
|
740
|
+
ctx.fillStyle = 'white';
|
|
741
|
+
for (let i = 0; i < 200; i++) {
|
|
742
|
+
const x = Math.random() * canvas.width;
|
|
743
|
+
const y = Math.random() * canvas.height;
|
|
744
|
+
const size = Math.random() * 2;
|
|
745
|
+
ctx.globalAlpha = nightIntensity * (0.3 + Math.random() * 0.7);
|
|
746
|
+
ctx.beginPath();
|
|
747
|
+
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
748
|
+
ctx.fill();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Draw moon
|
|
752
|
+
ctx.globalAlpha = nightIntensity;
|
|
753
|
+
if (this._moonImage) {
|
|
754
|
+
const moonSize = celestial.size;
|
|
755
|
+
const moonX = celestial.x;
|
|
756
|
+
const moonY = celestial.y;
|
|
757
|
+
ctx.drawImage(this._moonImage,
|
|
758
|
+
moonX - moonSize/2,
|
|
759
|
+
moonY - moonSize/2,
|
|
760
|
+
moonSize,
|
|
761
|
+
moonSize
|
|
762
|
+
);
|
|
763
|
+
} else {
|
|
764
|
+
// Fallback: draw procedural moon
|
|
765
|
+
ctx.fillStyle = '#f0f0e0';
|
|
766
|
+
ctx.shadowBlur = 50;
|
|
767
|
+
ctx.shadowColor = '#f0f0e0';
|
|
768
|
+
ctx.beginPath();
|
|
769
|
+
ctx.arc(celestial.x, celestial.y, celestial.size/2, 0, Math.PI * 2);
|
|
770
|
+
ctx.fill();
|
|
771
|
+
ctx.shadowBlur = 0;
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
// Day sky
|
|
775
|
+
const dayIntensity = 1 - phase * 2;
|
|
776
|
+
gradient.addColorStop(0, `rgba(135, 206, 250, ${dayIntensity})`);
|
|
777
|
+
gradient.addColorStop(0.5, `rgba(135, 206, 235, ${dayIntensity})`);
|
|
778
|
+
gradient.addColorStop(1, `rgba(255, 255, 200, ${dayIntensity * 0.5})`);
|
|
779
|
+
|
|
780
|
+
// Fill with gradient
|
|
781
|
+
ctx.fillStyle = gradient;
|
|
782
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
783
|
+
|
|
784
|
+
// Draw sun
|
|
785
|
+
ctx.globalAlpha = dayIntensity;
|
|
786
|
+
|
|
787
|
+
// Sun glow
|
|
788
|
+
const glowGradient = ctx.createRadialGradient(
|
|
789
|
+
celestial.x, celestial.y, 0,
|
|
790
|
+
celestial.x, celestial.y, celestial.size * 1.5
|
|
791
|
+
);
|
|
792
|
+
glowGradient.addColorStop(0, 'rgba(255, 255, 200, 0.8)');
|
|
793
|
+
glowGradient.addColorStop(0.3, 'rgba(255, 220, 100, 0.4)');
|
|
794
|
+
glowGradient.addColorStop(1, 'rgba(255, 200, 50, 0)');
|
|
795
|
+
|
|
796
|
+
ctx.fillStyle = glowGradient;
|
|
797
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
798
|
+
|
|
799
|
+
// Sun core
|
|
800
|
+
ctx.fillStyle = '#ffff99';
|
|
801
|
+
ctx.shadowBlur = 80;
|
|
802
|
+
ctx.shadowColor = '#ffcc00';
|
|
803
|
+
ctx.beginPath();
|
|
804
|
+
ctx.arc(celestial.x, celestial.y, celestial.size/2, 0, Math.PI * 2);
|
|
805
|
+
ctx.fill();
|
|
806
|
+
ctx.shadowBlur = 0;
|
|
807
|
+
|
|
808
|
+
// Sun rays
|
|
809
|
+
ctx.strokeStyle = `rgba(255, 220, 100, ${dayIntensity * 0.5})`;
|
|
810
|
+
ctx.lineWidth = 3;
|
|
811
|
+
for (let i = 0; i < 12; i++) {
|
|
812
|
+
const angle = (i / 12) * Math.PI * 2;
|
|
813
|
+
const innerRadius = celestial.size * 0.6;
|
|
814
|
+
const outerRadius = celestial.size * 1.2;
|
|
815
|
+
ctx.beginPath();
|
|
816
|
+
ctx.moveTo(
|
|
817
|
+
celestial.x + Math.cos(angle) * innerRadius,
|
|
818
|
+
celestial.y + Math.sin(angle) * innerRadius
|
|
819
|
+
);
|
|
820
|
+
ctx.lineTo(
|
|
821
|
+
celestial.x + Math.cos(angle) * outerRadius,
|
|
822
|
+
celestial.y + Math.sin(angle) * outerRadius
|
|
823
|
+
);
|
|
824
|
+
ctx.stroke();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Update texture
|
|
829
|
+
if (this._skyTexture) {
|
|
830
|
+
this._skyTexture.needsUpdate = true;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Update skybox colors for all faces with more dramatic changes
|
|
834
|
+
if (this._skyboxMaterials && this._THREE) {
|
|
835
|
+
const THREE = this._THREE;
|
|
836
|
+
const nightColor = new THREE.Color(0x000011); // Very dark blue
|
|
837
|
+
const dayColor = new THREE.Color(0x87ceeb); // Sky blue
|
|
838
|
+
const currentColor = new THREE.Color();
|
|
839
|
+
currentColor.copy(dayColor).lerp(nightColor, phase);
|
|
840
|
+
|
|
841
|
+
this._skyboxMaterials.forEach((mat, index) => {
|
|
842
|
+
if (index !== 4) { // Skip the back panel with moon/sun
|
|
843
|
+
mat.color.copy(currentColor);
|
|
844
|
+
mat.needsUpdate = true;
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// Force update the canvas texture
|
|
849
|
+
if (this._skyTexture) {
|
|
850
|
+
this._skyTexture.needsUpdate = true;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Store current phase
|
|
855
|
+
if (celestial) {
|
|
856
|
+
celestial.phase = phase;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
_createAdvancedLighting(THREE) {
|
|
861
|
+
// Enhanced ambient lighting with color variation
|
|
862
|
+
const ambientLight = new THREE.AmbientLight(0x404080, 0.4);
|
|
863
|
+
ambientLight.name = 'ambientLight';
|
|
864
|
+
this._scene.add(ambientLight);
|
|
865
|
+
|
|
866
|
+
// Main directional light (moon/sun)
|
|
867
|
+
const mainLight = new THREE.DirectionalLight(0xffffff, 1.8);
|
|
868
|
+
mainLight.position.set(30, 50, 20);
|
|
869
|
+
mainLight.castShadow = true;
|
|
870
|
+
mainLight.shadow.mapSize.width = 2048;
|
|
871
|
+
mainLight.shadow.mapSize.height = 2048;
|
|
872
|
+
mainLight.shadow.camera.near = 0.5;
|
|
873
|
+
mainLight.shadow.camera.far = 100;
|
|
874
|
+
mainLight.shadow.camera.left = -50;
|
|
875
|
+
mainLight.shadow.camera.right = 50;
|
|
876
|
+
mainLight.shadow.camera.top = 50;
|
|
877
|
+
mainLight.shadow.camera.bottom = -50;
|
|
878
|
+
mainLight.shadow.bias = -0.001;
|
|
879
|
+
mainLight.shadow.normalBias = 0.02;
|
|
880
|
+
this._scene.add(mainLight);
|
|
881
|
+
|
|
882
|
+
// Rim light for dramatic effect
|
|
883
|
+
const rimLight = new THREE.DirectionalLight(0x8080ff, 0.8);
|
|
884
|
+
rimLight.position.set(-20, 10, -30);
|
|
885
|
+
this._scene.add(rimLight);
|
|
886
|
+
|
|
887
|
+
// Atmospheric hemisphere light
|
|
888
|
+
const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x1e1e3f, 0.6);
|
|
889
|
+
this._scene.add(hemiLight);
|
|
890
|
+
|
|
891
|
+
// Ground fill light
|
|
892
|
+
const fillLight = new THREE.DirectionalLight(0x404080, 0.3);
|
|
893
|
+
fillLight.position.set(0, -1, 0);
|
|
894
|
+
this._scene.add(fillLight);
|
|
895
|
+
|
|
896
|
+
// Store references for phase updates
|
|
897
|
+
this._mainLight = mainLight;
|
|
898
|
+
this._rimLight = rimLight;
|
|
899
|
+
this._hemiLight = hemiLight;
|
|
900
|
+
this._fillLight = fillLight;
|
|
901
|
+
|
|
902
|
+
// Create a spotlight for night actions
|
|
903
|
+
const spotLight = new THREE.SpotLight(0xffffff, 5.0, 50, Math.PI / 4, 0.5, 2);
|
|
904
|
+
spotLight.position.set(0, 25, 0);
|
|
905
|
+
spotLight.castShadow = true;
|
|
906
|
+
spotLight.visible = false;
|
|
907
|
+
this._scene.add(spotLight);
|
|
908
|
+
this._spotLight = spotLight;
|
|
909
|
+
this._scene.add(spotLight.target);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
_createMysticalCircles(THREE, radius) {
|
|
913
|
+
// Create multiple concentric circles with mystical patterns
|
|
914
|
+
for (let i = 0; i < 3; i++) {
|
|
915
|
+
const circleRadius = radius - (i * 2) - 1;
|
|
916
|
+
const circleGeometry = new THREE.RingGeometry(circleRadius - 0.1, circleRadius + 0.1, 64);
|
|
917
|
+
const circleMaterial = new THREE.MeshStandardMaterial({
|
|
918
|
+
color: new THREE.Color().setHSL(0.6 + i * 0.1, 0.8, 0.3 + i * 0.1),
|
|
919
|
+
emissive: new THREE.Color().setHSL(0.6 + i * 0.1, 0.5, 0.1),
|
|
920
|
+
emissiveIntensity: 0.2,
|
|
921
|
+
transparent: true,
|
|
922
|
+
opacity: 0.6 - i * 0.1,
|
|
923
|
+
side: THREE.DoubleSide
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
const circle = new THREE.Mesh(circleGeometry, circleMaterial);
|
|
927
|
+
circle.rotation.x = -Math.PI / 2;
|
|
928
|
+
circle.position.y = 0.01 + i * 0.001;
|
|
929
|
+
this._scene.add(circle);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Add runic symbols around the outer circle
|
|
933
|
+
// this._createRunicSymbols(THREE, radius);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
_createRunicSymbols(THREE, radius) {
|
|
937
|
+
const symbolCount = 8;
|
|
938
|
+
const symbolGeometry = new THREE.PlaneGeometry(1, 1);
|
|
939
|
+
|
|
940
|
+
for (let i = 0; i < symbolCount; i++) {
|
|
941
|
+
const angle = (i / symbolCount) * Math.PI * 2;
|
|
942
|
+
const x = (radius + 3) * Math.sin(angle);
|
|
943
|
+
const z = (radius + 3) * Math.cos(angle);
|
|
944
|
+
|
|
945
|
+
// Create a simple runic-like pattern using canvas
|
|
946
|
+
const canvas = document.createElement('canvas');
|
|
947
|
+
canvas.width = 64;
|
|
948
|
+
canvas.height = 64;
|
|
949
|
+
const ctx = canvas.getContext('2d');
|
|
950
|
+
|
|
951
|
+
ctx.fillStyle = 'rgba(100, 150, 255, 0.8)';
|
|
952
|
+
ctx.font = '40px serif';
|
|
953
|
+
ctx.textAlign = 'center';
|
|
954
|
+
ctx.fillText(['ᚠ', 'ᚡ', 'ᚢ', 'ᚣ', 'ᚤ', 'ᚥ', 'ᚦ', 'ᚧ'][i], 32, 45);
|
|
955
|
+
|
|
956
|
+
const symbolMaterial = new THREE.MeshBasicMaterial({
|
|
957
|
+
map: new THREE.CanvasTexture(canvas),
|
|
958
|
+
transparent: true,
|
|
959
|
+
alphaTest: 0.1
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
const symbol = new THREE.Mesh(symbolGeometry, symbolMaterial);
|
|
963
|
+
symbol.position.set(x, 0.15, z);
|
|
964
|
+
symbol.rotation.x = -Math.PI / 2;
|
|
965
|
+
this._scene.add(symbol);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
_createParticleSystem(THREE) {
|
|
970
|
+
// Create floating mystical particles
|
|
971
|
+
const particleCount = 150;
|
|
972
|
+
const particles = new THREE.BufferGeometry();
|
|
973
|
+
const positions = new Float32Array(particleCount * 3);
|
|
974
|
+
const colors = new Float32Array(particleCount * 3);
|
|
975
|
+
const sizes = new Float32Array(particleCount);
|
|
976
|
+
|
|
977
|
+
for (let i = 0; i < particleCount; i++) {
|
|
978
|
+
const i3 = i * 3;
|
|
979
|
+
|
|
980
|
+
// Random position within a larger area
|
|
981
|
+
positions[i3] = (Math.random() - 0.5) * 80;
|
|
982
|
+
positions[i3 + 1] = Math.random() * 30 + 5;
|
|
983
|
+
positions[i3 + 2] = (Math.random() - 0.5) * 80;
|
|
984
|
+
|
|
985
|
+
// Mystical colors (blues, purples, greens)
|
|
986
|
+
const hue = Math.random() * 0.3 + 0.5; // 0.5-0.8 range
|
|
987
|
+
const color = new THREE.Color().setHSL(hue, 0.8, 0.6);
|
|
988
|
+
colors[i3] = color.r;
|
|
989
|
+
colors[i3 + 1] = color.g;
|
|
990
|
+
colors[i3 + 2] = color.b;
|
|
991
|
+
|
|
992
|
+
sizes[i] = Math.random() * 2 + 0.5;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
996
|
+
particles.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
997
|
+
particles.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
|
998
|
+
|
|
999
|
+
const particleMaterial = new THREE.ShaderMaterial({
|
|
1000
|
+
uniforms: {
|
|
1001
|
+
time: { value: 0 }
|
|
1002
|
+
},
|
|
1003
|
+
vertexShader: `
|
|
1004
|
+
attribute float size;
|
|
1005
|
+
attribute vec3 color;
|
|
1006
|
+
varying vec3 vColor;
|
|
1007
|
+
uniform float time;
|
|
1008
|
+
|
|
1009
|
+
void main() {
|
|
1010
|
+
vColor = color;
|
|
1011
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1012
|
+
gl_PointSize = size * (300.0 / -mvPosition.z) * (1.0 + sin(time * 2.0 + position.x * 0.1) * 0.3);
|
|
1013
|
+
gl_Position = projectionMatrix * mvPosition;
|
|
1014
|
+
}
|
|
1015
|
+
`,
|
|
1016
|
+
fragmentShader: `
|
|
1017
|
+
varying vec3 vColor;
|
|
1018
|
+
|
|
1019
|
+
void main() {
|
|
1020
|
+
float dist = distance(gl_PointCoord, vec2(0.5));
|
|
1021
|
+
if (dist > 0.5) discard;
|
|
1022
|
+
|
|
1023
|
+
float alpha = 1.0 - (dist * 2.0);
|
|
1024
|
+
alpha *= alpha; // Softer edges
|
|
1025
|
+
|
|
1026
|
+
gl_FragColor = vec4(vColor, alpha * 0.6);
|
|
1027
|
+
}
|
|
1028
|
+
`,
|
|
1029
|
+
transparent: true,
|
|
1030
|
+
blending: THREE.AdditiveBlending,
|
|
1031
|
+
depthWrite: false
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
this._particles = new THREE.Points(particles, particleMaterial);
|
|
1035
|
+
this._scene.add(this._particles);
|
|
1036
|
+
this._particleMaterial = particleMaterial;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
_setupPostProcessing(THREE, EffectComposer, RenderPass, UnrealBloomPass, ShaderPass, FilmPass) {
|
|
1040
|
+
// Create effect composer
|
|
1041
|
+
this._composer = new EffectComposer(this._threejs);
|
|
1042
|
+
|
|
1043
|
+
// Render pass
|
|
1044
|
+
const renderPass = new RenderPass(this._scene, this._camera);
|
|
1045
|
+
this._composer.addPass(renderPass);
|
|
1046
|
+
|
|
1047
|
+
// Bloom pass for glowing effects - balanced for day
|
|
1048
|
+
const bloomPass = new UnrealBloomPass(
|
|
1049
|
+
new THREE.Vector2(this._width, this._height),
|
|
1050
|
+
0.15, // strength - moderate for day
|
|
1051
|
+
0.333, // radius - good coverage for day
|
|
1052
|
+
0.4 // threshold - balanced to allow some bloom
|
|
1053
|
+
);
|
|
1054
|
+
this._composer.addPass(bloomPass);
|
|
1055
|
+
|
|
1056
|
+
// Film grain for atmosphere
|
|
1057
|
+
const filmPass = new FilmPass(
|
|
1058
|
+
0.15, // noise intensity
|
|
1059
|
+
0.1, // scanline intensity
|
|
1060
|
+
0, // scanline count
|
|
1061
|
+
false // grayscale
|
|
1062
|
+
);
|
|
1063
|
+
this._composer.addPass(filmPass);
|
|
1064
|
+
|
|
1065
|
+
// Custom atmospheric shader
|
|
1066
|
+
const atmosphereShader = {
|
|
1067
|
+
uniforms: {
|
|
1068
|
+
'tDiffuse': { value: null },
|
|
1069
|
+
'time': { value: 0.0 },
|
|
1070
|
+
'phase': { value: 0.0 } // 0 = day, 1 = night
|
|
1071
|
+
},
|
|
1072
|
+
vertexShader: `
|
|
1073
|
+
varying vec2 vUv;
|
|
1074
|
+
void main() {
|
|
1075
|
+
vUv = uv;
|
|
1076
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
1077
|
+
}
|
|
1078
|
+
`,
|
|
1079
|
+
fragmentShader: `
|
|
1080
|
+
uniform sampler2D tDiffuse;
|
|
1081
|
+
uniform float time;
|
|
1082
|
+
uniform float phase;
|
|
1083
|
+
varying vec2 vUv;
|
|
1084
|
+
|
|
1085
|
+
void main() {
|
|
1086
|
+
vec4 color = texture2D(tDiffuse, vUv);
|
|
1087
|
+
|
|
1088
|
+
// Add subtle color grading based on phase
|
|
1089
|
+
if (phase > 0.5) {
|
|
1090
|
+
// Night - add blue tint and increase contrast
|
|
1091
|
+
color.rgb = mix(color.rgb, color.rgb * vec3(0.8, 0.9, 1.2), 0.3);
|
|
1092
|
+
color.rgb = pow(color.rgb, vec3(1.1));
|
|
1093
|
+
} else {
|
|
1094
|
+
// Day - add warm tint
|
|
1095
|
+
color.rgb = mix(color.rgb, color.rgb * vec3(1.1, 1.05, 0.95), 0.2);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Add subtle vignette
|
|
1099
|
+
vec2 center = vec2(0.5, 0.5);
|
|
1100
|
+
float dist = distance(vUv, center);
|
|
1101
|
+
float vignette = 1.0 - smoothstep(0.3, 0.8, dist);
|
|
1102
|
+
color.rgb *= mix(0.7, 1.0, vignette);
|
|
1103
|
+
|
|
1104
|
+
gl_FragColor = color;
|
|
1105
|
+
}
|
|
1106
|
+
`
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
this._atmospherePass = new ShaderPass(atmosphereShader);
|
|
1110
|
+
this._composer.addPass(this._atmospherePass);
|
|
1111
|
+
|
|
1112
|
+
// Store references
|
|
1113
|
+
this._bloomPass = bloomPass;
|
|
1114
|
+
this._filmPass = filmPass;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
_LoadModels(THREE, FBXLoader, SkeletonUtils, CSS2DObject) {
|
|
1118
|
+
this._playerObjects = new Map();
|
|
1119
|
+
this._playerGroup = new THREE.Group();
|
|
1120
|
+
this._playerGroup.name = 'playerGroup';
|
|
1121
|
+
|
|
1122
|
+
// Create enhanced ground circle with better materials
|
|
1123
|
+
const radius = 15;
|
|
1124
|
+
const groundGeometry = new THREE.CircleGeometry(radius + 5, 64);
|
|
1125
|
+
const groundMaterial = new THREE.MeshStandardMaterial({
|
|
1126
|
+
color: 0x1a1a2a,
|
|
1127
|
+
roughness: 0.9,
|
|
1128
|
+
metalness: 0.1,
|
|
1129
|
+
normalScale: new THREE.Vector2(0.5, 0.5),
|
|
1130
|
+
transparent: true,
|
|
1131
|
+
opacity: 0.95
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
// Add a subtle normal map pattern
|
|
1135
|
+
const canvas = document.createElement('canvas');
|
|
1136
|
+
canvas.width = 512;
|
|
1137
|
+
canvas.height = 512;
|
|
1138
|
+
const ctx = canvas.getContext('2d');
|
|
1139
|
+
const imageData = ctx.createImageData(512, 512);
|
|
1140
|
+
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
1141
|
+
const noise = Math.random() * 0.1 + 0.5;
|
|
1142
|
+
imageData.data[i] = Math.floor(noise * 255); // R
|
|
1143
|
+
imageData.data[i + 1] = Math.floor(noise * 255); // G
|
|
1144
|
+
imageData.data[i + 2] = 255; // B
|
|
1145
|
+
imageData.data[i + 3] = 255; // A
|
|
1146
|
+
}
|
|
1147
|
+
ctx.putImageData(imageData, 0, 0);
|
|
1148
|
+
groundMaterial.normalMap = new THREE.CanvasTexture(canvas);
|
|
1149
|
+
|
|
1150
|
+
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
|
1151
|
+
ground.rotation.x = -Math.PI / 2;
|
|
1152
|
+
ground.position.y = -0.1;
|
|
1153
|
+
ground.receiveShadow = true;
|
|
1154
|
+
this._scene.add(ground);
|
|
1155
|
+
|
|
1156
|
+
// Add mystical circle patterns
|
|
1157
|
+
this._createMysticalCircles(THREE, radius);
|
|
1158
|
+
|
|
1159
|
+
this._scene.add(this._playerGroup);
|
|
1160
|
+
|
|
1161
|
+
// Store references for later use
|
|
1162
|
+
this._THREE = THREE;
|
|
1163
|
+
this._CSS2DObject = CSS2DObject;
|
|
1164
|
+
|
|
1165
|
+
// Create particle system for atmosphere
|
|
1166
|
+
this._createParticleSystem(THREE);
|
|
1167
|
+
|
|
1168
|
+
// Frame the empty group initially with better camera positioning
|
|
1169
|
+
this._camera.position.set(25, 30, 35);
|
|
1170
|
+
this._controls.target.set(0, 8, 0);
|
|
1171
|
+
this._controls.enableDamping = true;
|
|
1172
|
+
this._controls.dampingFactor = 0.05;
|
|
1173
|
+
this._controls.minDistance = 20;
|
|
1174
|
+
this._controls.maxDistance = 80;
|
|
1175
|
+
this._controls.maxPolarAngle = Math.PI * 0.75;
|
|
1176
|
+
this._controls.update();
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
focusOnPlayer(playerName, leftPanelWidth = 0, rightPanelWidth = 0) {
|
|
1180
|
+
if (!this._playerGroup || this._playerGroup.children.length === 0 || !this._THREE || !this._playerObjects) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const player = this._playerObjects.get(playerName);
|
|
1184
|
+
if (!player) return;
|
|
1185
|
+
|
|
1186
|
+
// --- 1. Calculate the required camera distance ---
|
|
1187
|
+
|
|
1188
|
+
// First, determine the real viewport size, excluding the UI panels
|
|
1189
|
+
const effectiveWidth = this._width - leftPanelWidth - rightPanelWidth;
|
|
1190
|
+
const effectiveHeight = this._height;
|
|
1191
|
+
|
|
1192
|
+
// Get the bounding box of the entire group of players
|
|
1193
|
+
const viewBox = new this._THREE.Box3().setFromObject(this._playerGroup);
|
|
1194
|
+
const viewSize = viewBox.getSize(new this._THREE.Vector3());
|
|
1195
|
+
const viewCenter = viewBox.getCenter(new this._THREE.Vector3());
|
|
1196
|
+
|
|
1197
|
+
// Calculate the camera's field of view in radians
|
|
1198
|
+
const fov = this._camera.fov * (Math.PI / 180);
|
|
1199
|
+
const aspect = effectiveWidth / effectiveHeight;
|
|
1200
|
+
|
|
1201
|
+
// Derive the horizontal FoV from the vertical FoV and the new aspect ratio
|
|
1202
|
+
const horizontalFov = 2 * Math.atan(Math.tan(fov / 2) * aspect);
|
|
1203
|
+
|
|
1204
|
+
// Calculate the distance needed to fit the content vertically and horizontally
|
|
1205
|
+
const distV = (viewSize.y / 2) / Math.tan(fov / 2);
|
|
1206
|
+
const distH = (viewSize.x / 2) / Math.tan(horizontalFov / 2);
|
|
1207
|
+
|
|
1208
|
+
// The required distance is the larger of the two, plus some padding
|
|
1209
|
+
let distance = Math.max(distV, distH) * 1.05;
|
|
1210
|
+
|
|
1211
|
+
// --- 2. Position the camera using the calculated distance ---
|
|
1212
|
+
|
|
1213
|
+
const playerPosition = player.container.position.clone();
|
|
1214
|
+
const direction = playerPosition.clone().normalize();
|
|
1215
|
+
|
|
1216
|
+
// We preserve the angle you liked by scaling the position based on the new distance.
|
|
1217
|
+
// The camera is positioned on the line extending from the center through the player.
|
|
1218
|
+
const endPos = playerPosition.clone().add(direction.multiplyScalar(distance * 0.6));
|
|
1219
|
+
endPos.y = playerPosition.y + distance * 0.5; // Elevate based on distance
|
|
1220
|
+
|
|
1221
|
+
// The target remains the center of the action
|
|
1222
|
+
const endTarget = viewCenter;
|
|
1223
|
+
|
|
1224
|
+
// --- 3. Animate the transition ---
|
|
1225
|
+
|
|
1226
|
+
this._cameraAnimation = {
|
|
1227
|
+
startTime: performance.now(),
|
|
1228
|
+
duration: 1200,
|
|
1229
|
+
startPos: this._camera.position.clone(),
|
|
1230
|
+
endPos: endPos,
|
|
1231
|
+
startTarget: this._controls.target.clone(),
|
|
1232
|
+
endTarget: endTarget,
|
|
1233
|
+
ease: t => 1 - Math.pow(1 - t, 3)
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
resetCameraView() {
|
|
1238
|
+
if (!this._playerGroup || this._playerGroup.children.length === 0 || !this._THREE) {
|
|
1239
|
+
return; // Can't frame an empty group
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Calculate the bounding box that contains all players
|
|
1243
|
+
const box = new this._THREE.Box3().setFromObject(this._playerGroup);
|
|
1244
|
+
const size = box.getSize(new this._THREE.Vector3());
|
|
1245
|
+
const center = box.getCenter(new this._THREE.Vector3());
|
|
1246
|
+
|
|
1247
|
+
// Determine the maximum dimension of the box
|
|
1248
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
1249
|
+
const fov = this._camera.fov * (Math.PI / 180);
|
|
1250
|
+
|
|
1251
|
+
// Calculate the distance the camera needs to be to fit the box
|
|
1252
|
+
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
|
|
1253
|
+
|
|
1254
|
+
// Add some padding so the players aren't right at the edge of the screen
|
|
1255
|
+
// cameraZ *= 1.4;
|
|
1256
|
+
cameraZ *= 1.1;
|
|
1257
|
+
|
|
1258
|
+
// Set a nice isometric-style camera position
|
|
1259
|
+
const endPos = new this._THREE.Vector3(
|
|
1260
|
+
center.x,
|
|
1261
|
+
center.y + cameraZ / 2, // Elevate the camera
|
|
1262
|
+
center.z + cameraZ // Pull it back
|
|
1263
|
+
);
|
|
1264
|
+
|
|
1265
|
+
// The target is the center of the player group
|
|
1266
|
+
const endTarget = center;
|
|
1267
|
+
|
|
1268
|
+
// Use the same animation system as focusOnPlayer
|
|
1269
|
+
this._cameraAnimation = {
|
|
1270
|
+
startTime: performance.now(),
|
|
1271
|
+
duration: 1200,
|
|
1272
|
+
startPos: this._camera.position.clone(),
|
|
1273
|
+
endPos: endPos,
|
|
1274
|
+
startTarget: this._controls.target.clone(),
|
|
1275
|
+
endTarget: endTarget,
|
|
1276
|
+
ease: t => 1 - Math.pow(1 - t, 3)
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
updatePlayerActive(playerName) {
|
|
1281
|
+
const player = this._playerObjects.get(playerName);
|
|
1282
|
+
if (!player) return;
|
|
1283
|
+
const { orb, orbLight, body, head, shoulders, glow, pedestal, container } = player;
|
|
1284
|
+
|
|
1285
|
+
orb.material.emissiveIntensity = 1.;
|
|
1286
|
+
orbLight.intensity = 1.;
|
|
1287
|
+
glow.material.emissiveIntensity = 0.5;
|
|
1288
|
+
// Slight scale up animation
|
|
1289
|
+
container.scale.setScalar(1.1);
|
|
1290
|
+
pedestal.material.emissiveIntensity = 0.3;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
updatePlayerStatus(playerName, status, threatLevel = 0, is_active = false) {
|
|
1294
|
+
const player = this._playerObjects.get(playerName);
|
|
1295
|
+
if (!player) return;
|
|
1296
|
+
|
|
1297
|
+
const { orb, orbLight, body, head, shoulders, glow, pedestal, container } = player;
|
|
1298
|
+
|
|
1299
|
+
orb.material.color.setHex(0x00ff00);
|
|
1300
|
+
orb.material.emissive.setHex(0x00ff00);
|
|
1301
|
+
orb.material.emissiveIntensity = 0.8;
|
|
1302
|
+
orb.material.opacity = 0.9;
|
|
1303
|
+
orb.visible = true;
|
|
1304
|
+
orbLight.color.setHex(0x00ff00);
|
|
1305
|
+
orbLight.intensity = 0.8;
|
|
1306
|
+
orbLight.visible = true;
|
|
1307
|
+
body.material.color.setHex(0x4466ff);
|
|
1308
|
+
body.material.emissive.setHex(0x111166);
|
|
1309
|
+
body.material.emissiveIntensity = 0.2;
|
|
1310
|
+
shoulders.material.color.setHex(0x4466ff);
|
|
1311
|
+
shoulders.material.emissive.setHex(0x111166);
|
|
1312
|
+
shoulders.material.emissiveIntensity = 0.2;
|
|
1313
|
+
head.material.color.setHex(0xfdbcb4);
|
|
1314
|
+
head.material.emissive.setHex(0x442211);
|
|
1315
|
+
head.material.emissiveIntensity = 0.1;
|
|
1316
|
+
glow.material.color.setHex(0x00ff00);
|
|
1317
|
+
glow.material.emissive.setHex(0x00ff00);
|
|
1318
|
+
glow.material.emissiveIntensity = 0.3;
|
|
1319
|
+
glow.visible = true;
|
|
1320
|
+
pedestal.material.emissive.setHex(0x111122);
|
|
1321
|
+
pedestal.material.emissiveIntensity = 0.1;
|
|
1322
|
+
container.scale.setScalar(1.0);
|
|
1323
|
+
container.position.y = 0;
|
|
1324
|
+
container.rotation.x = 0;
|
|
1325
|
+
if (player.nameplate && player.nameplate.element) {
|
|
1326
|
+
player.nameplate.element.style.transition = 'opacity 0.5s ease-in';
|
|
1327
|
+
player.nameplate.element.style.opacity = '1.0';
|
|
1328
|
+
}
|
|
1329
|
+
player.isAlive = true;
|
|
1330
|
+
|
|
1331
|
+
switch(status) {
|
|
1332
|
+
case 'dead':
|
|
1333
|
+
orb.visible = false;
|
|
1334
|
+
orbLight.visible = false;
|
|
1335
|
+
glow.visible = false;
|
|
1336
|
+
body.material.color.setHex(0x444444);
|
|
1337
|
+
body.material.emissive.setHex(0x000000);
|
|
1338
|
+
shoulders.material.color.setHex(0x444444);
|
|
1339
|
+
shoulders.material.emissive.setHex(0x000000);
|
|
1340
|
+
head.material.color.setHex(0x666666);
|
|
1341
|
+
head.material.emissive.setHex(0x000000);
|
|
1342
|
+
pedestal.material.emissive.setHex(0x000000);
|
|
1343
|
+
// Sink into ground
|
|
1344
|
+
container.position.y = -1.5;
|
|
1345
|
+
// Tilt slightly
|
|
1346
|
+
container.rotation.x = 0.2;
|
|
1347
|
+
// Fade out nameplate
|
|
1348
|
+
if (player.nameplate && player.nameplate.element) {
|
|
1349
|
+
player.nameplate.element.style.transition = 'opacity 2s ease-out';
|
|
1350
|
+
player.nameplate.element.style.opacity = '0.2';
|
|
1351
|
+
}
|
|
1352
|
+
player.isAlive = false;
|
|
1353
|
+
break;
|
|
1354
|
+
case 'werewolf':
|
|
1355
|
+
body.material.color.setHex(0x880000);
|
|
1356
|
+
body.material.emissive.setHex(0x440000);
|
|
1357
|
+
body.material.emissiveIntensity = 0.3;
|
|
1358
|
+
shoulders.material.color.setHex(0x880000);
|
|
1359
|
+
shoulders.material.emissive.setHex(0x440000);
|
|
1360
|
+
shoulders.material.emissiveIntensity = 0.3;
|
|
1361
|
+
glow.material.color.setHex(0xff0000);
|
|
1362
|
+
glow.material.emissive.setHex(0xff0000);
|
|
1363
|
+
glow.material.emissiveIntensity = 0.4;
|
|
1364
|
+
glow.visible = true;
|
|
1365
|
+
pedestal.material.emissive.setHex(0x440000);
|
|
1366
|
+
pedestal.material.emissiveIntensity = 0.2;
|
|
1367
|
+
break;
|
|
1368
|
+
case 'doctor':
|
|
1369
|
+
body.material.color.setHex(0x008800);
|
|
1370
|
+
body.material.emissive.setHex(0x004400);
|
|
1371
|
+
body.material.emissiveIntensity = 0.3;
|
|
1372
|
+
shoulders.material.color.setHex(0x008800);
|
|
1373
|
+
shoulders.material.emissive.setHex(0x004400);
|
|
1374
|
+
shoulders.material.emissiveIntensity = 0.3;
|
|
1375
|
+
glow.material.color.setHex(0x00ff00);
|
|
1376
|
+
glow.material.emissive.setHex(0x00ff00);
|
|
1377
|
+
glow.material.emissiveIntensity = 0.4;
|
|
1378
|
+
glow.visible = true;
|
|
1379
|
+
pedestal.material.emissive.setHex(0x004400);
|
|
1380
|
+
pedestal.material.emissiveIntensity = 0.2;
|
|
1381
|
+
break;
|
|
1382
|
+
case 'seer':
|
|
1383
|
+
body.material.color.setHex(0x4B0082);
|
|
1384
|
+
body.material.emissive.setHex(0x3A005A);
|
|
1385
|
+
body.material.emissiveIntensity = 0.3;
|
|
1386
|
+
shoulders.material.color.setHex(0x4B0082);
|
|
1387
|
+
shoulders.material.emissive.setHex(0x3A005A);
|
|
1388
|
+
shoulders.material.emissiveIntensity = 0.3;
|
|
1389
|
+
glow.material.color.setHex(0x9932CC);
|
|
1390
|
+
glow.material.emissive.setHex(0x9932CC);
|
|
1391
|
+
glow.material.emissiveIntensity = 0.4;
|
|
1392
|
+
glow.visible = true;
|
|
1393
|
+
pedestal.material.emissive.setHex(0x3A005A);
|
|
1394
|
+
pedestal.material.emissiveIntensity = 0.2;
|
|
1395
|
+
break;
|
|
1396
|
+
default:
|
|
1397
|
+
// This is now covered by the reset block at the top of the function.
|
|
1398
|
+
break;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (threatLevel >= 1.0) { // DANGER
|
|
1402
|
+
orb.material.color.setHex(0xff0000); // Red
|
|
1403
|
+
orb.material.emissive.setHex(0xff0000);
|
|
1404
|
+
orb.material.emissiveIntensity = 1.0;
|
|
1405
|
+
orb.material.opacity = 0.9;
|
|
1406
|
+
orbLight.color.setHex(0xff0000);
|
|
1407
|
+
orbLight.intensity = 1.2;
|
|
1408
|
+
glow.material.color.setHex(0xff0000);
|
|
1409
|
+
glow.material.emissive.setHex(0xff0000);
|
|
1410
|
+
glow.material.emissiveIntensity = 0.3;
|
|
1411
|
+
} else if (threatLevel >= 0.5) { // UNEASY
|
|
1412
|
+
orb.material.color.setHex(0xffff00); // Yellow
|
|
1413
|
+
orb.material.emissive.setHex(0xffff00);
|
|
1414
|
+
orb.material.emissiveIntensity = 1.0;
|
|
1415
|
+
orb.material.opacity = 0.9;
|
|
1416
|
+
orbLight.color.setHex(0xffff00);
|
|
1417
|
+
orbLight.intensity = 1.2;
|
|
1418
|
+
glow.material.color.setHex(0xffff00);
|
|
1419
|
+
glow.material.emissive.setHex(0xffff00);
|
|
1420
|
+
glow.material.emissiveIntensity = 0.3;
|
|
1421
|
+
} else { // SAFE
|
|
1422
|
+
// orb.material.color.setHex(0x00ff00); // Green
|
|
1423
|
+
orb.material.color.setHex(0x00ff00);
|
|
1424
|
+
orb.material.emissive.setHex(0x00ff00);
|
|
1425
|
+
orb.material.emissiveIntensity = 1.0;
|
|
1426
|
+
orb.material.opacity = 0.9;
|
|
1427
|
+
orbLight.color.setHex(0x00ff00);
|
|
1428
|
+
orbLight.intensity = 1.2;
|
|
1429
|
+
glow.material.color.setHex(0x00ff00);
|
|
1430
|
+
glow.material.emissive.setHex(0x00ff00);
|
|
1431
|
+
glow.material.emissiveIntensity = 0.3;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
triggerSpeakingAnimation(playerName) {
|
|
1436
|
+
const player = this._playerObjects.get(playerName);
|
|
1437
|
+
if (!player || !player.isAlive) return;
|
|
1438
|
+
|
|
1439
|
+
const wave = this._createSoundWave(this._THREE);
|
|
1440
|
+
player.container.add(wave);
|
|
1441
|
+
|
|
1442
|
+
// Add the wave to our animation manager array
|
|
1443
|
+
this._speakingAnimations.push({
|
|
1444
|
+
mesh: wave,
|
|
1445
|
+
startTime: performance.now(),
|
|
1446
|
+
duration: 1800, // Animation duration in milliseconds
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
_createSoundWave(THREE) {
|
|
1451
|
+
const waveGeometry = new THREE.RingGeometry(0.5, 0.7, 32);
|
|
1452
|
+
const waveMaterial = new THREE.MeshBasicMaterial({
|
|
1453
|
+
color: 0xffffff,
|
|
1454
|
+
transparent: true,
|
|
1455
|
+
opacity: 0.8,
|
|
1456
|
+
side: THREE.DoubleSide,
|
|
1457
|
+
});
|
|
1458
|
+
const wave = new THREE.Mesh(waveGeometry, waveMaterial);
|
|
1459
|
+
|
|
1460
|
+
// Position the wave horizontally at the player's feet
|
|
1461
|
+
wave.rotation.x = -Math.PI / 2;
|
|
1462
|
+
wave.position.y = 0.25; // Slightly above the pedestal
|
|
1463
|
+
return wave;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
_createVoteParticleTrail(voterName, targetName, color = 0x00ffff) {
|
|
1467
|
+
const voter = this._playerObjects.get(voterName);
|
|
1468
|
+
const target = this._playerObjects.get(targetName);
|
|
1469
|
+
if (!voter || !target) return;
|
|
1470
|
+
|
|
1471
|
+
const startPos = voter.container.position.clone();
|
|
1472
|
+
startPos.y += 1.5; // Start above the voter's head
|
|
1473
|
+
const endPos = target.container.position.clone();
|
|
1474
|
+
endPos.y += 1.5; // End above the target's head
|
|
1475
|
+
|
|
1476
|
+
const midPos = new this._THREE.Vector3().addVectors(startPos, endPos).multiplyScalar(0.5);
|
|
1477
|
+
const dist = startPos.distanceTo(endPos);
|
|
1478
|
+
midPos.y += dist * 0.3; // Arc height
|
|
1479
|
+
|
|
1480
|
+
const curve = new this._THREE.CatmullRomCurve3([startPos, midPos, endPos]);
|
|
1481
|
+
const particleCount = 50;
|
|
1482
|
+
const particleGeometry = new this._THREE.BufferGeometry();
|
|
1483
|
+
const positions = new Float32Array(particleCount * 3);
|
|
1484
|
+
particleGeometry.setAttribute('position', new this._THREE.BufferAttribute(positions, 3));
|
|
1485
|
+
|
|
1486
|
+
const particleMaterial = new this._THREE.PointsMaterial({
|
|
1487
|
+
color: color,
|
|
1488
|
+
size: 0.3,
|
|
1489
|
+
transparent: true,
|
|
1490
|
+
opacity: 0.8,
|
|
1491
|
+
blending: this._THREE.AdditiveBlending,
|
|
1492
|
+
sizeAttenuation: true,
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
const particles = new this._THREE.Points(particleGeometry, particleMaterial);
|
|
1496
|
+
this._votingArcsGroup.add(particles);
|
|
1497
|
+
|
|
1498
|
+
const trail = {
|
|
1499
|
+
particles,
|
|
1500
|
+
curve,
|
|
1501
|
+
target: targetName,
|
|
1502
|
+
startTime: Date.now(),
|
|
1503
|
+
update: () => {
|
|
1504
|
+
const elapsedTime = (Date.now() - trail.startTime) / 1000;
|
|
1505
|
+
const positions = trail.particles.geometry.attributes.position.array;
|
|
1506
|
+
for (let i = 0; i < particleCount; i++) {
|
|
1507
|
+
const t = (elapsedTime * 0.2 + (i / particleCount)) % 1;
|
|
1508
|
+
const pos = trail.curve.getPointAt(t);
|
|
1509
|
+
positions[i * 3] = pos.x;
|
|
1510
|
+
positions[i * 3 + 1] = pos.y;
|
|
1511
|
+
positions[i * 3 + 2] = pos.z;
|
|
1512
|
+
}
|
|
1513
|
+
trail.particles.geometry.attributes.position.needsUpdate = true;
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
this._activeVoteArcs.set(voterName, trail);
|
|
1517
|
+
|
|
1518
|
+
// Also add to a separate list for animation updates
|
|
1519
|
+
if (!this._animatingTrails) this._animatingTrails = [];
|
|
1520
|
+
this._animatingTrails.push(trail);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
_updateTargetRing(targetName, voteCount) {
|
|
1524
|
+
const target = this._playerObjects.get(targetName);
|
|
1525
|
+
if (!target) return;
|
|
1526
|
+
|
|
1527
|
+
let ringData = this._activeTargetRings.get(targetName);
|
|
1528
|
+
|
|
1529
|
+
if (voteCount > 0 && !ringData) {
|
|
1530
|
+
const geometry = new this._THREE.RingGeometry(2, 2.2, 32);
|
|
1531
|
+
const material = new this._THREE.MeshBasicMaterial({
|
|
1532
|
+
color: 0x00ffff,
|
|
1533
|
+
transparent: true,
|
|
1534
|
+
opacity: 0, // Start invisible, fade in
|
|
1535
|
+
side: this._THREE.DoubleSide,
|
|
1536
|
+
});
|
|
1537
|
+
const ring = new this._THREE.Mesh(geometry, material);
|
|
1538
|
+
ring.position.copy(target.container.position);
|
|
1539
|
+
ring.position.y = 0.1;
|
|
1540
|
+
ring.rotation.x = -Math.PI / 2;
|
|
1541
|
+
|
|
1542
|
+
this._targetRingsGroup.add(ring);
|
|
1543
|
+
ringData = { ring, material, targetOpacity: 0 };
|
|
1544
|
+
this._activeTargetRings.set(targetName, ringData);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
if (ringData) {
|
|
1548
|
+
if (voteCount > 0) {
|
|
1549
|
+
ringData.targetOpacity = 0.3 + Math.min(voteCount * 0.2, 0.7);
|
|
1550
|
+
} else {
|
|
1551
|
+
ringData.targetOpacity = 0;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
updateVoteVisuals(votes, clearAll = false) {
|
|
1557
|
+
if (!this._playerObjects || this._playerObjects.size === 0) return;
|
|
1558
|
+
|
|
1559
|
+
if (clearAll) {
|
|
1560
|
+
votes.clear();
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Remove arcs from players who are no longer voting or if clearing all
|
|
1564
|
+
this._activeVoteArcs.forEach((trail, voterName) => {
|
|
1565
|
+
if (!votes.has(voterName)) {
|
|
1566
|
+
this._votingArcsGroup.remove(trail.particles);
|
|
1567
|
+
this._activeVoteArcs.delete(voterName);
|
|
1568
|
+
if (this._animatingTrails) {
|
|
1569
|
+
this._animatingTrails = this._animatingTrails.filter(t => t !== trail);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
|
|
1575
|
+
// Update existing arcs or create new ones
|
|
1576
|
+
votes.forEach((voteData, voterName) => {
|
|
1577
|
+
const { target: targetName, type } = voteData;
|
|
1578
|
+
const existingTrail = this._activeVoteArcs.get(voterName);
|
|
1579
|
+
|
|
1580
|
+
let color = 0x00ffff; // Default to cyan
|
|
1581
|
+
if (type === 'night_vote') color = 0xff0000; // Red
|
|
1582
|
+
else if (type === 'doctor_heal_action') color = 0x00ff00; // Green
|
|
1583
|
+
else if (type === 'seer_inspection') color = 0x800080; // Purple
|
|
1584
|
+
|
|
1585
|
+
if (existingTrail) {
|
|
1586
|
+
if (existingTrail.target !== targetName) {
|
|
1587
|
+
this._votingArcsGroup.remove(existingTrail.particles);
|
|
1588
|
+
if (this._animatingTrails) {
|
|
1589
|
+
this._animatingTrails = this._animatingTrails.filter(t => t !== existingTrail);
|
|
1590
|
+
}
|
|
1591
|
+
this._createVoteParticleTrail(voterName, targetName, color);
|
|
1592
|
+
}
|
|
1593
|
+
} else {
|
|
1594
|
+
this._createVoteParticleTrail(voterName, targetName, color);
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
// Update target rings
|
|
1599
|
+
const targetVoteCounts = new Map();
|
|
1600
|
+
votes.forEach((voteData) => {
|
|
1601
|
+
const { target: targetName } = voteData;
|
|
1602
|
+
targetVoteCounts.set(targetName, (targetVoteCounts.get(targetName) || 0) + 1);
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
this._playerObjects.forEach((player, playerName) => {
|
|
1606
|
+
this._updateTargetRing(playerName, targetVoteCounts.get(playerName) || 0);
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
updatePhase(phase) {
|
|
1611
|
+
if (!this._scene) return;
|
|
1612
|
+
|
|
1613
|
+
// Handle various phase formats (DAY, NIGHT, or lowercase)
|
|
1614
|
+
const normalizedPhase = (phase || 'DAY').toUpperCase();
|
|
1615
|
+
|
|
1616
|
+
// Calculate target phase value (0 = day, 1 = night)
|
|
1617
|
+
const targetPhase = normalizedPhase === 'NIGHT' ? 1.0 : 0.0;
|
|
1618
|
+
|
|
1619
|
+
// Initialize transition system if not exists
|
|
1620
|
+
if (!this._phaseTransition) {
|
|
1621
|
+
this._phaseTransition = {
|
|
1622
|
+
current: targetPhase,
|
|
1623
|
+
target: targetPhase,
|
|
1624
|
+
speed: 0.05 // Increased transition speed for testing
|
|
1625
|
+
};
|
|
1626
|
+
// Immediately set to target on first call
|
|
1627
|
+
this._updateSceneForPhase(targetPhase);
|
|
1628
|
+
} else if (this._phaseTransition.target !== targetPhase) {
|
|
1629
|
+
// Only update if phase actually changed
|
|
1630
|
+
this._phaseTransition.target = targetPhase;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
_updateSceneForPhase(phaseValue) {
|
|
1635
|
+
const THREE = this._THREE;
|
|
1636
|
+
|
|
1637
|
+
// Update renderer tone mapping for day/night mood
|
|
1638
|
+
if (this._threejs) {
|
|
1639
|
+
this._threejs.toneMappingExposure = 1.2 - phaseValue * 0.5; // Darker at night
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// Smoothly interpolate lighting
|
|
1643
|
+
if (this._mainLight) {
|
|
1644
|
+
const nightColor = new THREE.Color(0x6666cc); // More blue at night
|
|
1645
|
+
const dayColor = new THREE.Color(0xffffcc);
|
|
1646
|
+
this._mainLight.color.copy(dayColor).lerp(nightColor, phaseValue);
|
|
1647
|
+
this._mainLight.intensity = 1.8 - phaseValue * 1.0; // Much dimmer at night
|
|
1648
|
+
|
|
1649
|
+
// Animate light position for sun/moon movement
|
|
1650
|
+
const angle = phaseValue * Math.PI * 0.3;
|
|
1651
|
+
this._mainLight.position.set(
|
|
1652
|
+
30 * Math.cos(angle),
|
|
1653
|
+
50 - phaseValue * 20,
|
|
1654
|
+
20 * Math.sin(angle)
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (this._rimLight) {
|
|
1659
|
+
const nightColor = new THREE.Color(0x6666ff);
|
|
1660
|
+
const dayColor = new THREE.Color(0xffcc99);
|
|
1661
|
+
this._rimLight.color.copy(dayColor).lerp(nightColor, phaseValue);
|
|
1662
|
+
this._rimLight.intensity = 0.6 + phaseValue * 0.4;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
if (this._hemiLight) {
|
|
1666
|
+
const nightSkyColor = new THREE.Color(0x4a4a6a);
|
|
1667
|
+
const daySkyColor = new THREE.Color(0x87ceeb);
|
|
1668
|
+
const nightGroundColor = new THREE.Color(0x1e1e3f);
|
|
1669
|
+
const dayGroundColor = new THREE.Color(0x8b7355);
|
|
1670
|
+
|
|
1671
|
+
this._hemiLight.color.copy(daySkyColor).lerp(nightSkyColor, phaseValue);
|
|
1672
|
+
this._hemiLight.groundColor.copy(dayGroundColor).lerp(nightGroundColor, phaseValue);
|
|
1673
|
+
this._hemiLight.intensity = 0.8 - phaseValue * 0.4;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// Update ambient light
|
|
1677
|
+
const ambientLight = this._scene.getObjectByName('ambientLight');
|
|
1678
|
+
if (ambientLight) {
|
|
1679
|
+
const nightColor = new THREE.Color(0x404080);
|
|
1680
|
+
const dayColor = new THREE.Color(0x606090);
|
|
1681
|
+
ambientLight.color.copy(dayColor).lerp(nightColor, phaseValue);
|
|
1682
|
+
ambientLight.intensity = 0.4 + phaseValue * 0.1;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// Smoothly transition fog - more dramatic change
|
|
1686
|
+
if (this._scene.fog) {
|
|
1687
|
+
const nightFogColor = new THREE.Color(0x050515); // Very dark blue at night
|
|
1688
|
+
const dayFogColor = new THREE.Color(0x2a2a4a); // Lighter blue during day
|
|
1689
|
+
this._scene.fog.color.copy(dayFogColor).lerp(nightFogColor, phaseValue);
|
|
1690
|
+
this._scene.fog.density = 0.01 + phaseValue * 0.015; // Denser fog at night
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Update skybox
|
|
1694
|
+
this._updateSkybox(phaseValue);
|
|
1695
|
+
|
|
1696
|
+
// Update stars visibility
|
|
1697
|
+
if (this._starsMaterial) {
|
|
1698
|
+
this._starsMaterial.uniforms.phase.value = phaseValue;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// Update atmosphere shader
|
|
1702
|
+
if (this._atmospherePass) {
|
|
1703
|
+
this._atmospherePass.uniforms.phase.value = phaseValue;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Update particle colors based on phase
|
|
1707
|
+
if (this._particles && this._particles.geometry.attributes.color) {
|
|
1708
|
+
const colors = this._particles.geometry.attributes.color.array;
|
|
1709
|
+
for (let i = 0; i < colors.length; i += 3) {
|
|
1710
|
+
// Shift particle hue based on phase
|
|
1711
|
+
const baseHue = 0.5 + Math.random() * 0.3; // Base blue-purple range
|
|
1712
|
+
const phaseShift = phaseValue * 0.1; // Shift towards purple at night
|
|
1713
|
+
const hue = baseHue + phaseShift;
|
|
1714
|
+
const saturation = 0.8 - phaseValue * 0.2; // Less saturated at night
|
|
1715
|
+
const lightness = 0.6 - phaseValue * 0.2; // Darker at night
|
|
1716
|
+
|
|
1717
|
+
const color = new THREE.Color().setHSL(hue, saturation, lightness);
|
|
1718
|
+
colors[i] = color.r;
|
|
1719
|
+
colors[i + 1] = color.g;
|
|
1720
|
+
colors[i + 2] = color.b;
|
|
1721
|
+
}
|
|
1722
|
+
this._particles.geometry.attributes.color.needsUpdate = true;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// Update bloom intensity based on phase - moderate during day, more at night
|
|
1726
|
+
if (this._bloomPass) {
|
|
1727
|
+
this._bloomPass.strength = 0.35 + phaseValue * 0.35; // Moderate bloom during day (0.35), stronger at night (0.7)
|
|
1728
|
+
this._bloomPass.radius = 0.6 + phaseValue * 0.3; // Good radius during day (0.6), wider at night (0.9)
|
|
1729
|
+
this._bloomPass.threshold = 0.4 - phaseValue * 0.15; // Balanced threshold
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
_createNameplate(name, displayName, imageUrl, CSS2DObject) {
|
|
1734
|
+
const container = document.createElement('div');
|
|
1735
|
+
container.style.backgroundColor = 'rgba(255, 255, 255, 0)';
|
|
1736
|
+
container.style.padding = '6px 10px'; // Slightly smaller padding
|
|
1737
|
+
container.style.borderRadius = '8px';
|
|
1738
|
+
container.style.display = 'flex';
|
|
1739
|
+
container.style.alignItems = 'center';
|
|
1740
|
+
container.style.justifyContent = 'center';
|
|
1741
|
+
container.style.gap = '8px'; // Reduced gap
|
|
1742
|
+
container.style.textAlign = 'center';
|
|
1743
|
+
|
|
1744
|
+
const img = document.createElement('img');
|
|
1745
|
+
img.src = imageUrl;
|
|
1746
|
+
img.style.width = '40px'; // Reduced from 60px
|
|
1747
|
+
img.style.height = '40px'; // Reduced from 60px
|
|
1748
|
+
img.style.borderRadius = '50%';
|
|
1749
|
+
img.style.objectFit = 'cover';
|
|
1750
|
+
img.style.backgroundColor = 'white';
|
|
1751
|
+
img.style.border = '2px solid rgba(255, 255, 255, 0.3)';
|
|
1752
|
+
|
|
1753
|
+
const textContainer = document.createElement('div');
|
|
1754
|
+
textContainer.style.display = 'flex';
|
|
1755
|
+
textContainer.style.flexDirection = 'column';
|
|
1756
|
+
textContainer.style.alignItems = 'center';
|
|
1757
|
+
|
|
1758
|
+
const nameText = document.createElement('div');
|
|
1759
|
+
nameText.textContent = name;
|
|
1760
|
+
nameText.style.color = 'white';
|
|
1761
|
+
nameText.style.fontFamily = 'Arial, sans-serif';
|
|
1762
|
+
nameText.style.fontSize = '14px';
|
|
1763
|
+
nameText.style.fontWeight = '500';
|
|
1764
|
+
textContainer.appendChild(nameText);
|
|
1765
|
+
|
|
1766
|
+
if (displayName && displayName !== "" && displayName !== name) {
|
|
1767
|
+
const displayNameText = document.createElement('div');
|
|
1768
|
+
displayNameText.textContent = displayName;
|
|
1769
|
+
displayNameText.style.color = 'grey';
|
|
1770
|
+
displayNameText.style.fontSize = '12px';
|
|
1771
|
+
displayNameText.style.fontFamily = 'Arial, sans-serif';
|
|
1772
|
+
displayNameText.style.marginTop = '4px';
|
|
1773
|
+
textContainer.appendChild(displayNameText);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
container.appendChild(img);
|
|
1777
|
+
container.appendChild(textContainer);
|
|
1778
|
+
|
|
1779
|
+
const label = new CSS2DObject(container);
|
|
1780
|
+
return label;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
_FrameGroup(group, THREE) {
|
|
1784
|
+
const box = new THREE.Box3().setFromObject(group);
|
|
1785
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
1786
|
+
const size = box.getSize(new THREE.Vector3());
|
|
1787
|
+
|
|
1788
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
1789
|
+
const fov = this._camera.fov * (Math.PI / 180);
|
|
1790
|
+
|
|
1791
|
+
let cameraZ = Math.abs(maxDim / Math.tan(fov / 2));
|
|
1792
|
+
cameraZ *= 0.5;
|
|
1793
|
+
|
|
1794
|
+
this._camera.position.set(center.x, center.y, center.z - cameraZ);
|
|
1795
|
+
|
|
1796
|
+
const shiftY = size.y / 2.5;
|
|
1797
|
+
this._camera.position.y += shiftY;
|
|
1798
|
+
|
|
1799
|
+
const newTarget = center.clone();
|
|
1800
|
+
newTarget.y += shiftY;
|
|
1801
|
+
this._controls.target.copy(newTarget);
|
|
1802
|
+
this._controls.update();
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
_NormalizeModel(model, THREE) {
|
|
1806
|
+
const box = new THREE.Box3().setFromObject(model);
|
|
1807
|
+
const size = box.getSize(new THREE.Vector3());
|
|
1808
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
1809
|
+
model.position.copy(center).negate();
|
|
1810
|
+
const wrapper = new THREE.Group();
|
|
1811
|
+
wrapper.add(model);
|
|
1812
|
+
const scale = 1.0 / size.y;
|
|
1813
|
+
wrapper.scale.set(scale, scale, scale);
|
|
1814
|
+
return wrapper;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
_RAF() {
|
|
1818
|
+
requestAnimationFrame((time) => {
|
|
1819
|
+
// Animate phase transition with visual feedback
|
|
1820
|
+
if (this._phaseTransition) {
|
|
1821
|
+
const diff = this._phaseTransition.target - this._phaseTransition.current;
|
|
1822
|
+
if (Math.abs(diff) > 0.001) {
|
|
1823
|
+
this._phaseTransition.current += diff * this._phaseTransition.speed;
|
|
1824
|
+
this._updateSceneForPhase(this._phaseTransition.current);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// Update time-based uniforms
|
|
1829
|
+
if (this._particleMaterial) {
|
|
1830
|
+
this._particleMaterial.uniforms.time.value = time * 0.001;
|
|
1831
|
+
}
|
|
1832
|
+
if (this._atmospherePass) {
|
|
1833
|
+
this._atmospherePass.uniforms.time.value = time * 0.001;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// Animate particle system with phase-aware movement
|
|
1837
|
+
if (this._particles) {
|
|
1838
|
+
const phaseValue = this._phaseTransition ? this._phaseTransition.current : 0;
|
|
1839
|
+
// Slower rotation at night
|
|
1840
|
+
this._particles.rotation.y = time * 0.0001 * (1 - phaseValue * 0.5);
|
|
1841
|
+
|
|
1842
|
+
const positions = this._particles.geometry.attributes.position.array;
|
|
1843
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
1844
|
+
// More gentle movement at night
|
|
1845
|
+
const movementScale = 1 - phaseValue * 0.5;
|
|
1846
|
+
positions[i + 1] += Math.sin(time * 0.001 + positions[i] * 0.01) * 0.02 * movementScale;
|
|
1847
|
+
// Wrap around if particles fall too low
|
|
1848
|
+
if (positions[i + 1] < 0) {
|
|
1849
|
+
positions[i + 1] = 35;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
this._particles.geometry.attributes.position.needsUpdate = true;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// Animate stars twinkling
|
|
1856
|
+
if (this._stars && this._phaseTransition && this._phaseTransition.current > 0.5) {
|
|
1857
|
+
const sizes = this._stars.geometry.attributes.size.array;
|
|
1858
|
+
for (let i = 0; i < sizes.length; i++) {
|
|
1859
|
+
sizes[i] = (Math.random() * 2 + 0.5) * (0.8 + Math.sin(time * 0.001 + i) * 0.2);
|
|
1860
|
+
}
|
|
1861
|
+
this._stars.geometry.attributes.size.needsUpdate = true;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// Use performance.now() for more precise animation timing
|
|
1865
|
+
const now = performance.now();
|
|
1866
|
+
|
|
1867
|
+
if (this._cameraAnimation) {
|
|
1868
|
+
const anim = this._cameraAnimation;
|
|
1869
|
+
const elapsed = now - anim.startTime;
|
|
1870
|
+
let progress = Math.min(elapsed / anim.duration, 1.0);
|
|
1871
|
+
|
|
1872
|
+
// Apply easing function
|
|
1873
|
+
const easedProgress = anim.ease(progress);
|
|
1874
|
+
|
|
1875
|
+
// Interpolate camera position and controls target
|
|
1876
|
+
this._camera.position.lerpVectors(anim.startPos, anim.endPos, easedProgress);
|
|
1877
|
+
this._controls.target.lerpVectors(anim.startTarget, anim.endTarget, easedProgress);
|
|
1878
|
+
this._controls.update();
|
|
1879
|
+
|
|
1880
|
+
// If animation is complete, clear it
|
|
1881
|
+
if (progress >= 1.0) {
|
|
1882
|
+
this._cameraAnimation = null;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// Animate speaking sound waves
|
|
1887
|
+
this._speakingAnimations = this._speakingAnimations.filter(anim => {
|
|
1888
|
+
const elapsedTime = now - anim.startTime;
|
|
1889
|
+
if (elapsedTime >= anim.duration) {
|
|
1890
|
+
// Animation is over, remove the mesh from the scene
|
|
1891
|
+
if (anim.mesh.parent) {
|
|
1892
|
+
anim.mesh.parent.remove(anim.mesh);
|
|
1893
|
+
}
|
|
1894
|
+
// Clean up Three.js objects to free memory
|
|
1895
|
+
anim.mesh.geometry.dispose();
|
|
1896
|
+
anim.mesh.material.dispose();
|
|
1897
|
+
return false; // Remove from the animations array
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// Calculate animation progress (from 0.0 to 1.0)
|
|
1901
|
+
const progress = elapsedTime / anim.duration;
|
|
1902
|
+
|
|
1903
|
+
// Make the wave expand and fade out
|
|
1904
|
+
anim.mesh.scale.setScalar(1 + progress * 5);
|
|
1905
|
+
anim.mesh.material.opacity = 0.8 * (1 - progress);
|
|
1906
|
+
|
|
1907
|
+
return true; // Keep the animation in the array
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
// Animate player objects with enhanced effects
|
|
1911
|
+
if (this._playerObjects) {
|
|
1912
|
+
this._playerObjects.forEach((player, name) => {
|
|
1913
|
+
if (player.isAlive) {
|
|
1914
|
+
// Enhanced floating animation for alive players
|
|
1915
|
+
const floatOffset = Math.sin(time * 0.001 + player.baseAngle) * 0.2;
|
|
1916
|
+
const bobOffset = Math.cos(time * 0.0015 + player.baseAngle * 2) * 0.05;
|
|
1917
|
+
player.container.position.y = floatOffset + bobOffset;
|
|
1918
|
+
|
|
1919
|
+
// More dynamic orb rotation
|
|
1920
|
+
player.orb.rotation.y = time * 0.003;
|
|
1921
|
+
player.orb.rotation.x = Math.sin(time * 0.002) * 0.15;
|
|
1922
|
+
player.orb.rotation.z = Math.cos(time * 0.0025) * 0.1;
|
|
1923
|
+
|
|
1924
|
+
// Enhanced glow animation
|
|
1925
|
+
if (player.glow && player.glow.visible) {
|
|
1926
|
+
player.glow.rotation.y = -time * 0.002;
|
|
1927
|
+
const glowScale = 1 + Math.sin(time * 0.004 + player.baseAngle) * 0.15;
|
|
1928
|
+
player.glow.scale.setScalar(glowScale);
|
|
1929
|
+
|
|
1930
|
+
// Pulsing emissive intensity
|
|
1931
|
+
const pulseIntensity = 0.3 + Math.sin(time * 0.005 + player.baseAngle) * 0.1;
|
|
1932
|
+
player.glow.material.emissiveIntensity = pulseIntensity;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// Enhanced pulse effect for active players
|
|
1936
|
+
if (player.container.scale.x > 1.0) {
|
|
1937
|
+
const pulseScale = 1.05 + Math.sin(time * 0.008) * 0.08;
|
|
1938
|
+
player.container.scale.setScalar(pulseScale);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// Enhanced breathing effect
|
|
1942
|
+
if (player.body) {
|
|
1943
|
+
const breathScale = 1 + Math.sin(time * 0.002 + player.baseAngle) * 0.03;
|
|
1944
|
+
player.body.scale.y = breathScale;
|
|
1945
|
+
if (player.shoulders) {
|
|
1946
|
+
player.shoulders.scale.y = 0.6 * breathScale;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// Subtle head movement
|
|
1951
|
+
if (player.head) {
|
|
1952
|
+
player.head.rotation.y = Math.sin(time * 0.001 + player.baseAngle) * 0.1;
|
|
1953
|
+
}
|
|
1954
|
+
} else {
|
|
1955
|
+
// Dead players have reduced animation
|
|
1956
|
+
if (player.orb) {
|
|
1957
|
+
player.orb.rotation.y = time * 0.0008;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// Animate voting trails
|
|
1964
|
+
if (this._animatingTrails) {
|
|
1965
|
+
this._animatingTrails.forEach(trail => trail.update());
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// Animate target rings
|
|
1969
|
+
if (this._activeTargetRings) {
|
|
1970
|
+
this._activeTargetRings.forEach((ringData, targetName) => {
|
|
1971
|
+
const diff = ringData.targetOpacity - ringData.material.opacity;
|
|
1972
|
+
if (Math.abs(diff) > 0.01) {
|
|
1973
|
+
ringData.material.opacity += diff * 0.1;
|
|
1974
|
+
} else if (ringData.targetOpacity === 0 && ringData.material.opacity > 0) {
|
|
1975
|
+
this._targetRingsGroup.remove(ringData.ring);
|
|
1976
|
+
this._activeTargetRings.delete(targetName);
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// Use post-processing composer if available, otherwise fallback to direct render
|
|
1982
|
+
if (this._composer) {
|
|
1983
|
+
this._composer.render();
|
|
1984
|
+
} else {
|
|
1985
|
+
this._threejs.render(this._scene, this._camera);
|
|
1986
|
+
}
|
|
1987
|
+
this._labelRenderer.render(this._scene, this._camera);
|
|
1988
|
+
this._RAF();
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
setupScene(BasicWorldDemo);
|
|
1994
|
+
} catch (error) {
|
|
1995
|
+
console.error("Failed to load Three.js modules:", error);
|
|
1996
|
+
parent.textContent = "Error loading 3D assets. Please refresh.";
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
loadAndSetup();
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
function setupScene(BasicWorldDemo) {
|
|
2004
|
+
if (threeState.initialized) return;
|
|
2005
|
+
threeState.demo = new BasicWorldDemo({ parent, width, height });
|
|
2006
|
+
threeState.initialized = true;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function updateSceneFromGameState(gameState, playerMap, actingPlayerName) {
|
|
2010
|
+
if (!threeState.demo || !threeState.demo._playerObjects) return;
|
|
2011
|
+
|
|
2012
|
+
const logUpToCurrentStep = gameState.eventLog;
|
|
2013
|
+
const lastEvent = logUpToCurrentStep.length > 0 ? logUpToCurrentStep[logUpToCurrentStep.length - 1] : null;
|
|
2014
|
+
|
|
2015
|
+
// Determine correct phase from the last event log entry
|
|
2016
|
+
let phase = gameState.game_state_phase; // Default
|
|
2017
|
+
if (lastEvent && lastEvent.phase) {
|
|
2018
|
+
phase = lastEvent.phase;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// Update player statuses
|
|
2022
|
+
gameState.players.forEach(player => {
|
|
2023
|
+
const playerObj = threeState.demo._playerObjects.get(player.name);
|
|
2024
|
+
if (!playerObj) return;
|
|
2025
|
+
|
|
2026
|
+
const threatLevel = gameState.playerThreatLevels.get(player.name) || 0;
|
|
2027
|
+
|
|
2028
|
+
let primaryStatus = 'default'; // Default for alive players in daytime.
|
|
2029
|
+
if (!player.is_alive) {
|
|
2030
|
+
primaryStatus = 'dead';
|
|
2031
|
+
} else if (player.role === 'Werewolf' && phase.toUpperCase() === 'NIGHT') {
|
|
2032
|
+
primaryStatus = 'werewolf';
|
|
2033
|
+
} else if (player.role === 'Doctor' && phase.toUpperCase() === 'NIGHT') {
|
|
2034
|
+
primaryStatus = 'doctor';
|
|
2035
|
+
} else if (player.role === 'Seer' && phase.toUpperCase() === 'NIGHT') {
|
|
2036
|
+
primaryStatus = 'seer';
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
threeState.demo.updatePlayerStatus(player.name, primaryStatus, threatLevel);
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
// Update phase lighting
|
|
2043
|
+
threeState.demo.updatePhase(phase);
|
|
2044
|
+
|
|
2045
|
+
// --- Vote Visualization Logic ---
|
|
2046
|
+
const currentVotes = new Map();
|
|
2047
|
+
|
|
2048
|
+
// Find the start of the current voting/action session
|
|
2049
|
+
const lastNightStart = logUpToCurrentStep.findLastIndex(e => e.type === 'phase_divider' && e.divider === 'NIGHT START');
|
|
2050
|
+
const lastDayVoteStart = logUpToCurrentStep.findLastIndex(e => e.type === 'phase_divider' && e.divider === 'DAY VOTE START');
|
|
2051
|
+
const sessionStartIndex = Math.max(lastNightStart, lastDayVoteStart);
|
|
2052
|
+
|
|
2053
|
+
let isVotingSession = false;
|
|
2054
|
+
if (sessionStartIndex > -1) {
|
|
2055
|
+
const lastOutcomeEventIndex = logUpToCurrentStep.findLastIndex(e => e.type === 'exile' || e.type === 'elimination' || e.type === 'save');
|
|
2056
|
+
// A session is active if it started after the last outcome, OR if the outcome is the current event.
|
|
2057
|
+
if (sessionStartIndex > lastOutcomeEventIndex || (lastOutcomeEventIndex > -1 && lastOutcomeEventIndex === logUpToCurrentStep.length - 1)) {
|
|
2058
|
+
isVotingSession = true;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
if (isVotingSession) {
|
|
2063
|
+
const alivePlayerNames = new Set(gameState.players.filter(p => p.is_alive).map(p => p.name));
|
|
2064
|
+
const relevantEvents = logUpToCurrentStep.slice(sessionStartIndex);
|
|
2065
|
+
for (const event of relevantEvents) {
|
|
2066
|
+
if (event.type === 'vote' || event.type === 'night_vote' || event.type === 'doctor_heal_action' || event.type === 'seer_inspection') {
|
|
2067
|
+
if (alivePlayerNames.has(event.actor_id)) {
|
|
2068
|
+
currentVotes.set(event.actor_id, { target: event.target, type: event.type });
|
|
2069
|
+
}
|
|
2070
|
+
} else if (event.type === 'timeout') {
|
|
2071
|
+
currentVotes.delete(event.actor_id);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const clearVotingVisuals = !isVotingSession;
|
|
2077
|
+
threeState.demo.updateVoteVisuals(currentVotes, clearVotingVisuals);
|
|
2078
|
+
|
|
2079
|
+
|
|
2080
|
+
// Spotlight logic for night actions
|
|
2081
|
+
if (threeState.demo._spotLight) {
|
|
2082
|
+
const lastEvent = gameState.eventLog[gameState.eventLog.length - 1];
|
|
2083
|
+
const nightActor = (gameState.game_state_phase === 'NIGHT' && lastEvent && lastEvent.actor_id && ['WerewolfNightVoteDataEntry', 'DoctorHealActionDataEntry', 'SeerInspectActionDataEntry'].includes(lastEvent.dataType)) ? lastEvent.actor_id : null;
|
|
2084
|
+
|
|
2085
|
+
if (nightActor) {
|
|
2086
|
+
const actorPlayer = threeState.demo._playerObjects.get(nightActor);
|
|
2087
|
+
if (actorPlayer) {
|
|
2088
|
+
const targetPosition = actorPlayer.container.position.clone();
|
|
2089
|
+
threeState.demo._spotLight.target.position.copy(targetPosition);
|
|
2090
|
+
threeState.demo._spotLight.position.set(targetPosition.x, targetPosition.y + 20, targetPosition.z + 5);
|
|
2091
|
+
threeState.demo._spotLight.visible = true;
|
|
2092
|
+
} else {
|
|
2093
|
+
threeState.demo._spotLight.visible = false;
|
|
2094
|
+
}
|
|
2095
|
+
} else {
|
|
2096
|
+
threeState.demo._spotLight.visible = false;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// Handle animation for the current event actor
|
|
2101
|
+
if (lastEvent) {
|
|
2102
|
+
if (lastEvent.event_name === 'moderator_announcement') {
|
|
2103
|
+
// Moderator is speaking, expand all alive players
|
|
2104
|
+
gameState.players.forEach(player => {
|
|
2105
|
+
if (player.is_alive) {
|
|
2106
|
+
threeState.demo.updatePlayerActive(player.name);
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
} else if (lastEvent.actor_id && playerMap.has(lastEvent.actor_id)) {
|
|
2110
|
+
// A player is the actor
|
|
2111
|
+
const actorName = lastEvent.actor_id;
|
|
2112
|
+
threeState.demo.updatePlayerActive(actorName);
|
|
2113
|
+
|
|
2114
|
+
// If the action was speaking, trigger the sound wave animation
|
|
2115
|
+
if (lastEvent.type === 'chat' && threeState.demo.triggerSpeakingAnimation) {
|
|
2116
|
+
threeState.demo.triggerSpeakingAnimation(actorName);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// --- CSS for the UI ---
|
|
2123
|
+
const css = `
|
|
2124
|
+
/* Game Status Scoreboard */
|
|
2125
|
+
.game-scoreboard {
|
|
2126
|
+
position: fixed;
|
|
2127
|
+
top: 70px;
|
|
2128
|
+
left: 50%;
|
|
2129
|
+
transform: translateX(-50%);
|
|
2130
|
+
z-index: 999;
|
|
2131
|
+
background: linear-gradient(135deg, rgba(33, 40, 54, 0.95), rgba(44, 52, 68, 0.95));
|
|
2132
|
+
backdrop-filter: blur(15px);
|
|
2133
|
+
border: 1px solid rgba(116, 185, 255, 0.3);
|
|
2134
|
+
border-radius: 12px;
|
|
2135
|
+
padding: 12px 20px;
|
|
2136
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
2137
|
+
display: flex;
|
|
2138
|
+
gap: 20px;
|
|
2139
|
+
align-items: center;
|
|
2140
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
2141
|
+
pointer-events: none;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
.scoreboard-item {
|
|
2145
|
+
display: flex;
|
|
2146
|
+
flex-direction: column;
|
|
2147
|
+
align-items: center;
|
|
2148
|
+
padding: 0 10px;
|
|
2149
|
+
border-right: 1px solid rgba(116, 185, 255, 0.2);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
.scoreboard-item:last-child {
|
|
2153
|
+
border-right: none;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
.scoreboard-label {
|
|
2157
|
+
font-size: 0.75rem;
|
|
2158
|
+
color: var(--text-muted);
|
|
2159
|
+
text-transform: uppercase;
|
|
2160
|
+
letter-spacing: 0.05em;
|
|
2161
|
+
margin-bottom: 4px;
|
|
2162
|
+
font-weight: 500;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
.scoreboard-value {
|
|
2166
|
+
font-size: 1.1rem;
|
|
2167
|
+
color: var(--text-primary);
|
|
2168
|
+
font-weight: 600;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
.scoreboard-value.alive {
|
|
2172
|
+
color: #00b894;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
.scoreboard-value.dead {
|
|
2176
|
+
color: #e17055;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
.scoreboard-value.werewolf {
|
|
2180
|
+
color: #e17055;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
.scoreboard-value.villager {
|
|
2184
|
+
color: #74b9ff;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
.scoreboard-action {
|
|
2188
|
+
background: linear-gradient(135deg, rgba(116, 185, 255, 0.2), rgba(116, 185, 255, 0.1));
|
|
2189
|
+
border: 1px solid rgba(116, 185, 255, 0.3);
|
|
2190
|
+
border-radius: 8px;
|
|
2191
|
+
padding: 6px 12px;
|
|
2192
|
+
font-size: 0.9rem;
|
|
2193
|
+
color: #74b9ff;
|
|
2194
|
+
font-weight: 500;
|
|
2195
|
+
animation: pulse 2s infinite;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
@keyframes pulse {
|
|
2199
|
+
0%, 100% { opacity: 1; }
|
|
2200
|
+
50% { opacity: 0.7; }
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
/* Phase Indicator */
|
|
2204
|
+
.phase-indicator {
|
|
2205
|
+
position: fixed;
|
|
2206
|
+
top: 60px;
|
|
2207
|
+
left: 50px;
|
|
2208
|
+
transform: translateX(-50%);
|
|
2209
|
+
z-index: 1000;
|
|
2210
|
+
padding: 12px 24px;
|
|
2211
|
+
border-radius: 30px;
|
|
2212
|
+
font-size: 1.2rem;
|
|
2213
|
+
font-weight: 600;
|
|
2214
|
+
letter-spacing: 0.05em;
|
|
2215
|
+
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
2216
|
+
pointer-events: none;
|
|
2217
|
+
backdrop-filter: blur(10px);
|
|
2218
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
2219
|
+
scale: 0.6;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
.phase-indicator.day {
|
|
2223
|
+
background: linear-gradient(135deg, rgba(255, 220, 100, 0.9), rgba(255, 180, 50, 0.9));
|
|
2224
|
+
color: #2d3436;
|
|
2225
|
+
border: 2px solid rgba(255, 255, 255, 0.5);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
.phase-indicator.night {
|
|
2229
|
+
background: linear-gradient(135deg, rgba(30, 30, 60, 0.9), rgba(60, 60, 120, 0.9));
|
|
2230
|
+
color: #f8f9fa;
|
|
2231
|
+
border: 2px solid rgba(100, 100, 200, 0.5);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
.phase-indicator .phase-icon {
|
|
2235
|
+
display: inline-block;
|
|
2236
|
+
margin-right: 8px;
|
|
2237
|
+
font-size: 1.4rem;
|
|
2238
|
+
vertical-align: middle;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
:root {
|
|
2242
|
+
--night-bg: linear-gradient(135deg, #1a1a2e, #16213e);
|
|
2243
|
+
--day-bg: linear-gradient(135deg, #74b9ff, #0984e3);
|
|
2244
|
+
--night-text: #f8f9fa;
|
|
2245
|
+
--day-text: #2d3436;
|
|
2246
|
+
--dead-filter: grayscale(100%) brightness(40%) contrast(0.8);
|
|
2247
|
+
--active-border: #fdcb6e;
|
|
2248
|
+
--active-glow: rgba(253, 203, 110, 0.4);
|
|
2249
|
+
--werewolf-color: #e17055;
|
|
2250
|
+
--villager-color: #00b894;
|
|
2251
|
+
--doctor-color: #6c5ce7;
|
|
2252
|
+
--seer-color: #fd79a8;
|
|
2253
|
+
--panel-bg: rgba(33, 40, 54, 0.95);
|
|
2254
|
+
--panel-border: rgba(116, 185, 255, 0.2);
|
|
2255
|
+
--hover-bg: rgba(116, 185, 255, 0.1);
|
|
2256
|
+
--card-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
2257
|
+
--text-primary: #f8f9fa;
|
|
2258
|
+
--text-secondary: #b2bec3;
|
|
2259
|
+
--text-muted: #74b9ff;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
2263
|
+
|
|
2264
|
+
.werewolf-parent {
|
|
2265
|
+
position: relative;
|
|
2266
|
+
overflow: hidden;
|
|
2267
|
+
width: 100%;
|
|
2268
|
+
height: 100%;
|
|
2269
|
+
background: radial-gradient(ellipse at center, #0f1419 0%, #000000 100%);
|
|
2270
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
.main-container {
|
|
2274
|
+
position: absolute;
|
|
2275
|
+
top: 0;
|
|
2276
|
+
left: 0;
|
|
2277
|
+
right: 0;
|
|
2278
|
+
bottom: 0;
|
|
2279
|
+
z-index: 2;
|
|
2280
|
+
pointer-events: none;
|
|
2281
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2282
|
+
color: var(--text-primary);
|
|
2283
|
+
font-weight: 400;
|
|
2284
|
+
line-height: 1.5;
|
|
2285
|
+
-webkit-font-smoothing: antialiased;
|
|
2286
|
+
-moz-osx-font-smoothing: grayscale;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
/* Enhanced Panel Styling */
|
|
2290
|
+
.left-panel, .right-panel {
|
|
2291
|
+
position: fixed;
|
|
2292
|
+
top: 54px;
|
|
2293
|
+
max-height: calc(100vh - 124px);
|
|
2294
|
+
background: var(--panel-bg);
|
|
2295
|
+
backdrop-filter: blur(20px) saturate(1.5);
|
|
2296
|
+
border-radius: 16px;
|
|
2297
|
+
border: 1px solid var(--panel-border);
|
|
2298
|
+
padding: 20px;
|
|
2299
|
+
display: flex;
|
|
2300
|
+
flex-direction: column;
|
|
2301
|
+
box-sizing: border-box;
|
|
2302
|
+
pointer-events: auto;
|
|
2303
|
+
box-shadow: var(--card-shadow), 0 0 40px rgba(116, 185, 255, 0.05);
|
|
2304
|
+
overflow: hidden;
|
|
2305
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
.left-panel {
|
|
2309
|
+
left: 20px;
|
|
2310
|
+
width: 320px;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
.right-panel {
|
|
2314
|
+
right: 20px;
|
|
2315
|
+
width: 420px;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
.left-panel:hover, .right-panel:hover {
|
|
2319
|
+
border-color: rgba(116, 185, 255, 0.3);
|
|
2320
|
+
box-shadow: var(--card-shadow), 0 0 60px rgba(116, 185, 255, 0.08);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
/* Enhanced Headers */
|
|
2324
|
+
.right-panel h1, #player-list-area h1 {
|
|
2325
|
+
margin: 0 0 20px 0;
|
|
2326
|
+
font-size: 1.75rem;
|
|
2327
|
+
font-weight: 600;
|
|
2328
|
+
color: var(--text-primary);
|
|
2329
|
+
position: relative;
|
|
2330
|
+
padding-bottom: 15px;
|
|
2331
|
+
flex-shrink: 0;
|
|
2332
|
+
display: flex;
|
|
2333
|
+
justify-content: center;
|
|
2334
|
+
align-items: center;
|
|
2335
|
+
gap: 10px;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
.right-panel h1 > span, #player-list-area h1 > span {
|
|
2339
|
+
background: linear-gradient(135deg, #74b9ff, #0984e3);
|
|
2340
|
+
-webkit-background-clip: text;
|
|
2341
|
+
-webkit-text-fill-color: transparent;
|
|
2342
|
+
background-clip: text;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
.right-panel h1::after, #player-list-area h1::after {
|
|
2346
|
+
content: '';
|
|
2347
|
+
position: absolute;
|
|
2348
|
+
bottom: 0;
|
|
2349
|
+
left: 50%;
|
|
2350
|
+
transform: translateX(-50%);
|
|
2351
|
+
width: 60px;
|
|
2352
|
+
height: 3px;
|
|
2353
|
+
background: linear-gradient(90deg, transparent, #74b9ff, transparent);
|
|
2354
|
+
border-radius: 2px;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
#global-reasoning-toggle {
|
|
2358
|
+
background: none;
|
|
2359
|
+
border: none;
|
|
2360
|
+
cursor: pointer;
|
|
2361
|
+
padding: 4px;
|
|
2362
|
+
border-radius: 50%;
|
|
2363
|
+
display: inline-flex;
|
|
2364
|
+
align-items: center;
|
|
2365
|
+
justify-content: center;
|
|
2366
|
+
color: var(--text-muted);
|
|
2367
|
+
transition: all 0.2s ease;
|
|
2368
|
+
}
|
|
2369
|
+
#global-reasoning-toggle:hover {
|
|
2370
|
+
background-color: var(--hover-bg);
|
|
2371
|
+
color: var(--text-primary);
|
|
2372
|
+
}
|
|
2373
|
+
#global-reasoning-toggle svg {
|
|
2374
|
+
stroke: currentColor;
|
|
2375
|
+
width: 20px;
|
|
2376
|
+
height: 20px;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
#global-audio-toggle {
|
|
2380
|
+
background: none;
|
|
2381
|
+
border: none;
|
|
2382
|
+
cursor: pointer;
|
|
2383
|
+
padding: 4px;
|
|
2384
|
+
border-radius: 50%;
|
|
2385
|
+
display: inline-flex;
|
|
2386
|
+
align-items: center;
|
|
2387
|
+
justify-content: center;
|
|
2388
|
+
color: var(--text-muted);
|
|
2389
|
+
transition: all 0.2s ease;
|
|
2390
|
+
font-size: 18px; /* For the emoji */
|
|
2391
|
+
vertical-align: middle; /* Align with the SVG icon */
|
|
2392
|
+
margin-left: 4px; /* Space from eye icon */
|
|
2393
|
+
}
|
|
2394
|
+
#global-audio-toggle:hover:not(.disabled) {
|
|
2395
|
+
background-color: var(--hover-bg);
|
|
2396
|
+
color: var(--text-primary);
|
|
2397
|
+
}
|
|
2398
|
+
#global-audio-toggle.disabled {
|
|
2399
|
+
color: #555; /* More dimmed */
|
|
2400
|
+
cursor: not-allowed;
|
|
2401
|
+
opacity: 0.5;
|
|
2402
|
+
}
|
|
2403
|
+
#global-audio-toggle.enabled {
|
|
2404
|
+
color: var(--text-primary); /* Brighter when enabled */
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
.reset-view-btn {
|
|
2408
|
+
background: none;
|
|
2409
|
+
border: none;
|
|
2410
|
+
cursor: pointer;
|
|
2411
|
+
padding: 4px;
|
|
2412
|
+
border-radius: 50%;
|
|
2413
|
+
display: inline-flex;
|
|
2414
|
+
align-items: center;
|
|
2415
|
+
justify-content: center;
|
|
2416
|
+
color: var(--text-muted);
|
|
2417
|
+
transition: all 0.2s ease;
|
|
2418
|
+
margin-left: 8px; /* Add some space */
|
|
2419
|
+
}
|
|
2420
|
+
.reset-view-btn:hover {
|
|
2421
|
+
background-color: var(--hover-bg);
|
|
2422
|
+
color: var(--text-primary);
|
|
2423
|
+
}
|
|
2424
|
+
.reset-view-btn svg {
|
|
2425
|
+
stroke: currentColor;
|
|
2426
|
+
width: 20px;
|
|
2427
|
+
height: 20px;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
/* Enhanced Player List */
|
|
2431
|
+
#player-list-area {
|
|
2432
|
+
flex: 1;
|
|
2433
|
+
display: flex;
|
|
2434
|
+
flex-direction: column;
|
|
2435
|
+
min-height: 0;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
#player-list-container {
|
|
2439
|
+
overflow-y: auto;
|
|
2440
|
+
flex-grow: 1;
|
|
2441
|
+
padding-right: 8px;
|
|
2442
|
+
margin-right: -8px;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
#player-list-container::-webkit-scrollbar {
|
|
2446
|
+
width: 6px;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
#player-list-container::-webkit-scrollbar-track {
|
|
2450
|
+
background: rgba(255, 255, 255, 0.05);
|
|
2451
|
+
border-radius: 3px;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
#player-list-container::-webkit-scrollbar-thumb {
|
|
2455
|
+
background: rgba(116, 185, 255, 0.3);
|
|
2456
|
+
border-radius: 3px;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
#player-list-container::-webkit-scrollbar-thumb:hover {
|
|
2460
|
+
background: rgba(116, 185, 255, 0.5);
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
#player-list {
|
|
2464
|
+
list-style: none;
|
|
2465
|
+
padding: 0;
|
|
2466
|
+
margin: 0;
|
|
2467
|
+
display: flex;
|
|
2468
|
+
flex-direction: column;
|
|
2469
|
+
gap: 12px;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/* Enhanced Player Cards */
|
|
2473
|
+
.player-card {
|
|
2474
|
+
position: relative;
|
|
2475
|
+
display: flex;
|
|
2476
|
+
align-items: center;
|
|
2477
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));
|
|
2478
|
+
padding: 16px;
|
|
2479
|
+
border-radius: 12px;
|
|
2480
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
2481
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
2482
|
+
cursor: pointer;
|
|
2483
|
+
overflow: hidden;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
.player-card::before {
|
|
2487
|
+
content: '';
|
|
2488
|
+
position: absolute;
|
|
2489
|
+
left: 0;
|
|
2490
|
+
top: 0;
|
|
2491
|
+
bottom: 0;
|
|
2492
|
+
width: 4px;
|
|
2493
|
+
background: transparent;
|
|
2494
|
+
transition: all 0.3s ease;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
.player-card:hover {
|
|
2498
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.06));
|
|
2499
|
+
border-color: rgba(116, 185, 255, 0.3);
|
|
2500
|
+
transform: translateY(-2px);
|
|
2501
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
.player-card.active {
|
|
2505
|
+
background: linear-gradient(135deg, rgba(253, 203, 110, 0.15), rgba(253, 203, 110, 0.05));
|
|
2506
|
+
border-color: var(--active-border);
|
|
2507
|
+
box-shadow: 0 0 20px var(--active-glow), 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
.player-card.active::before {
|
|
2511
|
+
background: linear-gradient(180deg, var(--active-border), rgba(253, 203, 110, 0.5));
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
.player-card.dead {
|
|
2515
|
+
opacity: 0.5;
|
|
2516
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
|
|
2517
|
+
filter: brightness(0.7);
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/* Enhanced Avatar */
|
|
2521
|
+
.avatar-container {
|
|
2522
|
+
position: relative;
|
|
2523
|
+
width: 40px;
|
|
2524
|
+
height: 40px;
|
|
2525
|
+
margin-right: 16px;
|
|
2526
|
+
flex-shrink: 0;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
.player-card .avatar {
|
|
2530
|
+
width: 100%;
|
|
2531
|
+
height: 100%;
|
|
2532
|
+
border-radius: 50%;
|
|
2533
|
+
object-fit: cover;
|
|
2534
|
+
background-color: #ffffff;
|
|
2535
|
+
border: 2px solid rgba(116, 185, 255, 0.2);
|
|
2536
|
+
transition: all 0.3s ease;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
.player-card:hover .avatar {
|
|
2540
|
+
border-color: rgba(116, 185, 255, 0.4);
|
|
2541
|
+
box-shadow: 0 0 15px rgba(116, 185, 255, 0.2);
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
.player-card.active .avatar {
|
|
2545
|
+
border-color: var(--active-border);
|
|
2546
|
+
box-shadow: 0 0 15px var(--active-glow);
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
.player-card.dead .avatar {
|
|
2550
|
+
filter: var(--dead-filter);
|
|
2551
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
/* Enhanced Player Info */
|
|
2555
|
+
.player-info {
|
|
2556
|
+
flex-grow: 1;
|
|
2557
|
+
overflow: hidden;
|
|
2558
|
+
min-width: 0;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
.player-name {
|
|
2562
|
+
font-weight: 600;
|
|
2563
|
+
font-size: 1.1rem;
|
|
2564
|
+
margin-bottom: 4px;
|
|
2565
|
+
color: var(--text-primary);
|
|
2566
|
+
white-space: nowrap;
|
|
2567
|
+
overflow: hidden;
|
|
2568
|
+
text-overflow: ellipsis;
|
|
2569
|
+
letter-spacing: -0.01em;
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
.player-role {
|
|
2573
|
+
font-size: 0.875rem;
|
|
2574
|
+
color: var(--text-secondary);
|
|
2575
|
+
font-weight: 500;
|
|
2576
|
+
display: flex;
|
|
2577
|
+
align-items: center;
|
|
2578
|
+
gap: 6px;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
.player-role.werewolf { color: var(--werewolf-color); }
|
|
2582
|
+
.player-role.villager { color: var(--villager-color); }
|
|
2583
|
+
.player-role.doctor { color: var(--doctor-color); }
|
|
2584
|
+
.player-role.seer { color: var(--seer-color); }
|
|
2585
|
+
|
|
2586
|
+
.display-name {
|
|
2587
|
+
font-size: 0.8em;
|
|
2588
|
+
color: #888;
|
|
2589
|
+
margin-left: 5px;
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
/* Enhanced Threat Indicator */
|
|
2593
|
+
.threat-indicator {
|
|
2594
|
+
position: absolute;
|
|
2595
|
+
top: 12px;
|
|
2596
|
+
right: 12px;
|
|
2597
|
+
width: 8px;
|
|
2598
|
+
height: 8px;
|
|
2599
|
+
border-radius: 50%;
|
|
2600
|
+
background-color: transparent;
|
|
2601
|
+
transition: all 0.3s ease;
|
|
2602
|
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
/* Enhanced Chat/Event Log */
|
|
2606
|
+
#chat-log {
|
|
2607
|
+
list-style: none;
|
|
2608
|
+
padding: 0;
|
|
2609
|
+
margin: 0;
|
|
2610
|
+
flex-grow: 1;
|
|
2611
|
+
overflow-y: auto;
|
|
2612
|
+
padding-right: 8px;
|
|
2613
|
+
margin-right: -8px;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
#chat-log::-webkit-scrollbar {
|
|
2617
|
+
width: 6px;
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
#chat-log::-webkit-scrollbar-track {
|
|
2621
|
+
background: rgba(255, 255, 255, 0.05);
|
|
2622
|
+
border-radius: 3px;
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
#chat-log::-webkit-scrollbar-thumb {
|
|
2626
|
+
background: rgba(116, 185, 255, 0.3);
|
|
2627
|
+
border-radius: 3px;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
#chat-log::-webkit-scrollbar-thumb:hover {
|
|
2631
|
+
background: rgba(116, 185, 255, 0.5);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
#chat-log li.now-playing > .message-content > .balloon,
|
|
2635
|
+
#chat-log li.now-playing > .moderator-announcement-content,
|
|
2636
|
+
#chat-log li.now-playing.msg-entry {
|
|
2637
|
+
background: linear-gradient(135deg, rgba(253, 203, 110, 0.2), rgba(253, 203, 110, 0.1));
|
|
2638
|
+
border-color: #fdcb6e; /* A bright yellow */
|
|
2639
|
+
box-shadow: 0 0 5px rgba(253, 203, 110, 0.3);
|
|
2640
|
+
transition: all 0.2s ease-in-out;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
/* Enhanced Chat Entries */
|
|
2644
|
+
.chat-entry {
|
|
2645
|
+
display: flex;
|
|
2646
|
+
margin-bottom: 20px;
|
|
2647
|
+
align-items: flex-start;
|
|
2648
|
+
animation: fadeInUp 0.3s ease-out;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
@keyframes fadeInUp {
|
|
2652
|
+
from {
|
|
2653
|
+
opacity: 0;
|
|
2654
|
+
transform: translateY(20px);
|
|
2655
|
+
}
|
|
2656
|
+
to {
|
|
2657
|
+
opacity: 1;
|
|
2658
|
+
transform: translateY(0);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
.chat-avatar {
|
|
2663
|
+
width: 44px;
|
|
2664
|
+
height: 44px;
|
|
2665
|
+
border-radius: 50%;
|
|
2666
|
+
margin-right: 12px;
|
|
2667
|
+
object-fit: cover;
|
|
2668
|
+
flex-shrink: 0;
|
|
2669
|
+
border: 2px solid rgba(116, 185, 255, 0.2);
|
|
2670
|
+
transition: all 0.3s ease;
|
|
2671
|
+
background-color: #ffffff;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
.chat-entry:hover .chat-avatar {
|
|
2675
|
+
border-color: rgba(116, 185, 255, 0.4);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
.message-content {
|
|
2679
|
+
display: flex;
|
|
2680
|
+
flex-direction: column;
|
|
2681
|
+
flex-grow: 1;
|
|
2682
|
+
min-width: 0;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
/* Enhanced Message Bubbles */
|
|
2686
|
+
.balloon {
|
|
2687
|
+
padding: 14px 16px;
|
|
2688
|
+
border-radius: 16px 16px 16px 4px;
|
|
2689
|
+
max-width: 85%;
|
|
2690
|
+
word-wrap: break-word;
|
|
2691
|
+
background: linear-gradient(135deg, rgba(116, 185, 255, 0.1), rgba(116, 185, 255, 0.05));
|
|
2692
|
+
border: 1px solid rgba(116, 185, 255, 0.2);
|
|
2693
|
+
transition: all 0.3s ease;
|
|
2694
|
+
position: relative;
|
|
2695
|
+
line-height: 1.4;
|
|
2696
|
+
font-size: 0.95rem;
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
.balloon:hover {
|
|
2700
|
+
background: linear-gradient(135deg, rgba(116, 185, 255, 0.2), rgba(116, 185, 255, 0.1));
|
|
2701
|
+
border-color: rgba(116, 185, 255, 0.4);
|
|
2702
|
+
// transform: scale(1.01);
|
|
2703
|
+
transform: translateX(2px);
|
|
2704
|
+
cursor: pointer;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
.chat-entry.event-day .balloon {
|
|
2708
|
+
background: linear-gradient(135deg, rgba(255, 193, 7, 0.1), rgba(255, 193, 7, 0.05));
|
|
2709
|
+
border-color: rgba(255, 193, 7, 0.2);
|
|
2710
|
+
color: var(--text-primary);
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
.chat-entry.event-day .balloon:hover {
|
|
2714
|
+
background: linear-gradient(135deg, rgba(255, 193, 7, 0.2), rgba(255, 193, 7, 0.1));
|
|
2715
|
+
border-color: rgba(255, 193, 7, 0.3);
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
.chat-entry.event-night .balloon {
|
|
2719
|
+
background: linear-gradient(135deg, rgba(108, 92, 231, 0.1), rgba(108, 92, 231, 0.05));
|
|
2720
|
+
border-color: rgba(108, 92, 231, 0.2);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
.event-log-list li.now-playing .balloon {
|
|
2724
|
+
background-color: #fcf8e3; /* A light yellow */
|
|
2725
|
+
border-color: #f7d794;
|
|
2726
|
+
transition: background-color 0.3s ease;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
/* Enhanced System Messages */
|
|
2730
|
+
.msg-entry {
|
|
2731
|
+
border-left: 4px solid #f39c12;
|
|
2732
|
+
padding: 16px;
|
|
2733
|
+
margin: 16px 0;
|
|
2734
|
+
border-radius: 8px;
|
|
2735
|
+
background: linear-gradient(135deg, rgba(243, 156, 18, 0.1), rgba(243, 156, 18, 0.05));
|
|
2736
|
+
border: 1px solid rgba(243, 156, 18, 0.2);
|
|
2737
|
+
transition: all 0.3s ease;
|
|
2738
|
+
animation: fadeInUp 0.3s ease-out;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
.msg-entry:hover {
|
|
2742
|
+
background: linear-gradient(135deg, rgba(243, 156, 18, 0.15), rgba(243, 156, 18, 0.08));
|
|
2743
|
+
border-color: rgba(243, 156, 18, 0.3);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
.msg-entry.event-day {
|
|
2747
|
+
background: linear-gradient(135deg, rgba(255, 193, 7, 0.1), rgba(255, 193, 7, 0.05));
|
|
2748
|
+
border-color: rgba(255, 193, 7, 0.2);
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
.msg-entry.event-night {
|
|
2752
|
+
background: linear-gradient(135deg, rgba(108, 92, 231, 0.1), rgba(108, 92, 231, 0.05));
|
|
2753
|
+
border-color: rgba(108, 92, 231, 0.2);
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
.msg-entry.game-event {
|
|
2757
|
+
border-left-color: #e74c3c;
|
|
2758
|
+
background: linear-gradient(135deg, rgba(231, 76, 60, 0.1), rgba(231, 76, 60, 0.05));
|
|
2759
|
+
border-color: rgba(231, 76, 60, 0.2);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
.msg-entry.game-win {
|
|
2763
|
+
border-left-color: #2ecc71;
|
|
2764
|
+
background: linear-gradient(135deg, rgba(46, 204, 113, 0.1), rgba(46, 204, 113, 0.05));
|
|
2765
|
+
border-color: rgba(46, 204, 113, 0.2);
|
|
2766
|
+
line-height: 1.6;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
/* Enhanced Reasoning Text */
|
|
2770
|
+
.reasoning-text {
|
|
2771
|
+
font-size: 0.85rem;
|
|
2772
|
+
color: var(--text-muted);
|
|
2773
|
+
font-style: italic;
|
|
2774
|
+
margin-top: 8px;
|
|
2775
|
+
padding-left: 12px;
|
|
2776
|
+
border-left: 2px solid rgba(116, 185, 255, 0.3);
|
|
2777
|
+
line-height: 1.4;
|
|
2778
|
+
font-family: 'JetBrains Mono', monospace;
|
|
2779
|
+
display: none;
|
|
2780
|
+
}
|
|
2781
|
+
.reasoning-text.visible {
|
|
2782
|
+
display: block;
|
|
2783
|
+
}
|
|
2784
|
+
.reasoning-toggle {
|
|
2785
|
+
cursor: pointer;
|
|
2786
|
+
font-size: 1rem;
|
|
2787
|
+
margin-left: 5;
|
|
2788
|
+
opacity: 0.6;
|
|
2789
|
+
transition: all 0.3s ease;
|
|
2790
|
+
display: inline-flex;
|
|
2791
|
+
align-items: center;
|
|
2792
|
+
}
|
|
2793
|
+
.reasoning-toggle:hover {
|
|
2794
|
+
opacity: 1;
|
|
2795
|
+
}
|
|
2796
|
+
.msg-entry .reasoning-toggle {
|
|
2797
|
+
float: right;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
/* Enhanced Citations */
|
|
2801
|
+
#chat-log cite {
|
|
2802
|
+
font-style: normal;
|
|
2803
|
+
font-weight: 600;
|
|
2804
|
+
display: flex;
|
|
2805
|
+
align-items: center;
|
|
2806
|
+
font-size: 0.9rem;
|
|
2807
|
+
color: var(--text-primary);
|
|
2808
|
+
margin-bottom: 6px;
|
|
2809
|
+
gap: 8px;
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
.cite-text-wrapper {
|
|
2813
|
+
display: flex;
|
|
2814
|
+
flex-direction: column;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
/* Enhanced Moderator Announcements */
|
|
2818
|
+
.moderator-announcement {
|
|
2819
|
+
margin: 16px 0;
|
|
2820
|
+
animation: fadeInUp 0.3s ease-out;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
.moderator-announcement-content {
|
|
2824
|
+
padding: 16px;
|
|
2825
|
+
border-radius: 12px;
|
|
2826
|
+
background: linear-gradient(135deg, rgba(46, 204, 113, 0.1), rgba(46, 204, 113, 0.05));
|
|
2827
|
+
border: 1px solid rgba(46, 204, 113, 0.2);
|
|
2828
|
+
border-left: 4px solid #2ecc71;
|
|
2829
|
+
color: var(--text-primary);
|
|
2830
|
+
line-height: 1.5;
|
|
2831
|
+
transition: all 0.3s ease;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
.moderator-announcement-content:hover {
|
|
2835
|
+
background: linear-gradient(135deg, rgba(46, 204, 113, 0.15), rgba(46, 204, 113, 0.08));
|
|
2836
|
+
border-color: rgba(46, 204, 113, 0.3);
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
/* Enhanced Timestamps */
|
|
2840
|
+
.timestamp {
|
|
2841
|
+
font-size: 0.75rem;
|
|
2842
|
+
color: var(--text-muted);
|
|
2843
|
+
font-weight: 500;
|
|
2844
|
+
font-family: 'JetBrains Mono', monospace;
|
|
2845
|
+
background: rgba(116, 185, 255, 0.1);
|
|
2846
|
+
padding: 2px 6px;
|
|
2847
|
+
border-radius: 4px;
|
|
2848
|
+
margin-left: auto;
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
/* Enhanced Player Capsules */
|
|
2852
|
+
.player-capsule {
|
|
2853
|
+
display: inline-flex;
|
|
2854
|
+
align-items: center;
|
|
2855
|
+
background: linear-gradient(135deg, rgba(116, 185, 255, 0.15), rgba(116, 185, 255, 0.08));
|
|
2856
|
+
border: 1px solid rgba(116, 185, 255, 0.2);
|
|
2857
|
+
border-radius: 16px;
|
|
2858
|
+
padding: 2px 10px 2px 2px;
|
|
2859
|
+
font-size: 0.875rem;
|
|
2860
|
+
font-weight: 500;
|
|
2861
|
+
margin: 0 2px;
|
|
2862
|
+
vertical-align: middle;
|
|
2863
|
+
transition: all 0.3s ease;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
.player-capsule:hover {
|
|
2867
|
+
background: linear-gradient(135deg, rgba(116, 185, 255, 0.2), rgba(116, 185, 255, 0.1));
|
|
2868
|
+
border-color: rgba(116, 185, 255, 0.3);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
.capsule-avatar {
|
|
2872
|
+
width: 20px;
|
|
2873
|
+
height: 20px;
|
|
2874
|
+
border-radius: 50%;
|
|
2875
|
+
margin-right: 6px;
|
|
2876
|
+
object-fit: cover;
|
|
2877
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
2878
|
+
background-color: #ffffff;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
.capsule-display-name {
|
|
2882
|
+
font-size: 0.9em;
|
|
2883
|
+
color: #888;
|
|
2884
|
+
margin-left: 5px;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
/* Enhanced TTS Button */
|
|
2888
|
+
.tts-button {
|
|
2889
|
+
cursor: pointer;
|
|
2890
|
+
font-size: 1.1rem;
|
|
2891
|
+
margin-left: 12px;
|
|
2892
|
+
padding: 4px;
|
|
2893
|
+
border-radius: 50%;
|
|
2894
|
+
transition: all 0.3s ease;
|
|
2895
|
+
opacity: 0.6;
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
.tts-button:hover {
|
|
2899
|
+
opacity: 1;
|
|
2900
|
+
background: rgba(116, 185, 255, 0.1);
|
|
2901
|
+
transform: scale(1.1);
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
/* Enhanced Audio Controls */
|
|
2905
|
+
.audio-controls {
|
|
2906
|
+
padding: 16px 0;
|
|
2907
|
+
border-top: 1px solid rgba(116, 185, 255, 0.2);
|
|
2908
|
+
margin-top: 16px;
|
|
2909
|
+
background: rgba(255, 255, 255, 0.02);
|
|
2910
|
+
border-radius: 8px;
|
|
2911
|
+
padding: 16px;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
.audio-controls label {
|
|
2915
|
+
display: block;
|
|
2916
|
+
margin-bottom: 8px;
|
|
2917
|
+
font-size: 0.875rem;
|
|
2918
|
+
font-weight: 500;
|
|
2919
|
+
color: var(--text-secondary);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
.audio-controls input[type="range"] {
|
|
2923
|
+
width: 100%;
|
|
2924
|
+
height: 6px;
|
|
2925
|
+
border-radius: 3px;
|
|
2926
|
+
background: rgba(116, 185, 255, 0.2);
|
|
2927
|
+
outline: none;
|
|
2928
|
+
-webkit-appearance: none;
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
.audio-controls input[type="range"]::-webkit-slider-thumb {
|
|
2932
|
+
-webkit-appearance: none;
|
|
2933
|
+
width: 16px;
|
|
2934
|
+
height: 16px;
|
|
2935
|
+
border-radius: 50%;
|
|
2936
|
+
background: #74b9ff;
|
|
2937
|
+
cursor: pointer;
|
|
2938
|
+
border: 2px solid #ffffff;
|
|
2939
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
#pause-audio {
|
|
2943
|
+
background-color: rgba(116, 185, 255, 0.1);
|
|
2944
|
+
border: 1px solid rgba(116, 185, 255, 0.3);
|
|
2945
|
+
border-radius: 50%;
|
|
2946
|
+
width: 36px;
|
|
2947
|
+
height: 36px;
|
|
2948
|
+
cursor: pointer;
|
|
2949
|
+
padding: 0;
|
|
2950
|
+
background-size: 16px;
|
|
2951
|
+
background-repeat: no-repeat;
|
|
2952
|
+
background-position: center;
|
|
2953
|
+
transition: all 0.3s ease;
|
|
2954
|
+
filter: none;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
#pause-audio:hover {
|
|
2958
|
+
background-color: rgba(116, 185, 255, 0.2);
|
|
2959
|
+
border-color: rgba(116, 185, 255, 0.5);
|
|
2960
|
+
transform: scale(1.1);
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
#pause-audio.paused {
|
|
2964
|
+
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM3NGI5ZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cG9seWdvbiBwb2ludHM9IjYgMyAyMCAxMiA2IDIxIi8+PC9zdmc+');
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
#pause-audio.playing {
|
|
2968
|
+
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM3NGI5ZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cmVjdCB4PSI2IiB5PSI0IiB3aWR0aD0iNCIgaGVpZ2h0PSIxNiIgcng9IjEiLz48cmVjdCB4PSIxNCIgeT0iNCIgd2lkdGg9IjQiIGhlaWdodD0iMTYiIHJ4PSIxIi8+PC9zdmc+');
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
/* Message text formatting */
|
|
2972
|
+
.msg-text {
|
|
2973
|
+
line-height: 1.5;
|
|
2974
|
+
font-size: 0.95rem;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
.msg-text br {
|
|
2978
|
+
display: block;
|
|
2979
|
+
margin-bottom: 0.5em;
|
|
2980
|
+
content: "";
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
/* Smooth scrolling */
|
|
2984
|
+
* {
|
|
2985
|
+
scrollbar-width: thin;
|
|
2986
|
+
scrollbar-color: rgba(116, 185, 255, 0.3) transparent;
|
|
2987
|
+
}
|
|
2988
|
+
`;
|
|
2989
|
+
|
|
2990
|
+
// --- TTS Management ---
|
|
2991
|
+
const audioMap = window.AUDIO_MAP || {};
|
|
2992
|
+
|
|
2993
|
+
if (!window.kaggleWerewolf) {
|
|
2994
|
+
window.kaggleWerewolf = {
|
|
2995
|
+
audioQueue: [],
|
|
2996
|
+
isAudioPlaying: false,
|
|
2997
|
+
isAudioEnabled: false,
|
|
2998
|
+
isPaused: false,
|
|
2999
|
+
lastPlayedStep: parseInt(sessionStorage.getItem('ww_lastPlayedStep') || '-1', 10),
|
|
3000
|
+
audioPlayer: new Audio(),
|
|
3001
|
+
playbackRate: 1.6,
|
|
3002
|
+
allEvents: null,
|
|
3003
|
+
audioContextActivated: false,
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
const audioState = window.kaggleWerewolf;
|
|
3007
|
+
|
|
3008
|
+
if (audioState.hasAudioTracks === undefined) {
|
|
3009
|
+
audioState.hasAudioTracks = Object.keys(audioMap).length > 0;
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
function setPlaybackRate(rate) {
|
|
3013
|
+
audioState.playbackRate = rate;
|
|
3014
|
+
if (audioState.isAudioPlaying) {
|
|
3015
|
+
audioState.audioPlayer.playbackRate = rate;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
function speak(allEventsIndex) {
|
|
3020
|
+
if (allEventsIndex === undefined) return;
|
|
3021
|
+
|
|
3022
|
+
// 1. Find the corresponding display step for the slider.
|
|
3023
|
+
const displayStep = window.werewolfGamePlayer.allEventsIndexToDisplayStep[allEventsIndex];
|
|
3024
|
+
|
|
3025
|
+
// 2. Jump the slider.
|
|
3026
|
+
// This will automatically trigger our setStep wrapper, which calls
|
|
3027
|
+
// stopAndClearAudio() and sets audioState.isPaused = true.
|
|
3028
|
+
if (displayStep !== undefined && window.kaggle && window.kaggle.setStep) {
|
|
3029
|
+
// window.kaggle.setStep(displayStep);
|
|
3030
|
+
context.__mainContext.setStep(displayStep);
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
// 3. Start continuous playback from that point.
|
|
3034
|
+
// playAudioFrom() will see we are paused, load the new queue,
|
|
3035
|
+
// and immediately start playing.
|
|
3036
|
+
playAudioFrom(allEventsIndex);
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// --- Helper Functions ---
|
|
3040
|
+
function formatTimestamp(isoString) {
|
|
3041
|
+
if (!isoString) return '';
|
|
3042
|
+
try {
|
|
3043
|
+
return new Date(isoString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
|
3044
|
+
} catch (e) {
|
|
3045
|
+
return '';
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
/**
|
|
3050
|
+
* Creates a memoized function to replace player IDs with HTML capsules.
|
|
3051
|
+
* This function pre-computes and caches sorted player data for efficiency.
|
|
3052
|
+
* @param {Map<string, object>} playerMap - A map from player ID to player object.
|
|
3053
|
+
* @returns {function(string): string} A function that takes text and returns it with player IDs replaced.
|
|
3054
|
+
*/
|
|
3055
|
+
function createPlayerIdReplacer(playerMap) {
|
|
3056
|
+
// Cache for already processed text strings (memoization)
|
|
3057
|
+
const textCache = new Map();
|
|
3058
|
+
|
|
3059
|
+
// --- Pre-computation Cache ---
|
|
3060
|
+
const sortedPlayerReplacements = [...playerMap.keys()]
|
|
3061
|
+
.sort((a, b) => b.length - a.length) // Sort by length to match longest names first
|
|
3062
|
+
.map(playerId => {
|
|
3063
|
+
const player = playerMap.get(playerId);
|
|
3064
|
+
if (!player) return null;
|
|
3065
|
+
|
|
3066
|
+
return {
|
|
3067
|
+
capsule: createPlayerCapsule(player),
|
|
3068
|
+
// IMPROVEMENT: This new regex correctly handles both internal periods in names (e.g., 'gemini-1.5-pro')
|
|
3069
|
+
// and sentence-ending periods (e.g., '... says Kai.').
|
|
3070
|
+
// Breakdown:
|
|
3071
|
+
// 1. (^|[^\w.-]) - The prefix boundary must not be a name character. This is unchanged.
|
|
3072
|
+
// 2. (PLAYER_ID) - The player's name.
|
|
3073
|
+
// 3. (\.?) - Optionally captures a single trailing period.
|
|
3074
|
+
// 4. (?![-\w]) - A negative lookahead asserts that the name is not followed by another name character (a-z, 0-9, _, -).
|
|
3075
|
+
// This is the key part that allows a trailing period to be treated as a boundary.
|
|
3076
|
+
regex: new RegExp(`(^|[^\\w.-])(${playerId.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')})(\\.?)(?![\\w-])`, 'g')
|
|
3077
|
+
};
|
|
3078
|
+
}).filter(Boolean);
|
|
3079
|
+
|
|
3080
|
+
return function (text) {
|
|
3081
|
+
if (!text) return '';
|
|
3082
|
+
if (textCache.has(text)) {
|
|
3083
|
+
return textCache.get(text);
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
let newText = text;
|
|
3087
|
+
for (const replacement of sortedPlayerReplacements) {
|
|
3088
|
+
// The replacement string now uses $3 to append the optionally captured period after the capsule.
|
|
3089
|
+
newText = newText.replace(replacement.regex, `$1${replacement.capsule}$3`);
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
textCache.set(text, newText);
|
|
3093
|
+
return newText;
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
function createPlayerCapsule(player) {
|
|
3098
|
+
if (!player) return '';
|
|
3099
|
+
let display_name_elem = (player.display_name && (player.name !== player.display_name)) ? `<span class="capsule-display-name">${player.display_name}</span>` : "";
|
|
3100
|
+
return `<span class="player-capsule" title="${player.name}">
|
|
3101
|
+
<img src="${player.thumbnail}" class="capsule-avatar" alt="${player.name}">
|
|
3102
|
+
<span class="capsule-name">${player.name}</span>${display_name_elem}
|
|
3103
|
+
</span>`;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
function replacePlayerIdsWithCapsules(text, playerIds, playerMap) {
|
|
3107
|
+
if (!text) return '';
|
|
3108
|
+
if (!playerIds || playerIds.length === 0) {
|
|
3109
|
+
return text;
|
|
3110
|
+
}
|
|
3111
|
+
let newText = text;
|
|
3112
|
+
const sortedPlayerIds = [...playerIds].sort((a, b) => b.length - a.length);
|
|
3113
|
+
|
|
3114
|
+
sortedPlayerIds.forEach(playerId => {
|
|
3115
|
+
const player = playerMap.get(playerId);
|
|
3116
|
+
if (player) {
|
|
3117
|
+
const capsule = createPlayerCapsule(player);
|
|
3118
|
+
const escapedPlayerId = playerId.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
3119
|
+
|
|
3120
|
+
// Using the same improved regex as in the factory function.
|
|
3121
|
+
const regex = new RegExp(`(^|[^\\w.-])(${escapedPlayerId})(\\.?)(?![\\w-])`, 'g');
|
|
3122
|
+
|
|
3123
|
+
// The replacement correctly places the captured prefix ($1) and optional period ($3) around the capsule.
|
|
3124
|
+
newText = newText.replace(regex, `$1${capsule}$3`);
|
|
3125
|
+
}
|
|
3126
|
+
});
|
|
3127
|
+
return newText;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
function replacePlayerIdsWithBold(text, playerIds) {
|
|
3131
|
+
if (!text) return '';
|
|
3132
|
+
if (!playerIds || playerIds.length === 0) {
|
|
3133
|
+
return text;
|
|
3134
|
+
}
|
|
3135
|
+
let newText = text;
|
|
3136
|
+
const sortedPlayerIds = [...playerIds].sort((a, b) => b.length - a.length);
|
|
3137
|
+
|
|
3138
|
+
sortedPlayerIds.forEach(playerId => {
|
|
3139
|
+
const regex = new RegExp(`\b${playerId.replace(/[-\/\\^$*+?.()|[\\]{}/g, '\\$&')}\b`, 'g');
|
|
3140
|
+
newText = newText.replace(regex, `<strong>${playerId}</strong>`);
|
|
3141
|
+
});
|
|
3142
|
+
return newText;
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
|
|
3146
|
+
function getThreatColor(threatLevel) {
|
|
3147
|
+
const value = Math.max(0, Math.min(1, threatLevel));
|
|
3148
|
+
const hue = 120 * (1 - value);
|
|
3149
|
+
return `hsl(${hue}, 100%, 50%)`;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
function updatePlayerList(container, gameState, actingPlayerName) {
|
|
3153
|
+
// Get or create header
|
|
3154
|
+
let header = container.querySelector('h1');
|
|
3155
|
+
if (!header) {
|
|
3156
|
+
header = document.createElement('h1');
|
|
3157
|
+
// Create a span for the title to sit next to the button
|
|
3158
|
+
const titleSpan = document.createElement('span');
|
|
3159
|
+
titleSpan.textContent = 'Players';
|
|
3160
|
+
header.appendChild(titleSpan);
|
|
3161
|
+
|
|
3162
|
+
// Create the reset button
|
|
3163
|
+
const resetButton = document.createElement('button');
|
|
3164
|
+
resetButton.id = 'reset-view-btn';
|
|
3165
|
+
resetButton.className = 'reset-view-btn';
|
|
3166
|
+
resetButton.title = 'Reset Camera View';
|
|
3167
|
+
resetButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2v6h6"/><path d="M21 12A9 9 0 0 0 6 5.3L3 8"/><path d="M21 22v-6h-6"/><path d="M3 12a9 9 0 0 0 15 6.7l3-2.7"/></svg>`;
|
|
3168
|
+
|
|
3169
|
+
header.appendChild(resetButton);
|
|
3170
|
+
container.appendChild(header);
|
|
3171
|
+
|
|
3172
|
+
// Add the click listener only once, when the button is created
|
|
3173
|
+
resetButton.onclick = () => {
|
|
3174
|
+
if (threeState && threeState.demo) {
|
|
3175
|
+
threeState.demo.resetCameraView();
|
|
3176
|
+
}
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
// Get or create list container
|
|
3181
|
+
let listContainer = container.querySelector('#player-list-container');
|
|
3182
|
+
if (!listContainer) {
|
|
3183
|
+
listContainer = document.createElement('div');
|
|
3184
|
+
listContainer.id = 'player-list-container';
|
|
3185
|
+
container.appendChild(listContainer);
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// Get or create player list
|
|
3189
|
+
let playerUl = listContainer.querySelector('#player-list');
|
|
3190
|
+
if (!playerUl) {
|
|
3191
|
+
playerUl = document.createElement('ul');
|
|
3192
|
+
playerUl.id = 'player-list';
|
|
3193
|
+
listContainer.appendChild(playerUl);
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// Update player cards
|
|
3197
|
+
gameState.players.forEach((player, index) => {
|
|
3198
|
+
let li = playerUl.children[index];
|
|
3199
|
+
if (!li) {
|
|
3200
|
+
li = document.createElement('li');
|
|
3201
|
+
playerUl.appendChild(li);
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
// Add the onclick handler of player's first person perspective
|
|
3205
|
+
// This will call the focus function on the Three.js demo instance
|
|
3206
|
+
li.onclick = () => {
|
|
3207
|
+
if (threeState && threeState.demo) {
|
|
3208
|
+
// Get the current widths of the UI panels
|
|
3209
|
+
const leftPanel = parent.querySelector('.left-panel');
|
|
3210
|
+
const rightPanel = parent.querySelector('.right-panel');
|
|
3211
|
+
const leftPanelWidth = leftPanel ? leftPanel.offsetWidth : 0;
|
|
3212
|
+
const rightPanelWidth = rightPanel ? rightPanel.offsetWidth : 0;
|
|
3213
|
+
|
|
3214
|
+
// Pass the panel widths to the focus function
|
|
3215
|
+
threeState.demo.focusOnPlayer(player.name, leftPanelWidth, rightPanelWidth);
|
|
3216
|
+
}
|
|
3217
|
+
};
|
|
3218
|
+
|
|
3219
|
+
// Update player card classes
|
|
3220
|
+
li.className = 'player-card';
|
|
3221
|
+
if (!player.is_alive) li.classList.add('dead');
|
|
3222
|
+
if (player.name === actingPlayerName) li.classList.add('active');
|
|
3223
|
+
|
|
3224
|
+
let roleDisplay = player.role;
|
|
3225
|
+
if (player.role === 'Werewolf') {
|
|
3226
|
+
roleDisplay = `🐺 ${player.role}`;
|
|
3227
|
+
} else if (player.role === 'Doctor') {
|
|
3228
|
+
roleDisplay = `🩺 ${player.role}`;
|
|
3229
|
+
} else if (player.role === 'Seer') {
|
|
3230
|
+
roleDisplay = `🔮 ${player.role}`;
|
|
3231
|
+
} else if (player.role === 'Villager') {
|
|
3232
|
+
roleDisplay = `🧑 ${player.role}`;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
const roleText = player.role !== 'Unknown' ? `Role: ${roleDisplay}` : 'Role: Unknown';
|
|
3236
|
+
|
|
3237
|
+
// Update content
|
|
3238
|
+
let player_name_element = `<div class="player-name" title="${player.name}">${player.name}</div>`
|
|
3239
|
+
if (player.display_name && player.display_name !== player.name) {
|
|
3240
|
+
player_name_element = `<div class="player-name" title="${player.name}">
|
|
3241
|
+
${player.name}<span class="display-name">${player.display_name}</span>
|
|
3242
|
+
</div>`
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
li.innerHTML = `
|
|
3246
|
+
<div class="avatar-container">
|
|
3247
|
+
<img src="${player.thumbnail}" alt="${player.name}" class="avatar">
|
|
3248
|
+
</div>
|
|
3249
|
+
<div class="player-info">
|
|
3250
|
+
${player_name_element}
|
|
3251
|
+
<div class="player-role">${roleText}</div>
|
|
3252
|
+
</div>
|
|
3253
|
+
<div class="threat-indicator"></div>
|
|
3254
|
+
`;
|
|
3255
|
+
|
|
3256
|
+
// Update threat indicator
|
|
3257
|
+
const indicator = li.querySelector('.threat-indicator');
|
|
3258
|
+
if (indicator && player.is_alive) {
|
|
3259
|
+
const threatLevel = gameState.playerThreatLevels.get(player.name) || 0;
|
|
3260
|
+
indicator.style.backgroundColor = getThreatColor(threatLevel);
|
|
3261
|
+
} else if (indicator) {
|
|
3262
|
+
indicator.style.backgroundColor = 'transparent';
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
|
|
3266
|
+
// Remove excess player cards
|
|
3267
|
+
while (playerUl.children.length > gameState.players.length) {
|
|
3268
|
+
playerUl.removeChild(playerUl.lastChild);
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
function updateEventLog(container, gameState, playerMap) {
|
|
3275
|
+
const audioState = window.kaggleWerewolf;
|
|
3276
|
+
const audioToggleDisabled = !audioState.hasAudioTracks;
|
|
3277
|
+
const audioToggleEnabled = audioState.isAudioEnabled && !audioToggleDisabled;
|
|
3278
|
+
const audioToggleTitle = audioToggleDisabled ? 'Audio Not Available' : 'Toggle Audio';
|
|
3279
|
+
const audioToggleIcon = audioToggleEnabled ? '🔊' : '🔇'; // Speaker vs Muted
|
|
3280
|
+
const audioToggleClasses = `audio-toggle-btn ${audioToggleDisabled ? 'disabled' : ''} ${audioToggleEnabled ? 'enabled' : ''}`;
|
|
3281
|
+
|
|
3282
|
+
container.innerHTML = `
|
|
3283
|
+
<h1>
|
|
3284
|
+
<span>Events</span>
|
|
3285
|
+
<div id="header-controls" style="display: flex; align-items: center; gap: 15px;">
|
|
3286
|
+
<button id="global-reasoning-toggle" title="Toggle All Reasoning">
|
|
3287
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
|
3288
|
+
</button>
|
|
3289
|
+
<div id="speed-control-container" style="display: flex; align-items: center; gap: 5px;">
|
|
3290
|
+
<input type="range" id="playback-speed" min="0.5" max="2.5" step="0.1" value="${audioState.playbackRate}" style="width: 70px;">
|
|
3291
|
+
<span id="speed-label" style="font-size: 12px; min-width: 30px;">${audioState.playbackRate.toFixed(1)}x</span>
|
|
3292
|
+
</div>
|
|
3293
|
+
<button id="global-audio-toggle" title="${audioToggleTitle}" class="${audioToggleClasses}">
|
|
3294
|
+
${audioToggleIcon}
|
|
3295
|
+
</button>
|
|
3296
|
+
</div>
|
|
3297
|
+
</h1>
|
|
3298
|
+
`;
|
|
3299
|
+
|
|
3300
|
+
const logUl = document.createElement('ul');
|
|
3301
|
+
logUl.id = 'chat-log';
|
|
3302
|
+
|
|
3303
|
+
const logEntries = gameState.eventLog;
|
|
3304
|
+
|
|
3305
|
+
if (logEntries.length === 0) {
|
|
3306
|
+
const li = document.createElement('li');
|
|
3307
|
+
li.className = 'msg-entry';
|
|
3308
|
+
li.innerHTML = `<cite>System</cite><div>The game is about to begin...</div>`;
|
|
3309
|
+
logUl.appendChild(li);
|
|
3310
|
+
} else {
|
|
3311
|
+
logEntries.forEach( (entry, entryIndex) => {
|
|
3312
|
+
const li = document.createElement('li');
|
|
3313
|
+
li.dataset.allEventsIndex = entry.allEventsIndex;
|
|
3314
|
+
let reasoningHtml = '';
|
|
3315
|
+
let reasoningToggleHtml = '';
|
|
3316
|
+
if (entry.reasoning) {
|
|
3317
|
+
const reasoningId = `reasoning-${window.werewolfGamePlayer.reasoningCounter++}`;
|
|
3318
|
+
reasoningHtml = `<div class="reasoning-text" id="${reasoningId}">"${entry.reasoning}"</div>`;
|
|
3319
|
+
reasoningToggleHtml = `<span class="reasoning-toggle" title="Show/Hide Reasoning" onclick="event.stopPropagation(); document.getElementById('${reasoningId}').classList.toggle('visible')">
|
|
3320
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
|
3321
|
+
</span>`;
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
let phase = (entry.phase || 'Day').toUpperCase();
|
|
3325
|
+
const entryType = entry.type;
|
|
3326
|
+
const systemText = (entry.text || '').toLowerCase();
|
|
3327
|
+
|
|
3328
|
+
const phaseClass = `event-${phase.toLowerCase()}`;
|
|
3329
|
+
|
|
3330
|
+
let phaseEmoji = phase;
|
|
3331
|
+
if (phase === 'DAY') {
|
|
3332
|
+
phaseEmoji = '☀️';
|
|
3333
|
+
} else if (phase === 'NIGHT') {
|
|
3334
|
+
phaseEmoji = '🌙';
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
const dayPhaseString = entry.day !== Infinity ? `[${phaseEmoji} ${entry.day}]` : '';
|
|
3338
|
+
const timestampHtml = `<span class="timestamp">${dayPhaseString} ${formatTimestamp(entry.timestamp)}</span>`;
|
|
3339
|
+
|
|
3340
|
+
switch (entry.type) {
|
|
3341
|
+
case 'chat':
|
|
3342
|
+
const speaker = playerMap.get(entry.speaker);
|
|
3343
|
+
if (!speaker) return;
|
|
3344
|
+
const messageText = window.werewolfGamePlayer.playerIdReplacer(entry.message);
|
|
3345
|
+
li.className = `chat-entry event-day`;
|
|
3346
|
+
li.innerHTML = `
|
|
3347
|
+
<img src="${speaker.thumbnail}" alt="${speaker.name}" class="chat-avatar">
|
|
3348
|
+
<div class="message-content">
|
|
3349
|
+
<cite>
|
|
3350
|
+
<span> <span>${speaker.name}</span>
|
|
3351
|
+
${speaker.display_name && speaker.name !== speaker.display_name ? `<span class="display-name">${speaker.display_name}</span>` : ''}
|
|
3352
|
+
</span> ${reasoningToggleHtml}
|
|
3353
|
+
${timestampHtml}
|
|
3354
|
+
</cite>
|
|
3355
|
+
<div class="balloon">
|
|
3356
|
+
<div class="balloon-text">
|
|
3357
|
+
<quote>${messageText}</quote>
|
|
3358
|
+
</div>
|
|
3359
|
+
${reasoningHtml}
|
|
3360
|
+
</div>
|
|
3361
|
+
</div>
|
|
3362
|
+
`;
|
|
3363
|
+
const balloon = li.querySelector('.balloon');
|
|
3364
|
+
if (balloon) {
|
|
3365
|
+
balloon.onclick = (e) => {
|
|
3366
|
+
e.stopPropagation();
|
|
3367
|
+
// This will either play (if audio is enabled)
|
|
3368
|
+
// or queue the request (if audio is disabled)
|
|
3369
|
+
speak(entry.allEventsIndex);
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
break;
|
|
3373
|
+
case 'seer_inspection':
|
|
3374
|
+
const seerInspector = playerMap.get(entry.actor_id);
|
|
3375
|
+
if (!seerInspector) return;
|
|
3376
|
+
const seerTargetCap = createPlayerCapsule(playerMap.get(entry.target));
|
|
3377
|
+
const seerCap = createPlayerCapsule(playerMap.get(entry.actor_id));
|
|
3378
|
+
li.className = `chat-entry event-night`;
|
|
3379
|
+
li.innerHTML = `
|
|
3380
|
+
<img src="${seerInspector.thumbnail}" alt="${seerInspector.name}" class="chat-avatar">
|
|
3381
|
+
<div class="message-content">
|
|
3382
|
+
<cite>Seer Secret Inspect ${reasoningToggleHtml} ${timestampHtml}</cite>
|
|
3383
|
+
<div class="balloon">
|
|
3384
|
+
<div class="balloon-text">${seerCap} chose to inspect ${seerTargetCap}'s role.</div>
|
|
3385
|
+
${reasoningHtml}
|
|
3386
|
+
</div>
|
|
3387
|
+
</div>
|
|
3388
|
+
`;
|
|
3389
|
+
const seer_balloon = li.querySelector('.balloon');
|
|
3390
|
+
if (seer_balloon) {
|
|
3391
|
+
seer_balloon.onclick = (e) => {
|
|
3392
|
+
e.stopPropagation();
|
|
3393
|
+
// This will either play (if audio is enabled)
|
|
3394
|
+
// or queue the request (if audio is disabled)
|
|
3395
|
+
speak(entry.allEventsIndex);
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
break;
|
|
3399
|
+
case 'seer_inspection_result':
|
|
3400
|
+
const seerResultViewer = playerMap.get(entry.seer);
|
|
3401
|
+
if (!seerResultViewer) return;
|
|
3402
|
+
const seerCap_ = createPlayerCapsule(playerMap.get(entry.seer));
|
|
3403
|
+
const seerResultTargetCap = createPlayerCapsule(playerMap.get(entry.target));
|
|
3404
|
+
const resultString = entry.role
|
|
3405
|
+
? `role is a <strong>${entry.role}</strong>`
|
|
3406
|
+
: `team is <strong>${entry.team}</strong>`;
|
|
3407
|
+
|
|
3408
|
+
li.className = `chat-entry ${phaseClass}`;
|
|
3409
|
+
li.innerHTML = `
|
|
3410
|
+
<img src="${seerResultViewer.thumbnail}" alt="${seerResultViewer.name}" class="chat-avatar">
|
|
3411
|
+
<div class="message-content">
|
|
3412
|
+
<cite>Seer Inspect Result ${timestampHtml}</cite>
|
|
3413
|
+
<div class="balloon">
|
|
3414
|
+
<div class="balloon-text">${seerCap_} saw ${seerResultTargetCap}'s ${resultString}.</div>
|
|
3415
|
+
</div>
|
|
3416
|
+
</div>
|
|
3417
|
+
`;
|
|
3418
|
+
const seer_balloon_ = li.querySelector('.balloon');
|
|
3419
|
+
if (seer_balloon_) {
|
|
3420
|
+
seer_balloon_.onclick = (e) => {
|
|
3421
|
+
e.stopPropagation();
|
|
3422
|
+
// This will either play (if audio is enabled)
|
|
3423
|
+
// or queue the request (if audio is disabled)
|
|
3424
|
+
speak(entry.allEventsIndex);
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
break;
|
|
3428
|
+
case 'doctor_heal_action':
|
|
3429
|
+
const doctor = playerMap.get(entry.actor_id);
|
|
3430
|
+
if (!doctor) return;
|
|
3431
|
+
const docTargetCap = createPlayerCapsule(playerMap.get(entry.target));
|
|
3432
|
+
const docCap = createPlayerCapsule(playerMap.get(entry.actor_id));
|
|
3433
|
+
li.className = `chat-entry event-night`;
|
|
3434
|
+
li.innerHTML = `
|
|
3435
|
+
<img src="${doctor.thumbnail}" alt="${doctor.name}" class="chat-avatar">
|
|
3436
|
+
<div class="message-content">
|
|
3437
|
+
<cite>Doctor Secret Heal ${reasoningToggleHtml} ${timestampHtml}</cite>
|
|
3438
|
+
<div class="balloon">
|
|
3439
|
+
<div class="balloon-text">${docCap} chose to heal ${docTargetCap}.</div>
|
|
3440
|
+
${reasoningHtml}
|
|
3441
|
+
</div>
|
|
3442
|
+
</div>
|
|
3443
|
+
`;
|
|
3444
|
+
const dr_balloon = li.querySelector('.balloon');
|
|
3445
|
+
if (dr_balloon) {
|
|
3446
|
+
dr_balloon.onclick = (e) => {
|
|
3447
|
+
e.stopPropagation();
|
|
3448
|
+
// This will either play (if audio is enabled)
|
|
3449
|
+
// or queue the request (if audio is disabled)
|
|
3450
|
+
speak(entry.allEventsIndex);
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
break;
|
|
3454
|
+
case 'system':
|
|
3455
|
+
if (entry.text && entry.text.includes('has begun')) return;
|
|
3456
|
+
|
|
3457
|
+
let systemText = entry.text;
|
|
3458
|
+
|
|
3459
|
+
// This enhanced regex captures the list content (group 1) and any optional
|
|
3460
|
+
// trailing punctuation like a period or comma (group 2).
|
|
3461
|
+
const listRegex = /\[(.*?)\](\s*[.,?!])?/g;
|
|
3462
|
+
|
|
3463
|
+
systemText = systemText.replace(listRegex, (match, listContent, punctuation) => {
|
|
3464
|
+
// Clean the list content as before
|
|
3465
|
+
const cleanedContent = listContent.replace(/'/g, "").replace(/, /g, " ").trim();
|
|
3466
|
+
|
|
3467
|
+
// If punctuation was captured, return the content with a space before the punctuation
|
|
3468
|
+
if (punctuation) {
|
|
3469
|
+
return cleanedContent + " " + punctuation.trim();
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// Otherwise, just return the cleaned content
|
|
3473
|
+
return cleanedContent;
|
|
3474
|
+
});
|
|
3475
|
+
|
|
3476
|
+
// NOW, run the efficient replacer on the cleaned-up string.
|
|
3477
|
+
const finalSystemText = window.werewolfGamePlayer.playerIdReplacer(systemText);
|
|
3478
|
+
|
|
3479
|
+
li.className = `moderator-announcement`;
|
|
3480
|
+
li.innerHTML = `
|
|
3481
|
+
<cite>Moderator
|
|
3482
|
+
${timestampHtml}</cite>
|
|
3483
|
+
<div class="moderator-announcement-content ${phaseClass}">
|
|
3484
|
+
<div class="msg-text">${finalSystemText.replace(/\n/g, '<br>')}</div>
|
|
3485
|
+
</div>
|
|
3486
|
+
`;
|
|
3487
|
+
|
|
3488
|
+
const content = li.querySelector('.moderator-announcement-content');
|
|
3489
|
+
if (content) {
|
|
3490
|
+
content.style.cursor = 'pointer'; // Optional: make it look clickable
|
|
3491
|
+
content.onclick = (e) => {
|
|
3492
|
+
e.stopPropagation();
|
|
3493
|
+
speak(entry.allEventsIndex);
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
break;
|
|
3497
|
+
case 'exile':
|
|
3498
|
+
const exiledPlayerCap = createPlayerCapsule(playerMap.get(entry.name));
|
|
3499
|
+
li.className = `msg-entry game-event event-day`;
|
|
3500
|
+
let role_text = (entry.role) ? ` (${entry.role})` : "";
|
|
3501
|
+
li.innerHTML = `<cite>Exile ${timestampHtml}</cite><div class="msg-text">${exiledPlayerCap}${role_text} was exiled by vote.</div>`;
|
|
3502
|
+
li.style.cursor = 'pointer';
|
|
3503
|
+
li.onclick = (e) => {
|
|
3504
|
+
e.stopPropagation();
|
|
3505
|
+
speak(entry.allEventsIndex);
|
|
3506
|
+
};
|
|
3507
|
+
break;
|
|
3508
|
+
case 'elimination':
|
|
3509
|
+
const elimPlayerCap = createPlayerCapsule(playerMap.get(entry.name));
|
|
3510
|
+
li.className = `msg-entry game-event event-night`;
|
|
3511
|
+
let elim_role_text = (entry.role) ? ` Their role was a ${entry.role}.` : "";
|
|
3512
|
+
li.innerHTML = `<cite>Elimination ${timestampHtml}</cite><div class="msg-text">${elimPlayerCap} was eliminated.${elim_role_text}</div>`;
|
|
3513
|
+
li.style.cursor = 'pointer';
|
|
3514
|
+
li.onclick = (e) => {
|
|
3515
|
+
e.stopPropagation();
|
|
3516
|
+
speak(entry.allEventsIndex);
|
|
3517
|
+
};
|
|
3518
|
+
break;
|
|
3519
|
+
case 'save':
|
|
3520
|
+
const savedPlayerCap = createPlayerCapsule(playerMap.get(entry.saved_player));
|
|
3521
|
+
li.className = `msg-entry event-night`;
|
|
3522
|
+
li.innerHTML = `<cite>Doctor Save ${timestampHtml}</cite><div class="msg-text">Player ${savedPlayerCap} was attacked but saved by a Doctor!</div>`;
|
|
3523
|
+
li.style.cursor = 'pointer';
|
|
3524
|
+
li.onclick = (e) => {
|
|
3525
|
+
e.stopPropagation();
|
|
3526
|
+
speak(entry.allEventsIndex);
|
|
3527
|
+
};
|
|
3528
|
+
break;
|
|
3529
|
+
case 'vote':
|
|
3530
|
+
const voter = playerMap.get(entry.actor_id);
|
|
3531
|
+
if (!voter) return;
|
|
3532
|
+
const voterCap = createPlayerCapsule(playerMap.get(entry.actor_id));
|
|
3533
|
+
const voteTargetCap = createPlayerCapsule(playerMap.get(entry.target));
|
|
3534
|
+
li.className = `chat-entry event-day`;
|
|
3535
|
+
li.innerHTML = `
|
|
3536
|
+
<img src="${voter.thumbnail}" alt="${voter.name}" class="chat-avatar">
|
|
3537
|
+
<div class="message-content">
|
|
3538
|
+
<cite>
|
|
3539
|
+
<span> <span>${voter.name}</span>
|
|
3540
|
+
${voter.display_name && voter.name !== voter.display_name ? `<span class="display-name">${voter.display_name}</span>` : ''}
|
|
3541
|
+
</span> ${reasoningToggleHtml}
|
|
3542
|
+
${timestampHtml}
|
|
3543
|
+
</cite>
|
|
3544
|
+
<div class="balloon">
|
|
3545
|
+
<div class="balloon-text">${voterCap} votes to exile ${voteTargetCap}.</div>
|
|
3546
|
+
${reasoningHtml}
|
|
3547
|
+
</div>
|
|
3548
|
+
</div>
|
|
3549
|
+
`;
|
|
3550
|
+
const vote_balloon = li.querySelector('.balloon');
|
|
3551
|
+
if (vote_balloon) {
|
|
3552
|
+
vote_balloon.onclick = (e) => {
|
|
3553
|
+
e.stopPropagation();
|
|
3554
|
+
speak(entry.allEventsIndex);
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
break;
|
|
3558
|
+
case 'timeout':
|
|
3559
|
+
const to_voter = playerMap.get(entry.actor_id);
|
|
3560
|
+
if (!to_voter) return;
|
|
3561
|
+
const to_voterCap = createPlayerCapsule(playerMap.get(entry.actor_id));
|
|
3562
|
+
li.className = `chat-entry event-day`;
|
|
3563
|
+
li.innerHTML = `
|
|
3564
|
+
<img src="${to_voter.thumbnail}" alt="${to_voter.name}" class="chat-avatar">
|
|
3565
|
+
<div class="message-content">
|
|
3566
|
+
<cite>${to_voter.name} ${reasoningToggleHtml} ${timestampHtml}</cite>
|
|
3567
|
+
<div class="balloon">
|
|
3568
|
+
<div class="balloon-text">${to_voterCap} timed out and abstained from voting.</div>
|
|
3569
|
+
${reasoningHtml}
|
|
3570
|
+
</div>
|
|
3571
|
+
</div>
|
|
3572
|
+
`;
|
|
3573
|
+
break;
|
|
3574
|
+
case 'night_vote':
|
|
3575
|
+
const nightVoter = playerMap.get(entry.actor_id);
|
|
3576
|
+
if (!nightVoter) return;
|
|
3577
|
+
const nightVoterCap = createPlayerCapsule(playerMap.get(entry.actor_id));
|
|
3578
|
+
const nightVoteTargetCap = createPlayerCapsule(playerMap.get(entry.target));
|
|
3579
|
+
li.className = `chat-entry event-night`;
|
|
3580
|
+
li.innerHTML = `
|
|
3581
|
+
<img src="${nightVoter.thumbnail}" alt="${nightVoter.name}" class="chat-avatar">
|
|
3582
|
+
<div class="message-content">
|
|
3583
|
+
<cite>Werewolf Secret Vote ${reasoningToggleHtml} ${timestampHtml}</cite>
|
|
3584
|
+
<div class="balloon">
|
|
3585
|
+
<div class="balloon-text">${nightVoterCap} votes to eliminate ${nightVoteTargetCap}.</div>
|
|
3586
|
+
${reasoningHtml}
|
|
3587
|
+
</div>
|
|
3588
|
+
</div>
|
|
3589
|
+
`;
|
|
3590
|
+
const nvote_balloon = li.querySelector('.balloon');
|
|
3591
|
+
if (nvote_balloon) {
|
|
3592
|
+
nvote_balloon.onclick = (e) => {
|
|
3593
|
+
e.stopPropagation();
|
|
3594
|
+
speak(entry.allEventsIndex);
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
break;
|
|
3598
|
+
case 'game_over':
|
|
3599
|
+
const winnersText = entry.winners.map(p => createPlayerCapsule(playerMap.get(p))).join(' ');
|
|
3600
|
+
const losersText = entry.losers.map(p => createPlayerCapsule(playerMap.get(p))).join(' ');
|
|
3601
|
+
li.className = `msg-entry game-win ${phaseClass}`;
|
|
3602
|
+
li.innerHTML = `
|
|
3603
|
+
<cite>Game Over ${timestampHtml}</cite>
|
|
3604
|
+
<div class="msg-text">
|
|
3605
|
+
<div>The <strong>${entry.winner}</strong> team has won!</div><br>
|
|
3606
|
+
<div><strong>Winning Team:</strong> ${winnersText}</div>
|
|
3607
|
+
<div><strong>Losing Team:</strong> ${losersText}</div>
|
|
3608
|
+
</div>
|
|
3609
|
+
`;
|
|
3610
|
+
li.style.cursor = 'pointer';
|
|
3611
|
+
li.onclick = (e) => {
|
|
3612
|
+
e.stopPropagation();
|
|
3613
|
+
speak(entry.allEventsIndex);
|
|
3614
|
+
};
|
|
3615
|
+
break;
|
|
3616
|
+
}
|
|
3617
|
+
if (li.innerHTML) logUl.appendChild(li);
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
container.appendChild(logUl);
|
|
3622
|
+
logUl.scrollTop = logUl.scrollHeight;
|
|
3623
|
+
|
|
3624
|
+
const globalToggle = container.querySelector('#global-reasoning-toggle');
|
|
3625
|
+
if (globalToggle) {
|
|
3626
|
+
globalToggle.addEventListener('click', (event) => {
|
|
3627
|
+
event.stopPropagation();
|
|
3628
|
+
const reasoningTexts = logUl.querySelectorAll('.reasoning-text');
|
|
3629
|
+
if (reasoningTexts.length === 0) return;
|
|
3630
|
+
|
|
3631
|
+
// Determine if we should show or hide all. If any are visible, we hide all. Otherwise, show all.
|
|
3632
|
+
const shouldShow = ![...reasoningTexts].some(el => el.classList.contains('visible'));
|
|
3633
|
+
|
|
3634
|
+
reasoningTexts.forEach(el => {
|
|
3635
|
+
el.classList.toggle('visible', shouldShow);
|
|
3636
|
+
});
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
const globalAudioToggle = container.querySelector('#global-audio-toggle');
|
|
3641
|
+
if (globalAudioToggle) {
|
|
3642
|
+
globalAudioToggle.addEventListener('click', (event) => {
|
|
3643
|
+
event.stopPropagation();
|
|
3644
|
+
if (globalAudioToggle.classList.contains('disabled')) return;
|
|
3645
|
+
|
|
3646
|
+
const audioState = window.kaggleWerewolf; // Get the state
|
|
3647
|
+
const wasEnabled = audioState.isAudioEnabled;
|
|
3648
|
+
|
|
3649
|
+
if (wasEnabled) {
|
|
3650
|
+
// --- DISABLING ---
|
|
3651
|
+
audioState.isAudioEnabled = false;
|
|
3652
|
+
globalAudioToggle.classList.remove('enabled');
|
|
3653
|
+
globalAudioToggle.innerHTML = '🔇'; // Muted icon
|
|
3654
|
+
|
|
3655
|
+
stopAndClearAudio(); // Stop current playback
|
|
3656
|
+
audioState.isPaused = true; // Ensure it stays paused
|
|
3657
|
+
|
|
3658
|
+
// Update left-panel pause button
|
|
3659
|
+
} else {
|
|
3660
|
+
// --- ENABLING ---
|
|
3661
|
+
audioState.isAudioEnabled = true;
|
|
3662
|
+
globalAudioToggle.classList.add('enabled');
|
|
3663
|
+
globalAudioToggle.innerHTML = '🔊'; // Speaker icon
|
|
3664
|
+
|
|
3665
|
+
// Activate audio context if this is the very first time
|
|
3666
|
+
if (!audioState.audioContextActivated) {
|
|
3667
|
+
const audio = new Audio('data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA');
|
|
3668
|
+
audio.play().catch(e => console.warn("Audio context activation failed:", e));
|
|
3669
|
+
audioState.audioContextActivated = true; // Set new flag
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
// Check if there was a pending playback request (e.g., from play button)
|
|
3673
|
+
if (audioState.pendingPlaybackRequest) {
|
|
3674
|
+
const { startIndex, isContinuous } = audioState.pendingPlaybackRequest;
|
|
3675
|
+
audioState.pendingPlaybackRequest = null;
|
|
3676
|
+
// We are now enabled, so this will work.
|
|
3677
|
+
playAudioFrom(startIndex, isContinuous);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
const speedSlider = container.querySelector('#playback-speed');
|
|
3684
|
+
const speedLabel = container.querySelector('#speed-label');
|
|
3685
|
+
|
|
3686
|
+
if (speedSlider) {
|
|
3687
|
+
speedSlider.addEventListener('input', (e) => {
|
|
3688
|
+
const newRate = parseFloat(e.target.value);
|
|
3689
|
+
setPlaybackRate(newRate);
|
|
3690
|
+
if(speedLabel) speedLabel.textContent = newRate.toFixed(1) + 'x';
|
|
3691
|
+
});
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
function renderPlayerList(container, gameState, actingPlayerName) {
|
|
3696
|
+
container.innerHTML = '<h1>Players</h1>';
|
|
3697
|
+
const listContainer = document.createElement('div');
|
|
3698
|
+
listContainer.id = 'player-list-container';
|
|
3699
|
+
const playerUl = document.createElement('ul');
|
|
3700
|
+
playerUl.id = 'player-list';
|
|
3701
|
+
|
|
3702
|
+
gameState.players.forEach(player => {
|
|
3703
|
+
const li = document.createElement('li');
|
|
3704
|
+
li.className = 'player-card';
|
|
3705
|
+
if (!player.is_alive) li.classList.add('dead');
|
|
3706
|
+
if (player.name === actingPlayerName) li.classList.add('active');
|
|
3707
|
+
|
|
3708
|
+
let roleDisplay = player.role;
|
|
3709
|
+
if (player.role === 'Werewolf') {
|
|
3710
|
+
roleDisplay = `🐺 ${player.role}`;
|
|
3711
|
+
} else if (player.role === 'Doctor') {
|
|
3712
|
+
roleDisplay = `🩺 ${player.role}`;
|
|
3713
|
+
} else if (player.role === 'Seer') {
|
|
3714
|
+
roleDisplay = `🔮 ${player.role}`;
|
|
3715
|
+
} else if (player.role === 'Villager') {
|
|
3716
|
+
roleDisplay = `🧑 ${player.role}`;
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
const roleText = player.role !== 'Unknown' ? `Role: ${roleDisplay}` : 'Role: Unknown';
|
|
3720
|
+
|
|
3721
|
+
li.innerHTML = `
|
|
3722
|
+
<div class="avatar-container">
|
|
3723
|
+
<img src="${player.thumbnail}" alt="${player.name}" class="avatar">
|
|
3724
|
+
</div>
|
|
3725
|
+
<div class="player-info">
|
|
3726
|
+
<div class="player-name" title="${player.name}">${player.name}</div>
|
|
3727
|
+
<div class="player-role">${roleText}</div>
|
|
3728
|
+
</div>
|
|
3729
|
+
<div class="threat-indicator"></div>
|
|
3730
|
+
`;
|
|
3731
|
+
playerUl.appendChild(li);
|
|
3732
|
+
});
|
|
3733
|
+
|
|
3734
|
+
listContainer.appendChild(playerUl);
|
|
3735
|
+
container.appendChild(listContainer);
|
|
3736
|
+
|
|
3737
|
+
gameState.players.forEach((player, index) => {
|
|
3738
|
+
const li = playerUl.children[index];
|
|
3739
|
+
const indicator = li.querySelector('.threat-indicator');
|
|
3740
|
+
if (!indicator) return;
|
|
3741
|
+
|
|
3742
|
+
if (player.is_alive) {
|
|
3743
|
+
const threatLevel = gameState.playerThreatLevels.get(player.name) || 0;
|
|
3744
|
+
indicator.style.backgroundColor = getThreatColor(threatLevel);
|
|
3745
|
+
} else {
|
|
3746
|
+
indicator.style.backgroundColor = 'transparent';
|
|
3747
|
+
}
|
|
3748
|
+
});
|
|
3749
|
+
|
|
3750
|
+
const audioControls = document.createElement('div');
|
|
3751
|
+
audioControls.className = 'audio-controls';
|
|
3752
|
+
audioControls.innerHTML = `
|
|
3753
|
+
<label for="playback-speed">Audio Speed: <span id="speed-label">${audioState.playbackRate.toFixed(1)}</span>x</label>
|
|
3754
|
+
<div style="display: flex; align-items: center; gap: 10px; margin-top: 5px;">
|
|
3755
|
+
<input type="range" id="playback-speed" min="0.5" max="2.5" step="0.1" value="${audioState.playbackRate}" style="flex-grow: 1;">
|
|
3756
|
+
</div>
|
|
3757
|
+
`;
|
|
3758
|
+
container.appendChild(audioControls);
|
|
3759
|
+
|
|
3760
|
+
const speedSlider = audioControls.querySelector('#playback-speed');
|
|
3761
|
+
const speedLabel = audioControls.querySelector('#speed-label');
|
|
3762
|
+
|
|
3763
|
+
speedSlider.addEventListener('input', (e) => {
|
|
3764
|
+
const newRate = parseFloat(e.target.value);
|
|
3765
|
+
setPlaybackRate(newRate);
|
|
3766
|
+
speedLabel.textContent = newRate.toFixed(1);
|
|
3767
|
+
});
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
// --- Main Rendering Logic (Incremental) ---
|
|
3771
|
+
// Only create UI elements if they don't exist
|
|
3772
|
+
let mainContainer = parent.querySelector('.main-container');
|
|
3773
|
+
let style = parent.querySelector('style');
|
|
3774
|
+
|
|
3775
|
+
if (!style) {
|
|
3776
|
+
style = document.createElement('style');
|
|
3777
|
+
style.textContent = css;
|
|
3778
|
+
parent.appendChild(style);
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
initThreeJs();
|
|
3782
|
+
|
|
3783
|
+
if (!environment || !environment.steps || environment.steps.length === 0 || step >= environment.steps.length) {
|
|
3784
|
+
if (!mainContainer) {
|
|
3785
|
+
const tempContainer = document.createElement("div");
|
|
3786
|
+
tempContainer.textContent = "Waiting for game data or invalid step...";
|
|
3787
|
+
parent.appendChild(tempContainer);
|
|
3788
|
+
}
|
|
3789
|
+
return;
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
// Initialize player mapping for 3D scene
|
|
3793
|
+
let playerNamesFor3D = [];
|
|
3794
|
+
let playerThumbnailsFor3D = {};
|
|
3795
|
+
|
|
3796
|
+
// --- State Reconstruction ---
|
|
3797
|
+
const player = window.werewolfGamePlayer;
|
|
3798
|
+
const { allEvents, displayStepToAllEventsIndex, originalSteps, eventToKaggleStep } = player;
|
|
3799
|
+
|
|
3800
|
+
if (step >= displayStepToAllEventsIndex.length) {
|
|
3801
|
+
console.error("Step is out of bounds for displayStepToAllEventsIndex", step, displayStepToAllEventsIndex.length);
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
3804
|
+
const allEventsIndex = displayStepToAllEventsIndex[step];
|
|
3805
|
+
const eventStep = allEventsIndex; // for clarity
|
|
3806
|
+
const kaggleStep = eventToKaggleStep[eventStep] || 0;
|
|
3807
|
+
|
|
3808
|
+
let gameState = {
|
|
3809
|
+
players: [],
|
|
3810
|
+
day: 0,
|
|
3811
|
+
phase: 'GAME_SETUP',
|
|
3812
|
+
game_state_phase: 'DAY',
|
|
3813
|
+
gameWinner: null,
|
|
3814
|
+
eventLog: [],
|
|
3815
|
+
playerThreatLevels: new Map()
|
|
3816
|
+
};
|
|
3817
|
+
|
|
3818
|
+
const firstObs = originalSteps[0]?.[0]?.observation?.raw_observation;
|
|
3819
|
+
let allPlayerNamesList;
|
|
3820
|
+
let playerThumbnails = {};
|
|
3821
|
+
|
|
3822
|
+
if (firstObs && firstObs.all_player_ids) {
|
|
3823
|
+
allPlayerNamesList = firstObs.all_player_ids;
|
|
3824
|
+
playerThumbnails = firstObs.player_thumbnails || {};
|
|
3825
|
+
playerNamesFor3D = [...allPlayerNamesList];
|
|
3826
|
+
playerThumbnailsFor3D = {...playerThumbnails};
|
|
3827
|
+
} else if (environment.configuration && environment.configuration.agents) {
|
|
3828
|
+
// console.warn("Renderer: Initial observation missing or incomplete. Reconstructing players from configuration.");
|
|
3829
|
+
allPlayerNamesList = environment.configuration.agents.map(agent => agent.id);
|
|
3830
|
+
environment.configuration.agents.forEach(agent => {
|
|
3831
|
+
if (agent.id && agent.thumbnail) {
|
|
3832
|
+
playerThumbnails[agent.id] = agent.thumbnail;
|
|
3833
|
+
}
|
|
3834
|
+
});
|
|
3835
|
+
playerNamesFor3D = [...allPlayerNamesList];
|
|
3836
|
+
playerThumbnailsFor3D = {...playerThumbnails};
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
if (!allPlayerNamesList || allPlayerNamesList.length === 0) {
|
|
3840
|
+
const tempContainer = document.createElement("div");
|
|
3841
|
+
tempContainer.textContent = "Waiting for game data: No players found in observation or configuration.";
|
|
3842
|
+
parent.appendChild(tempContainer);
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
gameState.players = environment.configuration.agents.map( agent => ({
|
|
3847
|
+
name: agent.id, is_alive: true, role: agent.role, team: 'Unknown',
|
|
3848
|
+
status: 'Alive', thumbnail: agent.thumbnail || `https://via.placeholder.com/40/2c3e50/ecf0f1?text=${agent.id.charAt(0)}`,
|
|
3849
|
+
display_name: agent.display_name
|
|
3850
|
+
}));
|
|
3851
|
+
const playerMap = new Map(gameState.players.map(p => [p.name, p]));
|
|
3852
|
+
|
|
3853
|
+
// Initialize and cache the replacer function if it doesn't exist
|
|
3854
|
+
if (!player.playerIdReplacer) {
|
|
3855
|
+
player.playerIdReplacer = createPlayerIdReplacer(playerMap);
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
gameState.players.forEach(p => gameState.playerThreatLevels.set(p.name, 0));
|
|
3859
|
+
|
|
3860
|
+
const roleAndTeamMap = new Map();
|
|
3861
|
+
const moderatorInitialLog = environment.info?.MODERATOR_OBSERVATION?.[0] || [];
|
|
3862
|
+
moderatorInitialLog.flat().forEach(dataEntry => {
|
|
3863
|
+
if (dataEntry.data_type === 'GameStartRoleDataEntry') {
|
|
3864
|
+
const historyEvent = JSON.parse(dataEntry.json_str);
|
|
3865
|
+
const data = historyEvent.data;
|
|
3866
|
+
if (data) roleAndTeamMap.set(data.player_id, { role: data.role, team: data.team });
|
|
3867
|
+
}
|
|
3868
|
+
});
|
|
3869
|
+
roleAndTeamMap.forEach((info, playerId) => {
|
|
3870
|
+
const player = playerMap.get(playerId);
|
|
3871
|
+
if (player) { player.role = info.role; player.team = info.team; }
|
|
3872
|
+
});
|
|
3873
|
+
|
|
3874
|
+
function threatStringToLevel(threatString) {
|
|
3875
|
+
switch(threatString) {
|
|
3876
|
+
case 'SAFE': return 0;
|
|
3877
|
+
case 'UNEASY': return 0.5;
|
|
3878
|
+
case 'DANGER': return 1.0;
|
|
3879
|
+
default: return 0;
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
// Reconstruct state up to current kaggleStep
|
|
3884
|
+
for (let s = 0; s <= kaggleStep; s++) {
|
|
3885
|
+
const stepStateList = originalSteps[s];
|
|
3886
|
+
if (!stepStateList) continue;
|
|
3887
|
+
|
|
3888
|
+
const currentObsForStep = stepStateList[0]?.observation?.raw_observation;
|
|
3889
|
+
if (currentObsForStep) {
|
|
3890
|
+
gameState.day = currentObsForStep.day;
|
|
3891
|
+
gameState.phase = currentObsForStep.phase;
|
|
3892
|
+
gameState.game_state_phase = currentObsForStep.game_state_phase;
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
// Populate event log up to current eventStep
|
|
3897
|
+
for (let i = 0; i <= eventStep; i++) {
|
|
3898
|
+
const historyEvent = allEvents[i];
|
|
3899
|
+
const data = historyEvent.data;
|
|
3900
|
+
const timestamp = historyEvent.created_at;
|
|
3901
|
+
|
|
3902
|
+
if (data && data.actor_id && data.perceived_threat_level) {
|
|
3903
|
+
const threatScore = threatStringToLevel(data.perceived_threat_level);
|
|
3904
|
+
gameState.playerThreatLevels.set(data.actor_id, threatScore);
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
if (!data) {
|
|
3908
|
+
if (historyEvent.event_name === 'vote_action') {
|
|
3909
|
+
const match = historyEvent.description.match(/P(player_\d+)/);
|
|
3910
|
+
if (match) {
|
|
3911
|
+
const actor_id = match[1];
|
|
3912
|
+
gameState.eventLog.push({ type: 'timeout', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, actor_id: actor_id, reasoning: "Timed out", timestamp: historyEvent.created_at });
|
|
3913
|
+
}
|
|
3914
|
+
} else if (historyEvent.event_name === 'day_start' || historyEvent.event_name === 'night_start') {
|
|
3915
|
+
gameState.eventLog.push({ type: 'system', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, text: historyEvent.description, allEventsIndex: i, timestamp});
|
|
3916
|
+
}
|
|
3917
|
+
continue;
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
switch (historyEvent.dataType) {
|
|
3921
|
+
case 'ChatDataEntry':
|
|
3922
|
+
gameState.eventLog.push({ type: 'chat', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, actor_id: data.actor_id, speaker: data.actor_id, message: data.message, reasoning: data.reasoning, timestamp, allEventsIndex: i, mentioned_player_ids: data.mentioned_player_ids || [] });
|
|
3923
|
+
break;
|
|
3924
|
+
case 'DayExileVoteDataEntry':
|
|
3925
|
+
gameState.eventLog.push({ type: 'vote', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, actor_id: data.actor_id, target: data.target_id, reasoning: data.reasoning, allEventsIndex: i, timestamp });
|
|
3926
|
+
break;
|
|
3927
|
+
case 'WerewolfNightVoteDataEntry':
|
|
3928
|
+
gameState.eventLog.push({ type: 'night_vote', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, actor_id: data.actor_id, target: data.target_id, reasoning: data.reasoning, allEventsIndex: i, timestamp });
|
|
3929
|
+
break;
|
|
3930
|
+
case 'DoctorHealActionDataEntry':
|
|
3931
|
+
gameState.eventLog.push({ type: 'doctor_heal_action', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, actor_id: data.actor_id, target: data.target_id, reasoning: data.reasoning, allEventsIndex: i, timestamp });
|
|
3932
|
+
break;
|
|
3933
|
+
case 'SeerInspectActionDataEntry':
|
|
3934
|
+
gameState.eventLog.push({ type: 'seer_inspection', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, actor_id: data.actor_id, target: data.target_id, reasoning: data.reasoning, allEventsIndex: i, timestamp });
|
|
3935
|
+
break;
|
|
3936
|
+
case 'DayExileElectedDataEntry':
|
|
3937
|
+
gameState.eventLog.push({ type: 'exile', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, name: data.elected_player_id, role: data.elected_player_role_name, allEventsIndex: i, timestamp });
|
|
3938
|
+
break;
|
|
3939
|
+
case 'WerewolfNightEliminationDataEntry':
|
|
3940
|
+
gameState.eventLog.push({ type: 'elimination', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, name: data.eliminated_player_id, role: data.eliminated_player_role_name, allEventsIndex: i, timestamp });
|
|
3941
|
+
break;
|
|
3942
|
+
case 'SeerInspectResultDataEntry':
|
|
3943
|
+
gameState.eventLog.push({ type: 'seer_inspection_result', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, actor_id: data.actor_id, seer: data.actor_id, target: data.target_id, role: data.role, team: data.team, allEventsIndex: i, timestamp });
|
|
3944
|
+
break;
|
|
3945
|
+
case 'DoctorSaveDataEntry':
|
|
3946
|
+
gameState.eventLog.push({ type: 'save', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, saved_player: data.saved_player_id, allEventsIndex: i, timestamp });
|
|
3947
|
+
break;
|
|
3948
|
+
case 'PhaseDividerDataEntry':
|
|
3949
|
+
gameState.eventLog.push({ type: 'phase_divider', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, divider: data.divider_type, allEventsIndex: i, timestamp });
|
|
3950
|
+
break;
|
|
3951
|
+
case 'GameEndResultsDataEntry':
|
|
3952
|
+
gameState.gameWinner = data.winner_team;
|
|
3953
|
+
const winners = gameState.players.filter(p => p.team === data.winner_team).map(p => p.name);
|
|
3954
|
+
const losers = gameState.players.filter(p => p.team !== data.winner_team).map(p => p.name);
|
|
3955
|
+
gameState.eventLog.push({ type: 'game_over', step: historyEvent.kaggleStep, day: Infinity, phase: 'GAME_OVER', winner: data.winner_team, winners, losers, allEventsIndex: i, timestamp });
|
|
3956
|
+
break;
|
|
3957
|
+
case 'DiscussionOrderDataEntry':
|
|
3958
|
+
gameState.eventLog.push({ type: 'system', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, text: historyEvent.description, allEventsIndex: i, timestamp });
|
|
3959
|
+
break;
|
|
3960
|
+
default:
|
|
3961
|
+
if (systemEntryTypeSet.has(historyEvent.event_name)) {
|
|
3962
|
+
gameState.eventLog.push({ type: 'system', step: historyEvent.kaggleStep, day: historyEvent.day, phase: historyEvent.phase, text: historyEvent.description, allEventsIndex: i, timestamp, data: data});
|
|
3963
|
+
}
|
|
3964
|
+
break;
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
if (eventStep < audioState.lastPlayedStep) {
|
|
3969
|
+
audioState.audioQueue = [];
|
|
3970
|
+
audioState.isAudioPlaying = false;
|
|
3971
|
+
if (audioState.audioPlayer) {
|
|
3972
|
+
audioState.audioPlayer.pause();
|
|
3973
|
+
}
|
|
3974
|
+
const chatLog = parent.querySelector('#chat-log');
|
|
3975
|
+
if (chatLog) {
|
|
3976
|
+
chatLog.innerHTML = '';
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
audioState.lastPlayedStep = eventStep;
|
|
3981
|
+
sessionStorage.setItem('ww_lastPlayedStep', eventStep);
|
|
3982
|
+
|
|
3983
|
+
gameState.players.forEach(p => { p.is_alive = true; p.status = 'Alive'; });
|
|
3984
|
+
gameState.eventLog.forEach(entry => {
|
|
3985
|
+
if (entry.type === 'exile' || entry.type === 'elimination') {
|
|
3986
|
+
const player = playerMap.get(entry.name);
|
|
3987
|
+
if (player) {
|
|
3988
|
+
player.is_alive = false;
|
|
3989
|
+
player.status = entry.type === 'exile' ? 'Exiled' : 'Eliminated';
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
});
|
|
3993
|
+
|
|
3994
|
+
// 1. Get the actual event being displayed at this step.
|
|
3995
|
+
const currentEvent = allEvents[eventStep];
|
|
3996
|
+
let nameToHighlight = null; // Initialize to null as requested.
|
|
3997
|
+
|
|
3998
|
+
if (currentEvent) {
|
|
3999
|
+
// 2. Check for a player actor in the event's data (covers chat, votes, night actions).
|
|
4000
|
+
if (currentEvent.data && currentEvent.data.actor_id) {
|
|
4001
|
+
nameToHighlight = currentEvent.data.actor_id;
|
|
4002
|
+
}
|
|
4003
|
+
// 3. Handle the special case for a timeout.
|
|
4004
|
+
else if (currentEvent.event_name === 'vote_action' && !currentEvent.data) {
|
|
4005
|
+
const match = currentEvent.description.match(/P(player_\d+)/);
|
|
4006
|
+
if (match && playerMap.has(match[1])) {
|
|
4007
|
+
nameToHighlight = match[1];
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
Object.assign(parent.style, { width: `${width}px`, height: `${height}px` });
|
|
4013
|
+
parent.className = 'werewolf-parent';
|
|
4014
|
+
|
|
4015
|
+
// Create or get existing main container
|
|
4016
|
+
if (!mainContainer) {
|
|
4017
|
+
mainContainer = document.createElement('div');
|
|
4018
|
+
mainContainer.className = 'main-container';
|
|
4019
|
+
parent.appendChild(mainContainer);
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
// Create or update phase indicator
|
|
4023
|
+
let phaseIndicator = parent.querySelector('.phase-indicator');
|
|
4024
|
+
if (!phaseIndicator) {
|
|
4025
|
+
phaseIndicator = document.createElement('div');
|
|
4026
|
+
phaseIndicator.className = 'phase-indicator';
|
|
4027
|
+
parent.appendChild(phaseIndicator);
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
// Update phase indicator based on current game state
|
|
4031
|
+
const currentPhase = allEvents[eventStep].phase.toUpperCase() || 'DAY';
|
|
4032
|
+
const isNight = currentPhase === 'NIGHT';
|
|
4033
|
+
phaseIndicator.className = `phase-indicator ${isNight ? 'night' : 'day'}`;
|
|
4034
|
+
if (allEvents[eventStep]?.event_name == 'game_end') {
|
|
4035
|
+
phaseIndicator.innerHTML = `
|
|
4036
|
+
<span class="phase-icon">${isNight ? '🌙' : '☀'}</span>
|
|
4037
|
+
`;
|
|
4038
|
+
} else {
|
|
4039
|
+
phaseIndicator.innerHTML = `
|
|
4040
|
+
<span class="phase-icon">${isNight ? '🌙' : '☀'}</span>
|
|
4041
|
+
<span>${allEvents[eventStep].day}</span>
|
|
4042
|
+
`;
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
// Create or update game scoreboard
|
|
4046
|
+
let scoreboard = parent.querySelector('.game-scoreboard');
|
|
4047
|
+
if (!scoreboard) {
|
|
4048
|
+
scoreboard = document.createElement('div');
|
|
4049
|
+
scoreboard.className = 'game-scoreboard';
|
|
4050
|
+
parent.appendChild(scoreboard);
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
// Calculate game statistics
|
|
4054
|
+
const alivePlayers = gameState.players.filter(p => p.is_alive).length;
|
|
4055
|
+
const deadPlayers = gameState.players.filter(p => !p.is_alive).length;
|
|
4056
|
+
const werewolves = gameState.players.filter(p => p.is_alive && p.role === 'Werewolf').length;
|
|
4057
|
+
const villagers = gameState.players.filter(p => p.is_alive && p.role !== 'Werewolf' && p.role !== 'Unknown').length;
|
|
4058
|
+
|
|
4059
|
+
// Determine current action based on phase and recent events
|
|
4060
|
+
let currentAction = 'Waiting...';
|
|
4061
|
+
const lastEvent = gameState.eventLog[gameState.eventLog.length - 1];
|
|
4062
|
+
|
|
4063
|
+
if (gameState.gameWinner) {
|
|
4064
|
+
currentAction = `${gameState.gameWinner} Win!`;
|
|
4065
|
+
} else if (gameState.phase === 'VOTING') {
|
|
4066
|
+
currentAction = 'Voting Phase';
|
|
4067
|
+
} else if (gameState.phase === 'DISCUSSION') {
|
|
4068
|
+
currentAction = 'Discussion';
|
|
4069
|
+
} else if (isNight) {
|
|
4070
|
+
// Check for recent night actions
|
|
4071
|
+
if (lastEvent) {
|
|
4072
|
+
if (lastEvent.type === 'night_vote') {
|
|
4073
|
+
currentAction = 'Werewolves Voting';
|
|
4074
|
+
} else if (lastEvent.type === 'doctor_heal_action') {
|
|
4075
|
+
currentAction = 'Doctor Saving';
|
|
4076
|
+
} else if (lastEvent.type === 'seer_inspection') {
|
|
4077
|
+
currentAction = 'Seer Inspecting';
|
|
4078
|
+
} else {
|
|
4079
|
+
currentAction = 'Night Actions';
|
|
4080
|
+
}
|
|
4081
|
+
} else {
|
|
4082
|
+
currentAction = 'Night Phase';
|
|
4083
|
+
}
|
|
4084
|
+
} else {
|
|
4085
|
+
// Day phase
|
|
4086
|
+
if (lastEvent && lastEvent.type === 'chat') {
|
|
4087
|
+
currentAction = 'Discussion';
|
|
4088
|
+
} else if (lastEvent && lastEvent.type === 'vote') {
|
|
4089
|
+
currentAction = 'Exile Voting';
|
|
4090
|
+
} else {
|
|
4091
|
+
currentAction = 'Day Phase';
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
// Update scoreboard content
|
|
4096
|
+
scoreboard.innerHTML = `
|
|
4097
|
+
<div class="scoreboard-item">
|
|
4098
|
+
<div class="scoreboard-label">Day</div>
|
|
4099
|
+
<div class="scoreboard-value">${gameState.day || 0}</div>
|
|
4100
|
+
</div>
|
|
4101
|
+
<div class="scoreboard-item">
|
|
4102
|
+
<div class="scoreboard-label">Alive</div>
|
|
4103
|
+
<div class="scoreboard-value alive">${alivePlayers}</div>
|
|
4104
|
+
</div>
|
|
4105
|
+
<div class="scoreboard-item">
|
|
4106
|
+
<div class="scoreboard-label">Out</div>
|
|
4107
|
+
<div class="scoreboard-value dead">${deadPlayers}</div>
|
|
4108
|
+
</div>
|
|
4109
|
+
${werewolves > 0 || villagers > 0 ? `
|
|
4110
|
+
<div class="scoreboard-item">
|
|
4111
|
+
<div class="scoreboard-label">Werewolves</div>
|
|
4112
|
+
<div class="scoreboard-value werewolf">${werewolves}</div>
|
|
4113
|
+
</div>
|
|
4114
|
+
<div class="scoreboard-item">
|
|
4115
|
+
<div class="scoreboard-label">Villagers</div>
|
|
4116
|
+
<div class="scoreboard-value villager">${villagers}</div>
|
|
4117
|
+
</div>
|
|
4118
|
+
` : ''}
|
|
4119
|
+
<div class="scoreboard-item">
|
|
4120
|
+
<div class="scoreboard-action">${currentAction}</div>
|
|
4121
|
+
</div>
|
|
4122
|
+
`;
|
|
4123
|
+
|
|
4124
|
+
// Create or get existing panels
|
|
4125
|
+
let leftPanel = mainContainer.querySelector('.left-panel');
|
|
4126
|
+
if (!leftPanel) {
|
|
4127
|
+
leftPanel = document.createElement('div');
|
|
4128
|
+
leftPanel.className = 'left-panel';
|
|
4129
|
+
mainContainer.appendChild(leftPanel);
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
let playerListArea = leftPanel.querySelector('#player-list-area');
|
|
4133
|
+
if (!playerListArea) {
|
|
4134
|
+
playerListArea = document.createElement('div');
|
|
4135
|
+
playerListArea.id = 'player-list-area';
|
|
4136
|
+
leftPanel.appendChild(playerListArea);
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
let rightPanel = mainContainer.querySelector('.right-panel');
|
|
4140
|
+
if (!rightPanel) {
|
|
4141
|
+
rightPanel = document.createElement('div');
|
|
4142
|
+
rightPanel.className = 'right-panel';
|
|
4143
|
+
mainContainer.appendChild(rightPanel);
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
// Update existing content instead of clearing and rebuilding
|
|
4147
|
+
updatePlayerList(playerListArea, gameState, nameToHighlight);
|
|
4148
|
+
updateEventLog(rightPanel, gameState, playerMap);
|
|
4149
|
+
|
|
4150
|
+
// Update 3D scene based on game state
|
|
4151
|
+
updateSceneFromGameState(gameState, playerMap, nameToHighlight);
|
|
4152
|
+
|
|
4153
|
+
// Initialize 3D players if needed
|
|
4154
|
+
if (threeState.demo && threeState.demo._playerObjects && threeState.demo._playerObjects.size === 0 && playerNamesFor3D.length > 0) {
|
|
4155
|
+
initializePlayers3D(gameState, playerNamesFor3D, playerThumbnailsFor3D, threeState);
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
function initializePlayers3D(gameState, playerNames, playerThumbnails, threeState) {
|
|
4160
|
+
if (!threeState || !threeState.demo || !threeState.demo._playerObjects) return;
|
|
4161
|
+
|
|
4162
|
+
// Clear existing player objects
|
|
4163
|
+
if (threeState.demo._playerGroup) {
|
|
4164
|
+
// Remove all children from the group
|
|
4165
|
+
while(threeState.demo._playerGroup.children.length > 0) {
|
|
4166
|
+
threeState.demo._playerGroup.remove(threeState.demo._playerGroup.children[0]);
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
threeState.demo._playerObjects.clear();
|
|
4170
|
+
|
|
4171
|
+
const numPlayers = playerNames.length;
|
|
4172
|
+
const radius = 18; // Increased radius to use more space
|
|
4173
|
+
const playerHeight = 4;
|
|
4174
|
+
|
|
4175
|
+
const THREE = threeState.demo._THREE;
|
|
4176
|
+
const CSS2DObject = threeState.demo._CSS2DObject;
|
|
4177
|
+
|
|
4178
|
+
// Create a circular platform
|
|
4179
|
+
const platformGeometry = new THREE.RingGeometry(radius - 2, radius + 3, 64);
|
|
4180
|
+
const platformMaterial = new THREE.MeshStandardMaterial({
|
|
4181
|
+
color: 0x2a2a3a,
|
|
4182
|
+
roughness: 0.9,
|
|
4183
|
+
metalness: 0.1,
|
|
4184
|
+
transparent: true,
|
|
4185
|
+
opacity: 0.5,
|
|
4186
|
+
side: THREE.DoubleSide
|
|
4187
|
+
});
|
|
4188
|
+
const platform = new THREE.Mesh(platformGeometry, platformMaterial);
|
|
4189
|
+
platform.rotation.x = -Math.PI / 2;
|
|
4190
|
+
platform.position.y = -0.05;
|
|
4191
|
+
platform.receiveShadow = true;
|
|
4192
|
+
threeState.demo._playerGroup.add(platform);
|
|
4193
|
+
|
|
4194
|
+
// Create center decoration
|
|
4195
|
+
const centerGeometry = new THREE.CylinderGeometry(3, 3, 0.2, 32);
|
|
4196
|
+
const centerMaterial = new THREE.MeshStandardMaterial({
|
|
4197
|
+
color: 0x444466,
|
|
4198
|
+
roughness: 0.7,
|
|
4199
|
+
metalness: 0.3,
|
|
4200
|
+
emissive: 0x222244,
|
|
4201
|
+
emissiveIntensity: 0.2
|
|
4202
|
+
});
|
|
4203
|
+
const centerPlatform = new THREE.Mesh(centerGeometry, centerMaterial);
|
|
4204
|
+
centerPlatform.position.y = 0.1;
|
|
4205
|
+
centerPlatform.receiveShadow = true;
|
|
4206
|
+
threeState.demo._playerGroup.add(centerPlatform);
|
|
4207
|
+
|
|
4208
|
+
// Add decorative lines from center to each player position
|
|
4209
|
+
const linesMaterial = new THREE.LineBasicMaterial({
|
|
4210
|
+
color: 0x334455,
|
|
4211
|
+
transparent: true,
|
|
4212
|
+
opacity: 0.3
|
|
4213
|
+
});
|
|
4214
|
+
|
|
4215
|
+
playerNames.forEach((name, i) => {
|
|
4216
|
+
const displayName = gameState.players[i].display_name || '';
|
|
4217
|
+
const playerContainer = new THREE.Group();
|
|
4218
|
+
// Use full circle (360 degrees)
|
|
4219
|
+
const angle = (i / numPlayers) * Math.PI * 2;
|
|
4220
|
+
|
|
4221
|
+
const x = radius * Math.sin(angle);
|
|
4222
|
+
const z = radius * Math.cos(angle);
|
|
4223
|
+
playerContainer.position.set(x, 0, z);
|
|
4224
|
+
|
|
4225
|
+
// Create line from center to player
|
|
4226
|
+
const lineGeometry = new THREE.BufferGeometry().setFromPoints([
|
|
4227
|
+
new THREE.Vector3(0, 0.05, 0),
|
|
4228
|
+
new THREE.Vector3(x, 0.05, z)
|
|
4229
|
+
]);
|
|
4230
|
+
const line = new THREE.Line(lineGeometry, linesMaterial);
|
|
4231
|
+
threeState.demo._playerGroup.add(line);
|
|
4232
|
+
|
|
4233
|
+
// Create pedestal for each player
|
|
4234
|
+
const pedestalGeometry = new THREE.CylinderGeometry(1.5, 1.8, 0.4, 16);
|
|
4235
|
+
const pedestalMaterial = new THREE.MeshStandardMaterial({
|
|
4236
|
+
color: 0x333344,
|
|
4237
|
+
roughness: 0.8,
|
|
4238
|
+
metalness: 0.2,
|
|
4239
|
+
emissive: 0x111122,
|
|
4240
|
+
emissiveIntensity: 0.1
|
|
4241
|
+
});
|
|
4242
|
+
const pedestal = new THREE.Mesh(pedestalGeometry, pedestalMaterial);
|
|
4243
|
+
pedestal.position.y = 0.2;
|
|
4244
|
+
pedestal.castShadow = true;
|
|
4245
|
+
pedestal.receiveShadow = true;
|
|
4246
|
+
playerContainer.add(pedestal);
|
|
4247
|
+
|
|
4248
|
+
// Create player body (more detailed)
|
|
4249
|
+
const bodyGeometry = new THREE.CylinderGeometry(0.8, 1, playerHeight * 0.6, 16);
|
|
4250
|
+
const bodyMaterial = new THREE.MeshStandardMaterial({
|
|
4251
|
+
color: 0x4466ff,
|
|
4252
|
+
roughness: 0.5,
|
|
4253
|
+
metalness: 0.3,
|
|
4254
|
+
emissive: 0x111166,
|
|
4255
|
+
emissiveIntensity: 0.2
|
|
4256
|
+
});
|
|
4257
|
+
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
|
4258
|
+
body.position.y = playerHeight * 0.4;
|
|
4259
|
+
body.castShadow = true;
|
|
4260
|
+
body.receiveShadow = true;
|
|
4261
|
+
playerContainer.add(body);
|
|
4262
|
+
|
|
4263
|
+
// Create shoulders
|
|
4264
|
+
const shoulderGeometry = new THREE.SphereGeometry(1, 16, 8);
|
|
4265
|
+
const shoulderMaterial = new THREE.MeshStandardMaterial({
|
|
4266
|
+
color: 0x4466ff,
|
|
4267
|
+
roughness: 0.5,
|
|
4268
|
+
metalness: 0.3,
|
|
4269
|
+
emissive: 0x111166,
|
|
4270
|
+
emissiveIntensity: 0.2
|
|
4271
|
+
});
|
|
4272
|
+
const shoulders = new THREE.Mesh(shoulderGeometry, shoulderMaterial);
|
|
4273
|
+
shoulders.position.y = playerHeight * 0.65;
|
|
4274
|
+
shoulders.scale.set(1.2, 0.6, 0.8);
|
|
4275
|
+
shoulders.castShadow = true;
|
|
4276
|
+
playerContainer.add(shoulders);
|
|
4277
|
+
|
|
4278
|
+
// Create player head (sphere)
|
|
4279
|
+
const headGeometry = new THREE.SphereGeometry(0.7, 16, 16);
|
|
4280
|
+
const headMaterial = new THREE.MeshStandardMaterial({
|
|
4281
|
+
color: 0xfdbcb4,
|
|
4282
|
+
roughness: 0.7,
|
|
4283
|
+
metalness: 0.1,
|
|
4284
|
+
emissive: 0x442211,
|
|
4285
|
+
emissiveIntensity: 0.1
|
|
4286
|
+
});
|
|
4287
|
+
const head = new THREE.Mesh(headGeometry, headMaterial);
|
|
4288
|
+
head.position.y = playerHeight * 0.85;
|
|
4289
|
+
head.castShadow = true;
|
|
4290
|
+
head.receiveShadow = true;
|
|
4291
|
+
playerContainer.add(head);
|
|
4292
|
+
|
|
4293
|
+
// Create eyes
|
|
4294
|
+
const eyeGeometry = new THREE.SphereGeometry(0.08, 8, 6);
|
|
4295
|
+
const eyeMaterial = new THREE.MeshStandardMaterial({
|
|
4296
|
+
color: 0x000000,
|
|
4297
|
+
roughness: 0.3,
|
|
4298
|
+
metalness: 0.8
|
|
4299
|
+
});
|
|
4300
|
+
const leftEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
|
|
4301
|
+
leftEye.position.set(-0.2, playerHeight * 0.87, 0.6);
|
|
4302
|
+
playerContainer.add(leftEye);
|
|
4303
|
+
|
|
4304
|
+
const rightEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
|
|
4305
|
+
rightEye.position.set(0.2, playerHeight * 0.87, 0.6);
|
|
4306
|
+
playerContainer.add(rightEye);
|
|
4307
|
+
|
|
4308
|
+
// Create glowing orb for status (more dramatic)
|
|
4309
|
+
const orbGeometry = new THREE.IcosahedronGeometry(0.3, 2);
|
|
4310
|
+
const orbMaterial = new THREE.MeshStandardMaterial({
|
|
4311
|
+
color: 0x00ff00,
|
|
4312
|
+
emissive: 0x00ff00,
|
|
4313
|
+
emissiveIntensity: 0.8,
|
|
4314
|
+
transparent: true,
|
|
4315
|
+
opacity: 0.9
|
|
4316
|
+
});
|
|
4317
|
+
const orb = new THREE.Mesh(orbGeometry, orbMaterial);
|
|
4318
|
+
orb.position.y = playerHeight * 1.2;
|
|
4319
|
+
orb.name = 'statusOrb';
|
|
4320
|
+
playerContainer.add(orb);
|
|
4321
|
+
|
|
4322
|
+
// Add outer glow sphere
|
|
4323
|
+
const glowGeometry = new THREE.SphereGeometry(0.5, 12, 8);
|
|
4324
|
+
const glowMaterial = new THREE.MeshStandardMaterial({
|
|
4325
|
+
color: 0x00ff00,
|
|
4326
|
+
emissive: 0x00ff00,
|
|
4327
|
+
emissiveIntensity: 0.3,
|
|
4328
|
+
transparent: true,
|
|
4329
|
+
opacity: 0.3
|
|
4330
|
+
});
|
|
4331
|
+
const glow = new THREE.Mesh(glowGeometry, glowMaterial);
|
|
4332
|
+
glow.position.y = playerHeight * 1.2;
|
|
4333
|
+
playerContainer.add(glow);
|
|
4334
|
+
|
|
4335
|
+
// Add point light for glow effect
|
|
4336
|
+
const orbLight = new THREE.PointLight(0x00ff00, 0.8, 8);
|
|
4337
|
+
orbLight.position.y = playerHeight * 1.2;
|
|
4338
|
+
orbLight.name = 'orbLight';
|
|
4339
|
+
orbLight.castShadow = true;
|
|
4340
|
+
playerContainer.add(orbLight);
|
|
4341
|
+
|
|
4342
|
+
// Make player face center without flipping
|
|
4343
|
+
// Calculate the angle to face the center
|
|
4344
|
+
playerContainer.rotation.y = -angle + Math.PI / 2;
|
|
4345
|
+
|
|
4346
|
+
// Create nameplate with actual player thumbnail
|
|
4347
|
+
const thumbnailUrl = playerThumbnails[name] || `https://via.placeholder.com/60/2c3e50/ecf0f1?text=${name.charAt(0)}`;
|
|
4348
|
+
const nameplate = threeState.demo._createNameplate(name, displayName, thumbnailUrl, CSS2DObject);
|
|
4349
|
+
nameplate.position.set(0, playerHeight * 2.0, 0);
|
|
4350
|
+
playerContainer.add(nameplate);
|
|
4351
|
+
|
|
4352
|
+
// Store references
|
|
4353
|
+
threeState.demo._playerObjects.set(name, {
|
|
4354
|
+
container: playerContainer,
|
|
4355
|
+
body: body,
|
|
4356
|
+
head: head,
|
|
4357
|
+
shoulders: shoulders,
|
|
4358
|
+
orb: orb,
|
|
4359
|
+
glow: glow,
|
|
4360
|
+
orbLight: orbLight,
|
|
4361
|
+
nameplate: nameplate,
|
|
4362
|
+
pedestal: pedestal,
|
|
4363
|
+
originalPosition: playerContainer.position.clone(),
|
|
4364
|
+
baseAngle: angle,
|
|
4365
|
+
isAlive: true
|
|
4366
|
+
});
|
|
4367
|
+
|
|
4368
|
+
threeState.demo._playerGroup.add(playerContainer);
|
|
4369
|
+
});
|
|
4370
|
+
|
|
4371
|
+
// Adjust camera to see the full circle
|
|
4372
|
+
if (threeState.demo._camera) {
|
|
4373
|
+
threeState.demo._camera.position.set(25, 30, 25);
|
|
4374
|
+
threeState.demo._controls.target.set(0, 5, 0);
|
|
4375
|
+
threeState.demo._controls.update();
|
|
4376
|
+
}
|
|
4377
|
+
}
|