restore: import verified wave3 baseline
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="听娜娜讲故事">
|
||||
<defs>
|
||||
<linearGradient id="background" x1="72" y1="56" x2="440" y2="456" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#263a54"/>
|
||||
<stop offset="1" stop-color="#0b0e16"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="mark" x1="128" y1="176" x2="384" y2="336" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#f2d0c0"/>
|
||||
<stop offset="1" stop-color="#d99378"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="24" y="24" width="464" height="464" rx="112" fill="url(#background)"/>
|
||||
<rect x="25.5" y="25.5" width="461" height="461" rx="110.5" fill="none" stroke="#f3dfd4" stroke-opacity=".16" stroke-width="3"/>
|
||||
<path
|
||||
d="M256 222c-27-43-55-70-91-70-51 0-85 44-85 104s34 104 85 104c36 0 64-27 91-70 27 43 55 70 91 70 51 0 85-44 85-104s-34-104-85-104c-36 0-64 27-91 70Zm-44 34c-18 29-31 47-47 47-17 0-29-18-29-47s12-47 29-47c16 0 29 18 47 47Zm88 0c18-29 31-47 47-47 17 0 29 18 29 47s-12 47-29 47c-16 0-29-18-47-47Z"
|
||||
fill="url(#mark)"
|
||||
/>
|
||||
<circle cx="256" cy="256" r="11" fill="#fff5ef" fill-opacity=".8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
+115
-68
@@ -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(¤t.story_id, &node_id)?;
|
||||
cursor = node.parent_id.clone();
|
||||
let node = self.store.load_node(¤t.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]
|
||||
|
||||
Reference in New Issue
Block a user