* test(web): add comprehensive tests for all Pinia stores
* test(web): add edge case tests for Pinia stores
* test(web): document race condition bugs in ui and world stores
* fix(web): fix race condition bugs in ui and world stores
- ui.ts: Add detailRequestId counter to prevent stale responses from
overwriting fresh data when reselecting the same target
- world.ts: Add eventsRequestId counter to prevent stale responses
when filter changes rapidly via resetEvents
- Update tests to verify the fix works correctly
* fix(web): fix race condition in fetchInitStatus
- Add fetchStatusRequestId counter to prevent stale responses from
overwriting fresh data when fetchInitStatus is called rapidly
- Add test that first proves the bug exists, then verifies the fix
* fix(web): fix race condition in fetchState
Add fetchStateRequestId counter to prevent stale responses from
overwriting fresh data when fetchState is called rapidly.
* test(web): add missing edge case tests for world store
- Add changePhenomenon API failure test
- Add initialize concurrent calls test
- Add getPhenomenaList concurrent calls test
Total: 108 tests
* test(web): add comprehensive socket store tests
- Add init() duplicate call guard test
- Add setup listener tests
- Add message handling tests (tick, game_reinitialized)
- Add status change handling tests
Total: 118 tests
* test(web): add missing socket message handling tests
- Add llm_config_required message tests
- Add unknown message type test
Total: 121 tests
* test(web): add handleTick edge case tests
- Add test for avatars without id (ignored)
- Add test for empty events array
- Add test for events filtered to empty
Total: 124 tests
Use single-pass regex replacement instead of multiple replaceAll calls.
This prevents shorter names from matching inside already-replaced longer names.
For example, with names '张三' and '张三丰', the text '张三丰是大师' now
correctly highlights only '张三丰', not '张三' within it.
When avatars overlap (e.g., during sparring, talking, dual cultivation),
it's hard to click on them directly. This adds the ability to click on
colored avatar names in the event panel to open their detail view.
- Modify highlightAvatarNames to include data-avatar-id attribute
- Add click event delegation in EventPanel
- Add hover styles for clickable names
The pause indicator was showing 'paused' while the game was still running
because isManualPaused was being modified by both user actions (clicking
pause button) and system actions (opening menu).
Changes:
- systemStore: pause()/resume() no longer modify isManualPaused, only
togglePause() does (with optimistic update + rollback on failure)
- useGameControl: consolidate 3 overlapping watches into 1 clean watch
that only handles menu open/close without polluting manual pause state
- App.vue: explicitly call resumeGame() API when game initializes
Add splash layer, support game start, settings, exit
Modify settings layer, add "go back to splash" and "exit"
Add character threshold for history input
Closes#28
- Add async initialization with 6 phases: scanning_assets, loading_map,
initializing_sects, generating_avatars, checking_llm, generating_initial_events
- Add /api/init-status endpoint for frontend polling
- Add /api/control/reinit endpoint for error recovery
- Add LoadingOverlay.vue component with:
- Progress ring with gradient
- Phase text in xianxia style (rotating messages for LLM phase)
- Tips that rotate every 5 seconds
- Time-based background transparency (fades to 80% over 20s)
- Backdrop blur effect
- Error state with retry button
- Preload map and avatars during LLM initialization for smoother UX
- Add comprehensive tests for init status API
Problem:
When loading a save (e.g., from year 106), events from year 100 would appear.
This happened because the game auto-started on server startup and client
connection, generating initialization events before the user could load a save.
Solution:
1. Backend: Keep game paused on startup even if LLM check passes
2. Backend: Remove auto-resume on first WebSocket connection
3. Frontend: Start with game paused (isManualPaused = true)
Now the user must explicitly click 'resume' to start a new game, or load a
save first. This prevents the race condition where game_loop generates events
with stale world state.
Implement SQLite-based event persistence as specified in sqlite-event-manager.md.
## Changes
### Backend
- **EventStorage** (`src/classes/event_storage.py`): New SQLite storage layer
- Cursor-based pagination with compound cursor `{month_stamp}_{rowid}`
- Avatar filtering (single and pair queries)
- Major/minor event separation
- Cleanup API with `keep_major` and `before_month_stamp` filters
- **EventManager** (`src/classes/event_manager.py`): Refactored to use SQLite
- Delegates to EventStorage for persistence
- Memory fallback mode for testing
- New `get_events_paginated()` method
- **API** (`src/server/main.py`):
- `GET /api/events` - Paginated event retrieval with filtering
- `DELETE /api/events/cleanup` - User-triggered cleanup
### Frontend
- **EventPanel.vue**: Scroll-to-load pagination, dual-person filter UI
- **world.ts**: Event state management with pagination
- **game.ts**: New API client methods
### Testing
- 81 new tests for EventStorage, EventManager, and API
- Added `pytest-asyncio` and `httpx` to requirements.txt
## Known Issues: Save/Load is Currently Broken
After loading a saved game, the following issues occur:
1. **Wrong database used**: API returns events from the startup database instead
of the loaded save's `_events.db` file
2. **Events from wrong time period**: Shows events from year 115 when loaded
save is at year 114
3. **Pagination broken after load**: `has_more` returns `False` despite hundreds
of events in the saved database
4. **Filter functionality broken**: Character selection filter stops working
after loading a game
Root cause: `load_game.py` does not properly switch the EventManager's database
connection to the loaded save's events database.