diff --git a/.env.example b/.env.example
index f1e9ee3..38ea27c 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,10 @@
# rename me to .env and change below keys
STREAM_KEY="live_XXXXXXXXXX"
+DEEPSEEK_API_KEY=
+OPENAI_API_KEY=
+ANTHROPIC_API_KEY=
+GEMINI_API_KEY=
+VITE_ELEVENLABS_API_KEY =
+ELEVENLABS_API_KEY =
+OPENROUTER_API_KEY=
+VITE_WEBHOOK_URL=
\ No newline at end of file
diff --git a/ai_animation/experiment_log.md b/ai_animation/experiment_log.md
index 95bc055..3d96212 100644
--- a/ai_animation/experiment_log.md
+++ b/ai_animation/experiment_log.md
@@ -3,3 +3,4 @@
| Problem | Attempted Solution | Real Outcome | Current $ Balance |
| :----------------------------------------------------------- | :----------------- | :----------- | :---------------- |
| 1. Relationships chart is blank.
2. Game stops after narrator summary in phase 2. | 1. Updated `PhaseSchema` in `types/gameState.ts` to include `agent_relationships` definition.
2. Uncommented `updateChatWindows(currentPhase, true);` in `phase.ts`. | | $0 |
+| Add webhook notifications for phase changes | 1. Added `webhookUrl` config to `src/config.ts`
2. Created `src/webhooks/phaseNotifier.ts` with fire-and-forget webhook notification
3. Added `notifyPhaseChange()` call in `_setPhase()` function
4. Updated `.env.example` with `VITE_WEBHOOK_URL` | | $0 |
diff --git a/ai_animation/src/config.ts b/ai_animation/src/config.ts
index 0664143..ff56613 100644
--- a/ai_animation/src/config.ts
+++ b/ai_animation/src/config.ts
@@ -15,5 +15,8 @@ export const config = {
soundEffectFrequency: 3,
// Whether speech/TTS is enabled (can be toggled via debug menu)
- speechEnabled: import.meta.env.VITE_DEBUG_MODE ? false : true
+ speechEnabled: import.meta.env.VITE_DEBUG_MODE ? false : true,
+
+ // Webhook URL for phase change notifications (optional)
+ webhookUrl: import.meta.env.VITE_WEBHOOK_URL || ''
}
diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts
index 8721bf9..a39d257 100644
--- a/ai_animation/src/phase.ts
+++ b/ai_animation/src/phase.ts
@@ -10,6 +10,7 @@ import { config } from "./config";
import { debugMenuInstance } from "./debug/debugMenu";
import { showTwoPowerConversation, closeTwoPowerConversation } from "./components/twoPowerConversation";
import { PowerENUM } from "./types/map";
+import { notifyPhaseChange } from "./webhooks/phaseNotifier";
const MOMENT_THRESHOLD = 8.0
// If we're in debug mode, show it quick, otherwise show it for 30 seconds
@@ -17,6 +18,11 @@ const MOMENT_DISPLAY_TIMEOUT_MS = config.isDebugMode ? 5000 : 30000
// FIXME: Going to previous phases is borked. Units do not animate properly, map doesn't update.
export function _setPhase(phaseIndex: number) {
+ console.log(`[Phase] _setPhase called with index: ${phaseIndex}`);
+
+ // Store the old phase index at the very beginning
+ const oldPhaseIndex = gameState.phaseIndex;
+
if (config.isDebugMode) {
debugMenuInstance.updateTools()
}
@@ -58,6 +64,9 @@ export function _setPhase(phaseIndex: number) {
// Finally, update the gameState with the current phaseIndex
gameState.phaseIndex = phaseIndex
+
+ // Send webhook notification for phase change
+ notifyPhaseChange(oldPhaseIndex, phaseIndex);
}
diff --git a/ai_animation/src/webhooks/phaseNotifier.ts b/ai_animation/src/webhooks/phaseNotifier.ts
new file mode 100644
index 0000000..c5eb3fe
--- /dev/null
+++ b/ai_animation/src/webhooks/phaseNotifier.ts
@@ -0,0 +1,140 @@
+import { config } from '../config';
+import { gameState } from '../gameState';
+
+// Test webhook URL validity on startup
+testWebhookUrl().catch(err => console.error("Webhook test failed:", err));
+
+async function testWebhookUrl() {
+ const webhookUrl = config.webhookUrl || import.meta.env.VITE_WEBHOOK_URL || '';
+
+ if (!webhookUrl) {
+ console.log("⚠️ No webhook URL configured (optional feature)");
+ return;
+ }
+
+ try {
+ // For Discord webhooks, we can test with a GET request
+ if (webhookUrl.includes('discord.com/api/webhooks')) {
+ const response = await fetch(webhookUrl, {
+ method: 'GET',
+ });
+
+ if (response.ok) {
+ const webhookInfo = await response.json();
+ console.log(`✅ Discord webhook is valid and ready (${webhookInfo.name || 'Unnamed webhook'})`);
+ } else if (response.status === 401) {
+ console.error(`❌ Discord webhook invalid: Unauthorized (check webhook URL)`);
+ } else {
+ console.error(`❌ Discord webhook error: ${response.status}`);
+ }
+ } else {
+ // For non-Discord webhooks, just validate the URL format
+ try {
+ new URL(webhookUrl);
+ console.log(`✅ Webhook URL is valid: ${webhookUrl.substring(0, 50)}...`);
+ } catch {
+ console.error(`❌ Invalid webhook URL format`);
+ }
+ }
+ } catch (error) {
+ console.error("❌ Webhook connection error:", error);
+ }
+}
+
+/**
+ * Sends a webhook notification when a phase changes
+ * This is a fire-and-forget operation that won't block the UI
+ */
+export async function notifyPhaseChange(oldPhaseIndex: number, newPhaseIndex: number): Promise {
+ console.log(`[Webhook] Phase change detected: ${oldPhaseIndex} -> ${newPhaseIndex}`);
+
+ // Skip if no webhook URL is configured
+ if (!config.webhookUrl) {
+ console.log('[Webhook] No webhook URL configured, skipping notification');
+ return;
+ }
+
+ // Skip if game data is not loaded
+ if (!gameState.gameData || !gameState.gameData.phases) {
+ console.warn('[Webhook] Game data not loaded, cannot send notification');
+ return;
+ }
+
+ const currentPhase = gameState.gameData.phases[newPhaseIndex];
+ if (!currentPhase) {
+ console.warn(`[Webhook] Phase at index ${newPhaseIndex} not found`);
+ return;
+ }
+
+ // Determine direction of phase change
+ let direction: 'forward' | 'backward' | 'jump';
+ if (newPhaseIndex === oldPhaseIndex + 1) {
+ direction = 'forward';
+ } else if (newPhaseIndex === oldPhaseIndex - 1) {
+ direction = 'backward';
+ } else {
+ direction = 'jump';
+ }
+
+ const payload = {
+ event: 'phase_change',
+ timestamp: new Date().toISOString(),
+ game_id: gameState.gameId || 0,
+ phase_index: newPhaseIndex,
+ phase_name: currentPhase.name,
+ phase_year: currentPhase.year || parseInt(currentPhase.name.substring(1, 5)) || null,
+ is_playing: gameState.isPlaying,
+ direction: direction,
+ total_phases: gameState.gameData.phases.length
+ };
+
+ // Discord webhooks need a different format
+ const isDiscordWebhook = config.webhookUrl.includes('discord.com/api/webhooks');
+ const webhookPayload = isDiscordWebhook ? {
+ content: `Phase Change: **${currentPhase.name}** (${direction})`,
+ embeds: [{
+ title: "AI Diplomacy Phase Update",
+ color: direction === 'forward' ? 0x00ff00 : direction === 'backward' ? 0xff0000 : 0x0000ff,
+ fields: [
+ { name: "Phase", value: currentPhase.name, inline: true },
+ { name: "Year", value: String(payload.phase_year || "Unknown"), inline: true },
+ { name: "Direction", value: direction, inline: true },
+ { name: "Game ID", value: String(payload.game_id), inline: true },
+ { name: "Phase Index", value: `${newPhaseIndex}/${payload.total_phases}`, inline: true },
+ { name: "Auto-playing", value: payload.is_playing ? "Yes" : "No", inline: true }
+ ],
+ timestamp: payload.timestamp
+ }]
+ } : payload;
+
+ console.log(`[Webhook] Sending notification for phase ${currentPhase.name} to ${config.webhookUrl}`);
+
+ try {
+ // Fire and forget - we don't await this
+ fetch(config.webhookUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(webhookPayload)
+ })
+ .then(response => {
+ if (response.ok) {
+ console.log(`[Webhook] ✅ Successfully sent notification for phase ${currentPhase.name}`);
+ } else {
+ console.warn(`[Webhook] ❌ Failed with status ${response.status}: ${response.statusText}`);
+ }
+ })
+ .catch(error => {
+ // Log errors but don't let them break the animation
+ console.error('[Webhook] ❌ Network error:', error);
+ });
+
+ if (config.isDebugMode) {
+ console.log('[Webhook] Debug - Full payload:', payload);
+ }
+ } catch (error) {
+ // Catch any synchronous errors (shouldn't happen with fetch)
+ console.error('[Webhook] ❌ Unexpected error:', error);
+ }
+}
\ No newline at end of file
diff --git a/lm_game.py b/lm_game.py
index 138cb5d..867da62 100644
--- a/lm_game.py
+++ b/lm_game.py
@@ -623,7 +623,6 @@ async def main():
try:
current_year = int(current_year_str)
consolidation_year = current_year - 2 # Two years ago
-
logger.info(f"[DIARY CONSOLIDATION] Current year: {current_year}, Consolidation year: {consolidation_year}")
logger.info(f"[DIARY CONSOLIDATION] Phase check - ends with 'M': {current_short_phase.endswith('M')}, starts with 'S': {current_short_phase.startswith('S')}")
logger.info(f"[DIARY CONSOLIDATION] Consolidation year check: {consolidation_year} >= 1901: {consolidation_year >= 1901}")