v0.2.0 — M7: LLM Integration Frontend ↔ Backend

- api.js: API bridge to FastAPI backend
- Dynamic NPC dialogues via gemma4:12b (cached 30s)
- Periodic world event generation (every ~10s)
- Event validation (no soft-lock)
- Backend health check on startup
- Fallback to static dialogue if LLM offline
- Tested: events , dialogues 
This commit is contained in:
arch_agent
2026-07-17 20:24:58 +02:00
parent 5cf42a5e5d
commit face8023a4
5 changed files with 190 additions and 5 deletions
+23 -3
View File
@@ -28,6 +28,26 @@
- NPC dialogue is static (LLM not yet wired to frontend)
- Only one map (Eichenhafen)
### Next Milestone
- M2: Refine pathfinding, add map transitions
- M3: Wire LLM dialogue to frontend NPC interactions
## v0.2.0 — M7: LLM Integration Frontend ↔ Backend (2026-07-17)
### Added
- API bridge (`api.js`) connecting frontend to FastAPI backend
- Backend health check on startup — shows LLM model name in event log
- Dynamic NPC dialogues via Ollama (gemma4:12b)
- Each NPC gets unique dialogue based on personality, mood, and player class/level
- 30-second dialogue cache to reduce LLM calls
- Fallback to static dialogue if LLM unavailable
- Periodic world event generation (every ~10 seconds)
- LLM generates optional events (weather, discoveries, encounters)
- Events validated by backend (no soft-lock possible)
- Events displayed in event log with ✨ prefix and purple color
- NPC interaction now shows "LLM generating..." placeholder while waiting
### Changed
- NPC interaction is now async (waits for LLM response)
- Event log color coding: purple (llm), red (combat), green (quest), yellow (system)
### Tested
- Backend health endpoint: ✅
- Event generation: ✅ (gemma4:12b generates atmospheric events)
- NPC dialogue: ✅ (personalized, in-character responses)
Binary file not shown.
+1
View File
@@ -74,6 +74,7 @@
<script src="js/camera.js"></script>
<script src="js/player.js"></script>
<script src="js/input.js"></script>
<script src="js/api.js"></script>
<script src="js/game.js"></script>
</body>
</html>
+89
View File
@@ -0,0 +1,89 @@
// api.js — Frontend ↔ Backend API Bridge
const API_BASE = 'http://localhost:8000/api';
class GameAPI {
constructor() {
this.cache = new Map();
this.cacheTimeout = 30000; // 30s cache for dialogues
}
async getPlayer() {
try {
const res = await fetch(`${API_BASE}/player`);
return await res.json();
} catch (e) {
console.warn('[API] getPlayer failed:', e);
return null;
}
}
async savePlayer(state) {
try {
await fetch(`${API_BASE}/player/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state)
});
} catch (e) {
console.warn('[API] savePlayer failed:', e);
}
}
async getEvents(mapId) {
try {
const res = await fetch(`${API_BASE}/events/${mapId}`);
const data = await res.json();
return data.events || [];
} catch (e) {
console.warn('[API] getEvents failed:', e);
return [];
}
}
async generateEvent(mapId, playerLevel) {
try {
const res = await fetch(`${API_BASE}/llm/event?map_id=${mapId}&player_level=${playerLevel}`, {
method: 'POST'
});
const data = await res.json();
return data.event;
} catch (e) {
console.warn('[API] generateEvent failed:', e);
return null;
}
}
async getNPCDialogue(npcId, playerClass, playerLevel, mood) {
const cacheKey = `${npcId}_${mood}`;
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
if (Date.now() - cached.time < this.cacheTimeout) {
return cached.dialogue;
}
}
try {
const res = await fetch(`${API_BASE}/llm/dialogue?npc_id=${npcId}&player_class=${playerClass}&player_level=${playerLevel}&mood=${mood}`, {
method: 'POST'
});
const data = await res.json();
if (data.dialogue) {
this.cache.set(cacheKey, { dialogue: data.dialogue, time: Date.now() });
return data.dialogue;
}
} catch (e) {
console.warn('[API] getNPCDialogue failed:', e);
}
return null;
}
async health() {
try {
const res = await fetch(`${API_BASE}/health`);
return await res.json();
} catch (e) {
return { status: 'offline' };
}
}
}
+76 -1
View File
@@ -17,6 +17,14 @@ class Game {
this.npcs = [];
this.damageNumbers = [];
this.events = [];
this.activeWorldEvents = [];
// API
this.api = new GameAPI();
this.currentMap = 'eichenhafen';
this.eventCheckTimer = 0;
this.eventCheckInterval = 600; // Check every 10 seconds (at 60fps)
this.llmBusy = false;
// Spawn some NPCs
this.spawnNPCs();
@@ -33,9 +41,51 @@ class Game {
this.addEvent('Willkommen in Eichenhafen!', 'system');
this.addEvent('Klicke um dich zu bewegen. Klicke auf Monster zum angreifen.', 'system');
// Check backend health
this.checkBackend();
// Load existing events
this.loadWorldEvents();
this.loop();
}
async checkBackend() {
const health = await this.api.health();
if (health.status === 'ok') {
this.addEvent(`LLM verbunden: ${health.llm_model}`, 'llm');
} else {
this.addEvent('Backend offline — LLM-Features deaktiviert', 'system');
}
}
async loadWorldEvents() {
const events = await this.api.getEvents(this.currentMap);
this.activeWorldEvents = events;
for (const ev of events) {
this.addEvent(`[Event] ${ev.description}`, 'llm');
}
}
async checkForNewEvents() {
if (this.llmBusy) return;
this.llmBusy = true;
const event = await this.api.generateEvent(this.currentMap, this.player.level);
if (event) {
this.activeWorldEvents.push(event);
this.addEvent(`${event.description}`, 'llm');
// Apply event effects
if (event.effect === 'spawn') {
this.addEvent('Ein neues Monster ist aufgetaucht!', 'combat');
// Could spawn a special monster based on event data
}
}
this.llmBusy = false;
}
spawnNPCs() {
// NPC near the first building
this.npcs.push({
@@ -162,6 +212,13 @@ class Game {
update() {
this.frameCount++;
// Periodic event check (every ~10 seconds)
this.eventCheckTimer++;
if (this.eventCheckTimer >= this.eventCheckInterval) {
this.eventCheckTimer = 0;
this.checkForNewEvents();
}
// Update player
this.player.update(this.map);
@@ -399,9 +456,27 @@ class Game {
document.getElementById('char-class').textContent = this.player.class;
}
interactNPC(npc) {
async interactNPC(npc) {
this.addEvent(`${npc.name}: "..." (LLM generiert...)`, 'llm');
// Generate mood based on current events
const moods = ['neutral', 'glücklich', 'müde', 'besorgt', 'aufgeregt'];
const mood = moods[Math.floor(Math.random() * moods.length)];
const dialogue = await this.api.getNPCDialogue(
npc.id,
this.player.class,
this.player.level,
mood
);
if (dialogue) {
this.addEvent(`${npc.name}: ${dialogue}`, 'llm');
} else {
// Fallback to static dialogue
this.addEvent(`${npc.name}: "${npc.dialogue}"`, 'quest');
}
}
useSkill(slot) {
this.addEvent(`Skill ${slot} (noch nicht implementiert)`, 'system');