Skip to content

Commit fd822a9

Browse files
Bli-AIkCopilot
andauthored
refactor: sequence-driven architecture with dynamic modes and extensible dispatch (#75)
* refactor(sequencer): move sequencer from battle to core Move sequencer module and chapter_schema from app_state/battle/ to core/sequencer/ to make it reusable across AppStates. - Rename BattleContext → SequenceContext - Rename BattleExecutionState → SequenceExecutionState - Add SequencerUpdate SystemSet (callers configure when it runs) - Move SequenceAsset type into core::sequencer - BattlePlugin configures SequencerUpdate.in_set(BattleUpdate) - Update all imports in core/view/ files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(sequencer): move sequencer to core and add overworld sequence support Phase 1: Move battle/sequencer/ to core/sequencer/ - Rename BattleContext → SequenceContext - Rename BattleExecutionState → SequenceExecutionState - Create SequencerUpdate SystemSet (state-agnostic) - SequenceAsset and chapter_schema now live in core - BattlePlugin configures SequencerUpdate via lib.rs run_if Phase 2: Add LoadMap and SetBgm Chapter variants - LoadMap: spawns TiledMap entity from .tmx path - SetBgm: plays/stops BGM via bevy_kira_audio - New processing systems: load_map.rs, bgm.rs Phase 3: Overworld sequence entry support - Add load_overworld_sequence_system on OnEnter(Overworld) - Sequence-driven mode skips hardcoded player spawning - Create example overworld_entry.sequence.ron Phase 4: Config integration - Add initial_sequence_path to GameConfig - Generalize BattleRulesHandle → SequenceRulesHandle - Configure SequencerUpdate for both Overworld and Battle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove overworld_sequence_refactor.md from git tracking Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(overworld): remove hardcoded OnEnter setup, require sequence-driven entry - Remove create_overworld_entities_system (hardcoded SpawnPlayerRequest) - Remove setup_tilemap_system from OnEnter(Overworld) (LoadMap chapter replaces this) - load_overworld_sequence_system now errors when no initial_sequence_path configured - Move example sequence to actual mod (projects/example_mod/) - Delete doc/examples/ directory - Remove overworld_sequence_refactor.md from .gitignore (just not committed instead) - Update app_setup state transition to check initial_sequence_path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(config): add initial_sequence_path to GameConfigPartial for mod.toml loading Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(sequencer): make player spawn sequence-driven for overworld - PlayerAction::Spawn fields are now Option (config_path, position) - config_path: Some → battle player spawn (BattlePlayerConfig) - config_path: None → overworld player spawn (PlayerBehavior via SpawnPlayerRequest) - Remove hardcoded SpawnPlayerRequest from load_overworld_sequence_system - Add process_overworld_player_spawn_system for overworld chapter handling - Re-export ActiveChapter, ChapterFinished, WaitTimer from sequencer module - Update entry.sequence.ron to include SetPlayer(Spawn()) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: replace AppState::Overworld/Battle with GameMode state - AppState now only has AppSetup, Menu, Playing (lifecycle only) - New GameMode state: None, Overworld, Battle (sequence-controlled) - All system sets gate on GameMode instead of AppState - All OnEnter/OnExit use GameMode for mode-specific transitions - Remove dead code: setup_tilemap_system, PreloadedMaps, preload_maps_system - Make player config_path required (explicit reference in sequence RON) - Update 17 files across the codebase - Debug panel now shows both AppState and GameMode Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: mark TiledMap entities with OverworldEntity for cleanup on mode switch TiledMap entities spawned by core sequencer's LoadMap lacked the OverworldEntity marker, preventing cleanup when switching to Battle. Added reactive mark_tilemap_as_overworld_entity system. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(sequencer): reorganize imports and adjust system ordering * refactor: implement Architecture v2 - sequence-driven dynamic modes - Replace GameMode enum with SequenceMode(Option<String>) resource - Replace OverworldEntity/BattleEntity with ModeScoped(String) component - Replace OverworldSubState with generic SequenceSubState(String) state - Add ModeChanged message for mode transition detection - Add detect_mode_changes and cleanup_mode_scoped_entities systems - Add mode/exits fields to SequenceAsset for data-driven mode control - Set SequenceMode automatically when loading sequence assets - Rename AppState::AppSetup→Loading, AppState::Playing→Running - Update 35 files across all modules (view, fre_bridge, debug, etc.) - All tests pass, zero hardcoded mode references remain Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(debug): remove stale AppState::Menu reference from fre_panel The AppState enum was simplified to Loading/Running in the sequence-driven architecture refactor, but the debug FRE panel still referenced the removed Menu variant, causing a compile error with --all-features. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(fre): update stale @overworld_state fact reference to @sequence_sub_state The FRE rule 'overworld_open_backpack' in demo.fre.ron referenced the old fact name `@overworld_state` which was renamed to `@sequence_sub_state` during the Sequence-Driven Architecture v2 refactor. This caused the backpack to never open because the condition never matched. Also updated stale doc comments in fre_bridge.rs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(fre): rename action types to match sequence-driven architecture - SetOverworldState → SetSubState (operates on SequenceSubState, mode-agnostic) - StartBattle → SetMode (generic mode switch with 'mode' param, replaces hardcoded 'battle') - Update demo.fre.ron to use new action names (gitignored, local only) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(chase): move hardcoded visual params to ChaseConfig Move overlay_size, z_offset, and outline padding from hardcoded values to configurable fields in DarkOverlayConfig and OutlineConfig: - DarkOverlayConfig: +overlay_size (default: 10000.0), +z_offset (default: 50.0) - OutlineConfig: +padding (default: 2.0), +z_offset (default: 100.0) All values retain their original defaults via serde(default). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(am): move default battle box size to AmBattleConfig Add default_battle_box_size field to AmBattleConfig (default: 565x140). Previously hardcoded as fallback when AM layer size couldn't be determined. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(fre): extract hardcoded fact keys to fre_facts constants Create core::fre_facts module with centralized constants for all FRE fact key strings (~30 keys across dialogue, state, view, player, enemy). Replace all hardcoded string literals in 8 files with constant references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(battle): remove deprecated TriggerDialogue action handler This action was a no-op that only logged a deprecation warning. Dialogue triggering should be done via FRE rule modifications. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(am): rename AmBattleEntity to AmEntity The 'Battle' prefix was misleading - this is an AM subsystem internal marker component, not related to the old BattleEntity (now ModeScoped). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(fre): unify Custom action dispatch via FreCustomActionEvent Replace triple rule-evaluation dispatch (handle_chase_state_actions, collect_danmaku_actions, process_view_actions for Custom) with a single unified dispatch pipeline: 1. dispatch_custom_actions_system reads events, matches rules, evaluates global conditions, emits FreCustomActionEvent 2. handle_overworld_custom_actions_system handles all action types (EnterChaseState, SetMode, SpawnView, PlayDanmaku, etc.) This eliminates redundant rule evaluation and makes adding new Custom action types trivial - just add a match arm in the handler or register a new system consuming FreCustomActionEvent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(sequencer): add Custom chapter variant for extensible sequences Add Chapter::Custom { action_type, params } variant that dispatches as FreCustomActionEvent during execution, completing immediately. Editors and mods can define custom sequence nodes; handler systems consume FreCustomActionEvent for their specific action types. Use AwaitFact after Custom if the action needs to signal completion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(view): unify View lifecycle through SpawnViewRequest All View spawning (backpack, battle, chase HUD, dialogue) now goes through a single unified path: SpawnViewRequest → handle_spawn_view_request_system → spawn_dynamic_view_system. Removed: - BackpackViewRoot, BattleViewRoot, ChaseHUDRoot marker components - ViewLayoutHandle global resource (replaced by per-entity HotReloadableViewRoot) - spawn_ron_view_system (merged into spawn_dynamic_view_system) - PendingViewBindings global resource (replaced by per-entity PendingViewData) - update_view_from_map_system (simplified to validate_map_properties_system) Added: - SpawnViewRequest.mode_scope: Optional ModeScoped for auto-cleanup - SpawnViewRequest.bindings: Optional data bindings for interface requirements - PendingViewData component: Per-entity binding/FRE handle storage - spawn_dynamic_view_system now handles bindings and FRE asset waiting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(core): streamline FRE action dispatch and integrate ActionHandlerRegistry * refactor(overworld): simplify conditionals and improve code style --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 259de0f commit fd822a9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1771
-1629
lines changed

crates/bevy_fact_rule_event

crates/souprune/src/app_state.rs

Lines changed: 97 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,115 @@
11
//! # app_state.rs
22
//!
3-
//! # app_state.rs 文件
4-
//!
5-
//! ## Module Overview
6-
//!
73
//! ## 模块概述
84
//!
9-
//! The `app_state` module contains the concrete modules for each application state.
10-
//!
11-
//! app_state 模块包含了每个应用程序状态的具体模块。
12-
//!
13-
//! ## Source File Overview
14-
//!
15-
//! ## 源文件概述
16-
//!
17-
//! This file defines the application's state enumeration used throughout the game.
18-
//!
19-
//! 此处定义了贯穿整个游戏的应用程序状态枚举。
20-
//!
21-
//! It includes states for setup, menu, overworld, and battle.
22-
//!
23-
//! 包含初始化、菜单界面、Overworld 和战斗状态。
24-
//!
25-
//! The entire game's state management is based on this enumeration, with the Setup state entered first.
26-
//!
27-
//! 整个游戏的状态管理都基于此枚举,且 Setup 状态会最先被进入。
5+
//! 定义应用程序状态、序列模式、模式作用域和动态子状态。
6+
//! 全部由数据驱动,无硬编码模式枚举。
287
29-
use bevy::prelude::{Commands, Component, Entity, Query, States, With};
8+
use bevy::ecs::message::{Message, MessageReader, MessageWriter};
9+
use bevy::prelude::*;
3010

3111
pub(crate) mod app_setup;
3212
pub(crate) mod battle;
3313
pub(crate) mod overworld;
3414

15+
/// 应用程序生命周期状态。Loading = 资源加载,Running = 序列运行中。
3516
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, States)]
36-
#[allow(dead_code)]
3717
pub enum AppState {
3818
#[default]
39-
AppSetup,
40-
Menu,
41-
Overworld,
42-
Battle,
19+
Loading,
20+
Running,
21+
}
22+
23+
/// 当前序列模式。由序列 RON 文件的 `mode` 字段声明,决定哪些系统集激活。
24+
/// 替代原 `GameMode` 枚举——完全数据驱动。
25+
#[derive(Resource, Default, Debug, Clone, PartialEq, Eq)]
26+
pub struct SequenceMode(pub Option<String>);
27+
28+
impl SequenceMode {
29+
pub fn is(&self, mode: &str) -> bool {
30+
self.0.as_deref() == Some(mode)
31+
}
32+
}
33+
34+
/// 判断当前模式是否匹配。用于系统集的 `run_if` 条件。
35+
pub fn is_mode(mode: &'static str) -> impl Fn(Res<SequenceMode>) -> bool + Clone {
36+
move |res: Res<SequenceMode>| res.0.as_deref() == Some(mode)
37+
}
38+
39+
/// 将实体与特定模式关联。模式切换时旧模式的实体被自动清理。
40+
/// 替代原 `OverworldEntity` / `BattleEntity` 标记。
41+
#[derive(Component, Clone, Debug)]
42+
pub struct ModeScoped(pub String);
43+
44+
/// 模式变更事件。在 `SequenceMode` 资源变化时由检测系统自动发出。
45+
#[derive(Message, Debug, Clone)]
46+
pub struct ModeChanged {
47+
pub from: Option<String>,
48+
pub to: Option<String>,
4349
}
4450

45-
// TODO: 状态管理、状态转换
51+
/// 模式内的动态子状态。替代原 `OverworldSubState`,任何模式都可使用。
52+
/// 模式切换时自动重置为 "Normal"。
53+
#[derive(Debug, Clone, PartialEq, Eq, Hash, States)]
54+
pub struct SequenceSubState(pub String);
55+
56+
impl Default for SequenceSubState {
57+
fn default() -> Self {
58+
Self("Normal".to_string())
59+
}
60+
}
61+
62+
impl SequenceSubState {
63+
pub fn new(name: impl Into<String>) -> Self {
64+
Self(name.into())
65+
}
66+
67+
pub fn is(&self, name: &str) -> bool {
68+
self.0 == name
69+
}
70+
71+
pub fn name(&self) -> &str {
72+
&self.0
73+
}
74+
}
75+
76+
/// 检测 `SequenceMode` 变化并发出 `ModeChanged` 事件。同时重置 `SequenceSubState`。
77+
pub fn detect_mode_changes(
78+
mode: Res<SequenceMode>,
79+
mut previous: Local<Option<String>>,
80+
mut events: MessageWriter<ModeChanged>,
81+
mut next_sub_state: ResMut<NextState<SequenceSubState>>,
82+
) {
83+
if mode.is_changed() {
84+
let from = previous.clone();
85+
let to = mode.0.clone();
86+
if from != to {
87+
events.write(ModeChanged {
88+
from: from.clone(),
89+
to: to.clone(),
90+
});
91+
next_sub_state.set(SequenceSubState::default());
92+
*previous = to;
93+
}
94+
}
95+
}
96+
97+
/// 模式切换时清理旧模式的 `ModeScoped` 实体。
98+
pub fn cleanup_mode_scoped_entities(
99+
mut commands: Commands,
100+
mut events: MessageReader<ModeChanged>,
101+
query: Query<(Entity, &ModeScoped)>,
102+
) {
103+
for event in events.read() {
104+
if let Some(ref from) = event.from {
105+
for (entity, scoped) in query.iter() {
106+
if scoped.0 == *from {
107+
commands.entity(entity).despawn();
108+
}
109+
}
110+
}
111+
}
112+
}
46113

47114
pub fn cleanup_entities_system<T: Component>(
48115
mut commands: Commands,

crates/souprune/src/app_state/app_setup.rs

Lines changed: 9 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,25 @@ use crate::core::sprite::ModuleSpriteRegistry;
2525
use bevy::app::{App, Plugin, Update};
2626
use bevy::asset::LoadedFolder;
2727
use bevy::prelude::*;
28-
use bevy_ecs_tiled::prelude::TiledMapAsset;
2928
use std::fs;
3029

3130
pub(crate) struct AppSetupPlugin;
3231

3332
impl Plugin for AppSetupPlugin {
3433
fn build(&self, app: &mut App) {
3534
app.add_systems(
36-
OnEnter(AppState::AppSetup),
35+
OnEnter(AppState::Loading),
3736
(
3837
load_textures_system,
3938
setup_camera_system,
40-
preload_maps_system,
4139
#[cfg(not(target_os = "android"))]
4240
setup_touch_overlay_system,
4341
),
4442
)
4543
.add_systems(
4644
Update,
4745
(
48-
check_textures_system.run_if(in_state(AppState::AppSetup)),
46+
check_textures_system.run_if(in_state(AppState::Loading)),
4947
crate::core::input::touch::update_touch_button_visuals,
5048
crate::core::input::touch::tick_touch_button_animations,
5149
crate::core::input::touch::update_controller_directions,
@@ -142,18 +140,14 @@ pub struct DiscoveredModules(pub Vec<String>);
142140

143141
fn check_textures_system(
144142
mut next_state: ResMut<NextState<AppState>>,
143+
mut sequence_mode: ResMut<crate::app_state::SequenceMode>,
145144
sprite_registry: Res<ModuleSpriteRegistry>,
146145
asset_server: Res<AssetServer>,
147146
mut events: MessageReader<AssetEvent<LoadedFolder>>,
148147
souprune_config: Res<crate::config::SoupruneConfig>,
149148
discovered_modules: Res<DiscoveredModules>,
150149
) {
151150
for _ in events.read() {
152-
// Check that all required modules are loaded
153-
// Required modules come from config, but must be present in discovered modules
154-
//
155-
// 检查所有必需模块是否已加载
156-
// 必需模块来自配置,但必须存在于发现的模块中
157151
let required_loaded = souprune_config.game.required_modules.iter().all(|module| {
158152
if !discovered_modules.0.contains(module) {
159153
warn!(
@@ -169,9 +163,6 @@ fn check_textures_system(
169163
}
170164
});
171165

172-
// Also check that all discovered modules are loaded
173-
//
174-
// 同时检查所有发现的模块是否已加载
175166
let all_discovered_loaded = discovered_modules.0.iter().all(|module| {
176167
if let Some(handle) = sprite_registry.get_module(module) {
177168
asset_server.is_loaded_with_dependencies(handle)
@@ -183,16 +174,19 @@ fn check_textures_system(
183174
if required_loaded && all_discovered_loaded {
184175
info!("All texture modules loaded: {:?}", discovered_modules.0);
185176

186-
if souprune_config.game.initial_map_path.is_empty()
177+
next_state.set(AppState::Running);
178+
179+
if souprune_config.game.initial_sequence_path.is_none()
180+
&& souprune_config.game.initial_map_path.is_empty()
187181
&& !souprune_config.game.initial_battle_path.is_empty()
188182
{
189183
info!(
190184
"No initial map path, but initial battle path found. Entering Battle: {}",
191185
souprune_config.game.initial_battle_path
192186
);
193-
next_state.set(AppState::Battle);
187+
sequence_mode.0 = Some("battle".to_string());
194188
} else {
195-
next_state.set(AppState::Overworld);
189+
sequence_mode.0 = Some("overworld".to_string());
196190
}
197191
break;
198192
}
@@ -343,39 +337,6 @@ fn deferred_touch_overlay_system(
343337
commands.insert_resource(TouchOverlaySpawned);
344338
}
345339

346-
/// Preload map assets during AppSetup to avoid loading spikes when entering Overworld.
347-
///
348-
/// 在 AppSetup 阶段预加载地图资源,避免进入 Overworld 时的加载卡顿。
349-
fn preload_maps_system(
350-
mut commands: Commands,
351-
asset_server: Res<AssetServer>,
352-
souprune_config: Res<crate::config::SoupruneConfig>,
353-
) {
354-
let initial_map = &souprune_config.game.initial_map_path;
355-
356-
if !initial_map.is_empty() {
357-
info!("Preloading initial map: {}", initial_map);
358-
let handle: Handle<TiledMapAsset> = asset_server.load(initial_map);
359-
commands.insert_resource(PreloadedMaps {
360-
initial_map: Some(handle),
361-
});
362-
} else {
363-
commands.insert_resource(PreloadedMaps { initial_map: None });
364-
}
365-
}
366-
367-
/// Resource storing preloaded map handles.
368-
/// Using preloaded maps avoids the loading spike when entering Overworld.
369-
///
370-
/// 存储预加载地图句柄的资源。
371-
/// 使用预加载地图可以避免进入 Overworld 时的加载尖峰。
372-
#[derive(Resource, Default)]
373-
pub struct PreloadedMaps {
374-
/// The initial overworld map handle, preloaded during AppSetup.
375-
/// 初始 Overworld 地图句柄,在 AppSetup 期间预加载。
376-
pub initial_map: Option<Handle<TiledMapAsset>>,
377-
}
378-
379340
#[derive(Resource)]
380341
pub(crate) struct ResolutionScale(pub(crate) u32);
381342

0 commit comments

Comments
 (0)