mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-29 17:35:18 +00:00
Another pass at basic moments
Adding diary, name of powers, name of moment type, and some supporting thoughts. Added a debug menu item to disable or enable the eleven labs speech. Useful for removing it when debugging
This commit is contained in:
parent
e1309c4012
commit
ecf8e1db06
7 changed files with 227 additions and 23 deletions
|
|
@ -2,6 +2,7 @@ import { gameState } from '../gameState';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { getPowerDisplayName } from '../utils/powerNames';
|
import { getPowerDisplayName } from '../utils/powerNames';
|
||||||
import { PowerENUM } from '../types/map';
|
import { PowerENUM } from '../types/map';
|
||||||
|
import { Moment } from '../types/moments';
|
||||||
|
|
||||||
interface ConversationMessage {
|
interface ConversationMessage {
|
||||||
sender: string;
|
sender: string;
|
||||||
|
|
@ -16,6 +17,7 @@ interface TwoPowerDialogueOptions {
|
||||||
power2: string;
|
power2: string;
|
||||||
messages?: ConversationMessage[];
|
messages?: ConversationMessage[];
|
||||||
title?: string;
|
title?: string;
|
||||||
|
moment?: Moment;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,7 +28,7 @@ let dialogueOverlay: HTMLElement | null = null;
|
||||||
* @param options Configuration for the dialogue display
|
* @param options Configuration for the dialogue display
|
||||||
*/
|
*/
|
||||||
export function showTwoPowerConversation(options: TwoPowerDialogueOptions): void {
|
export function showTwoPowerConversation(options: TwoPowerDialogueOptions): void {
|
||||||
const { power1, power2, messages, title, onClose } = options;
|
const { power1, power2, messages, title, moment, onClose } = options;
|
||||||
|
|
||||||
// Close any existing dialogue
|
// Close any existing dialogue
|
||||||
closeTwoPowerConversation();
|
closeTwoPowerConversation();
|
||||||
|
|
@ -43,11 +45,12 @@ export function showTwoPowerConversation(options: TwoPowerDialogueOptions): void
|
||||||
dialogueOverlay = createDialogueOverlay();
|
dialogueOverlay = createDialogueOverlay();
|
||||||
|
|
||||||
// Create dialogue container
|
// Create dialogue container
|
||||||
const dialogueContainer = createDialogueContainer(power1, power2, title);
|
const dialogueContainer = createDialogueContainer(power1, power2, title, moment);
|
||||||
|
|
||||||
// Create conversation area
|
// Create conversation area and append to the conversation wrapper
|
||||||
|
const conversationWrapper = dialogueContainer.querySelector('div[style*="flex: 2"]') as HTMLElement;
|
||||||
const conversationArea = createConversationArea();
|
const conversationArea = createConversationArea();
|
||||||
dialogueContainer.appendChild(conversationArea);
|
conversationWrapper.appendChild(conversationArea);
|
||||||
|
|
||||||
// Add close button
|
// Add close button
|
||||||
const closeButton = createCloseButton();
|
const closeButton = createCloseButton();
|
||||||
|
|
@ -134,7 +137,7 @@ function createDialogueOverlay(): HTMLElement {
|
||||||
/**
|
/**
|
||||||
* Creates the main dialogue container
|
* Creates the main dialogue container
|
||||||
*/
|
*/
|
||||||
function createDialogueContainer(power1: string, power2: string, title?: string): HTMLElement {
|
function createDialogueContainer(power1: string, power2: string, title?: string, moment?: Moment): HTMLElement {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.className = 'dialogue-container';
|
container.className = 'dialogue-container';
|
||||||
container.style.cssText = `
|
container.style.cssText = `
|
||||||
|
|
@ -142,30 +145,178 @@ function createDialogueContainer(power1: string, power2: string, title?: string)
|
||||||
border: 3px solid #4f3b16;
|
border: 3px solid #4f3b16;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 0 15px rgba(0,0,0,0.5);
|
box-shadow: 0 0 15px rgba(0,0,0,0.5);
|
||||||
width: 80%;
|
width: 90%;
|
||||||
height: 80%;
|
height: 85%;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add title
|
// Create header section with title and moment info
|
||||||
|
const headerSection = document.createElement('div');
|
||||||
|
headerSection.style.cssText = `
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add main title
|
||||||
const titleElement = document.createElement('h2');
|
const titleElement = document.createElement('h2');
|
||||||
titleElement.textContent = title || `Conversation: ${getPowerDisplayName(power1 as PowerENUM)} & ${getPowerDisplayName(power2 as PowerENUM)}`;
|
titleElement.textContent = title || `Conversation: ${getPowerDisplayName(power1 as PowerENUM)} & ${getPowerDisplayName(power2 as PowerENUM)}`;
|
||||||
titleElement.style.cssText = `
|
titleElement.style.cssText = `
|
||||||
margin: 0 0 20px 0;
|
margin: 0 0 10px 0;
|
||||||
text-align: center;
|
|
||||||
color: #4f3b16;
|
color: #4f3b16;
|
||||||
font-family: 'Times New Roman', serif;
|
font-family: 'Times New Roman', serif;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
`;
|
`;
|
||||||
container.appendChild(titleElement);
|
headerSection.appendChild(titleElement);
|
||||||
|
|
||||||
|
// Add moment type if available
|
||||||
|
if (moment) {
|
||||||
|
const momentTypeElement = document.createElement('div');
|
||||||
|
momentTypeElement.textContent = `${moment.category} (Interest: ${moment.interest_score}/10)`;
|
||||||
|
momentTypeElement.style.cssText = `
|
||||||
|
background: rgba(75, 59, 22, 0.8);
|
||||||
|
color: #f7ecd1;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
`;
|
||||||
|
headerSection.appendChild(momentTypeElement);
|
||||||
|
|
||||||
|
// Add moment description if available
|
||||||
|
if (moment.promise_agreement || moment.actual_action || moment.impact) {
|
||||||
|
const momentDescription = document.createElement('div');
|
||||||
|
let description = '';
|
||||||
|
if (moment.promise_agreement) description += `Promise: ${moment.promise_agreement}. `;
|
||||||
|
if (moment.actual_action) description += `Action: ${moment.actual_action}. `;
|
||||||
|
if (moment.impact) description += `Impact: ${moment.impact}`;
|
||||||
|
|
||||||
|
momentDescription.textContent = description;
|
||||||
|
momentDescription.style.cssText = `
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5a4b2b;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 5px 20px 0 20px;
|
||||||
|
line-height: 1.4;
|
||||||
|
`;
|
||||||
|
headerSection.appendChild(momentDescription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(headerSection);
|
||||||
|
|
||||||
|
// Create main content area with three columns: diary1, conversation, diary2
|
||||||
|
const mainContent = document.createElement('div');
|
||||||
|
mainContent.style.cssText = `
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Left diary box for power1
|
||||||
|
const diary1Box = createDiaryBox(power1 as PowerENUM, moment?.diary_context?.[power1 as PowerENUM] || '');
|
||||||
|
mainContent.appendChild(diary1Box);
|
||||||
|
|
||||||
|
// Center conversation area
|
||||||
|
const conversationWrapper = document.createElement('div');
|
||||||
|
conversationWrapper.style.cssText = `
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
mainContent.appendChild(conversationWrapper);
|
||||||
|
|
||||||
|
// Right diary box for power2
|
||||||
|
const diary2Box = createDiaryBox(power2 as PowerENUM, moment?.diary_context?.[power2 as PowerENUM] || '');
|
||||||
|
mainContent.appendChild(diary2Box);
|
||||||
|
|
||||||
|
container.appendChild(mainContent);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a diary box for displaying power-specific thoughts and context
|
||||||
|
*/
|
||||||
|
function createDiaryBox(power: PowerENUM, diaryContent: string): HTMLElement {
|
||||||
|
const diaryBox = document.createElement('div');
|
||||||
|
diaryBox.className = `diary-box diary-${power.toLowerCase()}`;
|
||||||
|
diaryBox.style.cssText = `
|
||||||
|
flex: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
border: 2px solid #8b7355;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Power name header
|
||||||
|
const powerHeader = document.createElement('h4');
|
||||||
|
powerHeader.textContent = `${getPowerDisplayName(power)} Thoughts`;
|
||||||
|
powerHeader.className = `power-${power.toLowerCase()}`;
|
||||||
|
powerHeader.style.cssText = `
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
background: rgba(75, 59, 22, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
border-bottom: 1px solid #8b7355;
|
||||||
|
`;
|
||||||
|
diaryBox.appendChild(powerHeader);
|
||||||
|
|
||||||
|
// Diary content area
|
||||||
|
const contentArea = document.createElement('div');
|
||||||
|
contentArea.className = 'diary-content';
|
||||||
|
contentArea.style.cssText = `
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #4a3b1f;
|
||||||
|
font-style: italic;
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (diaryContent.trim()) {
|
||||||
|
// Split diary content into paragraphs for better readability
|
||||||
|
const paragraphs = diaryContent.split('\n').filter(p => p.trim());
|
||||||
|
paragraphs.forEach((paragraph, index) => {
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.textContent = paragraph.trim();
|
||||||
|
p.style.cssText = `
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
padding: 4px;
|
||||||
|
background: ${index % 2 === 0 ? 'rgba(255,255,255,0.2)' : 'transparent'};
|
||||||
|
border-radius: 3px;
|
||||||
|
`;
|
||||||
|
contentArea.appendChild(p);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No diary content available
|
||||||
|
const noDiaryMsg = document.createElement('div');
|
||||||
|
noDiaryMsg.textContent = 'No diary entries available for this moment.';
|
||||||
|
noDiaryMsg.style.cssText = `
|
||||||
|
color: #8b7355;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
`;
|
||||||
|
contentArea.appendChild(noDiaryMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
diaryBox.appendChild(contentArea);
|
||||||
|
return diaryBox;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the conversation display area
|
* Creates the conversation display area
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,8 @@ export const config = {
|
||||||
animationDuration: 1500,
|
animationDuration: 1500,
|
||||||
|
|
||||||
// How frequently to play sound effects (1 = every message, 3 = every third message)
|
// How frequently to play sound effects (1 = every message, 3 = every third message)
|
||||||
soundEffectFrequency: 3
|
soundEffectFrequency: 3,
|
||||||
|
|
||||||
|
// Whether speech/TTS is enabled (can be toggled via debug menu)
|
||||||
|
speechEnabled: import.meta.env.VITE_DEBUG_MODE ? false : true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { updateNextMomentDisplay, initNextMomentTool } from "./nextMoment";
|
import { updateNextMomentDisplay, initNextMomentTool } from "./nextMoment";
|
||||||
import { initDebugProvinceHighlighting } from "./provinceHighlight";
|
import { initDebugProvinceHighlighting } from "./provinceHighlight";
|
||||||
import { initInstantChatTool } from "./instantChat";
|
import { initInstantChatTool } from "./instantChat";
|
||||||
|
import { initSpeechToggleTool } from "./speechToggle";
|
||||||
|
|
||||||
export class DebugMenu {
|
export class DebugMenu {
|
||||||
private toggleBtn: HTMLButtonElement;
|
private toggleBtn: HTMLButtonElement;
|
||||||
|
|
@ -172,6 +173,7 @@ export class DebugMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
private initTools(): void {
|
private initTools(): void {
|
||||||
|
initSpeechToggleTool(this);
|
||||||
initInstantChatTool(this);
|
initInstantChatTool(this);
|
||||||
initNextMomentTool(this);
|
initNextMomentTool(this);
|
||||||
initDebugProvinceHighlighting()
|
initDebugProvinceHighlighting()
|
||||||
|
|
|
||||||
37
ai_animation/src/debug/speechToggle.ts
Normal file
37
ai_animation/src/debug/speechToggle.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* Speech Toggle Debug Tool
|
||||||
|
* Allows toggling text-to-speech functionality on/off
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { config } from "../config";
|
||||||
|
import type { DebugMenu } from "./debugMenu";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the speech toggle debug tool
|
||||||
|
* @param debugMenu - The debug menu instance to add this tool to
|
||||||
|
*/
|
||||||
|
export function initSpeechToggleTool(debugMenu: DebugMenu): void {
|
||||||
|
const content = `
|
||||||
|
<div style="margin: 8px 0;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="speech-toggle-checkbox" ${config.speechEnabled ? 'checked' : ''}>
|
||||||
|
<span>Enable Text-to-Speech</span>
|
||||||
|
</label>
|
||||||
|
<small style="color: #666; margin-top: 4px; display: block;">
|
||||||
|
Controls whether phase summaries are spoken aloud
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
debugMenu.addDebugTool('Speech Control', content);
|
||||||
|
|
||||||
|
// Add event listener for the checkbox
|
||||||
|
const checkbox = document.getElementById('speech-toggle-checkbox') as HTMLInputElement;
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.addEventListener('change', (event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
config.speechEnabled = target.checked;
|
||||||
|
console.log(`Speech ${config.speechEnabled ? 'enabled' : 'disabled'}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,9 @@ enum AvailableMaps {
|
||||||
* Return a random power from the PowerENUM for the player to control
|
* Return a random power from the PowerENUM for the player to control
|
||||||
*/
|
*/
|
||||||
function getRandomPower(): PowerENUM {
|
function getRandomPower(): PowerENUM {
|
||||||
const values = Object.values(PowerENUM);
|
const values = Object.values(PowerENUM).filter(power =>
|
||||||
|
power !== PowerENUM.GLOBAL && power !== PowerENUM.EUROPE
|
||||||
|
);
|
||||||
const idx = Math.floor(Math.random() * values.length);
|
const idx = Math.floor(Math.random() * values.length);
|
||||||
return values[idx];
|
return values[idx];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,19 +63,24 @@ function _setPhase(phaseIndex: number) {
|
||||||
export function nextPhase() {
|
export function nextPhase() {
|
||||||
if (!gameState.isDisplayingMoment && gameState.gameData && gameState.momentsData) {
|
if (!gameState.isDisplayingMoment && gameState.gameData && gameState.momentsData) {
|
||||||
let moment = gameState.checkPhaseHasMoment(gameState.gameData.phases[gameState.phaseIndex].name)
|
let moment = gameState.checkPhaseHasMoment(gameState.gameData.phases[gameState.phaseIndex].name)
|
||||||
if (moment !== null && moment.interest_score >= MOMENT_THRESHOLD && moment.hasBeenDisplayed === undefined) {
|
if (moment !== null && moment.interest_score >= MOMENT_THRESHOLD && moment.powers_involved.length >= 2) {
|
||||||
gameState.isDisplayingMoment = true
|
gameState.isDisplayingMoment = true
|
||||||
moment.hasBeenDisplayed = true
|
moment.hasBeenDisplayed = true
|
||||||
showTwoPowerConversation({ power1: PowerENUM.AUSTRIA, power2: PowerENUM.FRANCE })
|
|
||||||
|
const power1 = moment.powers_involved[0];
|
||||||
|
const power2 = moment.powers_involved[1];
|
||||||
|
|
||||||
|
showTwoPowerConversation({
|
||||||
|
power1: power1,
|
||||||
|
power2: power2,
|
||||||
|
moment: moment
|
||||||
|
})
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
closeTwoPowerConversation()
|
closeTwoPowerConversation()
|
||||||
gameState.isDisplayingMoment = false
|
gameState.isDisplayingMoment = false
|
||||||
_setPhase(gameState.phaseIndex + 1)
|
_setPhase(gameState.phaseIndex + 1)
|
||||||
}, MOMENT_DISPLAY_TIMEOUT_MS)
|
}, MOMENT_DISPLAY_TIMEOUT_MS)
|
||||||
}
|
} else {
|
||||||
|
|
||||||
else {
|
|
||||||
|
|
||||||
_setPhase(gameState.phaseIndex + 1)
|
_setPhase(gameState.phaseIndex + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,11 @@ async function testElevenLabsKey() {
|
||||||
* @returns Promise that resolves when audio completes or rejects on error
|
* @returns Promise that resolves when audio completes or rejects on error
|
||||||
*/
|
*/
|
||||||
export async function speakSummary(summaryText: string): Promise<void> {
|
export async function speakSummary(summaryText: string): Promise<void> {
|
||||||
|
if (!config.speechEnabled) {
|
||||||
|
console.log("Speech disabled via config, skipping TTS");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!summaryText || summaryText.trim() === '') {
|
if (!summaryText || summaryText.trim() === '') {
|
||||||
console.warn("No summary text provided to speakSummary function");
|
console.warn("No summary text provided to speakSummary function");
|
||||||
return;
|
return;
|
||||||
|
|
@ -98,12 +103,11 @@ export async function speakSummary(summaryText: string): Promise<void> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Truncate text to first 100 characters for ElevenLabs
|
// Truncate text to first 100 characters for ElevenLabs
|
||||||
// FIXME: Is this meant to be truncated? Presumably not
|
let textForSpeaking;
|
||||||
//const textForSpeaking = textToSpeak.substring(0, 100);
|
|
||||||
if (config.isDebugMode) {
|
if (config.isDebugMode) {
|
||||||
const textForSpeaking = textToSpeak.substring(0, 100);
|
textForSpeaking = textToSpeak.substring(0, 100);
|
||||||
} else {
|
} else {
|
||||||
const textForSpeaking = textToSpeak
|
textForSpeaking = textToSpeak
|
||||||
}
|
}
|
||||||
// Hit ElevenLabs TTS endpoint with the truncated text
|
// Hit ElevenLabs TTS endpoint with the truncated text
|
||||||
const headers = {
|
const headers = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue