feat: WorldState memory, increased context, better error handling
- Added WorldState types for character location tracking - Increased RECENT_MESSAGES_COUNT from 6 to 15 - Increased server limits (50k/200k chars) - Added language reminders to system prompts - Better error logging for 400 errors
This commit is contained in:
+6
-4
@@ -143,8 +143,8 @@ function pickAllowedFields(obj, allowedFields) {
|
|||||||
const DEEPSEEK_LIMITS = {
|
const DEEPSEEK_LIMITS = {
|
||||||
MAX_TOKENS_LIMIT: 4096,
|
MAX_TOKENS_LIMIT: 4096,
|
||||||
MAX_MESSAGES: 100,
|
MAX_MESSAGES: 100,
|
||||||
MAX_MESSAGE_LENGTH: 32000, // ~8k tokens per message
|
MAX_MESSAGE_LENGTH: 50000, // ~12k tokens per message (increased for long context)
|
||||||
MAX_TOTAL_LENGTH: 128000, // ~32k tokens total
|
MAX_TOTAL_LENGTH: 200000, // ~50k tokens total (increased for world state + summary)
|
||||||
RATE_LIMIT_WINDOW_MS: 60 * 1000, // 1 minute
|
RATE_LIMIT_WINDOW_MS: 60 * 1000, // 1 minute
|
||||||
RATE_LIMIT_MAX_REQUESTS: 20, // 20 requests per minute per user
|
RATE_LIMIT_MAX_REQUESTS: 20, // 20 requests per minute per user
|
||||||
};
|
};
|
||||||
@@ -1196,6 +1196,7 @@ app.post("/api/deepseek/chat", requireAuth, async (req, res) => {
|
|||||||
// Validation
|
// Validation
|
||||||
const validationErrors = validateDeepSeekRequest(req.body);
|
const validationErrors = validateDeepSeekRequest(req.body);
|
||||||
if (validationErrors.length > 0) {
|
if (validationErrors.length > 0) {
|
||||||
|
console.warn("DeepSeek chat validation failed:", validationErrors);
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.status(400)
|
||||||
.json({ error: "Validation failed", details: validationErrors });
|
.json({ error: "Validation failed", details: validationErrors });
|
||||||
@@ -1206,7 +1207,7 @@ app.post("/api/deepseek/chat", requireAuth, async (req, res) => {
|
|||||||
return res.status(500).json({ error: "DeepSeek API key not configured" });
|
return res.status(500).json({ error: "DeepSeek API key not configured" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { messages, temperature = 0.8, max_tokens = 1000 } = req.body;
|
const { temperature = 0.8, max_tokens = 1000 } = req.body;
|
||||||
const sanitizedMessages = sanitizeDeepSeekMessages(messages);
|
const sanitizedMessages = sanitizeDeepSeekMessages(messages);
|
||||||
const clampedMaxTokens = Math.min(
|
const clampedMaxTokens = Math.min(
|
||||||
max_tokens,
|
max_tokens,
|
||||||
@@ -1256,6 +1257,7 @@ app.post("/api/deepseek/chat/stream", requireAuth, async (req, res) => {
|
|||||||
// Validation
|
// Validation
|
||||||
const validationErrors = validateDeepSeekRequest(req.body);
|
const validationErrors = validateDeepSeekRequest(req.body);
|
||||||
if (validationErrors.length > 0) {
|
if (validationErrors.length > 0) {
|
||||||
|
console.warn("DeepSeek stream validation failed:", validationErrors);
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.status(400)
|
||||||
.json({ error: "Validation failed", details: validationErrors });
|
.json({ error: "Validation failed", details: validationErrors });
|
||||||
@@ -1266,7 +1268,7 @@ app.post("/api/deepseek/chat/stream", requireAuth, async (req, res) => {
|
|||||||
return res.status(500).json({ error: "DeepSeek API key not configured" });
|
return res.status(500).json({ error: "DeepSeek API key not configured" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { messages, temperature = 0.8, max_tokens = 1000 } = req.body;
|
const { temperature = 0.8, max_tokens = 1000 } = req.body;
|
||||||
const sanitizedMessages = sanitizeDeepSeekMessages(messages);
|
const sanitizedMessages = sanitizeDeepSeekMessages(messages);
|
||||||
const clampedMaxTokens = Math.min(
|
const clampedMaxTokens = Math.min(
|
||||||
max_tokens,
|
max_tokens,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
sendMessage,
|
sendMessage,
|
||||||
generateStorySummary,
|
generateStorySummary,
|
||||||
extractKeyEvents,
|
extractKeyEvents,
|
||||||
|
updateWorldState,
|
||||||
|
shouldUpdateWorldState,
|
||||||
} from "../services/deepseek";
|
} from "../services/deepseek";
|
||||||
import type {
|
import type {
|
||||||
Story,
|
Story,
|
||||||
@@ -359,11 +361,26 @@ export default function GamePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update world state for better character tracking
|
||||||
|
let newWorldState = session.worldState;
|
||||||
|
if (shouldUpdateWorldState(allMessages.length, session.worldState)) {
|
||||||
|
try {
|
||||||
|
newWorldState = await updateWorldState(
|
||||||
|
story,
|
||||||
|
allMessages,
|
||||||
|
session.worldState,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to update world state:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const finalSession: GameSession = {
|
const finalSession: GameSession = {
|
||||||
...session,
|
...session,
|
||||||
messages: allMessages,
|
messages: allMessages,
|
||||||
keyEvents: newKeyEvents,
|
keyEvents: newKeyEvents,
|
||||||
storySummary: newSummary,
|
storySummary: newSummary,
|
||||||
|
worldState: newWorldState,
|
||||||
};
|
};
|
||||||
|
|
||||||
await apiSaveSession(story.id, currentSessionId, finalSession);
|
await apiSaveSession(story.id, currentSessionId, finalSession);
|
||||||
@@ -545,9 +562,24 @@ export default function GamePage() {
|
|||||||
assistantMessage,
|
assistantMessage,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Update world state after edit regeneration
|
||||||
|
let newWorldState = session.worldState;
|
||||||
|
if (shouldUpdateWorldState(allMessages.length, session.worldState)) {
|
||||||
|
try {
|
||||||
|
newWorldState = await updateWorldState(
|
||||||
|
story,
|
||||||
|
allMessages,
|
||||||
|
session.worldState,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to update world state:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const finalSession: GameSession = {
|
const finalSession: GameSession = {
|
||||||
...session,
|
...session,
|
||||||
messages: allMessages,
|
messages: allMessages,
|
||||||
|
worldState: newWorldState,
|
||||||
};
|
};
|
||||||
|
|
||||||
await apiSaveSession(story.id, currentSessionId, finalSession);
|
await apiSaveSession(story.id, currentSessionId, finalSession);
|
||||||
|
|||||||
+189
-15
@@ -5,13 +5,15 @@ import type {
|
|||||||
ChatMessage,
|
ChatMessage,
|
||||||
PlayerCharacter,
|
PlayerCharacter,
|
||||||
GameSession,
|
GameSession,
|
||||||
|
WorldState,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_URL || "http://localhost:3001";
|
const API_BASE = import.meta.env.VITE_API_URL || "http://localhost:3001";
|
||||||
|
|
||||||
// Context settings
|
// Context settings
|
||||||
const RECENT_MESSAGES_COUNT = 6; // Last N messages for context
|
const RECENT_MESSAGES_COUNT = 15; // Last N messages for context (increased for better scene continuity)
|
||||||
const SUMMARY_THRESHOLD = 15; // After how many messages to generate summary
|
const SUMMARY_THRESHOLD = 20; // After how many messages to generate summary
|
||||||
|
const WORLD_STATE_UPDATE_INTERVAL = 5; // Update world state every N messages
|
||||||
|
|
||||||
interface DeepSeekMessage {
|
interface DeepSeekMessage {
|
||||||
role: "system" | "user" | "assistant";
|
role: "system" | "user" | "assistant";
|
||||||
@@ -51,7 +53,17 @@ export async function sendMessage(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`DeepSeek API error: ${response.status}`);
|
let errorDetails = "";
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
errorDetails = errorData.details?.join(", ") || errorData.error || "";
|
||||||
|
} catch {
|
||||||
|
// Ignore parse errors
|
||||||
|
}
|
||||||
|
console.error("DeepSeek API error:", response.status, errorDetails);
|
||||||
|
throw new Error(
|
||||||
|
`DeepSeek API error: ${response.status}${errorDetails ? ` - ${errorDetails}` : ""}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: DeepSeekResponse = await response.json();
|
const data: DeepSeekResponse = await response.json();
|
||||||
@@ -82,7 +94,17 @@ export async function sendMessageStream(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`DeepSeek API error: ${response.status}`);
|
let errorDetails = "";
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
errorDetails = errorData.details?.join(", ") || errorData.error || "";
|
||||||
|
} catch {
|
||||||
|
// Ignore parse errors
|
||||||
|
}
|
||||||
|
console.error("DeepSeek stream error:", response.status, errorDetails);
|
||||||
|
throw new Error(
|
||||||
|
`DeepSeek API error: ${response.status}${errorDetails ? ` - ${errorDetails}` : ""}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = response.body?.getReader();
|
const reader = response.body?.getReader();
|
||||||
@@ -177,6 +199,8 @@ LANGUAGE: ${story.language}
|
|||||||
GENRE: ${story.genre.join(", ")}
|
GENRE: ${story.genre.join(", ")}
|
||||||
SETTING: ${settingInfo}
|
SETTING: ${settingInfo}
|
||||||
|
|
||||||
|
IMPORTANT: Respond ONLY in language: ${story.language}. Use proper grammar and spelling.
|
||||||
|
|
||||||
=== PLAYER CHARACTER ===
|
=== PLAYER CHARACTER ===
|
||||||
Name: ${player?.name || "Hero"}
|
Name: ${player?.name || "Hero"}
|
||||||
Description: ${playerDescription}`;
|
Description: ${playerDescription}`;
|
||||||
@@ -262,12 +286,11 @@ ${story.plot}`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds dynamic context (state + summary + rule reminders)
|
* Builds dynamic context (state + summary + world state + rule reminders)
|
||||||
*/
|
*/
|
||||||
export function buildDynamicContext(
|
export function buildDynamicContext(
|
||||||
session: GameSession,
|
session: GameSession,
|
||||||
messageCount?: number,
|
messageCount?: number,
|
||||||
hasCustomRules?: boolean,
|
|
||||||
): string {
|
): string {
|
||||||
const state = session.currentState;
|
const state = session.currentState;
|
||||||
const summary = session.storySummary || "The story just began.";
|
const summary = session.storySummary || "The story just began.";
|
||||||
@@ -275,24 +298,29 @@ export function buildDynamicContext(
|
|||||||
? session.keyEvents.slice(-5).join("\n- ")
|
? session.keyEvents.slice(-5).join("\n- ")
|
||||||
: "No significant events yet.";
|
: "No significant events yet.";
|
||||||
|
|
||||||
|
// World state for character tracking
|
||||||
|
const worldStateContext = formatWorldStateForContext(session.worldState);
|
||||||
|
|
||||||
// Add rule reminders after 10+ messages to prevent drift
|
// Add rule reminders after 10+ messages to prevent drift
|
||||||
// Skip if story has custom rules - they take priority
|
|
||||||
const ruleReminder =
|
const ruleReminder =
|
||||||
messageCount && messageCount >= 10 && !hasCustomRules
|
messageCount && messageCount >= 10
|
||||||
? `
|
? `
|
||||||
|
|
||||||
=== REMINDER ===
|
=== REMINDER ===
|
||||||
|
• Maintain scene continuity — remember current location, characters present, and ongoing actions
|
||||||
• Do NOT act for the player — only describe reactions and consequences
|
• Do NOT act for the player — only describe reactions and consequences
|
||||||
• Do NOT ask "What do you do?" — end with atmosphere, not questions
|
• Do NOT ask "What do you do?" — end with atmosphere, not questions
|
||||||
• Format dialogue: **"text"** (double asterisks = bold)
|
• Format dialogue: **"text"** (double asterisks = bold)
|
||||||
• React to player's words explicitly`
|
• React to player's words explicitly
|
||||||
|
• Use proper grammar and spelling in the story language
|
||||||
|
• Characters can only be where they logically should be based on their last known location`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
return `
|
return `
|
||||||
=== CURRENT STATE ===
|
=== PLAYER STATUS ===
|
||||||
Location: ${state.location}
|
|
||||||
Health: ${state.health}%
|
Health: ${state.health}%
|
||||||
Inventory: ${state.inventory.length > 0 ? state.inventory.join(", ") : "Empty"}
|
Inventory: ${state.inventory.length > 0 ? state.inventory.join(", ") : "Empty"}
|
||||||
|
${worldStateContext}
|
||||||
|
|
||||||
=== STORY SUMMARY ===
|
=== STORY SUMMARY ===
|
||||||
${summary}
|
${summary}
|
||||||
@@ -328,9 +356,8 @@ export async function generateStoryResponse(
|
|||||||
const worldContext = buildWorldContext(story);
|
const worldContext = buildWorldContext(story);
|
||||||
|
|
||||||
// 3. Dynamic context (state + summary + rule reminders after 10+ messages)
|
// 3. Dynamic context (state + summary + rule reminders after 10+ messages)
|
||||||
const hasCustomRules = Boolean(story.narrativeRules?.trim());
|
|
||||||
const dynamicContext = session
|
const dynamicContext = session
|
||||||
? buildDynamicContext(session, chatHistory.length, hasCustomRules)
|
? buildDynamicContext(session, chatHistory.length)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// 4. Last N messages (not the full history!)
|
// 4. Last N messages (not the full history!)
|
||||||
@@ -366,9 +393,8 @@ export async function generateStoryResponseStream(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const styleRules = buildStyleRules(story, player);
|
const styleRules = buildStyleRules(story, player);
|
||||||
const worldContext = buildWorldContext(story);
|
const worldContext = buildWorldContext(story);
|
||||||
const hasCustomRules = Boolean(story.narrativeRules?.trim());
|
|
||||||
const dynamicContext = session
|
const dynamicContext = session
|
||||||
? buildDynamicContext(session, chatHistory.length, hasCustomRules)
|
? buildDynamicContext(session, chatHistory.length)
|
||||||
: "";
|
: "";
|
||||||
const recentMessages = chatHistory.slice(-RECENT_MESSAGES_COUNT);
|
const recentMessages = chatHistory.slice(-RECENT_MESSAGES_COUNT);
|
||||||
const systemPrompt = styleRules + "\n" + worldContext + "\n" + dynamicContext;
|
const systemPrompt = styleRules + "\n" + worldContext + "\n" + dynamicContext;
|
||||||
@@ -532,3 +558,151 @@ The description should be intriguing and make the reader want to start the adven
|
|||||||
|
|
||||||
return sendMessage(messages, 0.7);
|
return sendMessage(messages, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates world state by analyzing recent messages
|
||||||
|
* Call this every WORLD_STATE_UPDATE_INTERVAL messages
|
||||||
|
*/
|
||||||
|
export async function updateWorldState(
|
||||||
|
story: Story,
|
||||||
|
messages: ChatMessage[],
|
||||||
|
currentWorldState?: WorldState,
|
||||||
|
): Promise<WorldState> {
|
||||||
|
const messageCount = messages.length;
|
||||||
|
|
||||||
|
// Get last N messages for analysis
|
||||||
|
const recentMessages = messages.slice(-10);
|
||||||
|
const conversationText = recentMessages
|
||||||
|
.map(
|
||||||
|
(m, i) =>
|
||||||
|
`[${messages.length - 10 + i + 1}] ${m.role === "user" ? "Player" : "Narrator"}: ${m.content}`,
|
||||||
|
)
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
// Get story characters for reference
|
||||||
|
const storyCharacters = story.characters.map((c) => c.name).join(", ");
|
||||||
|
|
||||||
|
const currentStateJson = currentWorldState
|
||||||
|
? JSON.stringify(currentWorldState, null, 2)
|
||||||
|
: "null";
|
||||||
|
|
||||||
|
const prompt = `Analyze the recent story messages and update the world state.
|
||||||
|
|
||||||
|
STORY CHARACTERS: ${storyCharacters || "Not specified"}
|
||||||
|
PLAYER CHARACTER: Main character (MC/Hero)
|
||||||
|
|
||||||
|
CURRENT WORLD STATE:
|
||||||
|
${currentStateJson}
|
||||||
|
|
||||||
|
RECENT MESSAGES:
|
||||||
|
${conversationText}
|
||||||
|
|
||||||
|
Based on the messages, output ONLY a valid JSON object with this exact structure:
|
||||||
|
{
|
||||||
|
"currentScene": {
|
||||||
|
"location": "Current location of the player (be specific)",
|
||||||
|
"presentCharacters": ["List of character names currently in the scene with player"],
|
||||||
|
"situation": "Brief description of what's happening now (1 sentence)"
|
||||||
|
},
|
||||||
|
"characters": {
|
||||||
|
"CharacterName": {
|
||||||
|
"location": "Where this character is now",
|
||||||
|
"lastSeenMessage": <message number when last seen>,
|
||||||
|
"status": "What they're doing",
|
||||||
|
"notes": "Important details (optional)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lastUpdated": ${messageCount}
|
||||||
|
}
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Only include characters that have appeared in the story
|
||||||
|
- If a character left the scene, track their destination
|
||||||
|
- presentCharacters = only those physically with the player RIGHT NOW
|
||||||
|
- Be consistent with previous state, update only what changed
|
||||||
|
- Output ONLY the JSON, no explanations`;
|
||||||
|
|
||||||
|
const worldStateMessages: DeepSeekMessage[] = [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content:
|
||||||
|
"You are a story state tracker. Analyze story messages and output world state as JSON. Be precise about character locations and movements. Output ONLY valid JSON.",
|
||||||
|
},
|
||||||
|
{ role: "user", content: prompt },
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await sendMessage(worldStateMessages, 0.2, 800);
|
||||||
|
|
||||||
|
// Extract JSON from response (in case there's extra text)
|
||||||
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
||||||
|
if (!jsonMatch) {
|
||||||
|
console.warn("Failed to extract JSON from world state response");
|
||||||
|
return currentWorldState || createDefaultWorldState(messageCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(jsonMatch[0]) as WorldState;
|
||||||
|
return parsed;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update world state:", error);
|
||||||
|
return currentWorldState || createDefaultWorldState(messageCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a default world state
|
||||||
|
*/
|
||||||
|
function createDefaultWorldState(messageCount: number): WorldState {
|
||||||
|
return {
|
||||||
|
currentScene: {
|
||||||
|
location: "Unknown",
|
||||||
|
presentCharacters: [],
|
||||||
|
situation: "The story has just begun.",
|
||||||
|
},
|
||||||
|
characters: {},
|
||||||
|
lastUpdated: messageCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if world state should be updated
|
||||||
|
*/
|
||||||
|
export function shouldUpdateWorldState(
|
||||||
|
messageCount: number,
|
||||||
|
currentWorldState?: WorldState,
|
||||||
|
): boolean {
|
||||||
|
if (!currentWorldState) return messageCount >= WORLD_STATE_UPDATE_INTERVAL;
|
||||||
|
|
||||||
|
const messagesSinceUpdate = messageCount - currentWorldState.lastUpdated;
|
||||||
|
return messagesSinceUpdate >= WORLD_STATE_UPDATE_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats world state for inclusion in context
|
||||||
|
*/
|
||||||
|
export function formatWorldStateForContext(worldState?: WorldState): string {
|
||||||
|
if (!worldState) return "";
|
||||||
|
|
||||||
|
const { currentScene, characters } = worldState;
|
||||||
|
|
||||||
|
let result = `
|
||||||
|
=== CURRENT SCENE ===
|
||||||
|
Location: ${currentScene.location}
|
||||||
|
Present: ${currentScene.presentCharacters.length > 0 ? currentScene.presentCharacters.join(", ") : "No one nearby"}
|
||||||
|
Situation: ${currentScene.situation}`;
|
||||||
|
|
||||||
|
const characterEntries = Object.entries(characters);
|
||||||
|
if (characterEntries.length > 0) {
|
||||||
|
result += `
|
||||||
|
|
||||||
|
=== CHARACTER LOCATIONS ===`;
|
||||||
|
for (const [name, state] of characterEntries) {
|
||||||
|
// Skip characters in current scene
|
||||||
|
if (currentScene.presentCharacters.includes(name)) continue;
|
||||||
|
result += `
|
||||||
|
• ${name}: ${state.location} (${state.status})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,6 +69,28 @@ export interface MessageVersion {
|
|||||||
aiResponse?: string; // соответствующий ответ ИИ для этой версии
|
aiResponse?: string; // соответствующий ответ ИИ для этой версии
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Состояние персонажа в мире (для памяти ИИ)
|
||||||
|
export interface CharacterState {
|
||||||
|
location: string; // Где персонаж сейчас
|
||||||
|
lastSeenMessage: number; // Номер сообщения когда последний раз видели
|
||||||
|
status: string; // Что делает ("Едет в город", "Ждёт у таверны")
|
||||||
|
notes?: string; // Дополнительные заметки
|
||||||
|
}
|
||||||
|
|
||||||
|
// Состояние мира (для памяти ИИ о локациях персонажей)
|
||||||
|
export interface WorldState {
|
||||||
|
// Текущая сцена игрока
|
||||||
|
currentScene: {
|
||||||
|
location: string; // Текущая локация ГГ
|
||||||
|
presentCharacters: string[]; // Кто присутствует в сцене
|
||||||
|
situation: string; // Краткое описание ситуации
|
||||||
|
};
|
||||||
|
// Состояние других персонажей (не в сцене)
|
||||||
|
characters: Record<string, CharacterState>;
|
||||||
|
// Последнее обновление
|
||||||
|
lastUpdated: number; // Номер сообщения
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
id: string;
|
id: string;
|
||||||
role: "user" | "assistant" | "system";
|
role: "user" | "assistant" | "system";
|
||||||
@@ -90,6 +112,8 @@ export interface GameSession {
|
|||||||
inventory: string[];
|
inventory: string[];
|
||||||
questProgress: Record<string, boolean>;
|
questProgress: Record<string, boolean>;
|
||||||
};
|
};
|
||||||
|
// Состояние мира (для памяти о локациях персонажей)
|
||||||
|
worldState?: WorldState;
|
||||||
// Сводка важных событий для контекста AI
|
// Сводка важных событий для контекста AI
|
||||||
storySummary?: string;
|
storySummary?: string;
|
||||||
// Ключевые события (для краткой памяти)
|
// Ключевые события (для краткой памяти)
|
||||||
|
|||||||
Reference in New Issue
Block a user