restore: import verified wave3 baseline

This commit is contained in:
Codex
2026-07-28 03:16:01 -04:00
parent 4157f8790d
commit cf9507a9dd
41 changed files with 12251 additions and 982 deletions
+115 -68
View File
@@ -1,18 +1,17 @@
use std::{fs, path::Path};
use nana_domain::{
ActionSuggestion, AppInfo, BeatKind, DemoPackSummary, HistoryNodeView, PlayerView,
PresentationBeat, Promise, PromiseStatus, PromiseWeight, RelationshipAdjustment,
RelationshipDimension, ResourceBundle, RuntimeState, StateDelta, StateOp, StoryNode,
TurnFailure, TurnFailureCode, TurnIntent, TurnRequest, TurnResult, ValidationIssue,
VisualDirective, DOMAIN_SCHEMA_VERSION, stable_json_hash, validate_bundle,
ActionSuggestion, AppInfo, BeatKind, DOMAIN_SCHEMA_VERSION, DemoPackSummary, HistoryNodeView,
PlayerView, PresentationBeat, PresentationCharacter, PresentationScene, PresentationSnapshot,
Promise, PromiseStatus, PromiseWeight, RelationshipAdjustment, RelationshipDimension,
ResourceBundle, RuntimeState, StateDelta, StateOp, StoryNode, TurnFailure, TurnFailureCode,
TurnIntent, TurnRequest, TurnResult, ValidationIssue, VisualDirective, stable_json_hash,
validate_bundle,
};
use nana_engine::{
PlayerViewProjectionContext, SceneMetadata, project_player_view,
};
use nana_runtime::{
ProviderError, TurnEngine, TurnPlan, TurnPlanProvider, TurnProjector,
SceneMetadata, StoryNodePlayerViewProjectionContext, project_story_node_player_view,
};
use nana_runtime::{ProviderError, TurnEngine, TurnPlan, TurnPlanProvider, TurnProjector};
use nana_store::{SqliteStoryStore, StoreError, StoryStore};
use serde::Serialize;
use tauri::Manager;
@@ -55,7 +54,7 @@ impl CommandError {
fn turn(failure: TurnFailure) -> Self {
Self {
code: turn_failure_code(failure.code).to_owned(),
code: turn_failure_code(&failure.code).to_owned(),
message: failure.message,
retryable: failure.retryable,
issues: Vec::new(),
@@ -132,40 +131,27 @@ impl DemoAppState {
let lineage = self
.load_lineage(&node)
.map_err(|error| CommandError::storage(&error))?;
Ok(self.project_view_with_lineage(
&state,
&node,
&default_suggestions(),
&lineage,
))
Ok(self.project_view_with_lineage(&state, &node, &lineage))
}
fn submit_turn(&self, request: &TurnRequest) -> Result<TurnResult, CommandError> {
let provider = DemoPlanProvider;
let projector = DemoProjector { app: self };
let mut engine = TurnEngine::new(&self.store, provider, projector);
engine
.submit_turn(request)
.map_err(CommandError::turn)
engine.submit_turn(request).map_err(CommandError::turn)
}
fn project_view(
&self,
state: &RuntimeState,
node: &StoryNode,
suggestions: &[ActionSuggestion],
) -> PlayerView {
fn project_view(&self, state: &RuntimeState, node: &StoryNode) -> PlayerView {
let lineage = self
.load_lineage(node)
.unwrap_or_else(|_| vec![node.clone()]);
self.project_view_with_lineage(state, node, suggestions, &lineage)
self.project_view_with_lineage(state, node, &lineage)
}
fn project_view_with_lineage(
&self,
state: &RuntimeState,
node: &StoryNode,
suggestions: &[ActionSuggestion],
lineage: &[StoryNode],
) -> PlayerView {
let history = history_for(lineage, node);
@@ -176,22 +162,20 @@ impl DemoAppState {
.iter()
.find(|character| character.header.id == self.bundle.entry_character)
.map_or("角色", |character| character.name.as_str());
project_player_view(
project_story_node_player_view(
state,
&PlayerViewProjectionContext {
node,
&StoryNodePlayerViewProjectionContext {
player_id: PLAYER_ID,
relationship_character_id: CHARACTER_ID,
relationship_updated_at_node,
item_specs: &self.bundle.item_specs,
scene: SceneMetadata {
history: &history,
legacy_scene: SceneMetadata {
scene_id: "old_station_platform",
scene_title: "旧青川站",
character_name,
},
beats: &node.beats,
suggestions,
history: &history,
can_continue: true,
},
)
}
@@ -200,14 +184,14 @@ impl DemoAppState {
let mut lineage = vec![current.clone()];
let mut cursor = current.parent_id.clone();
while let Some(node_id) = cursor {
while let Some(ref node_id) = cursor {
if lineage.len() >= 200 {
return Err(StoreError::StateMismatch(
"story lineage exceeds the projection limit",
));
}
let node = self.store.load_node(&current.story_id, &node_id)?;
cursor = node.parent_id.clone();
let node = self.store.load_node(&current.story_id, node_id)?;
cursor.clone_from(&node.parent_id);
lineage.push(node);
}
@@ -245,9 +229,8 @@ impl TurnPlanProvider for DemoPlanProvider {
Ok(TurnPlan {
committed_node_id,
beats,
presentation: demo_presentation(beats),
delta,
suggestions: default_suggestions(),
})
}
}
@@ -257,13 +240,8 @@ struct DemoProjector<'a> {
}
impl TurnProjector for DemoProjector<'_> {
fn project_committed_turn(
&mut self,
state: &RuntimeState,
node: &StoryNode,
suggestions: &[ActionSuggestion],
) -> PlayerView {
self.app.project_view(state, node, suggestions)
fn project_committed_turn(&mut self, state: &RuntimeState, node: &StoryNode) -> PlayerView {
self.app.project_view(state, node)
}
}
@@ -281,7 +259,21 @@ fn seed_demo_story(store: &SqliteStoryStore) -> Result<(), CommandError> {
parent_id: None,
action_id: "story_created".to_owned(),
user_input: String::new(),
beats: view.beats,
presentation: PresentationSnapshot {
scene: PresentationScene {
id: view.scene_id,
title: view.scene_title,
},
character: PresentationCharacter {
id: CHARACTER_ID.to_owned(),
name: view.character_name,
expression: view.character_expression,
pose: view.character_pose,
},
beats: view.beats,
suggestions: view.suggestions,
can_continue: view.can_continue,
},
delta: StateDelta { ops: Vec::new() },
state_hash: stable_json_hash(&state_bytes),
};
@@ -336,7 +328,10 @@ fn promise_turn(request: &TurnRequest, node_id: &str) -> (Vec<PresentationBeat>,
(beats, delta)
}
fn continue_turn(request: &TurnRequest, state: &RuntimeState) -> (Vec<PresentationBeat>, StateDelta) {
fn continue_turn(
request: &TurnRequest,
state: &RuntimeState,
) -> (Vec<PresentationBeat>, StateDelta) {
let beats = vec![
PresentationBeat {
id: format!("{}_rain", request.action_id),
@@ -353,7 +348,7 @@ fn continue_turn(request: &TurnRequest, state: &RuntimeState) -> (Vec<Presentati
visual: Some(VisualDirective {
character: Some(CHARACTER_ID.to_owned()),
expression: Some("guarded".to_owned()),
pose: None,
pose: Some("holding_coat".to_owned()),
scene: None,
}),
},
@@ -402,6 +397,42 @@ fn player_action_beat(request: &TurnRequest) -> PresentationBeat {
}
}
fn demo_presentation(beats: Vec<PresentationBeat>) -> PresentationSnapshot {
let scene_id = beats
.iter()
.filter_map(|beat| beat.visual.as_ref())
.filter_map(|visual| visual.scene.as_deref())
.next_back()
.unwrap_or("old_station_platform")
.to_owned();
let expression = beats
.iter()
.filter_map(|beat| beat.visual.as_ref())
.filter_map(|visual| visual.expression.clone())
.next_back();
let pose = beats
.iter()
.filter_map(|beat| beat.visual.as_ref())
.filter_map(|visual| visual.pose.clone())
.next_back();
PresentationSnapshot {
scene: PresentationScene {
id: scene_id,
title: "旧青川站".to_owned(),
},
character: PresentationCharacter {
id: CHARACTER_ID.to_owned(),
name: "娜娜".to_owned(),
expression,
pose,
},
beats,
suggestions: default_suggestions(),
can_continue: true,
}
}
fn default_suggestions() -> Vec<ActionSuggestion> {
vec![
ActionSuggestion {
@@ -441,18 +472,13 @@ fn history_label(node: &StoryNode) -> String {
if node.parent_id.is_none() {
return "雨夜车站".to_owned();
}
if node
.delta
.ops
.iter()
.any(|op| {
matches!(
op,
StateOp::CreatePromise { promise }
if is_player_visible_promise(promise)
)
})
{
if node.delta.ops.iter().any(|op| {
matches!(
op,
StateOp::CreatePromise { promise }
if is_player_visible_promise(promise)
)
}) {
return "天亮前的许诺".to_owned();
}
if node.user_input.is_empty() {
@@ -503,7 +529,7 @@ fn is_player_visible_promise(promise: &Promise) -> bool {
)
}
const fn turn_failure_code(code: TurnFailureCode) -> &'static str {
const fn turn_failure_code(code: &TurnFailureCode) -> &'static str {
match code {
TurnFailureCode::StaleNode => "stale_node",
TurnFailureCode::InvalidInput => "invalid_input",
@@ -525,16 +551,19 @@ fn get_app_info() -> AppInfo {
}
#[tauri::command]
#[allow(clippy::needless_pass_by_value)] // Tauri extracts managed state through this owned wrapper.
fn get_demo_player_view(state: tauri::State<'_, DemoAppState>) -> Result<PlayerView, CommandError> {
state.current_player_view()
}
#[tauri::command]
#[allow(clippy::needless_pass_by_value)] // Tauri extracts managed state through this owned wrapper.
fn get_demo_pack_summary(state: tauri::State<'_, DemoAppState>) -> DemoPackSummary {
state.pack_summary()
}
#[tauri::command]
#[allow(clippy::needless_pass_by_value)] // Tauri deserializes command arguments into owned values.
fn submit_turn(
state: tauri::State<'_, DemoAppState>,
request: TurnRequest,
@@ -542,6 +571,11 @@ fn submit_turn(
state.submit_turn(&request)
}
/// Starts the desktop application and opens its local story database.
///
/// # Panics
///
/// Panics when the Tauri runtime cannot be started.
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@@ -572,15 +606,14 @@ mod tests {
};
use nana_domain::{
Promise, PromiseStatus, PromiseWeight, RelationshipAdjustment,
RelationshipBand, RelationshipDimension, StateDelta, StateOp, StoryNode,
TurnIntent, TurnRequest,
PresentationSnapshot, Promise, PromiseStatus, PromiseWeight, RelationshipAdjustment,
RelationshipBand, RelationshipDimension, StateDelta, StateOp, StoryNode, TurnIntent,
TurnRequest,
};
use nana_store::StoryStore;
use super::{
DemoAppState, DEMO_BRANCH_ID, DEMO_STORY_ID, history_label,
last_player_relationship_update,
DEMO_BRANCH_ID, DEMO_STORY_ID, DemoAppState, history_label, last_player_relationship_update,
};
struct TemporaryDatabase {
@@ -633,7 +666,7 @@ mod tests {
parent_id: parent_id.map(str::to_owned),
action_id: format!("action_{id}"),
user_input: "继续前进".to_owned(),
beats: Vec::new(),
presentation: PresentationSnapshot::default(),
delta: StateDelta { ops },
state_hash: format!("hash_{id}"),
}
@@ -673,6 +706,13 @@ mod tests {
let restored = app.current_player_view().expect("restored PlayerView");
assert_eq!(restored.node_id, result.committed_node_id);
assert_eq!(restored.promises, result.player_view.promises);
assert_eq!(restored.beats, result.player_view.beats);
assert_eq!(restored.suggestions, result.player_view.suggestions);
assert_eq!(
restored.character_expression,
result.player_view.character_expression
);
assert_eq!(restored.character_pose, result.player_view.character_pose);
}
#[test]
@@ -692,6 +732,13 @@ mod tests {
assert_eq!(restored.promises, committed.promises);
assert_eq!(restored.relationship, committed.relationship);
assert_eq!(restored.history, committed.history);
assert_eq!(restored.beats, committed.beats);
assert_eq!(restored.suggestions, committed.suggestions);
assert_eq!(
restored.character_expression,
committed.character_expression
);
assert_eq!(restored.character_pose, committed.character_pose);
}
#[test]