chore: initial commit
This commit is contained in:
commit
ddd2960fa6
317 files changed
+41717
No files matched your search
@@ -0,0 +1 @@
|
||||
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
[editor_states]
|
||||
|
||||
$selected_nodes=Array[NodePath]([NodePath(".")])
|
||||
2D={
|
||||
"grid_offset": Vector2(0, 0),
|
||||
"grid_snap_active": false,
|
||||
"grid_step": Vector2(8, 8),
|
||||
"grid_visibility": 1,
|
||||
"ofs": Vector2(-166, -111),
|
||||
"primary_grid_step": Vector2i(8, 8),
|
||||
"show_group_gizmos": true,
|
||||
"show_guides": true,
|
||||
"show_helpers": false,
|
||||
"show_lock_gizmos": true,
|
||||
"show_origin": true,
|
||||
"show_position_gizmos": true,
|
||||
"show_rulers": true,
|
||||
"show_transformation_gizmos": true,
|
||||
"show_viewport": true,
|
||||
"show_zoom_control": true,
|
||||
"smart_snap_active": false,
|
||||
"snap_guides": true,
|
||||
"snap_node_anchors": true,
|
||||
"snap_node_center": true,
|
||||
"snap_node_parent": true,
|
||||
"snap_node_sides": true,
|
||||
"snap_other_nodes": true,
|
||||
"snap_pixel": true,
|
||||
"snap_relative": false,
|
||||
"snap_rotation": false,
|
||||
"snap_rotation_offset": 0.0,
|
||||
"snap_rotation_step": 0.2617994,
|
||||
"snap_scale": false,
|
||||
"snap_scale_step": 0.1,
|
||||
"zoom": 1.0
|
||||
}
|
||||
3D={
|
||||
"fov": 70.01,
|
||||
"gizmos_status": {
|
||||
"AudioListener3D": 0,
|
||||
"AudioStreamPlayer3D": 0,
|
||||
"CPUParticles3D": 0,
|
||||
"CSGShape3D": 0,
|
||||
"Camera3D": 0,
|
||||
"ChainIK3D": 0,
|
||||
"CollisionObject3D": 0,
|
||||
"CollisionPolygon3D": 0,
|
||||
"CollisionShape3D": 0,
|
||||
"Decal": 0,
|
||||
"FogVolume": 0,
|
||||
"GPUParticles3D": 0,
|
||||
"GPUParticlesCollision3D": 0,
|
||||
"Joint3D": 0,
|
||||
"Light3D": 0,
|
||||
"LightmapGI": 0,
|
||||
"LightmapProbe": 0,
|
||||
"Marker3D": 0,
|
||||
"MeshInstance3DCustomAABB": 0,
|
||||
"NavigationLink3D": 0,
|
||||
"NavigationObstacle3D": 0,
|
||||
"NavigationRegion3D": 0,
|
||||
"OccluderInstance3D": 0,
|
||||
"Particles3DEmissionShape": 0,
|
||||
"Path3D": 0,
|
||||
"PhysicalBone3D": 0,
|
||||
"RayCast3D": 0,
|
||||
"ReflectionProbe": 0,
|
||||
"ShapeCast3D": 0,
|
||||
"Skeleton3D": 0,
|
||||
"SoftBody3D": 0,
|
||||
"SpringArm3D": 0,
|
||||
"SpringBoneCollision3D": 0,
|
||||
"SpringBoneSimulator3D": 0,
|
||||
"TwoBoneIK3D": 0,
|
||||
"VehicleWheel3D": 0,
|
||||
"VisibleOnScreenNotifier3D": 0,
|
||||
"VoxelGI": 0
|
||||
},
|
||||
"local_coords": false,
|
||||
"preserve_children_transform": false,
|
||||
"preview_sun_env": {
|
||||
"environ_ao_enabled": false,
|
||||
"environ_enabled": true,
|
||||
"environ_energy": 1.0,
|
||||
"environ_gi_enabled": false,
|
||||
"environ_glow_enabled": false,
|
||||
"environ_ground_color": Color(0.2, 0.169, 0.133, 1),
|
||||
"environ_sky_color": Color(0.385, 0.454, 0.55, 1),
|
||||
"environ_tonemap_enabled": false,
|
||||
"sun_color": Color(1, 1, 1, 1),
|
||||
"sun_enabled": true,
|
||||
"sun_energy": 1.0,
|
||||
"sun_rotation": Vector2(-1.0471976, 2.6179938),
|
||||
"sun_shadow_max_distance": 100.0
|
||||
},
|
||||
"rotate_snap": 15.0,
|
||||
"scale_snap": 10.0,
|
||||
"show_grid": true,
|
||||
"show_origin": true,
|
||||
"snap_enabled": false,
|
||||
"trackball_enabled": false,
|
||||
"translate_snap": 1.0,
|
||||
"vertex_snap_origin_mode": false,
|
||||
"vertex_snap_use_collision": false,
|
||||
"viewport_mode": 1,
|
||||
"viewport_splits": {
|
||||
"first": 0.0,
|
||||
"main": 0.0,
|
||||
"second": 0.0
|
||||
},
|
||||
"viewports": [{
|
||||
"auto_orthogonal": false,
|
||||
"auto_orthogonal_enabled": true,
|
||||
"cinematic_preview": false,
|
||||
"display_mode": 22,
|
||||
"distance": 4.0,
|
||||
"doppler": false,
|
||||
"frame_time": false,
|
||||
"gizmos": true,
|
||||
"grid": true,
|
||||
"half_res": false,
|
||||
"information": false,
|
||||
"listener": true,
|
||||
"lock_rotation": false,
|
||||
"orthogonal": false,
|
||||
"position": Vector3(0, 0, 0),
|
||||
"transform_gizmo": true,
|
||||
"use_environment": false,
|
||||
"view_type": 0,
|
||||
"x_rotation": 0.5,
|
||||
"y_rotation": -0.5
|
||||
}, {
|
||||
"auto_orthogonal": false,
|
||||
"auto_orthogonal_enabled": true,
|
||||
"cinematic_preview": false,
|
||||
"display_mode": 22,
|
||||
"distance": 4.0,
|
||||
"doppler": false,
|
||||
"frame_time": false,
|
||||
"gizmos": true,
|
||||
"grid": true,
|
||||
"half_res": false,
|
||||
"information": false,
|
||||
"listener": false,
|
||||
"lock_rotation": false,
|
||||
"orthogonal": false,
|
||||
"position": Vector3(0, 0, 0),
|
||||
"transform_gizmo": true,
|
||||
"use_environment": false,
|
||||
"view_type": 0,
|
||||
"x_rotation": 0.5,
|
||||
"y_rotation": -0.5
|
||||
}, {
|
||||
"auto_orthogonal": false,
|
||||
"auto_orthogonal_enabled": true,
|
||||
"cinematic_preview": false,
|
||||
"display_mode": 22,
|
||||
"distance": 4.0,
|
||||
"doppler": false,
|
||||
"frame_time": false,
|
||||
"gizmos": true,
|
||||
"grid": true,
|
||||
"half_res": false,
|
||||
"information": false,
|
||||
"listener": false,
|
||||
"lock_rotation": false,
|
||||
"orthogonal": false,
|
||||
"position": Vector3(0, 0, 0),
|
||||
"transform_gizmo": true,
|
||||
"use_environment": false,
|
||||
"view_type": 0,
|
||||
"x_rotation": 0.5,
|
||||
"y_rotation": -0.5
|
||||
}, {
|
||||
"auto_orthogonal": false,
|
||||
"auto_orthogonal_enabled": true,
|
||||
"cinematic_preview": false,
|
||||
"display_mode": 22,
|
||||
"distance": 4.0,
|
||||
"doppler": false,
|
||||
"frame_time": false,
|
||||
"gizmos": true,
|
||||
"grid": true,
|
||||
"half_res": false,
|
||||
"information": false,
|
||||
"listener": false,
|
||||
"lock_rotation": false,
|
||||
"orthogonal": false,
|
||||
"position": Vector3(0, 0, 0),
|
||||
"transform_gizmo": true,
|
||||
"use_environment": false,
|
||||
"view_type": 0,
|
||||
"x_rotation": 0.5,
|
||||
"y_rotation": -0.5
|
||||
}],
|
||||
"zfar": 4000.01,
|
||||
"znear": 0.05
|
||||
}
|
||||
Anim={
|
||||
"visible": false
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
[docks]
|
||||
|
||||
dock_3="Scene,Import"
|
||||
dock_3_selected_tab_idx=0
|
||||
dock_4="FileSystem,History"
|
||||
dock_4_selected_tab_idx=0
|
||||
dock_5="Inspector,Signals,Groups"
|
||||
dock_5_selected_tab_idx=0
|
||||
dock_9="Output,Debugger,Audio,Animation,Shader Editor,Search Results,AnimationTree,MeshLibrary,ResourcePreloader,ShaderFile,SpriteFrames,Theme,Polygon,TileSet,TileMap,Replication,GridMap"
|
||||
dock_floating={}
|
||||
dock_closed=[]
|
||||
dock_split_2=0
|
||||
dock_split_3=0
|
||||
dock_hsplit_1=0
|
||||
dock_hsplit_2=280
|
||||
dock_hsplit_3=-220
|
||||
dock_hsplit_4=0
|
||||
dock_main_split=0
|
||||
dock_bottom_split=0
|
||||
dock_9_selected_tab_idx=0
|
||||
|
||||
[docks/FileSystem]
|
||||
|
||||
h_split_offset=240
|
||||
v_split_offset=0
|
||||
display_mode=0
|
||||
file_sort=0
|
||||
file_list_display_mode=1
|
||||
selected_paths=PackedStringArray("res://data/tables/enemy.tres")
|
||||
uncollapsed_paths=PackedStringArray("res://", "res://presentation/", "res://presentation/views/", "res://data/", "res://data/tables/", "res://data/schemas/", "res://core/")
|
||||
|
||||
[docks/History]
|
||||
|
||||
include_scene=true
|
||||
include_global=true
|
||||
|
||||
[docks/Theme]
|
||||
|
||||
split_offset=520
|
||||
|
||||
[EditorNode]
|
||||
|
||||
open_scenes=PackedStringArray("res://presentation/main.tscn")
|
||||
current_scene="res://presentation/main.tscn"
|
||||
bottom_panel_offsets={
|
||||
"Audio": -450.0,
|
||||
"Debugger": -369.0,
|
||||
"Output": -270.0,
|
||||
"Search Results": 0.0
|
||||
}
|
||||
selected_default_debugger_tab_idx=1
|
||||
selected_main_editor_idx=2
|
||||
|
||||
[EditorWindow]
|
||||
|
||||
screen=0
|
||||
mode="windowed"
|
||||
position=Vector2i(405, 80)
|
||||
size=Vector2i(1897, 1048)
|
||||
|
||||
[ScriptEditor]
|
||||
|
||||
open_scripts=["res://core/board_generator.gd", "res://core/reward_economy.gd", "res://core/reward_supply_controller.gd", "res://data/schemas/reward_table.gd", "res://presentation/views/status_slot_row.gd"]
|
||||
selected_script="res://core/board_generator.gd"
|
||||
open_help=[]
|
||||
script_split_offset=200
|
||||
list_split_offset=0
|
||||
zoom_factor=1.0
|
||||
|
||||
[GameView]
|
||||
|
||||
floating_window_rect=Rect2i(665, 277, 1292, 849)
|
||||
floating_window_screen=0
|
||||
|
||||
[ShaderEditor]
|
||||
|
||||
open_shaders=[]
|
||||
split_offset=200
|
||||
selected_shader=""
|
||||
text_shader_zoom_factor=1.0
|
||||
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
||||
[folding]
|
||||
|
||||
sections_unfolded=PackedStringArray()
|
||||
@@ -0,0 +1,93 @@
|
||||
78098b89c88a74ebe8c20117b8754620
|
||||
::res://::1789291459
|
||||
README.md::TextFile::-1::1789245234::0::1::::<><><>0<>0<><>::
|
||||
::res://core/::1789236746
|
||||
action_economy.gd::GDScript::539248926336522464::1789233852::0::1::::ActionEconomy<>RefCounted<><>0<>0<><>::
|
||||
battle_orchestrator.gd::GDScript::6153665499356077672::1789236632::0::1::::BattleOrchestrator<>RefCounted<><>0<>0<><>::
|
||||
board_data.gd::GDScript::7569800284505832826::1789232949::0::1::::BoardData<>RefCounted<><>0<>0<><>::
|
||||
board_generator.gd::GDScript::3603363749200663606::1789232934::0::1::::BoardGenerator<>RefCounted<><>0<>0<><>::
|
||||
config_db.gd::GDScript::5132721691389947172::1789232958::0::1::::<>Node<><>0<>0<><>::
|
||||
damage_math.gd::GDScript::190709078776196050::1789232474::0::1::::DamageMath<>RefCounted<><>0<>0<><>::
|
||||
level_runner.gd::GDScript::4382564976711397872::1789236746::0::1::::LevelRunner<>RefCounted<><>0<>0<><>::
|
||||
minesweeper_rules.gd::GDScript::7936287674986956201::1789233939::0::1::::MinesweeperRules<>RefCounted<><>0<>0<><>::
|
||||
reward_economy.gd::GDScript::2149614533123690078::1789232919::0::1::::RewardEconomy<>RefCounted<><>0<>0<><>::
|
||||
reward_supply_controller.gd::GDScript::7501430662356378774::1789232984::0::1::::RewardSupplyController<>RefCounted<><>0<>0<><>::
|
||||
rng_service.gd::GDScript::1338675472946526387::1789232968::0::1::::<>Node<><>0<>0<><>::
|
||||
status_container.gd::GDScript::7936126502523773580::1789129899::0::1::::StatusContainer<>RefCounted<><>0<>0<><>::
|
||||
unit_container.gd::GDScript::7462364046193057341::1789236551::0::1::::UnitContainer<>RefCounted<><>0<>0<><>::
|
||||
::res://data/::1789034260
|
||||
::res://data/schemas/::1789207599
|
||||
action_economy_table.gd::GDScript::8522817588810408628::1789144440::0::1::::ActionEconomyTable<>Resource<><>0<>0<><>::
|
||||
board_gen_table.gd::GDScript::290673939920197678::1789034188::0::1::::BoardGenTable<>Resource<><>0<>0<><>::
|
||||
difficulty_table.gd::GDScript::107972707759898139::1789148819::0::1::::DifficultyTable<>Resource<><>0<>0<><>::
|
||||
enemy_table.gd::GDScript::4876593032319850172::1789207522::0::1::::EnemyTable<>Resource<><>0<>0<><>::
|
||||
hero_table.gd::GDScript::9222491661618750738::1789061434::0::1::::HeroTable<>Resource<><>0<>0<><>::
|
||||
level_table.gd::GDScript::1724087523887552828::1789113333::0::1::::LevelTable<>Resource<><>0<>0<><>::
|
||||
reward_config_table.gd::GDScript::7355091203371621038::1789125672::0::1::::RewardConfigTable<>Resource<><>0<>0<><>::
|
||||
reward_table.gd::GDScript::5669541951934428507::1789125710::0::1::::RewardTable<>Resource<><>0<>0<><>::
|
||||
settlement_table.gd::GDScript::6131581376971031268::1789207599::0::1::::SettlementTable<>Resource<><>0<>0<><>::
|
||||
status_effect_table.gd::GDScript::4437475637910601838::1789034138::0::1::::StatusEffectTable<>Resource<><>0<>0<><>::
|
||||
variation_table.gd::GDScript::3386413835420127059::1789034137::0::1::::VariationTable<>Resource<><>0<>0<><>::
|
||||
::res://data/tables/::1789280172
|
||||
action_economy.tres::Resource/ActionEconomyTable::-1::1789144440::0::1::::<><><>0<>0<><>::res://data/schemas/action_economy_table.gd
|
||||
board_gen.tres::Resource/BoardGenTable::-1::1789118328::0::1::::<><><>0<>0<><>::res://data/schemas/board_gen_table.gd
|
||||
difficulty.tres::Resource/DifficultyTable::-1::1789148819::0::1::::<><><>0<>0<><>::res://data/schemas/difficulty_table.gd
|
||||
enemy.tres::Resource/EnemyTable::-1::1789207507::0::1::::<><><>0<>0<><>::res://data/schemas/enemy_table.gd
|
||||
hero.tres::Resource/HeroTable::-1::1789064873::0::1::::<><><>0<>0<><>::res://data/schemas/hero_table.gd
|
||||
level.tres::Resource/LevelTable::-1::1789113345::0::1::::<><><>0<>0<><>::res://data/schemas/level_table.gd
|
||||
reward.tres::Resource/RewardTable::-1::1789280009::0::1::::<><><>0<>0<><>::res://data/schemas/reward_table.gd
|
||||
reward_config.tres::Resource/RewardConfigTable::-1::1789125741::0::1::::<><><>0<>0<><>::res://data/schemas/reward_config_table.gd
|
||||
settlement.tres::Resource/SettlementTable::-1::1789280154::0::1::::<><><>0<>0<><>::res://data/schemas/settlement_table.gd
|
||||
status_effects.tres::Resource/StatusEffectTable::-1::1789280182::0::1::::<><><>0<>0<><>::res://data/schemas/status_effect_table.gd
|
||||
variation.tres::Resource/VariationTable::-1::1789034260::0::1::::<><><>0<>0<><>::res://data/schemas/variation_table.gd
|
||||
::res://presentation/::1789246033
|
||||
battle_narrator.gd::GDScript::5226435456607670966::1789229051::0::1::::<>RefCounted<><>0<>0<><>::
|
||||
debug_session.gd::GDScript::8434422127986491287::1789243501::0::1::::<>RefCounted<><>0<>0<><>::
|
||||
hud_presenter.gd::GDScript::266080447502779628::1789233101::0::1::::HudPresenter<>RefCounted<><>0<>0<><>::
|
||||
main.gd::GDScript::7302531409199717733::1789243501::0::1::::<>Control<><>0<>0<><>::
|
||||
main.tscn::PackedScene::6193690700815517790::1789034097::0::1::::<><><>0<>0<><>::res://presentation/main.gd
|
||||
modal_stack.gd::GDScript::3147898925570088454::1789229050::0::1::::<>RefCounted<><>0<>0<><>::
|
||||
playtest_logger.gd::GDScript::2237693618347780217::1789243222::0::1::::PlaytestLogger<>RefCounted<><>0<>0<><>::
|
||||
tune_sim_controller.gd::GDScript::1034382086574245163::1789243536::0::1::::<>RefCounted<><>0<>0<><>::
|
||||
ui_assembler.gd::GDScript::7013135001962184004::1789246033::0::1::::<>RefCounted<><>0<>0<><>::
|
||||
::res://presentation/views/::1789233039
|
||||
ap_bar_view.gd::GDScript::6706481391074465845::1789203369::0::1::::ApBarView<>PanelContainer<><>0<>0<><>::
|
||||
battle_end_overlay.gd::GDScript::3399056977949525852::1789203797::0::1::::BattleEndOverlay<>Control<><>0<>0<><>::
|
||||
battle_log_view.gd::GDScript::789450573482474203::1789207803::0::1::::BattleLogView<>PanelContainer<><>0<>0<><>::
|
||||
battle_stage_view.gd::GDScript::5667172321656987572::1789205728::0::1::::BattleStageView<>PanelContainer<><>0<>0<><>::
|
||||
board_fx_view.gd::GDScript::1601194353712525323::1789202137::0::1::::BoardFxView<>Control<><>0<>0<><>::
|
||||
board_view.gd::GDScript::2101785913681906214::1789233039::0::1::::BoardView<>Control<><>0<>0<><>::
|
||||
board_zone_view.gd::GDScript::5694869616755791188::1789202681::0::1::::BoardZoneView<>PanelContainer<><>0<>0<><>::
|
||||
desaturation.gdshader::Shader::3539526562288928378::1789202028::0::1::::<><><>0<>0<><>::
|
||||
enemy_card_view.gd::GDScript::5658106289382993794::1789228090::0::1::::EnemyCardView<>PanelContainer<><>0<>0<><>::
|
||||
hero_card_view.gd::GDScript::7848267529495934977::1789202280::0::1::::HeroCardView<>PanelContainer<><>0<>0<><>::
|
||||
hero_info_panel_view.gd::GDScript::890950290308946654::1789228091::0::1::::HeroInfoPanelView<>PanelContainer<><>0<>0<><>::
|
||||
inventory_panel_view.gd::GDScript::5693348628536103891::1789205728::0::1::::InventoryPanelView<>PanelContainer<><>0<>0<><>::
|
||||
legend_overlay.gd::GDScript::8747339015513470369::1789202163::0::1::::LegendOverlay<>Control<><>0<>0<><>::
|
||||
level_strip_view.gd::GDScript::887444383034759579::1789203296::0::1::::LevelStripView<>HBoxContainer<><>0<>0<><>::
|
||||
node_overlay.gd::GDScript::4207691819312229744::1789128470::0::1::::NodeOverlay<>Control<><>0<>0<><>::
|
||||
settlement_review_overlay.gd::GDScript::7219371183860710574::1789202239::0::1::::SettlementReviewOverlay<>Control<><>0<>0<><>::
|
||||
status_slot_row.gd::GDScript::3482792166004692461::1789215866::0::1::::StatusSlotRow<>HBoxContainer<><>0<>0<><>::
|
||||
toast_stack.gd::GDScript::4052418265818498251::1789202048::0::1::::ToastStack<>Control<><>0<>0<><>::
|
||||
tune_panel.gd::GDScript::8773926940492071944::1789228138::0::1::::TunePanel<>Control<><>0<>0<><>::
|
||||
ui_kit.gd::GDScript::3178727357685254666::1789128242::0::1::::UiKit<>RefCounted<><>0<>0<><>::
|
||||
::res://smoke/::1789280205
|
||||
auto_player.gd::GDScript::5451104337629630247::1789237174::0::1::::<>RefCounted<><>0<>0<><>::
|
||||
defense_scan.gd::GDScript::8379610624300424582::1789280205::0::1::::<>SceneTree<><>0<>0<><>::
|
||||
dump_battle.gd::GDScript::5133704044948130493::1789237266::0::1::::<>SceneTree<><>0<>0<><>::
|
||||
experiment_density.gd::GDScript::5260599749047087749::1789129898::0::1::::<>SceneTree<><>0<>0<><>::
|
||||
level_autoplay.gd::GDScript::662970221026420953::1789243327::0::1::::<>RefCounted<><>0<>0<><>::
|
||||
level_debug.gd::GDScript::2760977088613608031::1789126166::0::1::::<>SceneTree<><>0<>0<><>::
|
||||
level_monte_carlo.gd::GDScript::549864169145451125::1789243370::0::1::::<>SceneTree<><>0<>0<><>::
|
||||
monte_carlo.gd::GDScript::7869543635707275019::1789237266::0::1::::<>SceneTree<><>0<>0<><>::
|
||||
reward_expectation.gd::GDScript::6689296240354685181::1789129898::0::1::::<>SceneTree<><>0<>0<><>::
|
||||
smoke_all.gd::GDScript::3155837053821105962::1789237237::0::1::::<>SceneTree<><>0<>0<><>::
|
||||
supply_scan.gd::GDScript::8762258276062492019::1789243580::0::1::::<>SceneTree<><>0<>0<><>::
|
||||
ui_click_probe.gd::GDScript::8163827295684929025::1789237453::0::1::::<>Node<><>0<>0<><>::
|
||||
ui_click_probe.tscn::PackedScene::-1::1789117732::0::1::::<><><>0<>0<><>::res://smoke/ui_click_probe.gd
|
||||
ui_layout_probe.gd::GDScript::3805933281686222633::1789246033::0::1::::<>Node<><>0<>0<><>::
|
||||
ui_layout_probe.tscn::PackedScene::-1::1789245918::0::1::::<><><>0<>0<><>::res://smoke/ui_layout_probe.gd
|
||||
ui_probe.gd::GDScript::7006810144725363874::1789243535::0::1::::<>Node<><>0<>0<><>::
|
||||
ui_probe.tscn::PackedScene::-1::1789116662::0::1::::<><><>0<>0<><>::res://smoke/ui_probe.gd
|
||||
ui_tune_probe.gd::GDScript::1332260224320123543::1789237453::0::1::::<>Node<><>0<>0<><>::
|
||||
ui_tune_probe.tscn::PackedScene::-1::1789127580::0::1::::<><><>0<>0<><>::res://smoke/ui_tune_probe.gd
|
||||
@@ -0,0 +1,6 @@
|
||||
res://core/battle_orchestrator.gd
|
||||
res://core/reward_economy.gd
|
||||
res://smoke/defense_scan.gd
|
||||
res://smoke/level_monte_carlo.gd
|
||||
res://smoke/auto_player.gd
|
||||
res://smoke/level_autoplay.gd
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
[editor_states]
|
||||
|
||||
$selected_nodes=Array[NodePath]([])
|
||||
2D={
|
||||
"grid_offset": Vector2(0, 0),
|
||||
"grid_snap_active": false,
|
||||
"grid_step": Vector2(8, 8),
|
||||
"grid_visibility": 1,
|
||||
"ofs": Vector2(-166, -111),
|
||||
"primary_grid_step": Vector2i(8, 8),
|
||||
"show_group_gizmos": true,
|
||||
"show_guides": true,
|
||||
"show_helpers": false,
|
||||
"show_lock_gizmos": true,
|
||||
"show_origin": true,
|
||||
"show_position_gizmos": true,
|
||||
"show_rulers": true,
|
||||
"show_transformation_gizmos": true,
|
||||
"show_viewport": true,
|
||||
"show_zoom_control": true,
|
||||
"smart_snap_active": false,
|
||||
"snap_guides": true,
|
||||
"snap_node_anchors": true,
|
||||
"snap_node_center": true,
|
||||
"snap_node_parent": true,
|
||||
"snap_node_sides": true,
|
||||
"snap_other_nodes": true,
|
||||
"snap_pixel": true,
|
||||
"snap_relative": false,
|
||||
"snap_rotation": false,
|
||||
"snap_rotation_offset": 0.0,
|
||||
"snap_rotation_step": 0.2617994,
|
||||
"snap_scale": false,
|
||||
"snap_scale_step": 0.1,
|
||||
"zoom": 1.0
|
||||
}
|
||||
3D={
|
||||
"fov": 70.01,
|
||||
"gizmos_status": {
|
||||
"AudioListener3D": 0,
|
||||
"AudioStreamPlayer3D": 0,
|
||||
"CPUParticles3D": 0,
|
||||
"CSGShape3D": 0,
|
||||
"Camera3D": 0,
|
||||
"ChainIK3D": 0,
|
||||
"CollisionObject3D": 0,
|
||||
"CollisionPolygon3D": 0,
|
||||
"CollisionShape3D": 0,
|
||||
"Decal": 0,
|
||||
"FogVolume": 0,
|
||||
"GPUParticles3D": 0,
|
||||
"GPUParticlesCollision3D": 0,
|
||||
"Joint3D": 0,
|
||||
"Light3D": 0,
|
||||
"LightmapGI": 0,
|
||||
"LightmapProbe": 0,
|
||||
"Marker3D": 0,
|
||||
"MeshInstance3DCustomAABB": 0,
|
||||
"NavigationLink3D": 0,
|
||||
"NavigationObstacle3D": 0,
|
||||
"NavigationRegion3D": 0,
|
||||
"OccluderInstance3D": 0,
|
||||
"Particles3DEmissionShape": 0,
|
||||
"Path3D": 0,
|
||||
"PhysicalBone3D": 0,
|
||||
"RayCast3D": 0,
|
||||
"ReflectionProbe": 0,
|
||||
"ShapeCast3D": 0,
|
||||
"Skeleton3D": 0,
|
||||
"SoftBody3D": 0,
|
||||
"SpringArm3D": 0,
|
||||
"SpringBoneCollision3D": 0,
|
||||
"SpringBoneSimulator3D": 0,
|
||||
"TwoBoneIK3D": 0,
|
||||
"VehicleWheel3D": 0,
|
||||
"VisibleOnScreenNotifier3D": 0,
|
||||
"VoxelGI": 0
|
||||
},
|
||||
"local_coords": false,
|
||||
"preserve_children_transform": false,
|
||||
"preview_sun_env": {
|
||||
"environ_ao_enabled": false,
|
||||
"environ_enabled": true,
|
||||
"environ_energy": 1.0,
|
||||
"environ_gi_enabled": false,
|
||||
"environ_glow_enabled": false,
|
||||
"environ_ground_color": Color(0.2, 0.169, 0.133, 1),
|
||||
"environ_sky_color": Color(0.385, 0.454, 0.55, 1),
|
||||
"environ_tonemap_enabled": false,
|
||||
"sun_color": Color(1, 1, 1, 1),
|
||||
"sun_enabled": true,
|
||||
"sun_energy": 1.0,
|
||||
"sun_rotation": Vector2(-1.0471976, 2.6179938),
|
||||
"sun_shadow_max_distance": 100.0
|
||||
},
|
||||
"rotate_snap": 15.0,
|
||||
"scale_snap": 10.0,
|
||||
"show_grid": true,
|
||||
"show_origin": true,
|
||||
"snap_enabled": false,
|
||||
"trackball_enabled": false,
|
||||
"translate_snap": 1.0,
|
||||
"vertex_snap_origin_mode": false,
|
||||
"vertex_snap_use_collision": false,
|
||||
"viewport_mode": 1,
|
||||
"viewport_splits": {
|
||||
"first": 0.0,
|
||||
"main": 0.0,
|
||||
"second": 0.0
|
||||
},
|
||||
"viewports": [{
|
||||
"auto_orthogonal": false,
|
||||
"auto_orthogonal_enabled": true,
|
||||
"cinematic_preview": false,
|
||||
"display_mode": 22,
|
||||
"distance": 4.0,
|
||||
"doppler": false,
|
||||
"frame_time": false,
|
||||
"gizmos": true,
|
||||
"grid": true,
|
||||
"half_res": false,
|
||||
"information": false,
|
||||
"listener": true,
|
||||
"lock_rotation": false,
|
||||
"orthogonal": false,
|
||||
"position": Vector3(0, 0, 0),
|
||||
"transform_gizmo": true,
|
||||
"use_environment": false,
|
||||
"view_type": 0,
|
||||
"x_rotation": 0.5,
|
||||
"y_rotation": -0.5
|
||||
}, {
|
||||
"auto_orthogonal": false,
|
||||
"auto_orthogonal_enabled": true,
|
||||
"cinematic_preview": false,
|
||||
"display_mode": 22,
|
||||
"distance": 4.0,
|
||||
"doppler": false,
|
||||
"frame_time": false,
|
||||
"gizmos": true,
|
||||
"grid": true,
|
||||
"half_res": false,
|
||||
"information": false,
|
||||
"listener": false,
|
||||
"lock_rotation": false,
|
||||
"orthogonal": false,
|
||||
"position": Vector3(0, 0, 0),
|
||||
"transform_gizmo": true,
|
||||
"use_environment": false,
|
||||
"view_type": 0,
|
||||
"x_rotation": 0.5,
|
||||
"y_rotation": -0.5
|
||||
}, {
|
||||
"auto_orthogonal": false,
|
||||
"auto_orthogonal_enabled": true,
|
||||
"cinematic_preview": false,
|
||||
"display_mode": 22,
|
||||
"distance": 4.0,
|
||||
"doppler": false,
|
||||
"frame_time": false,
|
||||
"gizmos": true,
|
||||
"grid": true,
|
||||
"half_res": false,
|
||||
"information": false,
|
||||
"listener": false,
|
||||
"lock_rotation": false,
|
||||
"orthogonal": false,
|
||||
"position": Vector3(0, 0, 0),
|
||||
"transform_gizmo": true,
|
||||
"use_environment": false,
|
||||
"view_type": 0,
|
||||
"x_rotation": 0.5,
|
||||
"y_rotation": -0.5
|
||||
}, {
|
||||
"auto_orthogonal": false,
|
||||
"auto_orthogonal_enabled": true,
|
||||
"cinematic_preview": false,
|
||||
"display_mode": 22,
|
||||
"distance": 4.0,
|
||||
"doppler": false,
|
||||
"frame_time": false,
|
||||
"gizmos": true,
|
||||
"grid": true,
|
||||
"half_res": false,
|
||||
"information": false,
|
||||
"listener": false,
|
||||
"lock_rotation": false,
|
||||
"orthogonal": false,
|
||||
"position": Vector3(0, 0, 0),
|
||||
"transform_gizmo": true,
|
||||
"use_environment": false,
|
||||
"view_type": 0,
|
||||
"x_rotation": 0.5,
|
||||
"y_rotation": -0.5
|
||||
}],
|
||||
"zfar": 4000.01,
|
||||
"znear": 0.05
|
||||
}
|
||||
Anim={
|
||||
"visible": false
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
[folding]
|
||||
|
||||
node_unfolds=[NodePath("."), PackedStringArray("Layout")]
|
||||
resource_unfolds=[]
|
||||
nodes_folded=[]
|
||||
animation_groups_folded=[]
|
||||
@@ -0,0 +1,13 @@
|
||||
[game_view]
|
||||
|
||||
select_mode=0
|
||||
hide_selection=false
|
||||
|
||||
[editor_metadata]
|
||||
|
||||
executable_path="D:/code/godot/Godot_v4.7.2-stable_win64.exe"
|
||||
|
||||
[recent_files]
|
||||
|
||||
scenes=["res://presentation/main.tscn"]
|
||||
scripts=["res://core/board_generator.gd", "res://data/schemas/reward_table.gd", "res://core/reward_supply_controller.gd", "res://core/reward_economy.gd", "res://presentation/views/status_slot_row.gd", "res://core/a1_board_generator.gd", "res://presentation/main.gd"]
|
||||
@@ -0,0 +1,87 @@
|
||||
[res://presentation/main.gd]
|
||||
|
||||
state={
|
||||
"bookmarks": PackedInt32Array(),
|
||||
"breakpoints": PackedInt32Array(),
|
||||
"column": 0,
|
||||
"folded_lines": PackedInt32Array(),
|
||||
"h_scroll_position": 0,
|
||||
"row": 433,
|
||||
"scroll_position": 395.8,
|
||||
"selection": false,
|
||||
"syntax_highlighter": "GDScript"
|
||||
}
|
||||
|
||||
[res://presentation/views/status_slot_row.gd]
|
||||
|
||||
state={
|
||||
"bookmarks": PackedInt32Array(),
|
||||
"breakpoints": PackedInt32Array(),
|
||||
"column": 6,
|
||||
"folded_lines": PackedInt32Array(),
|
||||
"h_scroll_position": 0,
|
||||
"row": 121,
|
||||
"scroll_position": 112.0,
|
||||
"selection": false,
|
||||
"syntax_highlighter": "GDScript"
|
||||
}
|
||||
|
||||
[res://core/reward_economy.gd]
|
||||
|
||||
state={
|
||||
"bookmarks": PackedInt32Array(),
|
||||
"breakpoints": PackedInt32Array(),
|
||||
"column": 0,
|
||||
"folded_lines": PackedInt32Array(),
|
||||
"h_scroll_position": 0,
|
||||
"row": 317,
|
||||
"scroll_position": 289.0,
|
||||
"selection": false,
|
||||
"syntax_highlighter": "GDScript"
|
||||
}
|
||||
|
||||
[res://core/reward_supply_controller.gd]
|
||||
|
||||
state={
|
||||
"bookmarks": PackedInt32Array(),
|
||||
"breakpoints": PackedInt32Array(),
|
||||
"column": 0,
|
||||
"folded_lines": PackedInt32Array(),
|
||||
"h_scroll_position": 0,
|
||||
"row": 27,
|
||||
"scroll_position": 5.0,
|
||||
"selection": false,
|
||||
"syntax_highlighter": "GDScript"
|
||||
}
|
||||
|
||||
[res://data/schemas/reward_table.gd]
|
||||
|
||||
state={
|
||||
"bookmarks": PackedInt32Array(),
|
||||
"breakpoints": PackedInt32Array(),
|
||||
"column": 64,
|
||||
"folded_lines": PackedInt32Array(),
|
||||
"h_scroll_position": 0,
|
||||
"row": 11,
|
||||
"scroll_position": 11.0,
|
||||
"selection": true,
|
||||
"selection_from_column": 51,
|
||||
"selection_from_line": 11,
|
||||
"selection_to_column": 64,
|
||||
"selection_to_line": 11,
|
||||
"syntax_highlighter": "GDScript"
|
||||
}
|
||||
|
||||
[res://core/board_generator.gd]
|
||||
|
||||
state={
|
||||
"bookmarks": PackedInt32Array(),
|
||||
"breakpoints": PackedInt32Array(),
|
||||
"column": 24,
|
||||
"folded_lines": PackedInt32Array(),
|
||||
"h_scroll_position": 0,
|
||||
"row": 59,
|
||||
"scroll_position": 50.0,
|
||||
"selection": false,
|
||||
"syntax_highlighter": "GDScript"
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
list=[{
|
||||
"base": &"RefCounted",
|
||||
"class": &"ActionEconomy",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://core/action_economy.gd"
|
||||
}, {
|
||||
"base": &"Resource",
|
||||
"class": &"ActionEconomyTable",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://data/schemas/action_economy_table.gd"
|
||||
}, {
|
||||
"base": &"PanelContainer",
|
||||
"class": &"ApBarView",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/ap_bar_view.gd"
|
||||
}, {
|
||||
"base": &"Control",
|
||||
"class": &"BattleEndOverlay",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/battle_end_overlay.gd"
|
||||
}, {
|
||||
"base": &"PanelContainer",
|
||||
"class": &"BattleLogView",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/battle_log_view.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"BattleOrchestrator",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://core/battle_orchestrator.gd"
|
||||
}, {
|
||||
"base": &"PanelContainer",
|
||||
"class": &"BattleStageView",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/battle_stage_view.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"BoardData",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://core/board_data.gd"
|
||||
}, {
|
||||
"base": &"Control",
|
||||
"class": &"BoardFxView",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/board_fx_view.gd"
|
||||
}, {
|
||||
"base": &"Resource",
|
||||
"class": &"BoardGenTable",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://data/schemas/board_gen_table.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"BoardGenerator",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://core/board_generator.gd"
|
||||
}, {
|
||||
"base": &"Control",
|
||||
"class": &"BoardView",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/board_view.gd"
|
||||
}, {
|
||||
"base": &"PanelContainer",
|
||||
"class": &"BoardZoneView",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/board_zone_view.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"DamageMath",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://core/damage_math.gd"
|
||||
}, {
|
||||
"base": &"Resource",
|
||||
"class": &"DifficultyTable",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://data/schemas/difficulty_table.gd"
|
||||
}, {
|
||||
"base": &"PanelContainer",
|
||||
"class": &"EnemyCardView",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/enemy_card_view.gd"
|
||||
}, {
|
||||
"base": &"Resource",
|
||||
"class": &"EnemyTable",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://data/schemas/enemy_table.gd"
|
||||
}, {
|
||||
"base": &"PanelContainer",
|
||||
"class": &"HeroCardView",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/hero_card_view.gd"
|
||||
}, {
|
||||
"base": &"PanelContainer",
|
||||
"class": &"HeroInfoPanelView",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/hero_info_panel_view.gd"
|
||||
}, {
|
||||
"base": &"Resource",
|
||||
"class": &"HeroTable",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://data/schemas/hero_table.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"HudPresenter",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/hud_presenter.gd"
|
||||
}, {
|
||||
"base": &"PanelContainer",
|
||||
"class": &"InventoryPanelView",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/inventory_panel_view.gd"
|
||||
}, {
|
||||
"base": &"Control",
|
||||
"class": &"LegendOverlay",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/legend_overlay.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"LevelRunner",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://core/level_runner.gd"
|
||||
}, {
|
||||
"base": &"HBoxContainer",
|
||||
"class": &"LevelStripView",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/level_strip_view.gd"
|
||||
}, {
|
||||
"base": &"Resource",
|
||||
"class": &"LevelTable",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://data/schemas/level_table.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"MinesweeperRules",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://core/minesweeper_rules.gd"
|
||||
}, {
|
||||
"base": &"Control",
|
||||
"class": &"NodeOverlay",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/node_overlay.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"PlaytestLogger",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/playtest_logger.gd"
|
||||
}, {
|
||||
"base": &"Resource",
|
||||
"class": &"RewardConfigTable",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://data/schemas/reward_config_table.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"RewardEconomy",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://core/reward_economy.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"RewardSupplyController",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://core/reward_supply_controller.gd"
|
||||
}, {
|
||||
"base": &"Resource",
|
||||
"class": &"RewardTable",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://data/schemas/reward_table.gd"
|
||||
}, {
|
||||
"base": &"Control",
|
||||
"class": &"SettlementReviewOverlay",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/settlement_review_overlay.gd"
|
||||
}, {
|
||||
"base": &"Resource",
|
||||
"class": &"SettlementTable",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://data/schemas/settlement_table.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"StatusContainer",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://core/status_container.gd"
|
||||
}, {
|
||||
"base": &"Resource",
|
||||
"class": &"StatusEffectTable",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://data/schemas/status_effect_table.gd"
|
||||
}, {
|
||||
"base": &"HBoxContainer",
|
||||
"class": &"StatusSlotRow",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/status_slot_row.gd"
|
||||
}, {
|
||||
"base": &"Control",
|
||||
"class": &"ToastStack",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/toast_stack.gd"
|
||||
}, {
|
||||
"base": &"Control",
|
||||
"class": &"TunePanel",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/tune_panel.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"UiKit",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://presentation/views/ui_kit.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"UnitContainer",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://core/unit_container.gd"
|
||||
}, {
|
||||
"base": &"Resource",
|
||||
"class": &"VariationTable",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://data/schemas/variation_table.gd"
|
||||
}]
|
||||
Whitespace-only changes.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,405 @@
|
||||
# Dungeon Sweeper · 垂直切片(v2:第一关完整关卡)
|
||||
|
||||
> // VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
> // Validation Question(v2,2026-09-11):线性第一关(战×2→道具→战×2→恢复→Boss)能否承载
|
||||
> // "数值提升→通关期望改善"的 Roguelike 元进度验证——基线属性应该难到打不过、天赋数值提升把
|
||||
> // 通关期望拉回可玩带,且这条曲线能用数值调整板 + 批量模拟**测量出来**
|
||||
> // (通关攻击期望 / 败北血量数学期望);v1 的单战斗循环问题(逐行归因 P2 / Core headless 可跑)
|
||||
> // 已于 2026-09-10 验证并保持回归(smoke_all 前 46 项)。
|
||||
> // Date: 2026-09-10(v1)· 2026-09-11(v2 关卡扩展)· 引擎 Godot 4.7.2 · 本目录仅作设计验证参考,禁止 import 进生产代码
|
||||
|
||||
## 运行
|
||||
|
||||
1. 用 Godot 4.7.2 打开本目录(含 `project.godot` 的文件夹)
|
||||
2. 按 F5 运行(主场景 `presentation/main.tscn`)
|
||||
3. **主入口:`▶ 第一关 · 苔藓回廊`**——线性节点链:战→战→赏(随机主动道具)→战→战→疗(血量恢复点)→王(Boss 遗迹守卫)
|
||||
4. `测试用 · 角色数值调整板`:攻击/生命百分比加成(模拟天赋树解锁)+ 批量模拟(输出通关率/承伤期望/败北节点分布)
|
||||
|
||||
headless 冒烟(55 项断言,含关卡线性推进/满配通关/裸装败北/回放确定性,约 30 秒):
|
||||
|
||||
```
|
||||
Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/smoke_all.gd
|
||||
```
|
||||
|
||||
场景级 UI 探针(项目运行模式实例化 main.tscn 驱动第一关,断言棋盘真实渲染——D1 教训"呈现层零测试承载"的原型兑现):
|
||||
|
||||
```
|
||||
Godot_v4.7.2-stable_win64_console.exe --headless --path . res://smoke/ui_probe.tscn
|
||||
```
|
||||
|
||||
数值调整板场景探针(缺陷⑤回归:打开可见性/滑条联动/应用写表/重置/批量模拟全链路):
|
||||
|
||||
```
|
||||
Godot_v4.7.2-stable_win64_console.exe --headless --path . res://smoke/ui_tune_probe.tscn
|
||||
```
|
||||
|
||||
关卡期望扫描(C3 调参期关卡侧批量工具原型——atk%×hp% 网格,每格输出通关率/承伤期望/败北节点):
|
||||
|
||||
```
|
||||
Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/level_monte_carlo.gd -- [N] [seed0]
|
||||
```
|
||||
|
||||
可选类供给跨种子扫描(playtest-2026-09-12 Top1 复核——每局治疗包/小刀/精工短刀获取数、构成占比对 9:1 配比、"≥3 治疗包"局数分布;单种子坏抽与系统性偏置由此分割):
|
||||
|
||||
```
|
||||
Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/supply_scan.gd -- [N] [seed0]
|
||||
```
|
||||
|
||||
**日志合规(2026-09-12 用户要求)**:任何模拟运行与真人试玩均落全量 JSONL(user://logs/)——逐操作(op/cell/accepted/round)、逐结算明细行、轮次状态快照、battle_end、level_end。模拟(level_monte_carlo / 调整板批量模拟 / supply_scan)一文件多局,run 行携带种子/档位切分局次。
|
||||
|
||||
单关诊断:`--script res://smoke/level_debug.gd -- [seed] [atk%] [hp%]`。
|
||||
战斗级工具(v1 遗产):`dump_battle.gd`(结算流水)/ `monte_carlo.gd`(战斗期望)/ `experiment_density.gd`(密度调参)。
|
||||
|
||||
## 范围(v1 范围 + v2 追加,用户 2026-09-10/11 确认)
|
||||
|
||||
- **v1 在范围内**:D2(10 表 + ConfigDB + RngService)· A1(single_subset 求解器 + 双判据验收)· A2 · A3(AP8/RAP)· A4(迷雾,L2)· B1(七段结算 + 13 类行协议 + 高光双落)· B2(grub/ambusher/伏击池 cap4)· B3(四行状态容器)· B4(install 预分配/F5 保底/道具窗口)· D1-lite 程序化呈现
|
||||
- **v2 追加**:LevelTable(线性节点链数据表)· C1-lite 关卡推进器 `level_runner.gd`(原 c1_level_runner,2026-09-11 领域命名批更名)(关卡域生命周期:共享 B4 关内延续 + HP 跨战斗携带 + 奖励/恢复节点兑付 + 关卡末清库)· Boss 遭遇行(Dmax=4)· 节点奖励 API(B4 `grant_node_active`)· **测试用角色数值调整板**(atk%/hp% 覆盖 + 批量模拟,模拟天赋树——正式侧归 C4)
|
||||
- **裁剪不变**:A5 / C1 路径分支选择(用户拍板线性)· C2–C4 / B5 战斗动画 / D3–D6 / A1 黄金向量与 F4 批量工具 / gdUnit / 标错惩罚(A2 OQ1)
|
||||
|
||||
## 2026-09-11 可选类供给控制器(用户拍板批——修改记录)
|
||||
|
||||
**背景**:供给控制器批前实测每关可选类 ≈24.8 件(≈18.5 把刀,刀总伤 ≈180 > Boss 130 HP)——"道具获取期望太高"定量坐实。用户重新定义三尺度指标并拍板实现:
|
||||
|
||||
| 尺度 | 指标 | 实现机制 |
|
||||
|---|---|---|
|
||||
| 安全格 | 期望极小(全图摊平 < 0.01/格) | 可选类**退出格子权重池**——格子池只剩即时类四行 |
|
||||
| 雷图 | ≈0.78/板 | 战斗首件保证 ÷ 每战板数(≈1.28)折算 |
|
||||
| 战斗 | 期望 ≈1,**首件保证**:获取首件前每张新板必注入 1 件;之后按 **p0×decay^(j-1) 指数递减**(永不绝育) | `RewardSupplyController`(B4 私有组合件,战斗域计数) |
|
||||
|
||||
**拍板四条 → 落点**:① 注入占比 小刀 60% / 精工短刀 30% / **治疗包 10%**(治疗资源宝贵)→ `reward.tres` active_inject 池;② **清图奖励改一次性高额伤害**(clear_knife 入库刀 → clear_blast instant_aoe 40,当场兑付全体存活敌,不入库、不占可选类供给——清图与供给脱钩);③ 注入选格**均匀随机**(雾格不加权);④ **指数递减**(active_p0=0.4 / active_decay=0.5 → 0.4/0.2/0.1/0.05…,RewardConfigTable 新参数,provisional 归 C3 标定)。
|
||||
|
||||
**实现结构**(对象封装——伪代码审阅后落地):
|
||||
|
||||
- `core/reward_supply_controller.gd`(新):`notify_battle_start()`(计数清零)/ `should_inject(stream)`(j==0 恒真;否则 `randf < p0×decay^(j-1)`,取号经传入封闭流)/ `notify_granted()` / `granted_count()`。行值快照抗热重载;零自有随机数。
|
||||
- `B4RewardEconomy`(改造):`_supply` 组合件 · `notify_battle_start()`(B1.begin_battle 转发)· `roll_board` 重写(身份批即时类单遍 → 清图槽 → 注入判定/选格;旧 F5 草稿重 roll 循环删除)· `grant()` 挂钩 `notify_granted()`(**发放即获取**——预分配未挖到不计数;去重幂等天然防双计)· `supply_granted_count()` 观测面。
|
||||
- `B1BattleOrchestrator`:begin_battle 门序列增 `b4.notify_battle_start()`(编排唯一性不变)。
|
||||
- 表/护栏:RewardTable `POOL_TAGS` + `active_inject`;RewardConfigTable + `active_p0/active_decay`、k_dmg 1→0(F5 休眠);ConfigDB 跨表护栏更新(clear_no_value 含即时类 / no_damage_row 扫描全池活行 / 新增 inject_pool 护栏:非可选类拒绝 + 空池致命)。
|
||||
|
||||
**实测(N=200 板/深度,工具 reward_expectation.gd)**:注入 1.00 件/板(j=0,多件板 0)· 占比 小刀 59–65% / 精工 25–29% / 治疗包 7–17%(名义 60/30/10)· 格池零可选类泄漏 ✓。**实际获取期望**(level_monte_carlo「可选类/战」列,N=15):基线 0.88 → +200% 攻击 0.68 件/战(低于分配保证 1.0 的机理 = 注入格均匀随机,落在残余格/速杀未挖区时该件不获取;战斗末无兜底补发——**是否加"战斗末 j==0 补发 1 件"待用户拍板**)。每关合计 ≈ 4.5–6.5 件(原 24.8)✓ 拍板目标达成。
|
||||
|
||||
**⚠️ 供给侧副作用(本轮扫描发现,待拍板)**:可选类退出格子池后,其 25% 概率质量被动流向即时类——攻防 buff 每格频率从 44% 升至 59%,基线通关率 55%→80%(N=15)、整关承伤 127→80:**难度不降反升方向反了**。即时类是"每格必有货"锁定设计下无法清空的(除非引入空白格 = 违反锁定事实)。候选杠杆:① 接受更快的 buff 节奏,敌 HP 曲线上调对冲(C3 主杠杆)② 下调 attack_up/defense_up 在即时池内的相对权重(压 buff 频率)③ buff 幅度下调(触碰 B3 表值)。
|
||||
|
||||
**正式侧义务**:B4 GDD「install 期 roll / F5」章节修订(F5 → 战斗首件保证语义)+ registry reward_table/reward_config 条目改写 + RewardConfigTable knob 登记(active_p0/active_decay)——接口漂移批随实现落位。
|
||||
|
||||
## 2026-09-12 轮内刷新(方案 A,用户拍板批——修改记录)
|
||||
|
||||
**背景**:用户发现刷新实现(仅 Waiting 结算窗口受理)与设计预期不符——预期为**扫雷阶段遇到推理死路时,有剩余 AP 且 RAP 足够即可当场刷新继续挖**。影响分析(三方案对比 + 16 代码触点 + 8 文档 + 5 数值连锁)后用户拍板方案 A。
|
||||
|
||||
**拍板语义**:① Active 相位可刷(RAP ≥ 8 且本轮未达上限)→ 当场 retire+install 换新图,**轮次继续**:AP 保留不重置、相位不动、不触发结算(敌方时间税语义变更——GDD 修订义务);② Waiting 相位刷新保持现状(登记意图,exit_distribution 换图);③ **每轮兜底上限 2 次**(Active/Waiting 合计,`ActionEconomyTable.refresh_max_per_round = 2` 表值);④ RAP 来源不变(停手 1:1 转存)。
|
||||
|
||||
**实现落点**:A3 `request_refresh_active()`(仅扣 RAP,相位/AP 不动)· A2 `install_board/retire_board` 增 `during_round` 显式参数(Active 通道,其余相位守卫不变——fail-fast 保留)· B4 `query_full_snapshot()` · B1 `can_request_refresh()` 查询面 + `player_request_refresh()` 双相位重写 + `_refresh_used_this_round` 计数 + **facts 身份快照隔离**(缓冲期捕获身份快照、授予走快照——防旧图已挖格在轮末错拿新图同坐标身份;`_grant_cell` 增 snap_override)· 呈现层按钮"弃图刷新(RAP -8 · 每轮至多 2 次)"。
|
||||
|
||||
**核心实现难点(分析批未暴露)**:轮内换图后旧图已挖未结算格与新图**坐标重叠**——轮末授予 `b4.grant(cell)` 会错拿新图身份。解法 = facts 缓冲期用 `query_full_snapshot` 冻结身份,授予段直通快照(绕过现行 mapping/_granted,批次隔离;库存语义不变)。
|
||||
|
||||
**验证**:冒烟 88/88(+14 轮内刷新断言:当场换图/相位 AP 保留/RAP 扣减/上限拒绝/跨图 facts 完整兑现〔4 格跨三图快照授予〕/回放确定性)· 三探针全 PASS · 期望扫描与刷新前持平(80%/0.88——模拟策略不主动刷图,真人专用出口)。**调试教训**(规范 4.6 补充案例):蛆虫 HP 已调 24 而"18 未掉血"误判——排查数值问题先核对当前表值;GDScript lambda 对局部 int 按值捕获(计数器必须用容器)。
|
||||
|
||||
**正式侧义务(GDD/registry 修订清单)**:A3 GDD Rule 11(受理相位 Waiting → Active∪Waiting + 相位机表 + 时间税语义注记)· A2 GDD install 四不变式轮内场景 · B1 GDD 编排专章(轮内换图 + facts 快照协议)· B4 GDD E5/E6(query_full_snapshot/grant 快照直通)· game-concept 轮次流程 + "战斗:扫雷图 = 1:M"措辞(一轮跨多图新形态)· registry `action_economy`(refresh_max_per_round)· 数值连锁重跑(清图支配性条款⑤/获取期望——模拟不覆盖真人刷图行为,需 playtest 实测)。
|
||||
|
||||
## 2026-09-11 关卡扩展批 · 切片级偏差登记(实现期决策,正式化时须回 GDD/ADR)
|
||||
|
||||
1. **C1 线性推进器 provisional**:节点链来自 LevelTable(结构位 = 未来 C1 的数据承载面);路径分支/奖励点分布定稿权归 C1/C3。节点类型 closed enum {battle, reward, rest, boss};末节点必须 Boss(LevelTable schema 校验)。
|
||||
2. **B4 新增 `grant_node_active()`**(接口漂移):关卡奖励点在可选类活行 {damage_item, heal_pack} 中 roll 一件直接入库;消耗 `reward_roll` 恰一 scope(与 board install 批同流异 scope,流集合封闭不变)。正式侧应走 B4 GDD 修订(节点奖励 = clear_reward 插槽族的新来源候选)。
|
||||
3. **B2 spawn max_hp 语义修复**(切片 bug):原实现 `max_hp = hero_hp 入参`——跨战斗携带 HP 时 max_hp 被压扁成当前 HP,恢复点/治疗包满血守卫失效。修复为 `max_hp = HeroTable.max_hp`(接口语义澄清,正式侧回 B2 GDD Rule 注记)。
|
||||
4. **A4 迷雾纪律遵守**:L1 全程无雾(迷雾 `unlock_level=2` 已锁事实——迷雾自第二层解锁);Boss 板以 12×12 大板区分。
|
||||
5. **属性覆盖 = 测试专用**:调整板直接写运行时 HeroTable(atk 域 [1,999] 内 clamp ≥1);关卡结束/回菜单恢复基线。正式天赋树归 C4,禁止生产代码引用本机制。
|
||||
6. **UI 复用 b1 引用**:关卡模式下呈现层 `b1 = runner.b1`(与调试模式共用 HUD/棋盘路径);runner 在关卡结束置 `b1 = null`。
|
||||
7. **runner 信号时序(playtest 缺陷①修复登记)**:`node_entered` 必须先于 `begin_battle` 发出——后发时首板安装的 `board_changed` 到达呈现层时 `b1` 尚未接手,首战棋盘不渲染。场景级探针 `smoke/ui_probe.tscn` 为该缺陷的回归断言载体。
|
||||
8. **A2 旗动作末点 AP 软锁(playtest 缺陷②修复登记——A2 核心 bug,v1 即存在)**:`_do_flag_place/_do_flag_pull` 花完末点 AP 后 A3 相位翻 Waiting 但不发 round_end——B1 的 Waiting 结算点依赖 round_end 到达,事件缺席 = 整局软锁(Boss 图插旗多,末点 AP 花在旗上即触发)。修复 = 旗动作检查相位翻转并补发 `_emit_round_end_flags_and_payload()`(与 `_do_dig` 同款)。v1 冒烟盲区根源:自动玩家不用旗。回归断言入 smoke_all(旗动作耗尽 AP 必须进入第 2 轮)+ 行为探针 `smoke/ui_click_probe.tscn`(合成鼠标事件走真实 UI 路径,随机混合插旗/拔旗/停手/窗口刷新/无效点击,4 种子全关通扫)。**正式侧义务**:A2 GDD F4 事件序契约须补"旗动作路径的 round_end 发射"措辞(接口漂移)。
|
||||
9. **呈现层固定分区(playtest 缺陷③修复登记)**:侧栏全部动态区(RAP/敌人/增益/库存/悬停状态)固定槽高 + `clip_contents`——悬停文案切换与库存增长不再推挤整体布局(此前"第 N 轮"库存行被挤出视口 + 全 UI 抖动)。区域独立,超长裁剪不回流。
|
||||
10. **L1 12×12 板生成余量**:D2–D4(及 L2)`max_gen_attempts` 16→24(F2c 24×144=3456 ≤ 16384 合法)——16 次尝试下偶发"重试耗尽接受最优"(best≈0.85–0.87 < target 0.88);24 次后仍偶发(best 0.866),密度×推理率的根本解归 C3。
|
||||
11. **道具使用后不消失(playtest 缺陷④修复登记——UI 层契约违例)**:B1 窗口选择契约 = `_window_items` **位置**(义务④"选定序"),UI 选用按钮却传 `item.index`(库存数组句柄)——未消耗过时二者巧合相等;一旦消耗过一件,已耗条目仍占库存数组位,后续窗口条目"句柄 = 位置 + 已耗数"错位 → 选中件不消耗、邻件被误耗(可用次数语义本身存在且正确:`use()` 一次性标记 Used 不可逆)。修复 = UI/行为探针一律传窗口位置;探针加"选用必生效"断言(选用件库存态变 Used,失效即退出码非零)。
|
||||
12. **数值调整板被遮挡不可见(playtest 缺陷⑤修复登记 + 专属测试补建)**:调整板覆盖层在场景树中位于**不透明的开始覆盖层之前**,`_show_tune_panel` 只置 visible 不隐藏开始层 → 面板被完全遮住 = "点击无反应"。修复 = 打开隐藏开始层 / 关闭(含"应用并关闭")恢复。**专属测试**:`smoke/ui_tune_probe.tscn`(场景级全链路:打开可见性〔缺陷⑤回归〕/ 滑条→生效预览联动 / 应用写 hero 表 12/75 / 重置回基线 6/50 / 批量模拟 N=10 全链路〔与 headless 扫描同口径〕/ 关闭回开始界面 / 加成跨模式持久)。
|
||||
13. **呈现层模块化重构(2026-09-11 用户批——"上帝对象"拆分)**:`main.gd` 随功能追加长成 ~1100 行上帝对象(棋盘渲染/HUD/日志/四种覆盖层/调整板/模拟器/关卡流程全糊在一个类)。拆分为 **9 个单一职责单元**(总行数不变,最大单类 433 行):`views/board_view.gd`(棋盘渲染+输入意图翻译,动作经信号外发)· `views/hud_view.gd`(侧栏纯视图,setter+定长槽)· `hud_presenter.gd`(核心状态→文案装配,MVP 静态装配器)· `views/battle_log_view.gd`(13 类行协议格式化)· `views/item_window_overlay.gd`(窗口,选择=位置语义收口)· `views/node_overlay.gd` · `views/level_strip_view.gd` · `views/tune_panel.gd`(调整板自持加成语义+批量模拟)· `views/ui_kit.gd`(共享小部件)。**组合根纪律**:main.gd(433 行)= 模式流转 + 信号接线,核心写调用(settle_batch/submit_window_selection/runner 推进)只在此处发生;视图零核心写、装配器无状态。**回归验证**:冒烟 74/74 + ui_probe/ui_tune_probe PASS + 行为探针 2 种子(通关 + 选用生效 1/2 失效 0)。
|
||||
|
||||
## 切片调参登记 v2(2026-09-12 起已由 C3 GDD 转正为 MVP 默认曲线——下方登记保留作溯源)
|
||||
|
||||
**EnemyTable L1 深度轴(D0–D4,Boss=Dmax=4——A1 第三轴登记约定)**,设计意图 = 基线难、天赋拉回可玩带:
|
||||
|
||||
| 节点 | 行 | 敌种×数量 | hp | atk | 环 |
|
||||
|---|---|---|---|---|---|
|
||||
| 战1 | (1,0) | 蛆虫×1 | 24 | 4 | [attack] |
|
||||
| 战2 | (1,1) | 蛆虫群×2 | 24 | 4 | [attack] |
|
||||
| 战3 | (1,2) | 伏击兽×1 | 36 | 6 | [attack,charge,heavy] |
|
||||
| 战4 | (1,3) | 伏击兽群×2 | 26 | 4 | [attack,charge,heavy] |
|
||||
| Boss | (1,4) | 遗迹守卫×1 | 130 | 7 | [attack,attack,charge,heavy] |
|
||||
|
||||
首版(敌 HP 总量 208:18/36/26/40/88)实测**基线通关率 73%**——预警⑥速杀退化在关卡尺度依旧成立,与"基线非常难"意图相反;v2 上调至 290(24/48/36/52/130)+ Boss atk 7。**群战对价**:unit_count=2 行单位 hp/atk 下调(B1 条款③满配语境约束;C3 正式调参须按 clause3_supply 复算——registry 已登记 unit_count>1 行须降密度或降 m_amb)。
|
||||
|
||||
**BoardGenTable L1 深度行**:D0/D1 = 10×10、D2/D3/D4 = 12×12,全部 d0.25/t0.88。**登记**:d0.28/t0.87 首测**不达标**(best≈0.805–0.817,降级告警"接受最优出图")——密度轴上调与求解器能力一起解,留给 C3。
|
||||
|
||||
**奖励获取期望实测(问题2 汇报批,工具 `smoke/reward_expectation.gd`,N=200 板/深度,走真实 B4 roll 管线含 F5 保底条件化)**:
|
||||
|
||||
| 深度 | 板面 | 资格格均(min/max) | 可选类/板 均 | 其中刀 | 治疗包 |
|
||||
|---|---|---|---|---|---|
|
||||
| D0 | 10×10 | 12.4(1/36) | 3.32 | 2.57 | 0.75 |
|
||||
| D1 | 10×10 | 11.1(2/32) | 3.00 | 2.40 | 0.60 |
|
||||
| D2 | 12×12 | 22.8(4/63) | 5.70 | 4.42 | 1.27 |
|
||||
| D3 | 12×12 | 23.8(5/61) | 5.87 | 4.55 | 1.31 |
|
||||
| D4 | 12×12 | 24.1(6/64) | 5.87 | 4.54 | 1.33 |
|
||||
|
||||
**公式**:每资格格 P(可选类) = 25/100 = 25%(小刀 15% / 精工短刀 4% / 治疗包 6%);每板期望 ≈ 25%×|资格格| + F5 保底条件化加成(k_dmg=1 整批重 roll 至少 1 刀——12×12 板无刀概率 0.81²³<1%,加成可忽略;10×10 板 ≈+0.2);清图奖励 clear_knife 仅真清图兑付(理性策略不整清 ≈0);完美排雷 ×2 条件苛刻不计基线;**节点奖励恒为 1 件可选类**。
|
||||
|
||||
**每关合计 ≈ 23.8(板)+ 1(节点奖励)≈ 24.8 件可选类**(≈18.5 把刀 + ≈5.3 治疗包)——刀总伤 ≈180,超过 Boss 单体 130 HP。**用户"道具获取期望太高"的定量坐实**:关内不清库(2026-09-11 裁决③)× 每格必有货(B4 锁定)× 12×12 深板资格格翻倍,三者相乘。**候选杠杆(全部 C3 所有权,等拍板再动)**:① 可选类占比 25%→8–12%(改 knife_t1/knife_adv/heal_pack 权重)② 敌 HP 曲线上调对冲 ③ 刀伤下调 ④ 库存上限(触碰 B4 E 节结构,需修订)。
|
||||
|
||||
**期望扫描数据(N=20/格,种子 4200+20,工具 level_monte_carlo.gd;理性玩家策略近似)**:
|
||||
|
||||
| atk% / hp% | 通关率 | 整关承伤 均/p90 | 通关剩余HP均(胜) | 进BossHP均 | 平均轮数 | 败北节点分布 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| +0 / +0(基线) | **55%** | 126.8 / 190 | 33.1 | 44.6 | 18.4 | 蛆虫群×4 · 伏击兽×2 · 伏击兽群×3 |
|
||||
| +0 / +100 | 100% | 165.8 / 219 | 61.5 | 88.7 | 26.1 | — |
|
||||
| +50 / +0 | 70% | 109.8 / 155 | 27.1 | 45.5 | 17.1 | 分散(含 Boss×1) |
|
||||
| +100 / +0 | **100%** | 96.3 / 121 | 33.5 | 47.4 | 17.4 | — |
|
||||
| +200 / +0 | 95% | 61.1 / 99 | 36.6 | 49.0 | 12.9 | 伏击兽群×1 |
|
||||
|
||||
读法:**基线=明显吃紧(55%,死亡集中在前中期群战)→ 攻击+100% 即满通关带**;HP 翻倍是独立有效的第二杠杆(战斗拖长 → 承伤期望上涨 165 但预算翻倍)。v1 敌数值(HP 208)时基线通关 73%——预警⑥速杀退化在关卡尺度坐实,v2(HP 290)压到 55%。注意:模拟者 = 理性策略近似(免费链式开格、无避雷推理),真人基线通关率应显著低于 55%——**目标带定在哪儿(30%?10%?)待 playtest 拍板,杠杆 = EnemyTable hp/atk 深度曲线(C3)**。
|
||||
|
||||
## 已知热点(playtest 观察项)
|
||||
|
||||
- **基线难度体感**:基线(无面板加成)应"明显吃紧、可能打不过";如果基线就能轻松通关 → 敌数值仍偏弱(找 C3 杠杆:EnemyTable hp/atk 曲线);如果 +100% 攻击还打不过 → 奖励供给或恢复点数值偏弱。
|
||||
- **奖励点随机主动道具的体感**:roll 池 = 小刀 8 / 精工短刀 16 / 治疗包 12(等权);到手的刀是否改变了 Boss 战规划(囤刀 vs 即用)。
|
||||
- **恢复点(30% max_hp)时机**:位于战4 与 Boss 之间——是否制造"要不要省治疗包给 Boss"的决策。
|
||||
- **Boss 战节拍**:遗迹守卫环 [attack,attack,charge,heavy]——蓄力拍是否被玩家用作"安全挖掘拍";重击 14 点是否构成规划压力。
|
||||
- 节点进度条/奖励恢复覆盖层可读性;v1 遗留观测项(结算逐行归因、雾池体感、完美排雷可达性)继续有效。
|
||||
|
||||
## 2026-09-11 用户拍板(四条,已落地本切片)
|
||||
|
||||
| # | 拍板 | 切片实现落点 |
|
||||
|---|---|---|
|
||||
| ① | 基础攻击恒存在:atk 域 [1,999]、0 拒绝加载、MVP=6;战斗段主角先手攻击顺位最前敌,attack_up 唯一吃修正 | `hero.tres` atk=6 · `hero_table.gd` 域校验 · B1 战斗段⓪先手(`hero_attack` 行);attack_up 经 `effect_pool_map → hero_offense` 活行 |
|
||||
| ② | 奖励两类:即时 75%(当场兑付不进库存)/ 可选 25%(刀/治疗包入库走窗口);heal_pack 稀缺化 w6 p12 | `reward.tres` 行集 w75/25;B1 `_resolve_instant` |
|
||||
| ③ | 库存关内延续:跨战斗不清,关卡末 level_clear 清空 | B1 `_battle_over` 不清库;v2 起由 C1 runner 在关卡末统一调用 |
|
||||
| ④ | 道具窗口至多选一件 | B1 `submit_window_selection` 截断 `slice(0, 1)` |
|
||||
|
||||
**文档对齐**:四条拍板的 GDD/registry 回写已由文档会话先行完成(b1/b2/b4 GDD、registry、systems-index、d2、game-concept、b3);本切片程序与文档逐项对齐核查通过(详见 session-state 条目 16)。语义注:本切片为参考实现,与文档冲突以 GDD/registry 为准。
|
||||
|
||||
|
||||
## 2026-09-12 试玩日志器 + 诊断转储(playtest 数据采集基建)
|
||||
|
||||
**新增**:① **`PlaytestLogger`**(呈现层组件)——订阅 B1 新信号 `settlement_snapshot`(每结算末尾全量状态:关卡/深度/轮次/verdict/英雄/敌人/行动经济/板序号统计/buff/库存/本轮 facts)+ battle_finished/node_entered/level_finished,逐行写 **JSONL**:`user://logs/playtest_<seed>_<时间戳>.jsonl`(每行一个完整状态,直接可喂 pandas);② **诊断转储按钮**(侧栏底部"🩺 诊断转储")——卡死时点击,全量核心状态(B1 相位机/计数器/facts、A2 棋盘、A3、B4、runner)+ 最近 200 结算行/100 操作环形缓冲 → `user://logs/diag_*.json`(行为探针 _dump_state 的产品化)。
|
||||
|
||||
**顺手修复(日志首秀即抓真 bug)**:_start_full_level 面板加成**双写叠加**——tune_panel.apply() 先写表(6→18),runner.set_hero_override 再以 18 为基线 ×3(→54)。修复 = 关卡模式单一应用路径(runner 写表),日志快照 atk 54→18 证实。**教训(规范 5.x 补充案例)**:同一加成的多应用路径必须单一事实源;JSONL 快照是数值回归的最低成本探测器。
|
||||
|
||||
**冒烟 103/103**(+7:快照信号/字段齐备/板序号/JSONL 解析/诊断文件)。日志目录:`%APPDATA%/Godot/app_userdata/DungeonSweeperVerticalSlice/logs/`。
|
||||
|
||||
## 2026-09-12 C3 GDD 落地 + 试玩采集清单
|
||||
|
||||
**落地**:C3 GDD(`design/gdd/c3-difficulty-curve.md`,solo 出稿未评审)的设计承诺落回切片——① **DifficultyTable 结构位**(schema + 空表 + ConfigDB 挂载 + B2.spawn 套乘钩子:行值快照前施加、越域 clamp 至 [0.1,10];MVP 空表零行为——AC-10 冒烟断言含注入/越域验证);② 切片调参登记(敌深度轴/板参数/供给参数)**转正为 MVP 默认曲线**——切片当前数值 = C3 GDD 背书的设计态,试玩即体验 C3 定稿难度。冒烟 96/96。
|
||||
|
||||
**试玩数据采集清单(反哺 C3 六个 OQ——试玩时请记录)**:
|
||||
1. **OQ3 真人修正系数(首要)**:你无面板加成打第一关的通关/败北结果(模拟基线 80% 通关——真人预期显著更低,落差就是修正系数)。
|
||||
2. **OQ2 Boss 血量**:Boss 战实际打了多少轮、是否觉得血量压迫合理还是拖沓(模拟值 130HP 占比 0.81 越带)。
|
||||
3. **OQ6 轮内刷新**:你实际用了几次"弃图刷新"、是否觉得"挖-刷-挖"太顺滑(免结算=敌人不出手)。
|
||||
4. **OQ4 弃图倾向**:有没有"挖一半就想弃图刷 RAP"的冲动(模拟测不到真人策略)。
|
||||
5. **难度台阶可感知性**:D2 伏击兽/D4 Boss 相比 D0 蛆虫的变强是否有具体面孔(新敌种=新图像线索)。
|
||||
6. **奖励供给体感**:可选类道具(刀/治疗包)获取频率与囤积感受(供给控制器批后每关 ≈4.5–6.5 件)。
|
||||
|
||||
试玩方式:F5 → 「▶ 第一关 · 苔藓回廊」基线打一局 → 调整板 +100% 攻击再打一局(对照"数值提升→通关期望改善"的元进度体感)。
|
||||
|
||||
|
||||
## 2026-09-12 试玩批次 1 → v3 数值批(F7 流程实践)
|
||||
|
||||
**试玩反馈(v2 数值)**:满血全通关(三策略均通)/ Boss 血量 OK 或可上调 / 刷新零使用 / 零弃图倾向 / 治疗包零使用 / DND 命名指令(地精/熊地精/食人魔酋长)。
|
||||
|
||||
**v3 数值批**:atk 4/4/6/4/7 → **7/5/9/6/9**(承伤主杠杆);D0 goblin 24 保持(20 会被首轮粉尘秒杀——冒烟旗回归断言抓获)、D1 群 13×2、D2 bugbear 40/9、D3 群 26×2、**Boss ogre_chief 150/9**(用户拍板可上调);L2 锚行同步。**DND 命名**:goblin 地精 → bugbear 熊地精(DND 原型即伏击手)→ ogre_chief 食人魔酋长;enemy_id 同步更名。
|
||||
|
||||
**v3 扫描对照 F5**:基线 **60%**(带 40–60% 上沿 ✓,v2 80%)· 败北 4/6 集中 Boss(压迫预期兑现)· +50%→87% / +100%→93% / +200%→100%(天赋曲线成立)· 承伤 90(v2 80)。C3 GDD 回写:F3 带 [0.5,1.1](OQ2 裁决)、OQ2/3/4/6 试玩读数、试玩批次记录。
|
||||
|
||||
**冒烟 103/103**(旗回归断言改用 Boss 行——D0 20HP 级被盲挖粉尘秒杀无法承载场景;DifficultyTable 断言对齐 v3 行值)。**下一批 = v3 真人试玩**(OQ3 定量/OQ2 终裁)。
|
||||
|
||||
## 2026-09-12 遭遇随机化(Roguelike 局感引入批——Peglin 对比报告落地项)
|
||||
|
||||
**触发**:用户试玩反馈"关卡完全没有 Roguelike 的体现"→ Peglin 对比报告(`design/research/peglin-comparison-2026-09-12.md`)→ 用户拍板先做遭遇随机化(节点分支/多波/遗物协同后置)。
|
||||
|
||||
**设计**(C3 GDD Core Rule 8):EnemyTable 行 `unit_count` 升级为**区间语义** `[unit_count, unit_count_max]`(新字段可省略=定值,旧行零迁移);B1.begin_battle 实例化期按区间均匀骰,**RngService 新增 `encounter` 封闭流**(registry 接口漂移登记——挖掘决策域之外 P1 合规);**B2 零随机纪律保持**(spawn 收 override 即确定值);同种子全程可回放;新敌种首遇节点固定数量(教学位不加骰)。
|
||||
|
||||
**L1 区间**:地精 [1,2] · 地精一伙 [2,3](hp 13)· 熊地精固定(教学位)· 熊地精双雄 [2,3](hp 18)· Boss 固定——期望总量 ≈304(+8%),F6 带内。
|
||||
|
||||
**扫描(N=15)**:基线通关 **47%**(区间期望总量上移 +8% 的叠加效应——F5 带 40-60% 带内 ✓);+50% → 67%;败北分散(Boss×3/战1×4/战3×1)。**注意**:区间骰使难度分布更宽(运气成分进入遭遇层——Roguelike 体的核心),真人体感待批次 2。
|
||||
|
||||
**冒烟 149/149**(+46:区间双值出现/全域内/同种子可回放/D2 教学位固定)。
|
||||
|
||||
## 2026-09-12 试玩批次 1 → v3 数值批(F7 流程实践)
|
||||
|
||||
## 2026-09-12 伤害类型 × 护甲克制批(用户拍板——三段伤害 + 标错惩罚落地)
|
||||
|
||||
**触发**:设计会话(本日)定稿"怪物护甲类型(无甲/护甲A/护甲B)× 角色伤害三段构成(基础/伤害A/伤害B)"的克制对策系统(穿甲弹/高爆弹式三角)+ 标错旗随机惩罚。设定表经用户多轮审阅确认"没有问题"后落程序(GDD 正式修订另批进行——本批以表格结论为实现依据)。
|
||||
|
||||
**拍板结构 → 落点**:
|
||||
|
||||
| 设定 | 实现落点 |
|
||||
|---|---|
|
||||
| 敌人护甲类型:无甲 / 护甲A / 护甲B | EnemyTable 新字段 `armor_type`({none,a,b},缺省 none 向后兼容)· 映射:goblin=none / bugbear=a / ogre_chief=b(教学梯度候选,provisional)· B2.spawn 行值快照入单位字典 |
|
||||
| 伤害三段:①基础 ②伤害A(开格充能)③伤害B(标雷充能) | B1 战斗段主角先手攻击改三分量:基础 = atk×(1+attack_up)(**不吃克制**,attack_up 消费面不变);伤害A = 本轮开出的资格格计数 ×`damage_a_per_cell`(初始展开格不计——reward_eligible=false);伤害B = 首次标中雷 ×`damage_b_per_flag`(与 weaken 同键每雷实例一次去重——忘拔旗不重复充能);三分量各自 floor 后相加,hero_attack 行携带 base/dmg_a/dmg_b 分量(P2 归因) |
|
||||
| 克制矩阵:特化 ×150 / 无甲 ×100 / 对位甲 ×50 | SettlementTable 新字段 `armor_effectiveness`({base,dmg_a,dmg_b}×{none,a,b} percent,缺项回退 100)——刀/AOE/清图奖励**恒定表值不吃克制**(道具=万能稳定伤害,暂定待拍板) |
|
||||
| 伤害A 单轮封顶 | `damage_a_cap = 8`(与 ap_per_round 对齐——防连锁轮一次充能十几个格把特化甲怪一拍秒杀;0 = 不封顶) |
|
||||
| 标错旗 → 随机轻惩罚三选一(时效 1 结算) | `_apply_miss_backlash()`:① `enrage` 暴走(敌 +25% 进 enemy_output 池)② 敌方伏击 +2 层(复用 ambush 行)③ `off_guard` 失衡(我方 +25% 进 hero_defense 池 = 多挨打);新 B3 行 ×2(dur=1 与 weaken/ambush 同款先兑付后衰减);沿用 `_flag_miss_seen` 每雷格实例一次去重 |
|
||||
| 随机载体 | **零新流确定性派生**:(x×7 + y×13 + 板序号×5) mod 3——同种子可回放、不扰动既有三条封闭流取号序;正式侧候选 = RngService 新增封闭流(待拍板) |
|
||||
| 呈现 | 敌人卡环点行常驻护甲标签(无甲/护甲A/护甲B——克制对策可读前提)· 结算明细主角攻击行拆"(基础 X + 开格 Y + 标雷 Z)" · 反噬效果行入明细流 · settlement_snapshot 增 armor/charge 字段(JSONL 数值分析面) |
|
||||
|
||||
**数值全部 provisional 待拍板/调参**(登记于 enemy.tres / settlement.tres 注释):damage_a_per_cell=1 · damage_a_cap=8 · damage_b_per_flag=2 · 矩阵 150/100/50 · enrage/off_guard 25% · 伏击 +2 层 · 护甲映射 goblin/bugbear/ogre——C3 调参时按 F6 供给平衡式复算(三段充能是新增伤害供给,裸装败北/基线通关率两条冒烟路径已复验仍成立)。
|
||||
|
||||
**正式侧义务(GDD/registry 修订清单,待文档批)**:B2 GDD(EnemyTable armor_type 字段 + schema)、B1 GDD(F3 主角攻击三分量构成 + flag_miss 反噬编排 + 克制矩阵载体)、B3 GDD(六行 MVP 状态表 + source_class flag_miss 授予)、A2 OQ1(标错惩罚候选升级为三选一随机池 + 重复性口径=每雷格实例一次)、registry(enemy/settlement_table/status_effect 条目 + A3 条款②锚与条款③不等式重推导——标中旗收益从纯 weaken 变"weaken+伤害B 充能")、C3(数值 provisional 接管)。lore 侧:`design/research/lore-nest-metaphor-2026-09-12.md`(伤害A=地利/伤害B=封印暴露、反噬=封印失败——待新会话拍板后写回 game-concept)。
|
||||
|
||||
**验证**:冒烟 **161/161**(+12:护甲快照三档传播 / 克制矩阵三行表值 / hero_attack 行三分量字段〔全战斗回放 ×2〕/ 标错旗 flag_miss+反噬效果行 / 标中雷充能进主角攻击 / 反噬派生同种子回放)· ui_probe PASS · ui_tune_probe PASS · ui_click_probe(done=true、选用失效=0、问题=0)。
|
||||
|
||||
## 2026-09-12 战斗屏五区改造批(battle.md 落地——ui-programmer 批)
|
||||
|
||||
**背景**:`design/ux/battle.md`(/ux-review APPROVED)+ `design/ux/interaction-patterns.md`(核心 8 pattern)落地到切片呈现层。**core/ 与 data/ 零改动**(本批纯 presentation/ + smoke/)。
|
||||
|
||||
**五区布局**(1280×800 基座):A 区 28px 顶栏关卡进度条(`level_strip_view.gd` 改造:暗金已过/奖励金当前高亮);B 区 252px 战斗画面(`battle_stage_view.gd` 新建 = `hero_card_view.gd` + 轮次·阶段指示 + 兑现演出区 + `enemy_card_view.gd` ×1–3〔顺位徽标/意图徽标常驻/环点拍数/伏击共享池/死亡灰化+碎裂残位禁瞬消〕+ 底部结算明细带);C 区 240px `hero_info_panel_view.gd`(直角细线数据框 + 修正来源角标 + RAP 点阵 + `status_slot_row.gd` ×4〔形状+色相双通道/层数角标/剩余结算点/悬停 tooltip 来源+语义+剩余/空态槽隐藏〕);D 区 `board_zone_view.gd`(`ap_bar_view.gd` 44px〔AP 点阵+停手居左;图例+弃图刷新居右——确认 chips 异位 pattern 7〕+ `board_view.gd` 改造〔悬停三语义自绘光标/目标格边框/BFS 路径蓝点预览/可挖域 8 向呼吸微光/奖励徽章 R₀ 域外/已开雾格 tint 残留/雾区全开一次性信号/去饱和 shader〕+ `toast_stack.gd` 2.5s 消退 + `board_fx_view.gd` 演出层)+ `desaturation.gdshader`(去饱和+金橙偏移、明度不变);E 区 280px `inventory_panel_view.gd`(常读列表固定槽高 + 原位窗口态 pattern 2/8:描边金光/标题切换/时机指示/usable_now 禁选+原因/跳过/政策注记;属性简述投影、来源不呈现)。**旧 `hud_view.gd` 删除**(HUDView 职责由 B/C/D 三区视图承接)。
|
||||
|
||||
**全屏 States 与模态**:`legend_overlay.gd`(图例,「迷雾 = 无雷盲区」恒在 + 状态槽空态说明)/ `settlement_review_overlay.gd`(明细回看)/ `battle_end_overlay.gd`(胜=金蜡封落于棋盘中央+判定行+回看入口;败=暮蓝 #5A6C8C+旗化光点收回)——统一模态登记序管理(`_open_modal` 打开时显式隐藏上方遮挡层,缺陷⑤纪律)。清图金粒喷泉+彩纸 / 完美排雷金光带+横幅 / 标中旗加重弹道 / Boss 紫赤影子爬棋盘 / 零合法板诚实 toast / 雾首遇·chord·非相邻一次性提示(每 run 去重,`ux_hint_shown` 遥测)。**发现并修复历史 quirk**:TunePanel 默认 `visible=true` 靠被不透明开始层遮蔽的树序巧合——已显式初始化 false(缺陷⑤家族)。
|
||||
|
||||
**遥测**(playtest_logger 增 ux 行族,呈现层观测不改 core):`ux_round_input_ratio`(原始输入:AP 消耗,快照驱动)/ `ux_round_exit{path_tag}`(快照 round_facts.path——呈现层只订 B1 的 ADR 纪律下唯一合法源)/ `ux_refresh_attempt{result, round_refresh_index}` / `ux_window_choice` / `ux_reject{source, sub_reason}` / `ux_hint_shown{hint_id}`。
|
||||
|
||||
**探针**:ui_probe 增五区构建断言 + L2 雾图段(雾首遇提示/雾区连通分量);ui_click_probe 增刷新确认链/图例开关/tooltip 触发/模态遮挡断言(图例/回看/窗口态/终局)/窗口期去饱和断言/首格可点计时(进入战斗屏口径 133–151ms ≤500ms AC;场景冷启动 ~700ms 为脚本首载编译域,观测项)——既有全部断言保留。** Waiting 刷新代理已按 OQ12 退役**:UI 刷新按钮仅挖掘期 Active 受理(presenter 门禁),原探针"窗口期点刷新"行为由 Active 确认链测试替代。
|
||||
|
||||
**回归**:smoke_all 149/149 · ui_probe PASS · ui_tune_probe PASS · ui_click_probe PASS(PROBE_SEED 7/11/23/99/42/77 六种子 done=true、选用失效=0、问题=0)。
|
||||
|
||||
**占位登记**:敌人意图视觉映射按 enemy_id 程序化占位(资产定稿归 B5);D区演出(喷泉/彩纸/弹道/影子)全部程序化占位;CJK 字体依赖系统回退(Windows 可用,随包字体归 D1 正式化硬性前置——ADR-0003);金蜡封/暮蓝氛围为色块占位级。
|
||||
|
||||
## 2026-09-12 呈现层 code-review 修复批(/code-review CHANGES REQUIRED → 全项清偿)
|
||||
|
||||
**触发**:全切片 53 脚本 ~9400 行 /code-review(CLAUDE.md 代码规范 + ADR-0001/0003/0004 对照),裁决 CHANGES REQUIRED(7 项必改)。core/ 12 文件与 smoke 断言面整体健康;病灶集中于 tune_panel 封装破口、main.gd 结构回涨、三处呈现契约错位。
|
||||
|
||||
**BLOCKING 修复**:
|
||||
|
||||
| # | 缺陷 | 修复 |
|
||||
|---|---|---|
|
||||
| 1 | **TunePanel 视图写核心表**(`apply()` 直写 `_db.hero.atk/.max_hp`)+ `run_simulation` 内 `load("res://smoke/...")` 驱动 core——面板模拟按 pct² 口径、模拟后表值残留污染真实对局(runner 把放大值当基线再乘) | tune_panel 重构为纯视图(`apply_requested`/`reset_requested`/`simulate_requested` 信号外发 + setter 注入);加成落表收口组合根唯一路径 `_apply_hero_override`(每次从 `_ready` 捕获基线重算,幂等禁叠加);模拟编排拆 `tune_sim_controller.gd`(先复位基线再移交);报告文案归 `HudPresenter.tune_sim_report` |
|
||||
| 2 | **意图徽标双翻译**:presenter 先把 beat key 译中文投影、EnemyCardView 再按英文 key 匹配 → 色相/重击"▼"通道永久失效;`BEAT_NAMES` 两处重复 | presenter 直传 B2 原键(attack/charge/heavy);名/色/标记渲染映射唯一定义处 = EnemyCardView(set_intent 契约头注成文) |
|
||||
| 3 | **C 区 RAP 死分支**:`_refresh_economy` 从不投影 `a3.rap`,"RAP 滚动归零"反馈(A3 D1 义务)不可达;视图靠解析自身 label 文本当状态 | presenter 持续投影 `a3.rap`;`hero_info_panel_view` 改 `_last_rap` 成员存储,删除 `_rap_value()` 文本解析 |
|
||||
| 4 | **smoke_all 恒真断言**(`check(true, "全部计数在域内")`——越域也 PASS) | 改带分布标签的真实域内断言 |
|
||||
|
||||
**结构/封装修复**:
|
||||
|
||||
- **main.gd 761 → 667 行**(评审记录基线 433 行,400 行审视线):节点构建拆 `ui_assembler.gd`(只 new/add_child/布局,零接线零核心写);模态层级状态机拆 `modal_stack.gd`(缺陷⑤纪律载体);战斗流程叙事文案拆 `battle_narrator.gd`(兑现 main 头注"不拼呈现字符串");模拟编排拆 `tune_sim_controller.gd`。剩余行数 = ADR-0001 规定的接线 + 模式流转 + 核心写调用唯一出口(再压需移动 ADR 规定职责,登记为 D1 正式化批候审)。
|
||||
- **level_runner core↔core 接线 lambda → 方法 Callable ×4**(ADR-0001 静态规则 S6——lambda 捕获 self 延长 RefCounted 生命周期/成环)。
|
||||
- **B1 新增 `diagnostic_snapshot()` 诊断观测面**:组合根诊断转储与 click_probe 停滞诊断全部改走成文 API,零下划线私有字段直读(此前直读 8 个)。
|
||||
- **信号处理体去 lambda 化**:main 三处多行/单行 lambda(inter 层继续/单场入口/库存拒绝)改命名方法 + `bind`(规范 4.1)。
|
||||
- 顺带:`can_request_refresh()` 断行格式修复(原单行内嵌 tab 不可读);`_build_inter_overlay` 死参 `parent_root` 删除;`_start_battle` 重复 `_start_overlay.visible = false` 去重;调试两层入口改由 `_start_battle` 统一取加成后满血(原传参在加成前取表值)。
|
||||
|
||||
**探针同步(缺陷回归断言随契约更新——3.3)**:ui_tune_probe 重写——应用/重置走真实按钮信号路径(`press_apply_for_probe` 接缝,ADR-0004"合成输入走真实翻译出口"口径);**新增两条回归断言**:⑦模拟后 hero 表保持基线(无残留)⑧应用加成 + 启动完整关卡后 runner 写表值 = 基线 × 单次加成(12/75;旧缺陷表现 24/112 在此被抓获);ui_click_probe 停滞状态与诊断转储改走 `diagnostic_snapshot()`。
|
||||
|
||||
**验证**:smoke_all **141/141** · ui_probe PASS · ui_tune_probe PASS(含新回归⑦⑧)· ui_click_probe 六种子(7/11/23/99/42/77)全 PASS(done=true、选用失效=0、问题=0)。新增 4 脚本 `.uid` 经 headless 编辑器扫描生成(语言纪律⑤)。
|
||||
|
||||
**登记待办(非本批范围)**:自动玩家策略四份复制收口(评审 W-3);schema 溯源行补齐(W-9);S1–S8 静态扫描脚本化后 S1 自建 RNG 豁免口径登记(W-1);views 转发层信号契约头注补齐(W-2)。
|
||||
|
||||
## 2026-09-13 封装收口批(代码架构审查 E1–E5/O1–O5/P3 修复——ADR-0009 候选项的切片侧预收口)
|
||||
|
||||
**触发**:/architecture-review 改编批——以 architecture.md + ADR-0001~0005 + code-conventions 为基线做代码组织/封装专项审查(报告:`docs/architecture/code-architecture-review-2026-09-13.md`,Verdict: CONCERNS),用户拍板 P1+P2+P3 全修(P2 中涉接口漂移两项另出设计稿待审)。
|
||||
|
||||
**核心封装修复(P1)**:
|
||||
|
||||
| # | 项 | 修复 |
|
||||
|---|---|---|
|
||||
| E1 | **B1 协作对象全公开 → 呈现层链式穿透**(`b1.b2.hero`、`b1.a3.phase` 式层层下钻 main×18 / presenter×15 处) | a1/b2/b3/b4 私有化(a2/a3 保留公有 = 架构文档 API Boundaries 的 D1 契约面);新增 `presentation_snapshot()`(载荷 schema 头注成文)/`is_battle_live()`/`stock_count()`/`query_reward_identity()` 四个成文只读出口——diagnostic_snapshot 同款模式 |
|
||||
| E2 | **A2 状态字典公开且被视图绕过查询面直读**(board_view:278/303 直读 `.revealed`,头注自称"只读查询面"自相矛盾) | `board/revealed/flags` 私有化;新增 `current_board()`/`reveal_record()` 查询面;B1/组合根/探针全部改走命名面 |
|
||||
| E3 | **B2 裸字典容器公开**(`hero/enemies` 可被任意写;presenter 为死亡残位直读原始数组) | 私有化 + `hero_unit()`/`all_enemies()`(含死亡残位,句柄语义与 query_alive 同构)投影面;presenter 残位渲染需求收进 API |
|
||||
| E5 | **"只读"字段仅靠纪律**(phase/ap/rap/last_round_end/master_seed 架构文档定只读,实际公开可变) | getter-only 属性 + 下划线后备字段(外部读法 `a3.phase` 不变,写入仅剩方法通道——GDScript 层面机制化) |
|
||||
|
||||
**组织修复(P1/P2/P3)**:
|
||||
|
||||
- **O1/O2 调试模式抽离**:main.gd 调试分支(单场/连续两层/HP 携带/共享 B4)拆 `presentation/debug_session.gd`——调试语义单一实现点(O2 双实现漂移收口);组合根保留接线(battle_created 先于 begin_battle 发出,ADR-0001 接线唯一归组合根 + 缺陷①时序义务)与终局 UI 决策。ui_probe 抓获初版时序缺陷(battle_ready 才接手 b1 → 雾首遇提示丢失)——即缺陷①同款,battle_created 接手修复。
|
||||
- **O3 DamageMath 抽取**:B1 伤害公式族(F2 乘区/充能分量,零状态纯算术)拆 `core/damage_math.gd`(规范 1.2 工具函数挂宿主类);board_generator 同一合法板重复调用两次 measure_inferability 合并为一次(全库最贵求解器)。
|
||||
- **P3 清债**:头注溯源补齐 ×4(board_data/config_db/rng_service/reward_supply_controller——规范 1.4);B4 grant/grant_clear/stock/use 失败返回统一 `false`→`null`(E6 消费方 `is Dictionary` 判定天然兼容);equivalence_supply 表行 duplicate 防配置源泄漏;load()→preload/类名直引 ×3(main logger/runner、tune_sim autoplay)。
|
||||
|
||||
**加固(基线既有噪声根治)**:main.gd `_on_board_action` 的清图演出判定在 settle_batch 同步重入(结算→battle_over→关卡终局置空 b1)后仍摸 `b1.a2` → SCRIPT ERROR(基线 click_probe 每跑必现)——补 `b1 != null` 守卫,探针 stderr 归零。
|
||||
|
||||
**验证**:冒烟 **141/141**(与基线一致)· ui_probe PASS · ui_tune_probe PASS · ui_click_probe PASS(done=true、选用失效=0、问题=0,**SCRIPT ERROR 0**——优于基线)· 新增脚本 .uid 经 headless 编辑器扫描生成(语言纪律⑤)· presentation 层残留穿透引用 grep 归零(呈现面调用 19 处全部为命名 API)。
|
||||
|
||||
**登记待办(P2 设计稿待审——见审查报告 §4)**:E4 `db.hero` 表值运行时改写参数化(涉 B2.spawn 契约 = 接口漂移,须 GDD/registry 修订批);O4 自动玩家策略四份复制收口(与既有待办 W-3 合并)。
|
||||
|
||||
## 2026-09-13 P2 收口批(E4 表值参数化 + O4 自动玩家单一实现——用户拍板两项均做)
|
||||
|
||||
**E4(db.hero 表值运行时改写 → 参数显式传递)**:
|
||||
- `B2.spawn` 增 `atk_override/max_hp_override` 参数(-1 = 表值兜底;沿用 unit_count_override 先例;EC16 越界守卫改对生效上限判);
|
||||
- `B1.begin_battle` encounter 增 `hero_atk/hero_max_hp` 字段(缺省表值——冒烟直呼路径零改动),战斗段 `_hero_atk` 快照替代 `db.hero.atk` 读取;
|
||||
- `LevelRunner`:`start_level` 表写删除、生效值进 encounter、`restore_hero_base` 只清百分比(表值不可变后无基线可恢复)、`_base_*` 字段删除(直接读恒定的表);
|
||||
- main:`_apply_hero_override/_scaled_override` 表写路径废除 → `_debug_hero_override()` 生效值供给 Callable(面板百分比现算经 encounter 入战斗);`_on_tune_apply/_on_tune_simulate` 去复位步骤;`_db_base_*` 字段删除。
|
||||
- **收益**:双写叠加(6→18→54)与双基线同步整类隐患随表写移除;D2「单一事实源」原则机制化(表值运行期不可变)。
|
||||
- **登记义务**:registry architecture.yaml 新增 `hero_override_parameter_passing` 条目(ADR-0009 候选;B2.spawn 签名 +2 / B1 encounter +2 = 接口漂移,正式侧 B1/B2 GDD 修订随 ADR-0009 批)。
|
||||
- **探针语义升级**:ui_tune_probe ⑦「表值残留」→「hero 表运行期不可变」(reintroduce 表写即 FAIL);⑧ 双写叠加回归改为完整关卡路径 + 呈现快照单次生效断言(12/75;旧缺陷形态 24/112 仍被抓获)。ui_probe 的 `_start_single` 语义注记:调试单场恒基线(入口清零滑条——原行为,非缺陷)。
|
||||
|
||||
**O4(自动玩家策略四份复制 → 单一实现,合并既有待办 W-3)**:
|
||||
- 新增 `smoke/auto_player.gd`:`choose_dig`(最近可挖格)+ `window_selection`(治疗优先于刀;差异参数化 heal_hp_div 2/3 · round_gate 0/3 · knife_compare best/sum——四调用点原语义逐一保留:smoke_all 3/3/best · monte_carlo 2/0/sum · dump_battle 3/3/sum · level_autoplay 2/3/best);
|
||||
- 四消费方删除各自的 `_choose_dig`/窗口策略块改走单一实现;click_probe 的 `level_autoplay._choose_dig` 引用同步改指 AutoPlayer;
|
||||
- **等价性证明**:smoke_all 同种子回放断言(步骤流逐行相同)重构后保持全绿。
|
||||
|
||||
**验证**:冒烟 **141/141** · ui_probe PASS · ui_tune_probe PASS(含 E4 新断言)· ui_click_probe PASS(done=true、选用失效=0、问题=0)。
|
||||
|
||||
|
||||
## 2026-09-13 零承伤根因分割探针 + playtest-09-12 Bug #1–#3 处置批(报告 Top3 授权任务)
|
||||
|
||||
**触发**:playtest-2026-09-12-designer 报告 Top 3 Priorities(探针分割 → Bug 建档修复 → 多种子复扫基线);数值拍板与设计变更不在授权范围(待拍板清单见文末)。
|
||||
|
||||
**① 零承伤根因分割(新探针 `smoke/damage_probe.gd`——boss/level 两模式)**:
|
||||
- **快照盲区发现**:settlement_snapshot 取在衰减段之后,时效 1 的 defense_up 当轮授予当轮衰减——真人局 JSONL `buffs[]` 恒空 ≠ 池为空(报告悬置的直接原因)。探针改从结算行(effect 行 pool_value)重建战斗段时点池值,池归属经 settlement.effect_pool_map 表驱动(零字面量)。
|
||||
- **Run A(fresh 池零挖掘裸验)**:Boss 攻击拍 d_tick = atk.total 全额兑现(attack 9/9 ×2 · charge 0 设计语义 · heavy 18/18),6 轮击倒英雄(defeat)——**「Boss/敌攻击链缺陷」否证**;拍名轮转与真人局逐拍一致(attack→attack→charge→heavy→attack)。
|
||||
- **Run B(自动挖掘墙行为)**:池 -30 → d_tick 21→14、池 -60 → 残余 ×0.40,墙轮承伤恒 >0;表值判定墙最深 -60 > pool_clamp_lo -100 = **归零不可达(09-13 调参批在当前数据上重证)**。
|
||||
- **结论**:真人局 12/12 轮零承伤 = 旧数据 defense_up -50×2 = 池触 clamp 下限 ×0(设计语义)——任何 atk 曲线乘在 ×0 上(原报「v3 主杠杆乘在 ×0 上」坐实)。报告 Bug #2 = 设计问题非 bug。
|
||||
|
||||
**② Bug #1–#3 建档与处置(BUG-0002/0003/0004,`production/qa/bugs/`,编号自 BUG-0001 顺延)**:
|
||||
- **BUG-0002(报告 #1 浅深度空遭遇)——Rejected as coded**:seed 42 当前构建全关回放复现同款遥测签名(d0 spawn=1 / d1 spawn=3 + round-1 victory + unit_count 0)但实为**首轮全灭**(英雄爆发 + 爆裂粉尘 instant_aoe 全歼低 HP 敌群;与 20:41 局 d1 双地精各剩 1 HP 逐点吻合);遭遇骰数学下限 ≥1(B1 roll_lo)+ B2.spawn count<1 fail-fast 双闸,100 场扫描零越域。**快照盲区修复**:settlement_snapshot 增 `spawn_count`(实例化总数含死亡残位)——unit_count=0 时「全灭 vs 空场」可判别,同类误读机制化排除。
|
||||
- **BUG-0003(报告 #2 Boss 攻击零兑现)——Rejected as coded**:根因见①;候选改法(clamp 收紧 / 乘法衰减 / 重击拍穿防)列待设计批。
|
||||
- **BUG-0004(报告 #3 grant_node_active 绕过 _supply.notify_granted)——Fixed**:stock 受理成功后补入账(与 grant() 同一「发放即获取」口径);计数器战斗域清零语义不变 = **零战斗数值影响**,纯计数口径修复(报告 Top3 #3 的上报义务执行)。
|
||||
- **回归断言(代码规范 3.3)**:smoke_all +7——快照 spawn_count 字段与等值断言;遭遇全深度 × 12 种子全域内扫描(60 场 spawn ≥1 且 ∈[unit_count, unit_count_max]);`_smoke_attack_chain_fresh_pool`(fresh 池非蓄力拍 d_tick == atk.total 全额兑现——攻击链健康钉死,未来真缺陷在此被抓);`_smoke_supply` ⑦ 节点奖励发放即入账。
|
||||
|
||||
**③ 多种子关卡复扫(Q1 调参批基线——level_monte_carlo 增 F4 两列「普通承伤/场% · Boss承伤%」:逐战斗承伤切分 = round 回卷分段 + 末段 Boss 判定(须抵达 Boss 节点))**:
|
||||
- N=30 × 10 档(种子 4200–4229;F7 切片样本量口径)。**结论:全档位越带**——F5 基线通关率 **17%**(带 40–60%)· +100% 档 43%(中天赋 ≥95%)· +200% 档 63%(高天赋 100%);F4 普通场承伤 **35.1–43.1%/场**(带 ≤20%)、Boss 场 **71.3–134.9%**(带 ≤50%)(hp+0 档;hp+100 档普通 13.9–22.6 带内 / Boss 35.8–76.7 仍多数越带)。拆墙(-50→-30 且权重 24→16)与 v3 atk 曲线叠加后,难度由严重不足摆向**严重过剩**。**本批未动任何数值**(敌 hp 曲线与奖励供给禁独立调参——registry 预警⑥联动义务;杠杆方向列待拍板,本基线 = F7 流程的测量输入)。
|
||||
|
||||
**验证**:冒烟 **148/148**(141 既有 + 7 新回归)· 三探针 PASS(ui_probe / ui_click_probe 6 种子 done=true、选用失效=0、问题=0 / ui_tune_probe)· damage_probe boss 模式 7/7 + level 模式(种子 42/43/44)4/4 全 PASS。
|
||||
|
||||
**待拍板/待设计批清单(本会话零数值变更、零 GDD 变更)**:
|
||||
1. **难度杠杆重拍板**(F4/F5 全档位越带)——候选:enemy atk 曲线回撤部分 v3 增量 / defense_up 权重回升 / heal 供给联动——归 C3 按 F7 流程(本基线为测量输入);
|
||||
2. **墙语义候选改法**(clamp 收紧至 -60/-70 / stack 乘法衰减 / heavy 拍穿防)→ C3 + /propagate-design-change(连带 B1 池 clamp 语义 + B3 GDD "-50" 文字同步);
|
||||
3. **供给量重拍板**:「每战斗首件保证」的总量语义(候选:关卡域首件)→ C3(BUG-0004 已封计数漏洞,关卡供给账目可完整观测);
|
||||
4. **浅深度首轮跳战可玩性**(autoplay/真人 d0-d3 round-1 全灭跳过战斗过程)→ C3 结合 F4 复验数据裁决。
|
||||
|
||||
|
||||
## 2026-09-13 拍板落地批(四项拍板——难度杠杆/墙语义/供给量/跳战)
|
||||
|
||||
**拍板结果**(用户四问裁决):①难度主杠杆 = **先真人试玩再拍**(F5 真人修正系数标定优先于数值调整);②墙语义 = **维持现状 + 归零护栏**;③供给量语义 = **先观测再拍**;④跳战 = **并入难度复测再议**。本批零玩法数值变更。
|
||||
|
||||
**落地变更**:
|
||||
- smoke_all 新增 `_smoke_wall_zero_guard`(表级不变式:`2×defense_up.magnitude > pool_clamp_lo`)——BUG-0003 事故类(叠满触池底 = ×0 全额免伤)永久免疫:任何未来 magnitude/clamp 调参重新造出 ×0 墙即冒烟红灯,强制显式重审;
|
||||
- level_monte_carlo 增「**跳战率**」列(普通战斗 round-1 即胜占比——拍板④观测面,此后每次复测自动携带);`_damage_segments` 升级为逐段 {承伤, r1_win} 结构;
|
||||
- defense_scan 输出标签修正:「墙轮承伤(应为 0)」→「(合法——-30 批墙帽 ×0.40)」(旧 -50 数据时代的期望文案过时清理)。
|
||||
|
||||
**跳战率基线(N=30,拍板④"再议"数据)**:基线 **8%** · +50% 13% · +100% 24% · +150% 27% · +200% 38%(普通场 round-1 全灭占比)。**seed 42(试玩固定种子)为高跳战轨迹**(autoplay d0–d3 四场全部 round-1 全灭)——真人局 d0–d3 全跳的体验含显著种子成分;基线 talent 下跳战是少数派,拍板④"复测再议"的数据前提成立。其余各列与 Q1 基线逐值一致(collect_rows=true 开启不改变行为——确定性交叉验证)。
|
||||
|
||||
**验证**:冒烟 **149/149**(+1 护栏)。
|
||||
|
||||
**下一步(用户侧)**:开一局真人试玩(任意种子;JSONL 自动落 user://logs/)。观测清单(服务拍板①③复议):通关与否 + 败北节点(F5 修正系数 = 真人通关率 ÷ 模拟 17%)· 普通场掉血体感(对照 ≤20% 带)· Boss 战撑过几轮 · d0–d3 是否发生跳战 · 治疗包是否出现缺血时刻与库存残留(拍板③数据)。事后由会话分析 JSONL 出修正系数。
|
||||
|
||||
## 2026-09-14 完美策略档测量批(OQ7 技能轴上界仪——用户拍板"增加完美策略档测试验证",零玩法数值变更)
|
||||
|
||||
**背景**:用户(扫雷强玩家)指出"完全正确开图/标雷、零踩雷 ⇒ 存活无压力"——现行扫描只有弱策略基线档(不标旗/邻格盲挖/朴素选道具),技能轴不可测;registry `calibration_gap`(真人显著强于模拟者)此前只有方向无机制。本批 = 测量工具批。
|
||||
|
||||
**落地变更**:
|
||||
- `auto_player.gd` 增完美策略求解器(`perfect_action`/`_deduce`):single + subset 推理不动点(A1 F1 `measure_inferability` 同族规则集,**去 safe_set oracle**——约束未知邻格含雷格,比生成期闸保守);**零盲挖**(只挖已证安全格 = 构造保证零踩雷,扫描末断言兜底违例 quit(1))· **已证雷格全标旗**(旗先于挖——轮末 weaken + 伤害B 首次充能,雷上旗不阻连锁)· 卡死且资源可 → `b1.player_request_refresh()` 轮内刷新 · 否则停手(残 AP 1:1 入 RAP)。输入只经 A2 查询面(reveal_record/is_revealed/has_flag),**禁读棋盘雷区真值**——测推理上界,非全知作弊。core 层零改动;
|
||||
- `window_selection` 增 `heal_no_waste` 选项(缺口 < 治疗量不用;完美档启用)+ 完美档轮次门解除(round_gate=0);
|
||||
- `level_autoplay.gd` 增 `policy` 参数(默认 "normal"——tune_sim_controller/defense_scan/level_debug 既有调用零改动);完美档驱动 `_autoplay_battle_perfect`(move/dig/flag 走 A2 settle_batch、refresh 走 B1 编排、连续拒绝 ≥3 停手防死循环);st 增 `mine_hits`/`refreshes` 计数(mine_hit 证据行埋点);
|
||||
- `level_monte_carlo.gd` 增第 3 CLI 参 `[policy]`(normal/perfect/both,缺省 normal 兼容既有文档命令)+ 策略/踩雷/刷新列;脚注 F5 带文案同步 [30,50] 带锚定稿。
|
||||
|
||||
**完美档基线(N=30 · 种子 4200–4229 · 耗时 597.6s)**:**0 加成通关率 100%**(弱策略基线档 60%)、通关剩余 HP 均 **47.4/50**、整关承伤均 3.5(p90 18.0)、普通场承伤 0.5%(带 ≤20%)、Boss 场 5.3%(带 ≤50%)、踩雷 0.00/局(断言 ✓)、刷新 0.23/局、跳战率 20%。+50%~+200% 全档位恒 100% 满血通关(+200% 承伤均 0.4)——**技能轴上界完全碾压现行曲线:F5 带与 F6 S/K 区间对完美档零分辨力**(区间模型无策略水平轴的结构性证实;calibration_gap 获得机制解释与定量上界)。
|
||||
|
||||
**验证**:冒烟 **149/149** 全绿(默认行为零变更——弱策略档既有断言全通过)。
|
||||
|
||||
**待拍板**:① 0 加成完美档通关率的目标带(现行 [30,50] 按弱策略口径定义——是否改为按完美档口径验收?);② 杠杆选择(已预登记:r_boss 走廊 180–207 / 群战上探 / decay 0.6;本批新增候选:"上限掺运气"系——供给量值随机化(power 区间化)/ 承伤硬地板——均待完美档复测验证效果)。
|
||||
|
||||
## 2026-09-14 伤害量值区间批(用户拍板②——"上限掺运气"两步全落地 + 完美档无效性实测,宽度 provisional)
|
||||
|
||||
**拍板**:验收口径改完美策略档(0 加成通关 ≤40%);杠杆 = 伤害值区间随机(上阶/下阶,范围内 roll);覆盖面 = 奖励侧五行先行、不足扩敌方侧(分步授权已用尽)。roll 时机 = 身份预分配批(E14 快照口径不变);中点恒等现值(A3 条款①当量锚不漂移);宽度 provisional 归测量迭代。
|
||||
|
||||
**落地变更**:
|
||||
- **第一步·奖励侧五行**(reward.tres + reward_table.gd schema + reward_economy.gd):heal_minor [2,6] / knife_t1 [5,11] / heal_pack [6,18] / knife_adv [10,22] / clear_blast [25,55]——power_min/power_max 可选配对字段(缺省定值行向后兼容,armor_type 先例);schema 校验 = 成对/buff 禁止/中点锚;roll 在 roll_board 身份批内**追加取号**(既有取号序零扰动)+ grant_node_active 同路径;快照 power = roll 值,B1/呈现零改动,授予期零随机纪律不变;
|
||||
- **第二步·敌方攻击侧**(enemy.tres 六行 + enemy_table.gd schema + unit_container.spawn + battle_orchestrator):atk [min,max] = 行值 ±3 provisional(d0 7[4,10] / d1 5[2,8] / d2 9[6,12] / d3 6[3,9] / Boss 9[6,12] / L2 7[4,10]);roll = 遭遇骰同流(encounter 恰一 scope,数量骰先、atk 骰后)spawn 期**整战持久**——每拍抖动否决(数字可读性锚),B2 零随机纪律保持(收 override 即确定值);spawn 签名 +1 参数(接口漂移登记,ADR-0009 同批);
|
||||
- smoke_all 新增 `_smoke_power_interval`(4 断言:五行中点锚/roll 域内含清图槽/定值行零外溢/节点路径域内)。
|
||||
|
||||
**实测(N=30,种子 4200–4229)**:
|
||||
- 奖励侧单独:完美档 0 加成 **100%**(承伤 3.5——与区间前逐值一致);基线档 17%(不变);
|
||||
- 敌方侧叠加:完美档 0 加成仍 **100%**(承伤 3.5→3.9 / p90 18→19——方差真实流动但被血缓冲全吞);剩余 HP 均 47.4 不变;
|
||||
- **验收结论:≤40% 线用数值区间杠杆族到不了**——完美玩家结构性优势(零踩雷 = 零直伤 + 全标旗 = weaken −65% 顶格 + 快杀 = 敌出手次数最少)不受值域方差影响。三选待裁决:①承伤硬地板(每结算不可减免平伤——S_min 压缩,B1/B2 域);②weaken 饱和/递减语义(拆"全标旗顶格"基石,B3/B1 域);③极端宽度(数值可用但毁基线档与可读性锚——建议否决)。**本批区间机制保留**(对普通玩家方差体验有效,中点锚不伤当量)。
|
||||
|
||||
**验证**:冒烟 **154/149+5 全绿**(区间批 5 新断言 + 既有全部回归含遭遇骰分布/回放)。
|
||||
@@ -0,0 +1,163 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# A3 行动经济:被动账本(AP 硬重置 / 统一价目 / 三路径结束仲裁 cleared > stopped > ap_exhausted /
|
||||
# RAP 池战斗域 / 双发射源之 stopped 直发)。相位域 = {Idle, Active, Waiting}。
|
||||
# 实现 GDD:design/gdd/a3-action-economy.md(系统 ID A3——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name ActionEconomy
|
||||
extends RefCounted
|
||||
|
||||
signal round_end_stopped(payload: Dictionary) # 双发射源之停手路径(A3 直发;payload 与 A2 棋盘路径同构)
|
||||
|
||||
const PHASE_IDLE := "Idle"
|
||||
const PHASE_ACTIVE := "Active"
|
||||
const PHASE_WAITING := "Waiting"
|
||||
|
||||
var db
|
||||
# 相位与账本只读出口(E5 封装收口 2026-09-13):外部读取经 getter-only 属性(架构文档
|
||||
# "ap/rap/phase 只读" 的机制化),写入只经 notify_* / request_* 方法通道。
|
||||
var _phase: String = PHASE_IDLE
|
||||
var phase: String:
|
||||
get: return _phase
|
||||
var _ap := 0
|
||||
var ap: int:
|
||||
get: return _ap
|
||||
var _rap := 0
|
||||
var rap: int:
|
||||
get: return _rap
|
||||
var _last_round_end: Dictionary = {} # {path_tag, continuation_hint}——A2 组包 round_end 的合法数据源
|
||||
var last_round_end: Dictionary:
|
||||
get: return _last_round_end
|
||||
var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start
|
||||
var _next_round_mode := "" # 调试观测量(同板续轮 / 换新图)
|
||||
|
||||
|
||||
func _init(db_ref) -> void:
|
||||
db = db_ref
|
||||
|
||||
|
||||
func _cost(kind: String) -> int:
|
||||
var ae = db.action_economy
|
||||
match kind:
|
||||
"dig":
|
||||
return ae.dig_ap_cost
|
||||
"flag_place":
|
||||
return ae.flag_place_ap_cost
|
||||
"flag_pull":
|
||||
return ae.flag_pull_ap_cost
|
||||
"move":
|
||||
return ae.move_ap_cost
|
||||
"chain":
|
||||
return ae.chain_ap_cost
|
||||
assert(false, "未知动作价目: " + kind)
|
||||
return 0
|
||||
|
||||
|
||||
# 通用扣减(A2 用于旗动作;挖握走 request_dig——cleared 仲裁需要 clears_map 参数)
|
||||
func try_spend(action_kind: String) -> bool:
|
||||
if phase != PHASE_ACTIVE:
|
||||
_reject("phase")
|
||||
return false
|
||||
return _spend_and_arbitrate(action_kind, false)
|
||||
|
||||
|
||||
# A2 挖掘专用:chain_size = 连锁附加格数(不含首格);clears_map = 本次动作后全图安全格开完
|
||||
func request_dig(chain_size: int, clears_map: bool) -> bool:
|
||||
if phase != PHASE_ACTIVE:
|
||||
_reject("phase")
|
||||
return false
|
||||
return _spend_and_arbitrate("dig", clears_map)
|
||||
|
||||
|
||||
func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
|
||||
var cost := _cost(action_kind)
|
||||
if _ap < cost:
|
||||
_reject("resource_face")
|
||||
return false
|
||||
_ap -= cost
|
||||
if clears_map:
|
||||
# cleared 胜出(同帧仲裁 cleared > ap_exhausted;剩余 AP 作废不入 RAP——EC7/EC10)
|
||||
_finish_round("cleared", "new_board")
|
||||
elif _ap == 0:
|
||||
# AP 归 0 瞬间轮次结束——不存在 0-AP 操作窗口
|
||||
_finish_round("ap_exhausted", "same_board")
|
||||
return true
|
||||
|
||||
|
||||
# 停手:剩余 AP 1:1 转存 RAP;A3 直发 stopped 路径 round_end(双发射源)
|
||||
func request_stop() -> bool:
|
||||
if phase != PHASE_ACTIVE:
|
||||
_reject("phase")
|
||||
return false
|
||||
_rap += _ap
|
||||
_ap = 0
|
||||
_finish_round("stopped", "same_board")
|
||||
round_end_stopped.emit({"path_tag": "stopped", "continuation_hint": "same_board"})
|
||||
return true
|
||||
|
||||
|
||||
# 玩家发起的刷新(B1 在 Waiting 编排期代理调用;禁止 RAP 满自动刷)
|
||||
func request_refresh() -> bool:
|
||||
if phase != PHASE_WAITING:
|
||||
_reject("phase")
|
||||
return false
|
||||
if _rap < db.action_economy.rap_refresh_cost:
|
||||
return false # EC13 资源不足拒绝——合法相位内资源拒绝,非编排错误
|
||||
_rap -= db.action_economy.rap_refresh_cost
|
||||
return true
|
||||
|
||||
|
||||
# 轮内刷新(2026-09-12 方案 A 用户拍板):Active 相位受理——仅扣 RAP,相位与 AP 均不动。
|
||||
# 刷新不打断轮次、不触发结算(敌方时间税语义变更已登记 GDD 修订义务);换图编排归 B1
|
||||
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 D1 发起)纪律不变。
|
||||
func request_refresh_active() -> bool:
|
||||
if phase != PHASE_ACTIVE:
|
||||
_reject("phase")
|
||||
return false
|
||||
if _rap < db.action_economy.rap_refresh_cost:
|
||||
return false # EC13 资源不足拒绝——合法相位内资源拒绝,非编排错误
|
||||
_rap -= db.action_economy.rap_refresh_cost
|
||||
return true
|
||||
|
||||
|
||||
# B1 回发:结算完成(仅记录编排意图;phase 停留 Waiting——AC-14 门不被绕过)
|
||||
func notify_settlement_done(next_round_mode: String) -> bool:
|
||||
if phase != PHASE_WAITING:
|
||||
_reject("phase")
|
||||
return false
|
||||
_settlement_consumed = true
|
||||
_next_round_mode = next_round_mode
|
||||
return true
|
||||
|
||||
|
||||
func notify_round_start() -> bool:
|
||||
if phase == PHASE_IDLE:
|
||||
pass # 战斗首轮唯一出口
|
||||
elif phase == PHASE_WAITING and _settlement_consumed:
|
||||
pass
|
||||
else:
|
||||
_reject("phase")
|
||||
return false
|
||||
_ap = db.action_economy.ap_per_round # 每轮硬重置(RAP 不动——唯一跨轮携带量)
|
||||
_phase = PHASE_ACTIVE
|
||||
_settlement_consumed = false
|
||||
return true
|
||||
|
||||
|
||||
func notify_combat_end() -> bool:
|
||||
if phase != PHASE_WAITING:
|
||||
_reject("phase")
|
||||
return false
|
||||
_phase = PHASE_IDLE
|
||||
_rap = 0 # RAP 战斗域清零(用户拍板;D1 可见反馈义务)
|
||||
_ap = 0
|
||||
return true
|
||||
|
||||
|
||||
func _finish_round(path_tag: String, continuation_hint: String) -> void:
|
||||
_last_round_end = {"path_tag": path_tag, "continuation_hint": continuation_hint}
|
||||
_phase = PHASE_WAITING
|
||||
|
||||
|
||||
func _reject(source: String) -> void:
|
||||
push_warning("[A3] 动作被拒(%s 相位守卫/资源)" % source if source == "phase" else "[A3] 资源不足拒绝")
|
||||
@@ -0,0 +1 @@
|
||||
uid://hxhl617bokyi
|
||||
@@ -0,0 +1,792 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# B1 战斗结算编排中枢:轮次生命周期唯一 owner · 段序骨架(授予→窗口→增益/减益→道具→战斗→衰减→判定)·
|
||||
# 13 类行协议可回放步骤流(零随机零 await)· 高光双落(完美排雷 ×2 / 标中旗 highlight)· 伏击预截流 ·
|
||||
# Waiting 相位结算触发(不依赖事件到达序)。
|
||||
# 切片登记:① 事件总线载体 = 直连信号(实现期 ADR 项,provisional)② 多次刷新 EC15 简化为每 Waiting 至多
|
||||
# 一次(UI 禁二次发起;多刷契约义务不触)③ 效果行协议 = 授予段逐 grant 行 + 增益/减益段续存实例组合行。
|
||||
# 实现 GDD:design/gdd/b1-settlement.md(系统 ID B1——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name BattleOrchestrator
|
||||
extends RefCounted
|
||||
|
||||
signal step_row(row: Dictionary)
|
||||
signal window_opened(items: Array)
|
||||
signal battle_finished(payload: Dictionary) # {result, hero_hp, remaining_items}——hero 在 despawn 前捕获(呈现层销毁后读状态的时序防护)
|
||||
signal round_started(round_index: int)
|
||||
signal board_changed()
|
||||
signal settlement_snapshot(snap: Dictionary) # 每结算末尾的全量状态快照(playtest_logger 消费——JSONL 数值分析)
|
||||
|
||||
var db
|
||||
var rng
|
||||
# E1 封装收口(2026-09-13):协作对象 a1/b2/b3/b4 私有化——跨层读取一律经 presentation_snapshot()/
|
||||
# diagnostic_snapshot() 成文出口(呈现层链式穿透 b1._b2.hero_unit() 的根因收口)。
|
||||
# a2/a3 保持公有 = 架构文档 API Boundaries 的 D1 消费面(意图提交 settle_batch + compute_move_path
|
||||
# 只读;ap/rap/相位读取)——非本文档登记的契约面,勿增新用途。
|
||||
var _a1: BoardGenerator
|
||||
var a2: MinesweeperRules
|
||||
var a3: ActionEconomy
|
||||
var _b2: UnitContainer
|
||||
var _b3: StatusContainer
|
||||
var _b4: RewardEconomy
|
||||
|
||||
var encounter: Dictionary = {}
|
||||
var round_index := 0
|
||||
var battle_result := "" # "" / "victory" / "defeat"
|
||||
|
||||
var _facts_reveals: Array = []
|
||||
var _facts_flags: Array = []
|
||||
var _facts_path := ""
|
||||
var _settling := false
|
||||
var _awaiting_window := false
|
||||
var _window_items: Array = []
|
||||
var _refresh_used_this_waiting := false
|
||||
var _refresh_used_this_round := 0 # 每轮刷新计数(Active+Waiting 合计,兜底上限 = ActionEconomyTable.refresh_max_per_round)
|
||||
var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
|
||||
var _flag_consumed := {} # 兑付资格以雷格实例为键(板级;换图重置)
|
||||
var _flag_miss_seen := {} # 标旗未中行去重(每雷格实例一次)
|
||||
var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置)
|
||||
var _perfect_hit_false := 0
|
||||
var _enemy_cap := 0 # EnemyTable 行值快照(伏击 cap)
|
||||
var _m_amb := 0 # StatusEffectTable.ambush.magnitude 快照
|
||||
var _hero_atk := 0 # 主角攻击基值快照(encounter.hero_atk;E4 收口——表值运行期不可变)
|
||||
var _charge_dmg_a := 0 # 伤害A 充能:本轮开出的资格格计数(轮末结算消费后随 facts 重置)
|
||||
var _charge_dmg_b := 0 # 伤害B 充能:本轮首次标中雷计数 ×damage_b_per_flag(每雷实例一次)
|
||||
|
||||
|
||||
func _init(db_ref, rng_ref, shared_b4 = null) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
_a1 = BoardGenerator.new()
|
||||
a3 = ActionEconomy.new(db)
|
||||
a2 = MinesweeperRules.new(db, a3)
|
||||
_b2 = UnitContainer.new(db)
|
||||
_b3 = StatusContainer.new(db)
|
||||
# 库存关卡域(2026-09-11 关内延续):连续关卡模式注入共享 B4 容器,库存跨战斗延续
|
||||
_b4 = shared_b4 if shared_b4 != null else RewardEconomy.new(db, rng)
|
||||
a2.cell_revealed.connect(_on_cell_revealed)
|
||||
a2.flag_judged.connect(_on_flag_judged)
|
||||
a2.round_end_board.connect(_on_round_end)
|
||||
a3.round_end_stopped.connect(_on_round_end)
|
||||
|
||||
|
||||
# ---- 战斗开始序列(编排专章①:门 → B3 → B2 → install+roll → A3)----
|
||||
|
||||
func begin_battle(enc: Dictionary) -> bool:
|
||||
if round_index != 0:
|
||||
push_warning("[B1] begin_battle 重复受理")
|
||||
return false
|
||||
encounter = enc
|
||||
db.set_reload_gate(true)
|
||||
if not _b3.notify_battle_start():
|
||||
_fatal("B3 开域被拒(编排违例)")
|
||||
return false
|
||||
_b4.notify_battle_start() # 可选类供给控制器战斗域清零(2026-09-11 供给控制器批——首件保证按战重置)
|
||||
# 遭遇骰(C3 GDD Core Rule 8——2026-09-12 用户拍板):行有 unit_count_max 时按 [unit_count, max] 均匀骰;
|
||||
# 2026-09-14 敌方攻击区间批扩展:行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值
|
||||
# (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后);
|
||||
# 挖掘决策域之外——P1 合规;B2 零随机纪律保持(收 override 即确定值)。
|
||||
var erow_pre: Dictionary = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth))
|
||||
var roll_lo: int = int(erow_pre.get("unit_count", 1))
|
||||
var roll_hi: int = int(erow_pre.get("unit_count_max", roll_lo))
|
||||
var needs_count: bool = roll_hi > roll_lo
|
||||
var needs_atk: bool = erow_pre.has("atk_min")
|
||||
var rolled := roll_lo
|
||||
var rolled_atk := -1
|
||||
if needs_count or needs_atk:
|
||||
var estream: RandomNumberGenerator = rng.open_stream("encounter")
|
||||
if needs_count:
|
||||
rolled = roll_lo + estream.randi_range(0, roll_hi - roll_lo)
|
||||
if needs_atk:
|
||||
rolled_atk = int(erow_pre.atk_min) + estream.randi_range(0, int(erow_pre.atk_max) - int(erow_pre.atk_min))
|
||||
# E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径)
|
||||
_hero_atk = int(enc.get("hero_atk", int(db.hero.atk)))
|
||||
if not _b2.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), rolled,
|
||||
_hero_atk, int(enc.get("hero_max_hp", -1)), rolled_atk):
|
||||
_fatal("B2 spawn 被拒")
|
||||
return false
|
||||
var erow: Dictionary = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth))
|
||||
_enemy_cap = int(erow.get("advantage_per_round_cap", 0))
|
||||
_m_amb = int(db.status_effect_row("ambush").get("magnitude", 0))
|
||||
if not _install_new_board():
|
||||
return false
|
||||
_reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(reward_eligible 恒 false 的双保险)
|
||||
if not a3.notify_round_start():
|
||||
_fatal("A3 首轮 notify_round_start 被拒")
|
||||
return false
|
||||
round_index = 1
|
||||
round_started.emit(round_index)
|
||||
return true
|
||||
|
||||
|
||||
func _install_new_board(during_round := false) -> bool:
|
||||
var stream: RandomNumberGenerator = rng.open_stream("board_gen")
|
||||
var board: BoardData = _a1.generate(int(encounter.level_index), int(encounter.node_depth), stream, db)
|
||||
if board == null:
|
||||
return false
|
||||
var res: Dictionary = a2.install_board(board, during_round)
|
||||
if not res.accepted:
|
||||
_fatal("A2 install 拒绝(不变式违例): %s" % str(res))
|
||||
return false
|
||||
# B4 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = S ∖ R₀
|
||||
var r0 := {}
|
||||
for c in board.compute_r0():
|
||||
r0[c] = true
|
||||
var eligible: Array = []
|
||||
for c in board.safe_cells():
|
||||
if r0.has(c):
|
||||
continue
|
||||
eligible.append({"cell": c, "fog": board.variant_ids(c).size() > 0})
|
||||
if not _b4.roll_board(eligible):
|
||||
_fatal("B4 roll_board 被拒")
|
||||
return false
|
||||
_flag_consumed.clear() # 换图新雷 = 新实例,资格自然重置(A3 Rule 8)
|
||||
_flag_miss_seen.clear()
|
||||
_perfect_hit_true = 0 # 板级完美计数随 install 重置
|
||||
_perfect_hit_false = 0
|
||||
_board_seq += 1
|
||||
board_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
# ---- 事件缓冲(轮 facts 收集;结算触发 = Waiting 相位编排点,不依赖到达序)----
|
||||
|
||||
func _on_cell_revealed(evt: Dictionary) -> void:
|
||||
# 轮 facts 缓冲不设相位守卫——轮末最后一挖的揭示事件在 request_dig 翻相之后才到(事件序:
|
||||
# cell_revealed 批 → flag_judged 批 → round_end),phase==Active 守卫会丢最后一挖的事实;
|
||||
# initial 通道事件(install 初始展开)reward_eligible=false,进入授予段被资格检查自然跳过。
|
||||
if battle_result == "":
|
||||
if evt.kind == "safe" and bool(evt.get("reward_eligible", false)):
|
||||
# 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份,
|
||||
# 防旧图已挖格在轮末错拿新图同坐标身份)
|
||||
var snap = _b4.query_full_snapshot(evt.cell)
|
||||
if snap != null:
|
||||
evt = evt.duplicate()
|
||||
evt["reward_snapshot"] = snap
|
||||
_facts_reveals.append(evt)
|
||||
|
||||
|
||||
func _on_flag_judged(evt: Dictionary) -> void:
|
||||
_facts_flags.append(evt)
|
||||
|
||||
|
||||
func _on_round_end(_payload: Dictionary) -> void:
|
||||
_facts_path = String(_payload.get("path_tag", ""))
|
||||
_maybe_settle()
|
||||
|
||||
|
||||
func _maybe_settle() -> void:
|
||||
if _settling or battle_result != "":
|
||||
return
|
||||
if a3.phase != a3.PHASE_WAITING:
|
||||
return
|
||||
_settle()
|
||||
|
||||
|
||||
# ---- 每轮结算序列(段序骨架——Core Rule 4)----
|
||||
|
||||
func _settle() -> void:
|
||||
_settling = true
|
||||
var ri := round_index
|
||||
# ① 授予段(含 grant_clear 兑付)
|
||||
var mine_cells: Array = []
|
||||
for evt in _facts_reveals:
|
||||
if evt.kind == "mine":
|
||||
mine_cells.append(evt.cell)
|
||||
elif evt.kind == "safe" and evt.reward_eligible:
|
||||
_charge_dmg_a += 1 # 伤害A 充能:探索本身计费,与该格奖励是否兑付成功无关(初始展开格 reward_eligible=false 不计)
|
||||
_grant_cell(evt.cell, ri, "safe_cell", evt.get("reward_snapshot"))
|
||||
# reward_eligible=false(initial 通道)不入库——B1 义务②(A3 EC5 委派闭环)
|
||||
for evt in _facts_flags:
|
||||
_judge_flag(evt, ri)
|
||||
if not mine_cells.is_empty():
|
||||
_convert_ambush(mine_cells, ri)
|
||||
if _facts_path == "cleared":
|
||||
_grant_clear_reward(ri)
|
||||
# ② 道具使用窗口(授予段后、增益/减益段前——当轮拾取当轮可用)
|
||||
_window_items = _assemble_window()
|
||||
if not _window_items.is_empty():
|
||||
_awaiting_window = true
|
||||
window_opened.emit(_window_items)
|
||||
_settling = false
|
||||
return # 窗口选择序列 = 结算回放输入事件
|
||||
_finish_settlement([])
|
||||
|
||||
|
||||
func _assemble_window() -> Array:
|
||||
# 窗口数据组装 = B4 query_inventory 投影 + B1 侧守卫预判 usable_now(B4 零跨系统读不破)
|
||||
var out: Array = []
|
||||
for inst in _b4.query_inventory():
|
||||
if inst.status != "InStock":
|
||||
continue
|
||||
var usable := true
|
||||
if inst.category == "heal_pack":
|
||||
usable = int(_b2.hero_unit().hp) < int(_b2.hero_unit().max_hp) # 满血治疗包禁选
|
||||
out.append({
|
||||
"index": inst.index,
|
||||
"item_id": inst.item_id,
|
||||
"display_name": inst.display_name,
|
||||
"category": inst.category,
|
||||
"power": inst.power,
|
||||
"source_cell": inst.source_cell,
|
||||
"round_index": inst.round_index,
|
||||
"usable_now": usable,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
func submit_window_selection(selection: Array) -> void:
|
||||
if not _awaiting_window:
|
||||
push_warning("[B1] 无窗口期的选择提交(编排违例)")
|
||||
return
|
||||
_awaiting_window = false
|
||||
_settling = true
|
||||
if selection.size() > 1:
|
||||
push_warning("[B1] 窗口至多选用一件(2026-09-11 用户拍板——避免结算复杂度)——超出部分忽略")
|
||||
_finish_settlement(selection.slice(0, 1))
|
||||
|
||||
|
||||
func _finish_settlement(selection: Array) -> void:
|
||||
var ri := round_index
|
||||
# ③ 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——B3 义务⑦)
|
||||
_emit_composition_effects("hero", ri)
|
||||
_emit_composition_effects("enemy", ri)
|
||||
# ④ 道具段(窗口选定序逐件生效;逐件动态守卫)
|
||||
for idx in selection:
|
||||
_apply_selected_item(int(idx), ri)
|
||||
# ⑤ 战斗段(调用序钉死 query_attack → apply_damage → advance;hero hp=0 短路)
|
||||
_combat_segment(ri)
|
||||
# ⑥ 衰减段(恰一次恒执行含末轮——先兑付后衰减)
|
||||
var k: int = _b3.decay_round()
|
||||
_emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "")
|
||||
# ⑦ 判定段(防御性优先级 hero 先判——双死结构不可达的纵深防御)
|
||||
var verdict := "continue"
|
||||
if int(_b2.hero_unit().get("hp", 0)) <= 0:
|
||||
verdict = "defeat"
|
||||
elif _b2.query_alive("enemy").is_empty():
|
||||
verdict = "victory"
|
||||
_emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result)
|
||||
if verdict == "defeat":
|
||||
_emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗失败", 0, {}, "")
|
||||
_battle_over("defeat")
|
||||
return
|
||||
if verdict == "victory":
|
||||
_emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗胜利", 1, {}, "")
|
||||
_battle_over("victory")
|
||||
return
|
||||
_emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "")
|
||||
_exit_distribution()
|
||||
_settling = false
|
||||
|
||||
|
||||
# ---- 授予段 helpers ----
|
||||
|
||||
# snap_override 非空 = 冻结快照授予(轮内刷新批——缓冲期身份,绕过现行 mapping/_granted)
|
||||
func _grant_cell(cell: Vector2i, ri: int, source_class: String, snap_override = null) -> void:
|
||||
var snap = snap_override if snap_override != null else _b4.grant(cell)
|
||||
if not (snap is Dictionary):
|
||||
return
|
||||
if snap.category == "buff":
|
||||
var ok: bool = _b3.apply_effect(snap.effect_id, {"source_class": source_class, "origin_cell": cell, "round_index": ri})
|
||||
if ok:
|
||||
var row: Dictionary = db.status_effect_row(snap.effect_id)
|
||||
var pool := _pool_effective(snap.effect_id)
|
||||
_emit_row("effect", source_class, cell, ri, String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": pool})
|
||||
else:
|
||||
_emit_row("reward_ineffective", source_class, cell, ri, "奖励未生效(满层)", 0, {"batch_key": String(snap.effect_id)})
|
||||
elif _resolve_instant(snap, source_class, cell, ri):
|
||||
pass # 即时类:授予段当场兑付(不进库存、零决策——2026-09-11 两类奖励裁决)
|
||||
else:
|
||||
_b4.stock(snap, cell, ri)
|
||||
_emit_row("item", source_class, cell, ri, "%s 入库" % snap.display_name, int(snap.power), {})
|
||||
|
||||
|
||||
# 即时类当场兑付(2026-09-11 两类奖励裁决):instant_aoe = 全体存活敌各 power 点;
|
||||
# instant_heal = 主角 +power(clamp,满血 → "奖励未生效"行)。返回 false = 非即时类(道具类走库存)
|
||||
func _resolve_instant(snap: Dictionary, source_class: String, cell: Vector2i, ri: int, suffix: String = "", highlight: String = "") -> bool:
|
||||
if snap.category == "instant_aoe":
|
||||
var alive: Array = _b2.query_alive("enemy")
|
||||
if alive.is_empty():
|
||||
_emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(无存活敌人)%s" % [snap.display_name, suffix], 0, {"batch_key": "instant_aoe"}, highlight)
|
||||
return true
|
||||
for u in alive:
|
||||
_b2.apply_damage(u, int(snap.power)) # 恒定表值,逐个;受击即死——死亡即不动手
|
||||
_emit_row("damage", source_class, cell, ri, "%s → 全体敌人 ×%d%s" % [snap.display_name, alive.size(), suffix], int(snap.power), {"target": "all_enemies", "count": alive.size()}, highlight)
|
||||
return true
|
||||
if snap.category == "instant_heal":
|
||||
if int(_b2.hero_unit().hp) >= int(_b2.hero_unit().max_hp):
|
||||
_emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(满血)%s" % [snap.display_name, suffix], 0, {"batch_key": "instant_heal"}, highlight)
|
||||
return true
|
||||
var healed: int = _b2.heal(_b2.hero_unit(), int(snap.power)) # clamp 归 B1
|
||||
_emit_row("heal", source_class, cell, ri, "%s 回复%s" % [snap.display_name, suffix], healed, {"target": "hero"}, highlight)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _judge_flag(evt: Dictionary, ri: int) -> void:
|
||||
var cell: Vector2i = evt.cell
|
||||
if evt.hit:
|
||||
if _flag_consumed.has(cell):
|
||||
return # 去重跳过(资格以雷格实例为键——A2 诚实重复上报的对侧);零明细行
|
||||
_flag_consumed[cell] = true
|
||||
_perfect_hit_true += 1
|
||||
_charge_dmg_b += int(db.settlement.damage_b_per_flag) # 伤害B 充能:与 weaken 消费同键(每雷实例一次——忘拔旗不重复充能)
|
||||
var ok: bool = _b3.apply_effect("weaken", {"source_class": "flag_hit", "origin_cell": cell, "round_index": ri})
|
||||
if ok:
|
||||
var row: Dictionary = db.status_effect_row("weaken")
|
||||
_emit_row("effect", "flag_hit", cell, ri, String(row.display_name), int(row.magnitude), {"effect_id": "weaken", "pool_value": _pool_effective("weaken")}, "flag_hit")
|
||||
else:
|
||||
_emit_row("reward_ineffective", "flag_hit", cell, ri, "奖励未生效(满层)", 0, {"batch_key": "weaken"})
|
||||
else:
|
||||
_perfect_hit_false += 1
|
||||
if _flag_miss_seen.has(cell):
|
||||
return # 每雷格实例一次去重(防逐轮惩罚轰炸)
|
||||
_flag_miss_seen[cell] = true
|
||||
var hl := ""
|
||||
if _perfect_hit_false == 1:
|
||||
hl = "perfect_broken" # 本板完美谓词已破的归因锚
|
||||
_emit_row("flag_miss", "flag_miss", cell, ri, "标旗未中", 0, {}, hl)
|
||||
_apply_miss_backlash(cell, ri)
|
||||
|
||||
|
||||
# 标错惩罚(2026-09-12 用户拍板:随机给敌方一个增伤/buff,或我方 debuff;时效 1 结算——用户拍板"短点,比如 1 回合")。
|
||||
# 随机载体 = 坐标×板序号确定性派生(零新流、同种子可回放;正式侧候选 = RngService 新增封闭流——待拍板,
|
||||
# 见 design/research/lore-nest-metaphor-2026-09-12.md ③ 边界提醒)。三池项:enrage(敌增伤)/ ambush×2(敌 buff)/ off_guard(我方 debuff)。
|
||||
func _apply_miss_backlash(cell: Vector2i, ri: int) -> void:
|
||||
var kind: int = (absi(cell.x) * 7 + absi(cell.y) * 13 + _board_seq * 5) % 3
|
||||
var src: Dictionary = {"source_class": "flag_miss", "origin_cell": cell, "round_index": ri}
|
||||
match kind:
|
||||
0:
|
||||
if _b3.apply_effect("enrage", src):
|
||||
var row: Dictionary = db.status_effect_row("enrage")
|
||||
_emit_row("effect", "flag_miss", cell, ri, "反噬·%s(敌方)" % String(row.display_name), int(row.magnitude), {"effect_id": "enrage", "pool_value": _pool_effective("enrage")})
|
||||
else:
|
||||
_emit_row("reward_ineffective", "flag_miss", cell, ri, "反噬未生效(满层)", 0, {"batch_key": "enrage"})
|
||||
1:
|
||||
var granted := 0
|
||||
for i in range(2):
|
||||
if _b3.apply_effect("ambush", src):
|
||||
granted += 1
|
||||
if granted > 0:
|
||||
_emit_row("effect", "flag_miss", cell, ri, "反噬·敌方伏击 +%d 层" % granted, granted, {"effect_id": "ambush", "pool_value": _b3.pool_layers("ambush") * _m_amb})
|
||||
else:
|
||||
_emit_row("reward_ineffective", "flag_miss", cell, ri, "反噬未生效(满层)", 0, {"batch_key": "ambush"})
|
||||
2:
|
||||
if _b3.apply_effect("off_guard", src):
|
||||
var row2: Dictionary = db.status_effect_row("off_guard")
|
||||
_emit_row("effect", "flag_miss", cell, ri, "反噬·%s(我方)" % String(row2.display_name), int(row2.magnitude), {"effect_id": "off_guard", "pool_value": _pool_effective("off_guard")})
|
||||
else:
|
||||
_emit_row("reward_ineffective", "flag_miss", cell, ri, "反噬未生效(满层)", 0, {"batch_key": "off_guard"})
|
||||
|
||||
|
||||
func _convert_ambush(mine_cells: Array, ri: int) -> void:
|
||||
# B2 义务②⑥:预截流计划面 → 前 granted 颗逐颗转化(事件规范序);每颗雷恰一次
|
||||
var plan: Dictionary = _b2.ambush_grant_plan(mine_cells.size(), _enemy_cap)
|
||||
for i in range(plan.granted):
|
||||
var cell: Vector2i = mine_cells[i]
|
||||
var ok: bool = _b3.apply_effect("ambush", {"source_class": "mine_hit", "origin_cell": cell, "round_index": ri})
|
||||
if ok:
|
||||
var layers := _b3.pool_layers("ambush")
|
||||
_emit_row("effect", "mine_hit", cell, ri, "伏击", _m_amb, {"effect_id": "ambush", "pool_value": layers * _m_amb})
|
||||
for i in range(plan.granted, plan.granted + plan.overflow):
|
||||
var cell: Vector2i = mine_cells[i]
|
||||
_emit_row("overflow", "mine_hit", cell, ri, "伏击溢出未生效", 0, {"batch_key": "ambush_overflow"})
|
||||
|
||||
|
||||
func _grant_clear_reward(ri: int) -> void:
|
||||
var snap = _b4.grant_clear()
|
||||
if not (snap is Dictionary):
|
||||
return
|
||||
var perfect: bool = _perfect_hit_false == 0 and _perfect_hit_true >= 1
|
||||
# 首份兑付
|
||||
var first_ok := _grant_clear_identity(snap, ri)
|
||||
# 完美排雷:真实双份交付(第二份走既有授予通道;grant_clear 本身仍恰一次)
|
||||
var second_ok := false
|
||||
if perfect and first_ok:
|
||||
if snap.category == "buff":
|
||||
second_ok = _b3.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri})
|
||||
if second_ok:
|
||||
var row: Dictionary = db.status_effect_row(snap.effect_id)
|
||||
_emit_row("effect", "clear_reward", Vector2i(-1, -1), ri, "%s(完美第二份)" % String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": _pool_effective(snap.effect_id)})
|
||||
else:
|
||||
var s2 = _b4.stock(snap, Vector2i(-1, -1), ri)
|
||||
second_ok = s2 is Dictionary
|
||||
if second_ok:
|
||||
_emit_row("item", "clear_reward", Vector2i(-1, -1), ri, "%s 入库(完美第二份)" % snap.display_name, int(snap.power), {})
|
||||
if perfect and first_ok:
|
||||
# 高光行 value = 实际有效兑付份数(禁记名义值;首份满层零兑付 → 降级不发)
|
||||
_emit_row("highlight", "clear_reward", Vector2i(-1, -1), ri, "完美排雷!清图奖励 ×2", (2 if second_ok else 1), {}, "perfect_sweep")
|
||||
|
||||
|
||||
func _grant_clear_identity(snap: Dictionary, ri: int) -> bool:
|
||||
if snap.category == "buff":
|
||||
var ok: bool = _b3.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri})
|
||||
if ok:
|
||||
var row: Dictionary = db.status_effect_row(snap.effect_id)
|
||||
_emit_row("effect", "clear_reward", Vector2i(-1, -1), ri, String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": _pool_effective(snap.effect_id)}, "clear_reward_h2")
|
||||
return true
|
||||
_emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "清图奖励未生效(满层)", 0, {"batch_key": snap.effect_id})
|
||||
return false
|
||||
if _resolve_instant(snap, "clear_reward", Vector2i(-1, -1), ri, "(清图奖励)", "clear_reward_h2"):
|
||||
return true
|
||||
var s = _b4.stock(snap, Vector2i(-1, -1), ri)
|
||||
if not (s is Dictionary):
|
||||
return false
|
||||
_emit_row("item", "clear_reward", Vector2i(-1, -1), ri, "%s 入库(清图奖励)" % snap.display_name, int(snap.power), {}, "clear_reward_h2")
|
||||
return true
|
||||
|
||||
|
||||
# ---- 窗口 / 道具段 ----
|
||||
|
||||
func _apply_selected_item(window_index: int, ri: int) -> void:
|
||||
if window_index < 0 or window_index >= _window_items.size():
|
||||
return
|
||||
var item: Dictionary = _window_items[window_index]
|
||||
if item.category == "heal_pack":
|
||||
# 逐件动态守卫:前件推满后本件拦截(不烧包——B1 不调 use)
|
||||
if int(_b2.hero_unit().hp) >= int(_b2.hero_unit().max_hp):
|
||||
_emit_row("use_rejected", "item", item.source_cell, ri, "%s 被拒(满血)" % item.display_name, 0, {})
|
||||
return
|
||||
var receipt = _b4.use(item.index)
|
||||
if not (receipt is Dictionary):
|
||||
return
|
||||
var healed: int = _b2.heal(_b2.hero_unit(), receipt.power) # clamp 归 B1
|
||||
_emit_row("heal", "item", receipt.source_cell, ri, "%s 回复" % item.display_name, healed, {"target": "hero"})
|
||||
else:
|
||||
# 伤害道具:恒定表值不吃修正;零效果判定读 B2 存活集(照常消耗——E10)
|
||||
if _b2.query_alive("enemy").is_empty():
|
||||
var receipt0 = _b4.use(item.index)
|
||||
if not (receipt0 is Dictionary):
|
||||
return
|
||||
_emit_row("consumed_ineffective", "item", receipt0.source_cell, ri, "已消耗未生效(无存活敌人)", 0, {})
|
||||
return
|
||||
var receipt = _b4.use(item.index)
|
||||
if not (receipt is Dictionary):
|
||||
return
|
||||
var target = _b2.default_target()
|
||||
var killed_before: bool = not target.alive
|
||||
_b2.apply_damage(target, receipt.power)
|
||||
var killed: bool = not killed_before and not target.alive
|
||||
_emit_row("damage", "item", receipt.source_cell, ri, "%s 命中" % item.display_name, receipt.power, {"target": target.display_name, "killed": killed})
|
||||
|
||||
|
||||
# ---- 战斗段 ----
|
||||
|
||||
func _combat_segment(ri: int) -> void:
|
||||
var L := _b3.pool_layers("ambush") # 授予段后的当轮值;池共享不消耗
|
||||
# ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动)。
|
||||
# 三段伤害构成(2026-09-12 用户拍板):基础(atk×(1+attack_up),不吃克制)+ 伤害A(开格充能×克制)+ 伤害B(标雷充能×克制),
|
||||
# 按目标护甲类型分别乘克制系数后 floor 相加;attack_up 仍只乘基础攻击(刀/AOE 恒定表值不吃修正不变)。
|
||||
var hero_target = _b2.default_target()
|
||||
if hero_target != null:
|
||||
var armor: String = String(hero_target.get("armor_type", "none"))
|
||||
var up := clampi(_pool_effective("attack_up"), int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_hi))
|
||||
var base := maxi(0, int(floor(float(_hero_atk) * (100.0 + float(up)) / 100.0)))
|
||||
var comp_a := _charge_damage_component("dmg_a", _charge_dmg_a, armor)
|
||||
var comp_b := _charge_damage_component("dmg_b", _charge_dmg_b, armor)
|
||||
var d_hero: int = base + comp_a + comp_b
|
||||
if d_hero > 0:
|
||||
_b2.apply_damage(hero_target, d_hero)
|
||||
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, "主角攻击", d_hero, {"target": hero_target.display_name, "killed": not hero_target.alive, "base": base, "dmg_a": comp_a, "dmg_b": comp_b})
|
||||
for u in _b2.query_alive("enemy"):
|
||||
if int(_b2.hero_unit().hp) <= 0:
|
||||
break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板)
|
||||
var beat: String = _b2.query_beat(u)
|
||||
var atk: Dictionary = _b2.query_attack(u, L, _m_amb) # 调用序①兑付读值
|
||||
if beat == "charge":
|
||||
_emit_row("charge", "enemy_action", Vector2i(-1, -1), ri, "%s 蓄力(本轮踩雷白喂)" % u.display_name, 0, {"target": "hero"})
|
||||
else:
|
||||
var d_tick := _resolve_damage(int(atk.total))
|
||||
if d_tick <= 0:
|
||||
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, "%s 攻击(被减伤归零)" % u.display_name, 0, {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach)})
|
||||
else:
|
||||
_b2.apply_damage(_b2.hero_unit(), d_tick) # 调用序②受击上报
|
||||
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, "%s 攻击" % u.display_name, d_tick, {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach)})
|
||||
_b2.advance(u) # 调用序③推进——每结算每存活敌恰一次(hero 不在推进域)
|
||||
|
||||
|
||||
# F2 乘区:池内加算 → clamp [-100,+100] → 池间乘算 → floor(单一取整规则)——算术归 DamageMath(O3)
|
||||
func _resolve_damage(d_raw: int) -> int:
|
||||
var p_hero := _clamped_pool("hero_defense")
|
||||
var p_enemy := _clamped_pool("enemy_output")
|
||||
return DamageMath.pool_damage(d_raw, p_hero, p_enemy)
|
||||
|
||||
|
||||
# 伤害A/B 充能分量:充能 × 每单位值 × 克制 percent → floor(伤害A 先套单轮充能上限——防连锁轮失控)
|
||||
func _charge_damage_component(component: String, count: int, armor: String) -> int:
|
||||
var per: int = int(db.settlement.damage_a_per_cell) if component == "dmg_a" else int(db.settlement.damage_b_per_flag)
|
||||
var charge := count
|
||||
if component == "dmg_a" and int(db.settlement.damage_a_cap) > 0:
|
||||
charge = mini(charge, int(db.settlement.damage_a_cap))
|
||||
return DamageMath.charge_component(charge, per, _armor_effectiveness(component, armor))
|
||||
|
||||
|
||||
# 克制矩阵读值(SettlementTable.armor_effectiveness:{分量: {护甲: percent}}——缺项回退 100 中性)
|
||||
func _armor_effectiveness(component: String, armor: String) -> int:
|
||||
var tbl: Dictionary = db.settlement.armor_effectiveness
|
||||
var row: Dictionary = tbl.get(component, {})
|
||||
return int(row.get(armor, 100))
|
||||
|
||||
|
||||
func _clamped_pool(pool: String) -> int:
|
||||
var total := 0
|
||||
for eff in _b3.query_active("hero") + _b3.query_active("enemy"):
|
||||
var row: Dictionary = db.status_effect_row(eff.effect_id)
|
||||
if row.is_empty():
|
||||
continue
|
||||
if String(db.settlement.effect_pool_map.get(eff.effect_id, "")) == pool and String(row.unit) == "percent":
|
||||
total += eff.effective_value # 池内加算(L×m 原样——越域不 clamp 由本处收口)
|
||||
return clampi(total, int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_hi))
|
||||
|
||||
|
||||
func _pool_effective(effect_id: String) -> int:
|
||||
var side := "hero"
|
||||
var row: Dictionary = db.status_effect_row(effect_id)
|
||||
if not row.is_empty():
|
||||
side = String(row.target_side)
|
||||
for eff in _b3.query_active(side):
|
||||
if eff.effect_id == effect_id:
|
||||
return eff.effective_value
|
||||
return 0
|
||||
|
||||
|
||||
func _emit_composition_effects(side: String, ri: int) -> void:
|
||||
for eff in _b3.query_active(side):
|
||||
if eff.round_index < ri:
|
||||
# 续存实例(前轮授予)——每结算兑付一次的可见面(合并实例只持首授归因——B3 义务⑦)
|
||||
_emit_row("effect", eff.source_class, eff.origin_cell, ri, eff.display_name, eff.snap_magnitude, {"effect_id": eff.effect_id, "pool_value": eff.effective_value})
|
||||
|
||||
|
||||
# ---- 结算快照(playtest 数值分析数据面——每次结算全量状态;运行时零决策读取)----
|
||||
|
||||
func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
var enemies: Array = []
|
||||
for u in _b2.query_alive("enemy"):
|
||||
enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "beat": query_beat_of(u), "armor": String(u.get("armor_type", "none"))})
|
||||
var buffs: Array = []
|
||||
for eff in _b3.query_active("hero"):
|
||||
buffs.append({"id": eff.effect_id, "side": "hero", "layers": eff.layers, "left": eff.remaining_settlements})
|
||||
for eff in _b3.query_active("enemy"):
|
||||
buffs.append({"id": eff.effect_id, "side": "enemy", "layers": eff.layers, "left": eff.remaining_settlements})
|
||||
var mines_hit := 0
|
||||
var safe_n := 0
|
||||
for evt in _facts_reveals:
|
||||
if evt.kind == "mine":
|
||||
mines_hit += 1
|
||||
elif evt.kind == "safe":
|
||||
safe_n += 1
|
||||
var board_stats := {"seq": _board_seq}
|
||||
if a2.current_board() != null:
|
||||
board_stats["w"] = a2.current_board().width
|
||||
board_stats["h"] = a2.current_board().height
|
||||
board_stats["revealed"] = a2.revealed_cells().size()
|
||||
board_stats["flags"] = a2.flag_count()
|
||||
settlement_snapshot.emit({
|
||||
"kind": "settlement",
|
||||
"level": int(encounter.get("level_index", 0)),
|
||||
"depth": int(encounter.get("node_depth", 0)),
|
||||
"round": round_index,
|
||||
"verdict": verdict,
|
||||
"unit_count": enemies.size(),
|
||||
"spawn_count": _b2.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」(playtest-2026-09-12 报告 Bug#1 误读教训)
|
||||
"hero": {"hp": int(_b2.hero_unit().get("hp", 0)), "max_hp": int(_b2.hero_unit().get("max_hp", 0)), "atk": int(_b2.hero_unit().get("atk", 0))},
|
||||
"economy": {"phase": String(a3.phase), "ap": int(a3.ap), "rap": int(a3.rap), "refresh_used": _refresh_used_this_round},
|
||||
"board": board_stats,
|
||||
"enemies": enemies,
|
||||
"buffs": buffs,
|
||||
"ambush_layers": _b3.pool_layers("ambush"),
|
||||
"inventory_in_stock": _b4.in_stock_count(),
|
||||
"round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "flag_events": _facts_flags.size(), "charge_dmg_a": _charge_dmg_a, "charge_dmg_b": _charge_dmg_b},
|
||||
})
|
||||
|
||||
|
||||
func query_beat_of(u: Dictionary) -> String:
|
||||
return _b2.query_beat(u)
|
||||
|
||||
|
||||
# ---- 出口分发与战斗结束序列 ----
|
||||
|
||||
func _exit_distribution() -> void:
|
||||
var mode := "same_board"
|
||||
if _facts_path == "cleared":
|
||||
mode = "new_board" # cleared 轮恒走 new_board(契约义务)
|
||||
elif _refresh_used_this_waiting:
|
||||
mode = "new_board" # stopped/ap_exhausted 轮:仅 Waiting 期玩家刷新意图达成时换图(违约 fail-fast 的对侧)
|
||||
if mode == "new_board":
|
||||
_b4.retire_mapping()
|
||||
a2.retire_board()
|
||||
if not _install_new_board():
|
||||
_fatal("换图编排失败")
|
||||
return
|
||||
a3.notify_settlement_done(mode)
|
||||
a3.notify_round_start()
|
||||
_reset_round_facts()
|
||||
_refresh_used_this_waiting = false
|
||||
_refresh_used_this_round = 0
|
||||
round_index += 1
|
||||
round_started.emit(round_index)
|
||||
|
||||
|
||||
# 呈现层查询面(HUD 刷新按钮可用性)
|
||||
func can_request_refresh() -> bool:
|
||||
return battle_result == "" \
|
||||
and _refresh_used_this_round < int(db.action_economy.refresh_max_per_round) \
|
||||
and (a3.phase == a3.PHASE_ACTIVE or a3.phase == a3.PHASE_WAITING) \
|
||||
and (a3.phase != a3.PHASE_WAITING or not _refresh_used_this_waiting) \
|
||||
and a3.rap >= int(db.action_economy.rap_refresh_cost)
|
||||
|
||||
|
||||
# 玩家经 D1 发起、B1 代理调用(禁止 RAP 满自动刷)。
|
||||
# 轮内刷新(2026-09-12 方案 A 用户拍板):Active 相位立即换图继续挖(RAP -8;AP 保留、相位不动、
|
||||
# 不触发结算);Waiting 相位保持现状(登记意图,exit_distribution 换图)。每轮上限 = 表值(两相位合计)。
|
||||
func player_request_refresh() -> bool:
|
||||
if not can_request_refresh():
|
||||
return false
|
||||
if a3.phase == a3.PHASE_ACTIVE:
|
||||
if not a3.request_refresh_active():
|
||||
return false
|
||||
_refresh_used_this_round += 1
|
||||
_b4.retire_mapping() # 旧图批次退役(B4 Ready→retired;E6 新批重置前提)
|
||||
a2.retire_board(true) # 旧板退役(轮内通道)
|
||||
return _install_new_board(true) # 当场换图——失败即编排违例 fail-fast
|
||||
if not a3.request_refresh():
|
||||
return false
|
||||
_refresh_used_this_round += 1
|
||||
_refresh_used_this_waiting = true
|
||||
return true
|
||||
|
||||
|
||||
func _battle_over(result: String) -> void:
|
||||
battle_result = result
|
||||
# 库存关卡域(2026-09-11 关内延续):battle_over 不再清库存——跨战斗延续,关卡末经 level_clear() 清空;
|
||||
# 映射随战斗结束退役(下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提)
|
||||
var hero_hp_left := int(_b2.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层")
|
||||
_b4.retire_mapping()
|
||||
_b2.despawn_all()
|
||||
_b3.notify_battle_end()
|
||||
a3.notify_combat_end() # → Idle,RAP 清零
|
||||
db.set_reload_gate(false)
|
||||
_reset_round_facts()
|
||||
battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "remaining_items": _b4.in_stock_count()})
|
||||
|
||||
|
||||
# ---- 呈现状态面(E1 封装收口 2026-09-13):呈现层 / headless 自动玩家的唯一读取出口——
|
||||
# 协作对象(_b2/_b3/_b4)私有化后,跨层读取一律经本快照或下列命名面(diagnostic_snapshot 同款模式,
|
||||
# 禁再链式穿透 b1.b2.hero 一类路径)。载荷 schema 成文(code-conventions 2.1):
|
||||
# round / battle_result / phase / ap / rap / can_request_refresh / hero{hp,max_hp,atk,alive} /
|
||||
# enemies[{order,display_name,hp,max_hp,enemy_id,armor_type,beat,ring_size,ring_index,ambush_layers,alive}] /
|
||||
# ambush_layers / hero_effects[](B3 query_active 投影) / inventory[](B4 query_inventory 投影) / in_stock
|
||||
func presentation_snapshot() -> Dictionary:
|
||||
var enemies: Array = []
|
||||
var all: Array = _b2.all_enemies()
|
||||
var ambush: int = _b3.pool_layers("ambush")
|
||||
for i in all.size():
|
||||
var u: Dictionary = all[i]
|
||||
var alive: bool = bool(u.get("alive", false))
|
||||
var ring: Array = u.get("pattern", [])
|
||||
enemies.append({
|
||||
"order": i + 1,
|
||||
"display_name": String(u.display_name),
|
||||
"hp": int(u.hp), "max_hp": int(u.max_hp),
|
||||
"enemy_id": String(u.enemy_id),
|
||||
"armor_type": String(u.get("armor_type", "none")),
|
||||
"beat": String(_b2.query_beat(u)) if alive else "",
|
||||
"ring_size": ring.size(),
|
||||
"ring_index": int(u.get("steps", 0)) % maxi(ring.size(), 1),
|
||||
"ambush_layers": ambush,
|
||||
"alive": alive,
|
||||
})
|
||||
var hero := {}
|
||||
var h: Dictionary = _b2.hero_unit()
|
||||
if not h.is_empty():
|
||||
hero = {"hp": int(h.hp), "max_hp": int(h.max_hp), "atk": int(h.atk), "alive": bool(h.get("alive", false))}
|
||||
return {
|
||||
"round": round_index,
|
||||
"battle_result": battle_result,
|
||||
"phase": String(a3.phase),
|
||||
"ap": int(a3.ap), "rap": int(a3.rap),
|
||||
"can_request_refresh": can_request_refresh(),
|
||||
"hero": hero,
|
||||
"enemies": enemies,
|
||||
"ambush_layers": ambush,
|
||||
"hero_effects": _b3.query_active("hero"),
|
||||
"inventory": _b4.query_inventory(),
|
||||
"in_stock": _b4.in_stock_count(),
|
||||
}
|
||||
|
||||
|
||||
# 战斗进行中谓词(hero 在场且存活)——组合根高频相位判断的低成本面
|
||||
func is_battle_live() -> bool:
|
||||
return battle_result == "" and not _b2.hero_unit().is_empty()
|
||||
|
||||
|
||||
# 库存余量(层间过渡文案 / 终局清库反馈)
|
||||
func stock_count() -> int:
|
||||
return _b4.in_stock_count()
|
||||
|
||||
|
||||
# 奖励身份解析(组合根注入 BoardView 徽章回调的只读代理——Ready 相位守卫归 B4)
|
||||
func query_reward_identity(cell: Vector2i):
|
||||
return _b4.query_identity(cell)
|
||||
|
||||
|
||||
# 诊断观测面(组合根诊断转储 / 行为探针消费):私有编排状态的成文只读出口——
|
||||
# 消费方禁止再直读本类下划线字段(code-review 2026-09-12 封装收口批)
|
||||
func diagnostic_snapshot() -> Dictionary: return {
|
||||
"battle_result": battle_result,
|
||||
"round": round_index,
|
||||
"settling": _settling,
|
||||
"awaiting_window": _awaiting_window,
|
||||
"refresh_used_round": _refresh_used_this_round,
|
||||
"refresh_used_waiting": _refresh_used_this_waiting,
|
||||
"board_seq": _board_seq,
|
||||
"facts_path": _facts_path,
|
||||
"facts_reveals": _facts_reveals.size(),
|
||||
"facts_flags": _facts_flags.size(),
|
||||
"a3": {"phase": String(a3.phase), "ap": a3.ap, "rap": a3.rap},
|
||||
"a2_board_installed": a2.current_board() != null,
|
||||
"a2_revealed": a2.revealed_cells().size() if a2.current_board() != null else 0,
|
||||
"a2_flags": a2.flag_count() if a2.current_board() != null else 0,
|
||||
"b4_phase_stock": [_b4.in_stock_count()],
|
||||
"enemies_alive": _b2.query_alive("enemy").size(),
|
||||
"hero_hp": int(_b2.hero_unit().get("hp", -1)) if not _b2.hero_unit().is_empty() else -1,
|
||||
}
|
||||
|
||||
|
||||
# 关卡结束编排(C1/C2 关卡结构落位前由上层显式调用——2026-09-11 关内延续):
|
||||
# 清空库存并返回快照计数("剩余道具 ×N 随关卡结束清空"反馈行 / 折算结构位的数据面)
|
||||
func level_clear() -> Dictionary:
|
||||
var remaining := _b4.in_stock_count()
|
||||
_b4.clear_inventory()
|
||||
return {"remaining": remaining}
|
||||
|
||||
|
||||
func _reset_round_facts() -> void:
|
||||
_facts_reveals.clear()
|
||||
_facts_flags.clear()
|
||||
_facts_path = ""
|
||||
_charge_dmg_a = 0 # 三段伤害充能随轮 facts 重置(每轮重新充能——结算段消费当轮值)
|
||||
_charge_dmg_b = 0
|
||||
|
||||
|
||||
# ---- 行协议(Core Rule 14:公共字段 + 行类型特有字段)----
|
||||
|
||||
func _emit_row(type: String, source_class: String, origin_cell: Vector2i, ri: int, display: String, value: int, extra: Dictionary, highlight: String = "") -> void:
|
||||
var row := {
|
||||
"type": type,
|
||||
"source_class": source_class,
|
||||
"origin_cell": [origin_cell.x, origin_cell.y],
|
||||
"round_index": ri,
|
||||
"display_name": display,
|
||||
"value": value,
|
||||
}
|
||||
if highlight != "":
|
||||
row["highlight"] = highlight
|
||||
for k in extra.keys():
|
||||
row[k] = extra[k]
|
||||
step_row.emit(row)
|
||||
|
||||
|
||||
func _fatal(msg: String) -> void:
|
||||
push_error("[B1/fatal] " + msg)
|
||||
assert(false, msg)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ctw48bkgsuh7m
|
||||
@@ -0,0 +1,122 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# A1 棋盘产物数据契约(A2 消费;生成后不可变——A2 持快照即持活引用,生成器不再触碰)。
|
||||
# 实现 GDD:design/gdd/a1-board-generation.md(系统 ID A1——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name BoardData
|
||||
extends RefCounted
|
||||
|
||||
var width: int = 0
|
||||
var height: int = 0
|
||||
var spawn_center := Vector2i.ZERO
|
||||
var mines := {} # Vector2i → true
|
||||
var digits := {} # Vector2i → int(安全格恒有值;数字恒真 = 8 邻雷数)
|
||||
var variants := {} # Vector2i → Array[String](生成期快照,抗表重载)
|
||||
var biome := ""
|
||||
var level_index: int = 1
|
||||
var node_depth: int = 0
|
||||
var metrics := {} # 八项生成期指标
|
||||
var illegal_product := false
|
||||
|
||||
|
||||
func in_bounds(c: Vector2i) -> bool:
|
||||
return c.x >= 0 and c.y >= 0 and c.x < width and c.y < height
|
||||
|
||||
|
||||
func index_of(c: Vector2i) -> int:
|
||||
return c.y * width + c.x
|
||||
|
||||
|
||||
func is_mine(c: Vector2i) -> bool:
|
||||
return mines.has(c)
|
||||
|
||||
|
||||
func is_safe(c: Vector2i) -> bool:
|
||||
return in_bounds(c) and not mines.has(c)
|
||||
|
||||
|
||||
func digit_of(c: Vector2i) -> int:
|
||||
return digits.get(c, -1)
|
||||
|
||||
|
||||
func variant_ids(c: Vector2i) -> Array:
|
||||
return variants.get(c, [])
|
||||
|
||||
|
||||
func has_variant(c: Vector2i, vid: String) -> bool:
|
||||
return variant_ids(c).has(vid)
|
||||
|
||||
|
||||
func neighbors8(c: Vector2i) -> Array:
|
||||
var out: Array = []
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
if dx == 0 and dy == 0:
|
||||
continue
|
||||
var n := Vector2i(c.x + dx, c.y + dy)
|
||||
if in_bounds(n):
|
||||
out.append(n)
|
||||
return out # 行主序(规范序的邻接枚举基序)
|
||||
|
||||
|
||||
func spawn_cells() -> Array:
|
||||
var out: Array = []
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
var c := Vector2i(spawn_center.x + dx, spawn_center.y + dy)
|
||||
if in_bounds(c):
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
|
||||
func all_cells() -> Array:
|
||||
var out: Array = []
|
||||
for y in height:
|
||||
for x in width:
|
||||
out.append(Vector2i(x, y))
|
||||
return out
|
||||
|
||||
|
||||
func safe_cells() -> Array:
|
||||
var out: Array = []
|
||||
for c in all_cells():
|
||||
if is_safe(c):
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
|
||||
func fog_cells() -> Array:
|
||||
var out: Array = []
|
||||
for c in all_cells():
|
||||
if not variants.get(c, []).is_empty():
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
|
||||
# R₀ = 出生 3×3 ∪ (出生 3×3 经 0 格的 flood 闭包)——与 A2 F3 逐字同构(A1 F1 求解器初始态)。
|
||||
func compute_r0() -> Array:
|
||||
var b3 := spawn_cells()
|
||||
var r0 := {}
|
||||
for c in b3:
|
||||
r0[c] = true
|
||||
var queue: Array = b3.duplicate()
|
||||
while not queue.is_empty():
|
||||
var c: Vector2i = queue.pop_front()
|
||||
if is_safe(c) and digit_of(c) == 0 and variants.get(c, []).is_empty():
|
||||
for n in neighbors8(c):
|
||||
if is_safe(n) and not r0.has(n):
|
||||
r0[n] = true
|
||||
queue.append(n)
|
||||
return r0.keys()
|
||||
|
||||
|
||||
# A1 Rule 4 排除域(以最终产物板计):R₀ ∪ 外扩 1 环——生成期确定性计算。
|
||||
func exclusion_domain() -> Dictionary:
|
||||
var r0 := compute_r0()
|
||||
var dom := {}
|
||||
for c in r0:
|
||||
dom[c] = true
|
||||
for c in r0:
|
||||
for n in neighbors8(c):
|
||||
dom[n] = true
|
||||
return dom
|
||||
@@ -0,0 +1 @@
|
||||
uid://df34rfpxbxj03
|
||||
@@ -0,0 +1,541 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# A1 确定性棋盘生成器:五阶段管线(参数→布雷→变异→验证循环→产出)+ F1 可推理率度量
|
||||
# (single_subset 求解器)+ F3 雾预算 + F5 验收算子(达标即出/接受最优/接受最不坏)。
|
||||
# 切片裁剪登记:黄金向量回归套件与 F4 批量工具不做(C3 调参期工具域)。
|
||||
# 实现 GDD:design/gdd/a1-board-generation.md(系统 ID A1——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name BoardGenerator
|
||||
extends RefCounted
|
||||
|
||||
const METRIC_VERSION := 1
|
||||
|
||||
|
||||
# ==================== 生成入口 ====================
|
||||
|
||||
# rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)
|
||||
func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData:
|
||||
var row: Dictionary = db.resolve_board_gen(L, D)
|
||||
if row.is_empty():
|
||||
_fatal("BoardGen 行缺失 (%d,%d)" % [L, D])
|
||||
return null
|
||||
# 生成期表值单次读取快照(防热重载撕裂 attempt 语义)
|
||||
var params := {
|
||||
"L": L, "D": D,
|
||||
"w": int(row.board_width), "h": int(row.board_height),
|
||||
"density": float(row.mine_density),
|
||||
"target": float(row.inferability_target),
|
||||
"solver_rules": String(row.solver_rules),
|
||||
"min_safe": int(row.min_safe_count),
|
||||
"max_attempts": int(row.max_gen_attempts),
|
||||
"fog_count": int(row.fog_region_count),
|
||||
"fog_ratio": float(row.fog_target_ratio),
|
||||
"biomes": Array(row.board_biomes),
|
||||
}
|
||||
var best_legal: BoardData = null
|
||||
var best_legal_score := -1.0
|
||||
var best_bad: BoardData = null
|
||||
var best_badness := [999999, 999999, 999999]
|
||||
var attempts_used := 0
|
||||
var accepted: BoardData = null
|
||||
var accept_mode := ""
|
||||
for k in range(1, params.max_attempts + 1):
|
||||
attempts_used = k
|
||||
var b := _attempt(params, rng_stream, db)
|
||||
var badness := _badness(b, params)
|
||||
if best_bad == null or _lex_less(badness, best_badness):
|
||||
best_bad = b
|
||||
best_badness = badness
|
||||
if legality(b, params, db):
|
||||
var m: Dictionary = measure_inferability(b) # 求解器每合法板恰一次(O3 收口——此前重复调用两次)
|
||||
var inf: float = m["inferability"]
|
||||
if best_legal == null or inf > best_legal_score:
|
||||
best_legal = b
|
||||
best_legal_score = inf
|
||||
if inf >= params.target and m["inferability_core"] >= params.target:
|
||||
accepted = b
|
||||
accept_mode = "first_pass"
|
||||
break
|
||||
if accepted == null:
|
||||
if best_legal != null:
|
||||
accepted = best_legal
|
||||
accept_mode = "accept_best"
|
||||
push_warning("[A1] 重试耗尽无达标——接受最优出图(强告警埋点,target=%.2f best=%.3f)" % [params.target, best_legal_score])
|
||||
else:
|
||||
accepted = best_bad
|
||||
accept_mode = "accept_worst"
|
||||
accepted.illegal_product = true
|
||||
push_error("[A1] attempts 耗尽零合法——接受最不坏出图 + 非法产物标记(玩家永不卡死)")
|
||||
_finalize(accepted, params, attempts_used, accept_mode, db)
|
||||
return accepted
|
||||
|
||||
|
||||
# ==================== 五阶段:单次尝试 ====================
|
||||
|
||||
func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> BoardData:
|
||||
var b := BoardData.new()
|
||||
b.width = params.w
|
||||
b.height = params.h
|
||||
b.spawn_center = Vector2i(int((params.w - 1) / 2.0), int((params.h - 1) / 2.0))
|
||||
b.biome = str(params.biomes[0])
|
||||
b.level_index = params.L
|
||||
b.node_depth = params.D
|
||||
# ② 布雷:N = round(d×(W×H−9)) 先定数;域 = 全格 − 出生 3×3;均匀布雷后"先布后删"
|
||||
var spawn := {}
|
||||
for c in b.spawn_cells():
|
||||
spawn[c] = true
|
||||
var domain: Array = []
|
||||
for c in b.all_cells():
|
||||
if not spawn.has(c):
|
||||
domain.append(c)
|
||||
var n_mines: int = int(round(params.density * float(params.w * params.h - 9)))
|
||||
# 部分 Fisher-Yates(确定性,消费 board_gen 流)
|
||||
for i in range(0, mini(n_mines, domain.size())):
|
||||
var j := rng_stream.randi_range(i, domain.size() - 1)
|
||||
var tmp = domain[i]
|
||||
domain[i] = domain[j]
|
||||
domain[j] = tmp
|
||||
for i in range(0, mini(n_mines, domain.size())):
|
||||
b.mines[domain[i]] = true
|
||||
_compute_digits(b)
|
||||
# 先布后删:0 格闭包(布后基准)外扩 1 环内的雷移除(只减不增、无迭代)
|
||||
var ring := {}
|
||||
for c in b.compute_r0():
|
||||
for n in b.neighbors8(c):
|
||||
if not ring.has(n):
|
||||
ring[n] = true
|
||||
var removed := 0
|
||||
for c in ring.keys():
|
||||
if b.mines.has(c):
|
||||
b.mines.erase(c)
|
||||
removed += 1
|
||||
_compute_digits(b)
|
||||
# ③ 变异布置(先雷后雾;排除域以最终产物板计)
|
||||
var shortfall := _place_variants(b, params, rng_stream, db)
|
||||
b.metrics = {
|
||||
"metric_version": METRIC_VERSION,
|
||||
"ring_removed": removed,
|
||||
"shortfall": shortfall,
|
||||
}
|
||||
return b
|
||||
|
||||
|
||||
func _compute_digits(b: BoardData) -> void:
|
||||
b.digits.clear()
|
||||
for c in b.all_cells():
|
||||
if b.is_safe(c):
|
||||
var d := 0
|
||||
for n in b.neighbors8(c):
|
||||
if b.is_mine(n):
|
||||
d += 1
|
||||
b.digits[c] = d
|
||||
|
||||
|
||||
# 变异布置:候选池门控 + cell_constraint 生成期强制 + 排除域排除 + 厚度≤2;返回 shortfall
|
||||
func _place_variants(b: BoardData, params: Dictionary, rng_stream: RandomNumberGenerator, db) -> int:
|
||||
if params.fog_count <= 0 or params.fog_ratio <= 0.0:
|
||||
return 0
|
||||
var pool: Array = db.variant_pool(params.L, params.biomes)
|
||||
if pool.is_empty():
|
||||
return 0 # EC1 空池静默——合法非错误
|
||||
var exclusion := b.exclusion_domain()
|
||||
var total_target := int(round(params.fog_ratio * float(params.w * params.h)))
|
||||
var placed_total := 0
|
||||
var achieved := 0
|
||||
var placed := {}
|
||||
for r in range(params.fog_count):
|
||||
var region_target: int = total_target / params.fog_count
|
||||
if r < total_target % params.fog_count:
|
||||
region_target += 1
|
||||
if region_target <= 0:
|
||||
achieved += 1 # 零目标区视为达成(不消耗布置)
|
||||
continue
|
||||
var region := _grow_region(b, region_target, exclusion, placed, rng_stream)
|
||||
if region.is_empty():
|
||||
continue # EC2:无种子 → 该区布置失败(shortfall)
|
||||
for c in region.keys():
|
||||
b.variants[c] = ["fog"]
|
||||
placed[c] = true
|
||||
placed_total += 1
|
||||
if region.size() >= region_target:
|
||||
achieved += 1
|
||||
return params.fog_count - achieved
|
||||
|
||||
|
||||
# 区域生长:随机种子 + 随机边界扩张(连通域);厚度≤2 = 任意 3×3 窗口非全雾
|
||||
func _grow_region(b: BoardData, target: int, exclusion: Dictionary, placed: Dictionary, rng_stream: RandomNumberGenerator) -> Dictionary:
|
||||
var candidates: Array = []
|
||||
for c in b.all_cells():
|
||||
if _valid_fog_cell(b, c, exclusion, placed):
|
||||
candidates.append(c)
|
||||
if candidates.is_empty():
|
||||
return {}
|
||||
var region := {}
|
||||
var seed_c: Vector2i = candidates[rng_stream.randi_range(0, candidates.size() - 1)]
|
||||
region[seed_c] = true
|
||||
while region.size() < target:
|
||||
var frontier: Array = []
|
||||
for c in region.keys():
|
||||
for n in b.neighbors8(c):
|
||||
if not region.has(n) and not placed.has(n) and _valid_fog_cell(b, n, exclusion, placed) and not _breaks_thickness(region, n, b):
|
||||
frontier.append(n)
|
||||
if frontier.is_empty():
|
||||
break # 尽力布置——部分区域合法(shortfall 上报)
|
||||
var pick: Vector2i = frontier[rng_stream.randi_range(0, frontier.size() - 1)]
|
||||
region[pick] = true
|
||||
return region
|
||||
|
||||
|
||||
# cell_constraint=safe_nonzero 生成期强制:非雷、非 0(⟺ 8 邻含雷)、不在排除域
|
||||
func _valid_fog_cell(b: BoardData, c: Vector2i, exclusion: Dictionary, placed: Dictionary) -> bool:
|
||||
if placed.has(c) or exclusion.has(c):
|
||||
return false
|
||||
if not b.is_safe(c):
|
||||
return false
|
||||
if b.digit_of(c) <= 0:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _breaks_thickness(region: Dictionary, candidate: Vector2i, b: BoardData) -> bool:
|
||||
# 加入 candidate 后是否出现全雾 3×3 窗口
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
var origin := Vector2i(candidate.x + dx - 1, candidate.y + dy - 1)
|
||||
var all_fog := true
|
||||
for wy in range(3):
|
||||
for wx in range(3):
|
||||
var c := Vector2i(origin.x + wx, origin.y + wy)
|
||||
var is_fog := (region.has(c) and c != candidate) or c == candidate
|
||||
if not is_fog:
|
||||
all_fog = false
|
||||
if all_fog:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# ==================== F1 可推理率度量(独立入口——与内嵌调用同一实现) ====================
|
||||
|
||||
# 返回 {r0, r_inf, inferability, inferability_core}
|
||||
func measure_inferability(b: BoardData) -> Dictionary:
|
||||
var safe_set := {}
|
||||
for c in b.safe_cells():
|
||||
safe_set[c] = true
|
||||
var fog_set := {}
|
||||
for c in b.fog_cells():
|
||||
fog_set[c] = true
|
||||
var r := {}
|
||||
for c in b.compute_r0():
|
||||
r[c] = true
|
||||
var known_mines := {}
|
||||
var changed := true
|
||||
while changed:
|
||||
changed = false
|
||||
# 算子① 连锁:0 格的 8 邻安全格全入 R(雾格可被链开但不传播——雾恒非 0,天然为叶)
|
||||
var frontier: Array = r.keys()
|
||||
for x in frontier:
|
||||
if safe_set.has(x) and not fog_set.has(x) and b.digit_of(x) == 0:
|
||||
for n in b.neighbors8(x):
|
||||
if safe_set.has(n) and not r.has(n):
|
||||
r[n] = true
|
||||
changed = true
|
||||
# 信息源(b)雾元知识:与 R 邻接的雾格必非雷 → 入 R
|
||||
for f in fog_set.keys():
|
||||
if not r.has(f):
|
||||
for n in b.neighbors8(f):
|
||||
if r.has(n):
|
||||
r[f] = true
|
||||
changed = true
|
||||
break
|
||||
# 信息源(a)数字约束:single + subset(solver_rules=single_subset 冻结域)
|
||||
var constraints := _build_constraints(b, r, known_mines, safe_set)
|
||||
# single 规则
|
||||
for con in constraints:
|
||||
if con.mines == 0:
|
||||
for c in con.unknown:
|
||||
if not r.has(c):
|
||||
r[c] = true
|
||||
changed = true
|
||||
elif con.mines == con.unknown.size():
|
||||
for c in con.unknown:
|
||||
if not known_mines.has(c):
|
||||
known_mines[c] = true
|
||||
changed = true
|
||||
# subset 规则:U1 ⊂ U2 ⇒ (d2−d1, U2∖U1)
|
||||
var derived := []
|
||||
for i in constraints.size():
|
||||
for j in range(i + 1, constraints.size()):
|
||||
var a: Dictionary = constraints[i]
|
||||
var bb: Dictionary = constraints[j]
|
||||
var sub := _subset_of(a.unknown, bb.unknown)
|
||||
if sub == 1:
|
||||
derived.append(_diff_constraint(a, bb))
|
||||
elif sub == 2:
|
||||
derived.append(_diff_constraint(bb, a))
|
||||
for con in derived:
|
||||
if con.mines == 0:
|
||||
for c in con.unknown:
|
||||
if safe_set.has(c) and not r.has(c):
|
||||
r[c] = true
|
||||
changed = true
|
||||
elif con.mines == con.unknown.size():
|
||||
for c in con.unknown:
|
||||
if not known_mines.has(c):
|
||||
known_mines[c] = true
|
||||
changed = true
|
||||
var r_inf := r.keys()
|
||||
var s_count := safe_set.size()
|
||||
var s_nonfog := 0
|
||||
var r_nonfog := 0
|
||||
for c in safe_set.keys():
|
||||
if not fog_set.has(c):
|
||||
s_nonfog += 1
|
||||
if r.has(c):
|
||||
r_nonfog += 1
|
||||
var inferability: float = float(r_inf.size()) / float(maxi(1, s_count))
|
||||
var core: float = float(r_nonfog) / float(maxi(1, s_nonfog))
|
||||
return {
|
||||
"r0": b.compute_r0().size(),
|
||||
"r_inf_count": r_inf.size(),
|
||||
"inferability": inferability,
|
||||
"inferability_core": core,
|
||||
}
|
||||
|
||||
|
||||
func _build_constraints(b: BoardData, r: Dictionary, known_mines: Dictionary, safe_set: Dictionary) -> Array:
|
||||
var out: Array = []
|
||||
for c in r.keys():
|
||||
if not safe_set.has(c) or b.variant_ids(c).size() > 0:
|
||||
continue # 雾格数字不可见(hide_numbers)——零特判数据面:变体格不产约束
|
||||
var d := b.digit_of(c)
|
||||
var unknown := {}
|
||||
var known_here := 0
|
||||
for n in b.neighbors8(c):
|
||||
if known_mines.has(n):
|
||||
known_here += 1
|
||||
elif not r.has(n) and safe_set.has(n):
|
||||
unknown[n] = true
|
||||
var d_eff := d - known_here
|
||||
if d_eff < 0 or (d_eff > 0 and unknown.is_empty()):
|
||||
continue # 矛盾态(非法板)——不产约束,验收由 legality 闸承担
|
||||
if not unknown.is_empty():
|
||||
out.append({"mines": d_eff, "unknown": unknown})
|
||||
return out
|
||||
|
||||
|
||||
func _subset_of(a: Dictionary, b: Dictionary) -> int:
|
||||
# 1: a ⊂ b;2: b ⊂ a;0: 互不包含
|
||||
var a_in_b := true
|
||||
var b_in_a := true
|
||||
for k in a.keys():
|
||||
if not b.has(k):
|
||||
a_in_b = false
|
||||
for k in b.keys():
|
||||
if not a.has(k):
|
||||
b_in_a = false
|
||||
if a.size() < b.size() and a_in_b:
|
||||
return 1
|
||||
if b.size() < a.size() and b_in_a:
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
func _diff_constraint(big: Dictionary, small: Dictionary) -> Dictionary:
|
||||
var diff := {}
|
||||
for k in big.unknown.keys():
|
||||
if not small.unknown.has(k):
|
||||
diff[k] = true
|
||||
return {"mines": big.mines - small.mines, "unknown": diff}
|
||||
|
||||
|
||||
# ==================== F3 雾预算(独立入口) ====================
|
||||
|
||||
func measure_fog_budget(b: BoardData, dig_ap_cost: int) -> Dictionary:
|
||||
var safe_set := {}
|
||||
for c in b.safe_cells():
|
||||
safe_set[c] = true
|
||||
# Chain₀ = R₀ 经算子①单独闭包
|
||||
var chain0 := {}
|
||||
for c in b.compute_r0():
|
||||
chain0[c] = true
|
||||
var queue: Array = chain0.keys()
|
||||
while not queue.is_empty():
|
||||
var c: Vector2i = queue.pop_front()
|
||||
if safe_set.has(c) and b.digit_of(c) == 0 and b.variant_ids(c).is_empty():
|
||||
for n in b.neighbors8(c):
|
||||
if safe_set.has(n) and not chain0.has(n):
|
||||
chain0[n] = true
|
||||
queue.append(n)
|
||||
var u_count := 0
|
||||
for c in b.fog_cells():
|
||||
if not chain0.has(c):
|
||||
u_count += 1
|
||||
var pay := 0
|
||||
for c in safe_set.keys():
|
||||
if not chain0.has(c):
|
||||
pay += 1
|
||||
return {
|
||||
"chain0_count": chain0.size(),
|
||||
"u_count": u_count,
|
||||
"delta_budget_fog": u_count * dig_ap_cost,
|
||||
"fog_burden_ratio": float(u_count) / float(maxi(1, pay)),
|
||||
}
|
||||
|
||||
|
||||
# ==================== 合法性闸(独立入口;F5 legality + R₀≠S 修订义务) ====================
|
||||
|
||||
func is_clearable(b: BoardData) -> bool:
|
||||
var safe_set := {}
|
||||
for c in b.safe_cells():
|
||||
safe_set[c] = true
|
||||
var seen := {}
|
||||
var queue: Array = b.spawn_cells().duplicate()
|
||||
for c in queue:
|
||||
seen[c] = true
|
||||
while not queue.is_empty():
|
||||
var c: Vector2i = queue.pop_front()
|
||||
if not safe_set.has(c):
|
||||
continue
|
||||
for n in b.neighbors8(c):
|
||||
if safe_set.has(n) and not seen.has(n):
|
||||
seen[n] = true
|
||||
queue.append(n)
|
||||
for c in safe_set.keys():
|
||||
if not seen.has(c):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func legality(b: BoardData, params: Dictionary, db) -> bool:
|
||||
var reasons := _legality_faults(b, params)
|
||||
return reasons.is_empty()
|
||||
|
||||
|
||||
func _legality_faults(b: BoardData, params: Dictionary) -> Array:
|
||||
var faults: Array = []
|
||||
# ① 出生区无雷 ② 出生区∪R₀ 无变异 ③ 数字恒真 ④ R₀ ≠ S(A2 修订义务)
|
||||
for c in b.spawn_cells():
|
||||
if b.is_mine(c):
|
||||
faults.append("inv1_mine_in_spawn")
|
||||
var r0 := {}
|
||||
for c in b.compute_r0():
|
||||
r0[c] = true
|
||||
if b.variant_ids(c).size() > 0:
|
||||
faults.append("inv2b_variant_in_r0")
|
||||
for c in b.spawn_cells():
|
||||
if b.variant_ids(c).size() > 0:
|
||||
faults.append("inv2a_variant_in_spawn")
|
||||
for c in b.all_cells():
|
||||
if b.is_safe(c) and b.digit_of(c) != _neighbor_mine_count(b, c):
|
||||
faults.append("inv3_digit_invariant")
|
||||
if r0.size() == b.safe_cells().size():
|
||||
faults.append("inv4_r0_equals_s")
|
||||
# clearable(合法性闸——与质量度量两闸分离)
|
||||
if not is_clearable(b):
|
||||
faults.append("not_clearable")
|
||||
# min_safe 产物侧(容错两种入参形态:generate 的 params 快照 / 原始表行)
|
||||
var min_safe: int = int(params.get("min_safe", params.get("min_safe_count", 0)))
|
||||
if b.safe_cells().size() < min_safe:
|
||||
faults.append("min_safe_short")
|
||||
# 雾结构:cell_constraint / 厚度 / f≤8d / 排除域
|
||||
var fog := b.fog_cells()
|
||||
var exclusion := b.exclusion_domain()
|
||||
for c in fog:
|
||||
if b.is_mine(c) or b.digit_of(c) <= 0:
|
||||
faults.append("fog_cell_constraint")
|
||||
if exclusion.has(c):
|
||||
faults.append("fog_in_exclusion")
|
||||
if _has_all_fog_window(b, fog):
|
||||
faults.append("fog_thickness")
|
||||
if fog.size() > 8 * b.mines.size():
|
||||
faults.append("fog_over_8d")
|
||||
return faults
|
||||
|
||||
|
||||
func _neighbor_mine_count(b: BoardData, c: Vector2i) -> int:
|
||||
var d := 0
|
||||
for n in b.neighbors8(c):
|
||||
if b.is_mine(n):
|
||||
d += 1
|
||||
return d
|
||||
|
||||
|
||||
func _has_all_fog_window(b: BoardData, fog: Array) -> bool:
|
||||
var fog_set := {}
|
||||
for c in fog:
|
||||
fog_set[c] = true
|
||||
for y in b.height:
|
||||
for x in b.width:
|
||||
var all_fog := true
|
||||
for wy in range(3):
|
||||
for wx in range(3):
|
||||
var c := Vector2i(x + wx, y + wy)
|
||||
if not b.in_bounds(c) or not fog_set.has(c):
|
||||
all_fog = false
|
||||
if all_fog:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# ==================== 产出上报(八项指标) ====================
|
||||
|
||||
func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, db) -> void:
|
||||
var m := measure_inferability(b)
|
||||
var fog_budget := measure_fog_budget(b, db.action_economy.dig_ap_cost)
|
||||
var fog := b.fog_cells()
|
||||
var attempt_facts := b.metrics # 保留尝试期事实(ring_removed / shortfall)
|
||||
b.metrics = {
|
||||
"metric_version": METRIC_VERSION,
|
||||
"inferability": m.inferability,
|
||||
"inferability_core": m.inferability_core,
|
||||
"actual_fog_ratio": float(fog.size()) / float(b.width * b.height),
|
||||
"actual_mines": b.mines.size(),
|
||||
"actual_safe_count": b.safe_cells().size(),
|
||||
"fog_burden_ratio": fog_budget.fog_burden_ratio,
|
||||
"delta_budget_fog": fog_budget.delta_budget_fog,
|
||||
"attempts_used": attempts_used,
|
||||
"accept_mode": accept_mode,
|
||||
"shortfall": attempt_facts.get("shortfall", 0),
|
||||
"ring_removed": attempt_facts.get("ring_removed", 0),
|
||||
"r0_count": m["r0"],
|
||||
}
|
||||
b.illegal_product = accept_mode == "accept_worst"
|
||||
|
||||
|
||||
func _badness(b: BoardData, params: Dictionary) -> Array:
|
||||
# F5 k*** 确定性劣度字典序:(不可达安全格数, min_safe 缺口, 序号)
|
||||
var unreachable := 0
|
||||
var safe_set := {}
|
||||
for c in b.safe_cells():
|
||||
safe_set[c] = true
|
||||
var seen := {}
|
||||
var queue: Array = b.spawn_cells().duplicate()
|
||||
for c in queue:
|
||||
seen[c] = true
|
||||
while not queue.is_empty():
|
||||
var c: Vector2i = queue.pop_front()
|
||||
if not safe_set.has(c):
|
||||
continue
|
||||
for n in b.neighbors8(c):
|
||||
if safe_set.has(n) and not seen.has(n):
|
||||
seen[n] = true
|
||||
queue.append(n)
|
||||
for c in safe_set.keys():
|
||||
if not seen.has(c):
|
||||
unreachable += 1
|
||||
var gap: int = maxi(0, int(params.min_safe) - b.safe_cells().size())
|
||||
return [unreachable, gap, 0]
|
||||
|
||||
|
||||
func _lex_less(a: Array, b: Array) -> bool:
|
||||
for i in a.size():
|
||||
if a[i] < b[i]:
|
||||
return true
|
||||
if a[i] > b[i]:
|
||||
return false
|
||||
return false
|
||||
|
||||
|
||||
func _fatal(msg: String) -> void:
|
||||
push_error("[A1/fatal] " + msg)
|
||||
assert(false, msg)
|
||||
@@ -0,0 +1 @@
|
||||
uid://brlq4y6ahd6so
|
||||
@@ -0,0 +1,341 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 ConfigDB autoload——数值表单点访问入口(Rule 2)+ 校验双通道(Rule 6)+ R1 行解析。
|
||||
# 实现 GDD:design/gdd/d2-data-config.md(系统 ID D2——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 切片注:headless --script 模式下由冒烟脚本显式实例化(D2 AC 前言);玩法代码禁止绕过本单点自建 load()。
|
||||
extends Node
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
|
||||
const TABLE_MANIFEST := {
|
||||
"action_economy": "action_economy.tres",
|
||||
"variation": "variation.tres",
|
||||
"status_effects": "status_effects.tres",
|
||||
"board_gen": "board_gen.tres",
|
||||
"enemy": "enemy.tres",
|
||||
"hero": "hero.tres",
|
||||
"reward": "reward.tres",
|
||||
"reward_config": "reward_config.tres",
|
||||
"settlement": "settlement.tres",
|
||||
"level": "level.tres",
|
||||
"difficulty": "difficulty.tres",
|
||||
}
|
||||
|
||||
var action_economy: ActionEconomyTable
|
||||
var variation: VariationTable
|
||||
var status_effects: StatusEffectTable
|
||||
var board_gen: BoardGenTable
|
||||
var enemy: EnemyTable
|
||||
var hero: HeroTable
|
||||
var reward: RewardTable
|
||||
var reward_config: RewardConfigTable
|
||||
var settlement: SettlementTable
|
||||
var level: LevelTable
|
||||
var difficulty: DifficultyTable
|
||||
|
||||
var is_ready := false
|
||||
var _diagnostics: Array = [] # {severity, category, message}
|
||||
var _warn_dedup: Dictionary = {} # R1 四元组去重注册表
|
||||
var _reload_gate := false # D2 EC8——B1 唯一调用方
|
||||
|
||||
|
||||
func push_diag(severity: String, category: String, message: String) -> void:
|
||||
_diagnostics.append({"severity": severity, "category": category, "message": message})
|
||||
if severity == "error":
|
||||
push_error("[ConfigDB/%s] %s" % [category, message])
|
||||
elif severity == "warning":
|
||||
push_warning("[ConfigDB/%s] %s" % [category, message])
|
||||
|
||||
|
||||
func reset_diagnostics() -> void:
|
||||
_diagnostics.clear()
|
||||
_warn_dedup.clear()
|
||||
|
||||
|
||||
func diagnostics() -> Array:
|
||||
return _diagnostics.duplicate()
|
||||
|
||||
|
||||
func diagnostics_count(severity: String) -> int:
|
||||
var n := 0
|
||||
for d in _diagnostics:
|
||||
if d.severity == severity:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
load_tables("res://data/tables", false)
|
||||
|
||||
|
||||
func load_tables(root: String, collect_only: bool) -> bool:
|
||||
is_ready = false
|
||||
_diagnostics.clear()
|
||||
_warn_dedup.clear()
|
||||
var loaded := {}
|
||||
for key in TABLE_MANIFEST.keys():
|
||||
var path := root.path_join(TABLE_MANIFEST[key])
|
||||
if not ResourceLoader.exists(path):
|
||||
push_diag("error", "missing_file", "表文件缺失: %s" % path)
|
||||
continue
|
||||
var res: Resource = load(path)
|
||||
if res == null:
|
||||
push_diag("error", "load_failed", "表加载失败: %s" % path)
|
||||
continue
|
||||
loaded[key] = res
|
||||
for e in res.validate():
|
||||
push_diag("error", e.category, "%s → %s" % [key, e.message])
|
||||
# 跨表校验(悬空引用 / 池护栏——全部加载期致命)
|
||||
_cross_validate(loaded)
|
||||
var has_errors := diagnostics_count("error") > 0
|
||||
if has_errors and not collect_only:
|
||||
# 游戏通道:致命(Rule 6)——is_ready 恒 false,调用方(主场景)中止构建
|
||||
return false
|
||||
action_economy = loaded.get("action_economy")
|
||||
variation = loaded.get("variation")
|
||||
status_effects = loaded.get("status_effects")
|
||||
board_gen = loaded.get("board_gen")
|
||||
enemy = loaded.get("enemy")
|
||||
hero = loaded.get("hero")
|
||||
reward = loaded.get("reward")
|
||||
reward_config = loaded.get("reward_config")
|
||||
settlement = loaded.get("settlement")
|
||||
level = loaded.get("level")
|
||||
difficulty = loaded.get("difficulty")
|
||||
is_ready = not has_errors
|
||||
return not has_errors
|
||||
|
||||
|
||||
func _cross_validate(loaded: Dictionary) -> void:
|
||||
var st: StatusEffectTable = loaded.get("status_effects")
|
||||
var rt: RewardTable = loaded.get("reward")
|
||||
var rc: RewardConfigTable = loaded.get("reward_config")
|
||||
var st_tab: SettlementTable = loaded.get("settlement")
|
||||
if st == null or rt == null or rc == null or st_tab == null:
|
||||
return # 单表校验已报错,不连锁
|
||||
var effect_ids := {}
|
||||
for row in st.rows:
|
||||
effect_ids[row.get("effect_id", "")] = true
|
||||
# ① RewardTable effect_id 悬空(首个引用行已回填 → 激活)
|
||||
for row in rt.rows:
|
||||
var eff: String = row.get("effect_id", "")
|
||||
if eff != "" and not effect_ids.has(eff):
|
||||
push_diag("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff])
|
||||
# ② 池空护栏:safe_cell × 雾/非雾两态各 ≥1 活行;活 buff 行集非空
|
||||
var t_fog: int = rc.t_fog_min
|
||||
var safe_rows: Array = []
|
||||
var clear_rows: Array = []
|
||||
for row in rt.rows:
|
||||
if int(row.get("weight", 0)) <= 0:
|
||||
continue
|
||||
if row.get("pool_tag", "") == "safe_cell":
|
||||
safe_rows.append(row)
|
||||
elif row.get("pool_tag", "") == "clear_default":
|
||||
clear_rows.append(row)
|
||||
var nonfog_pool := safe_rows
|
||||
var fog_pool: Array = []
|
||||
for row in safe_rows:
|
||||
if int(row.get("tier", 0)) >= t_fog:
|
||||
fog_pool.append(row)
|
||||
if nonfog_pool.is_empty():
|
||||
push_diag("error", "pool_empty_safe", "safe_cell 非雾池无活行")
|
||||
if fog_pool.is_empty():
|
||||
push_diag("error", "pool_empty_fog", "safe_cell 雾态池无活行(tier ≥ t_fog_min)")
|
||||
var has_buff := false
|
||||
for row in safe_rows:
|
||||
if row.get("category", "") == "buff":
|
||||
has_buff = true
|
||||
if safe_rows.size() > 0 and not has_buff:
|
||||
push_diag("error", "no_live_buff_row", "safe_cell 池零活 buff 行——tier_min_buff 无定义")
|
||||
# ③ clear_default 恒正当量(A3 条款④——2026-09-11 供给控制器批:清图槽改即时类一次性伤害,正当量含即时类)
|
||||
var clear_value_ok := false
|
||||
for row in clear_rows:
|
||||
if row.get("category", "") == "buff" and String(row.get("effect_id", "")) != "":
|
||||
clear_value_ok = true
|
||||
elif row.get("category", "") in ["damage_item", "heal_pack", "instant_aoe", "instant_heal"] and int(row.get("power", 0)) >= 1:
|
||||
clear_value_ok = true
|
||||
if not clear_value_ok:
|
||||
push_diag("error", "clear_no_value", "clear_default 池无正当量活行(清图奖励 > 0 违例)")
|
||||
# ④ floor_supply_missing(F5 护栏 i)——k_dmg ≥ 1 时才检查(供给控制器批后 k_dmg=0 休眠)
|
||||
if rc.k_dmg >= 1:
|
||||
var supply_ok := false
|
||||
for row in safe_rows:
|
||||
if row.get("category", "") == "damage_item" and int(row.get("power", 0)) >= rc.p_min:
|
||||
supply_ok = true
|
||||
if not supply_ok:
|
||||
push_diag("error", "floor_supply_missing", "K_dmg ≥ 1 但 safe_cell 池无 power ≥ P_min 活刀行")
|
||||
# ⑤ no_damage_row(F5 护栏 ii——零伤害世界后闸;2026-09-11 供给控制器批:扫描域扩至全池活行,
|
||||
# 可选类退出格子池后伤害来源在 active_inject 池)
|
||||
if rc.k_dmg == 0:
|
||||
var any_knife := false
|
||||
for row in rt.rows:
|
||||
if int(row.get("weight", 0)) > 0 and row.get("category", "") == "damage_item":
|
||||
any_knife = true
|
||||
if not any_knife:
|
||||
push_diag("error", "no_damage_row", "K_dmg = 0 且全池零活刀行——零伤害世界后闸")
|
||||
# ⑨ active_inject 池护栏(可选类供给控制器批):非可选类拒绝;池须有活行(否则战斗首件保证落空)
|
||||
var inject_live := 0
|
||||
for row in rt.rows:
|
||||
if row.get("pool_tag", "") == "active_inject":
|
||||
if int(row.get("weight", 0)) > 0:
|
||||
inject_live += 1
|
||||
if row.get("category", "") not in ["damage_item", "heal_pack"]:
|
||||
push_diag("error", "inject_pool_foreign", "active_inject 池混入非可选类行: %s" % row.get("reward_id"))
|
||||
if inject_live == 0:
|
||||
push_diag("error", "inject_pool_empty", "active_inject 池无活行(战斗首件保证必落空)")
|
||||
# ⑥ SettlementTable.effect_pool_map 悬空
|
||||
for eff in st_tab.effect_pool_map.keys():
|
||||
if not effect_ids.has(eff):
|
||||
push_diag("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff)
|
||||
# ⑦ source_label_map 键集封闭性(键 = B3 source_class ∪ {item, enemy_action, flag_miss})
|
||||
var required_sources := ["safe_cell", "flag_hit", "mine_hit", "clear_reward", "item", "hero_attack", "enemy_action", "flag_miss"]
|
||||
for s in required_sources:
|
||||
if not st_tab.source_label_map.has(s):
|
||||
push_diag("error", "segorder_violation", "source_label_map 缺必需键: %s" % s)
|
||||
# ⑧ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行——
|
||||
# 线性推进器按精确深度行跑,floor 兜底静默降难度属调参事故,加载期直接拦)
|
||||
var lv: LevelTable = loaded.get("level")
|
||||
var bg: BoardGenTable = loaded.get("board_gen")
|
||||
var en: EnemyTable = loaded.get("enemy")
|
||||
if lv != null and bg != null and en != null:
|
||||
var bg_keys := {}
|
||||
for row in bg.rows:
|
||||
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
var en_keys := {}
|
||||
for row in en.rows:
|
||||
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
for lrow in lv.rows:
|
||||
var li: int = int(lrow.level_index)
|
||||
for node in lrow.nodes:
|
||||
var t: String = String(node.get("type", ""))
|
||||
if t != "battle" and t != "boss":
|
||||
continue
|
||||
var key := "%d_%d" % [li, int(node.get("node_depth", -1))]
|
||||
if not bg_keys.has(key):
|
||||
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key])
|
||||
if not en_keys.has(key):
|
||||
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key])
|
||||
|
||||
|
||||
# ---- R1 难度行解析(floor + warning 四元组去重;请求越域致命)----
|
||||
|
||||
func resolve_board_gen(L: int, D: int) -> Dictionary:
|
||||
return _resolve_progress(board_gen.rows, L, D, "board_gen")
|
||||
|
||||
|
||||
func resolve_enemy_rowset(L: int, D: int) -> Dictionary:
|
||||
return _resolve_progress(enemy.rows, L, D, "enemy")
|
||||
|
||||
|
||||
# C3 DifficultyTable 消费面(B2.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
|
||||
func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary:
|
||||
if difficulty == null:
|
||||
return {}
|
||||
for row in difficulty.rows:
|
||||
if int(row.get("level_index", 0)) == L and int(row.get("node_depth", -1)) == D:
|
||||
return {
|
||||
"hp_mult": clampf(float(row.get("hp_mult", 1.0)), 0.1, 10.0),
|
||||
"atk_mult": clampf(float(row.get("atk_mult", 1.0)), 0.1, 10.0),
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Dictionary:
|
||||
if L < 1 or D < 0:
|
||||
push_diag("error", "request_oob", "%s 请求越域 (%d,%d)" % [table_id, L, D])
|
||||
return {}
|
||||
var levels := {}
|
||||
for row in rows:
|
||||
levels[int(row.get("level_index", 0))] = true
|
||||
var level_keys := levels.keys()
|
||||
level_keys.sort()
|
||||
var l_star: int
|
||||
var mt_level := "exact"
|
||||
if L < level_keys[0]:
|
||||
l_star = level_keys[0]
|
||||
mt_level = "ceil"
|
||||
else:
|
||||
l_star = level_keys[0]
|
||||
for lv in level_keys:
|
||||
if lv <= L:
|
||||
l_star = lv
|
||||
if l_star < L:
|
||||
mt_level = "floor"
|
||||
var depth_keys := {}
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star:
|
||||
depth_keys[int(row.get("node_depth", 0))] = true
|
||||
var d_keys := depth_keys.keys()
|
||||
d_keys.sort()
|
||||
var d_star: int
|
||||
var mt_depth := "exact"
|
||||
if d_keys.is_empty():
|
||||
push_diag("error", "structural", "%s level %d 无行(锚行校验应已拦截)" % [table_id, l_star])
|
||||
return {}
|
||||
if D < d_keys[0]:
|
||||
d_star = d_keys[0]
|
||||
mt_depth = "floor"
|
||||
else:
|
||||
d_star = d_keys[0]
|
||||
for dv in d_keys:
|
||||
if dv <= D:
|
||||
d_star = dv
|
||||
if d_star < D:
|
||||
mt_depth = "floor"
|
||||
var missing := ""
|
||||
if mt_level == "ceil":
|
||||
missing = "base_ceil"
|
||||
elif mt_level == "floor" and mt_depth == "floor":
|
||||
missing = "both"
|
||||
elif mt_level == "floor":
|
||||
missing = "level_floor"
|
||||
elif mt_depth == "floor":
|
||||
missing = "depth_floor"
|
||||
if missing != "":
|
||||
var dedup_key := "%s|%s|%d|%d" % [table_id, missing, l_star, d_star]
|
||||
if not _warn_dedup.has(dedup_key):
|
||||
_warn_dedup[dedup_key] = true
|
||||
push_diag("warning", "resolve_" + missing, "%s 请求 (%d,%d) floor 解析 → (%d,%d) [%s]" % [table_id, L, D, l_star, d_star, missing])
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# ---- A4 variant_pool_gate:候选池门控(∅ biome_tag = 通配)----
|
||||
|
||||
func variant_pool(level_index: int, board_biomes: Array) -> Array:
|
||||
var pool: Array = []
|
||||
for row in variation.rows:
|
||||
if int(row.get("unlock_level", 999)) > level_index:
|
||||
continue
|
||||
var tags: Array = row.get("biome_tags", [])
|
||||
if tags.is_empty():
|
||||
pool.append(row)
|
||||
continue
|
||||
for t in tags:
|
||||
if board_biomes.has(t):
|
||||
pool.append(row)
|
||||
break
|
||||
return pool
|
||||
|
||||
|
||||
func status_effect_row(effect_id: String) -> Dictionary:
|
||||
return status_effects.row_by_id(effect_id)
|
||||
|
||||
|
||||
# ---- D2 EC8:debug 热重载门(B1 唯一调用方;切片实现 = 门标志 + 拒绝语义)----
|
||||
|
||||
func set_reload_gate(gated: bool) -> void:
|
||||
_reload_gate = gated
|
||||
|
||||
|
||||
func reload() -> bool:
|
||||
if _reload_gate:
|
||||
push_diag("warning", "reload_gated", "战斗中热重载被拒绝(D2 EC8)")
|
||||
return false
|
||||
var ok := load_tables("res://data/tables", false)
|
||||
if ok:
|
||||
push_diag("debug", "config_reloaded", "表热重载完成")
|
||||
return ok
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfelf84soyn3y
|
||||
@@ -0,0 +1,19 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# 实现 GDD:design/gdd/b1-settlement.md(系统 ID B1——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# B1 战斗段伤害公式族(零状态纯算术宿主——规范 1.2「工具性纯函数挂明确宿主类」;
|
||||
# 自 battle_orchestrator 拆出 2026-09-13,封装审查 O3)。池读取与表值读取仍归 B1,本类只做算术。
|
||||
class_name DamageMath
|
||||
extends RefCounted
|
||||
|
||||
|
||||
# F2 乘区(池值已由调用方 clamp 后传入):池内加算 → 池间乘算 → floor(单一取整规则)
|
||||
static func pool_damage(d_raw: int, p_hero: int, p_enemy: int) -> int:
|
||||
var v: int = int(floor(float(d_raw) * (100.0 + float(p_hero)) * (100.0 + float(p_enemy)) / 10000.0))
|
||||
return maxi(0, v)
|
||||
|
||||
|
||||
# 伤害A/B 充能分量:充能(调用方已套单轮充能上限)× 每单位值 × 克制 percent → floor
|
||||
static func charge_component(charge: int, per_unit: int, effectiveness_percent: int) -> int:
|
||||
var pts := charge * per_unit
|
||||
return int(floor(float(pts) * float(effectiveness_percent) / 100.0))
|
||||
@@ -0,0 +1 @@
|
||||
uid://cym7k76gt61k
|
||||
@@ -0,0 +1,215 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证
|
||||
# Date: 2026-09-11
|
||||
# C1 关卡节点地图(provisional 线性形态——节点链来自 LevelTable,路径分支/形态定稿权归 C1):
|
||||
# 关卡域生命周期唯一编排者——共享 B4 容器跨战斗延续(2026-09-11 关内延续裁决)、HP 跨战斗携带、
|
||||
# 奖励点/恢复点节点兑付、关卡末 level_clear 清库。Core 层零 UI 依赖(headless 可跑完整关卡——D2 Rule 7)。
|
||||
# 测试专用:hero_atk_pct/hero_hp_pct 数值覆盖(调整板模拟天赋树——正式侧归 C4 元进度,切片不落盘)。
|
||||
# E4 收口(2026-09-13):覆盖值经 encounter 显式传入战斗(不再写 db.hero 表——表值运行期不可变,
|
||||
# 双基线同步/双写叠加 6→18→54 整类隐患随表写一并移除)。
|
||||
# 对应 GDD:C1 关卡节点地图(未设计——provisional 切片先行,定稿后回写;命名规范见 CLAUDE.md)
|
||||
class_name LevelRunner
|
||||
extends RefCounted
|
||||
|
||||
signal node_entered(node: Dictionary, info: Dictionary) # info 按 type:battle/boss={} · reward={snapshot} · rest={healed,hp,max_hp}
|
||||
signal step_row(row: Dictionary) # 转发当前 B1 结算行
|
||||
signal window_opened(items: Array) # 转发当前 B1 道具窗口
|
||||
signal round_started(round_index: int)
|
||||
signal board_changed()
|
||||
signal level_finished(payload: Dictionary) # {result, hp, node_index, node_type, remaining_items}
|
||||
|
||||
var db
|
||||
var rng
|
||||
var level_row: Dictionary = {}
|
||||
var node_index := -1
|
||||
var hero_hp := 0
|
||||
# 协作对象私有(E1/E3 封装收口 2026-09-13):b1/b4 读取经 current_battle()/inventory() 成文出口
|
||||
var _b1 = null # 当前战斗编排器(关卡结束置 null)
|
||||
var _b4 = null # 关卡域共享 B4 容器(库存跨战斗延续)
|
||||
|
||||
var hero_atk_pct := 0 # 测试面板覆盖值(切片专用)
|
||||
var hero_hp_pct := 0
|
||||
var _running := false
|
||||
var level_result := "" # "" / "victory" / "defeat"
|
||||
|
||||
|
||||
func _init(db_ref, rng_ref) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
|
||||
|
||||
# ---- 测试面板覆盖(模拟天赋树属性提升——仅测试用,正式侧归 C4;经 encounter 传入战斗)----
|
||||
|
||||
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||||
hero_atk_pct = atk_pct
|
||||
hero_hp_pct = hp_pct
|
||||
|
||||
|
||||
func effective_atk() -> int:
|
||||
return maxi(1, int(round(float(db.hero.atk) * (100.0 + float(hero_atk_pct)) / 100.0)))
|
||||
|
||||
|
||||
func effective_max_hp() -> int:
|
||||
return maxi(1, int(round(float(db.hero.max_hp) * (100.0 + float(hero_hp_pct)) / 100.0)))
|
||||
|
||||
|
||||
# 覆盖复位(关卡结束归零——表值不可变,仅清百分比;调用方兼容保留)
|
||||
func restore_hero_base() -> void:
|
||||
hero_atk_pct = 0
|
||||
hero_hp_pct = 0
|
||||
|
||||
|
||||
# ---- 关卡生命周期 ----
|
||||
|
||||
func start_level(level_index: int, seed_val: int) -> bool:
|
||||
var row := _find_level_row(level_index)
|
||||
if row.is_empty():
|
||||
push_error("[C1] level_index %d 在 LevelTable 无行" % level_index)
|
||||
return false
|
||||
level_row = row
|
||||
node_index = -1
|
||||
level_result = ""
|
||||
hero_hp = effective_max_hp()
|
||||
_b4 = RewardEconomy.new(db, rng)
|
||||
rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 C2)
|
||||
_running = true
|
||||
return _enter_next_node()
|
||||
|
||||
|
||||
func _find_level_row(level_index: int) -> Dictionary:
|
||||
for row in db.level.rows:
|
||||
if int(row.level_index) == level_index:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
func nodes() -> Array:
|
||||
return level_row.get("nodes", [])
|
||||
|
||||
|
||||
func current_node() -> Dictionary:
|
||||
if node_index < 0 or node_index >= nodes().size():
|
||||
return {}
|
||||
return nodes()[node_index]
|
||||
|
||||
|
||||
func _enter_next_node() -> bool:
|
||||
if not _running:
|
||||
return false
|
||||
node_index += 1
|
||||
var node: Dictionary = current_node()
|
||||
if node.is_empty():
|
||||
push_error("[C1] 节点链越界(末节点 Boss 应已结束关卡)")
|
||||
return false
|
||||
match String(node.get("type", "")):
|
||||
"battle", "boss":
|
||||
return _start_battle(node)
|
||||
"reward":
|
||||
var inst = _b4.grant_node_active(Vector2i(-1, -1), 0)
|
||||
if not (inst is Dictionary):
|
||||
push_error("[C1] 奖励点入库被拒")
|
||||
return false
|
||||
node_entered.emit(node, {"snapshot": inst.snapshot})
|
||||
return true
|
||||
"rest":
|
||||
var healed := _apply_rest()
|
||||
node_entered.emit(node, {"healed": healed, "hp": hero_hp, "max_hp": effective_max_hp()})
|
||||
return true
|
||||
push_error("[C1] 未知节点类型: %s" % str(node))
|
||||
return false
|
||||
|
||||
|
||||
func _start_battle(node: Dictionary) -> bool:
|
||||
_b1 = BattleOrchestrator.new(db, rng, _b4) # 共享 B4——库存跨战斗延续(关内延续裁决)
|
||||
# core↔core 接线只许方法 Callable(ADR-0001 静态规则 S6——lambda 捕获 self 会延长
|
||||
# RefCounted 生命周期乃至成环:b1 的信号持捕获 runner 的 lambda,runner 又持 b1)
|
||||
_b1.step_row.connect(_forward_step_row)
|
||||
_b1.window_opened.connect(_forward_window_opened)
|
||||
_b1.round_started.connect(_forward_round_started)
|
||||
_b1.board_changed.connect(_forward_board_changed)
|
||||
_b1.battle_finished.connect(_on_battle_finished)
|
||||
# node_entered 先于 begin_battle:呈现层须先拿到 b1 引用,才能响应 begin 期间发出的
|
||||
# board_changed(首板安装)并重建棋盘——后发会导致首战棋盘不渲染(2026-09-11 playtest 缺陷①)
|
||||
node_entered.emit(node, {})
|
||||
if not _b1.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("node_depth", 0)),
|
||||
"hero_hp": hero_hp, "hero_atk": effective_atk(), "hero_max_hp": effective_max_hp()}):
|
||||
push_error("[C1] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _forward_step_row(row: Dictionary) -> void:
|
||||
step_row.emit(row)
|
||||
|
||||
|
||||
func _forward_window_opened(items: Array) -> void:
|
||||
window_opened.emit(items)
|
||||
|
||||
|
||||
func _forward_round_started(ri: int) -> void:
|
||||
round_started.emit(ri)
|
||||
|
||||
|
||||
func _forward_board_changed() -> void:
|
||||
board_changed.emit()
|
||||
|
||||
|
||||
func _on_battle_finished(payload: Dictionary) -> void:
|
||||
var node := current_node()
|
||||
var result: String = String(payload.get("result", ""))
|
||||
if result == "victory":
|
||||
hero_hp = maxi(1, int(payload.get("hero_hp", 0))) # despawn 前捕获值(载荷自包含)
|
||||
if String(node.get("type", "")) == "boss" or result == "defeat":
|
||||
# Boss 胜利 = 关卡通关;任意战斗败北 = 关卡失败(Roguelike——冒险即终,重开归 C2/C4)
|
||||
var left: int = _b1.level_clear()["remaining"] # 库存随关卡结束清空(关内延续裁决③)
|
||||
if _b4.in_stock_count() != 0:
|
||||
push_error("[C1] level_clear 后库存非空(清库违例——fail-fast)")
|
||||
return
|
||||
level_result = "victory" if result == "victory" else "defeat"
|
||||
var hp_left := hero_hp if result == "victory" else 0
|
||||
_running = false
|
||||
_b1 = null
|
||||
level_finished.emit({"result": level_result, "hp": hp_left, "node_index": node_index,
|
||||
"node_type": String(node.get("type", "")), "remaining_items": left})
|
||||
return
|
||||
_enter_next_node()
|
||||
|
||||
|
||||
func _apply_rest() -> int:
|
||||
var pct: int = int(level_row.get("rest_heal_pct", 0))
|
||||
var mhp := effective_max_hp()
|
||||
var heal := int(floor(float(mhp) * float(pct) / 100.0))
|
||||
var actual: int = mini(heal, mhp - hero_hp)
|
||||
hero_hp += actual
|
||||
return actual
|
||||
|
||||
|
||||
# ---- 只读出口(E1/E3 封装收口:呈现层/工具经此读当前战斗与关卡库存,禁直读协作对象)----
|
||||
|
||||
func current_battle():
|
||||
return _b1
|
||||
|
||||
|
||||
func inventory() -> Array:
|
||||
return _b4.query_inventory() if _b4 != null else []
|
||||
|
||||
|
||||
# ---- 玩家输入转发(呈现层只与 runner 对话)----
|
||||
|
||||
# 奖励/恢复点确认继续(呈现层在覆盖层上点击"继续"后调用;自动模拟在 node_entered 回调内立即调用)
|
||||
func continue_after_node() -> bool:
|
||||
var t := String(current_node().get("type", ""))
|
||||
if t != "reward" and t != "rest":
|
||||
return false
|
||||
return _enter_next_node()
|
||||
|
||||
|
||||
func submit_window_selection(selection: Array) -> void:
|
||||
if _b1 != null:
|
||||
_b1.submit_window_selection(selection)
|
||||
|
||||
|
||||
func player_request_refresh() -> bool:
|
||||
if _b1 == null:
|
||||
return false
|
||||
return _b1.player_request_refresh()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b3o7g73bnbsya
|
||||
@@ -0,0 +1,440 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# A2 排雷玩法规则引擎:零随机/零表读取/零 UI——输入 = A1 产物 + 意图序列,输出 = 三事件流。
|
||||
# 核心契约:install 四不变式 · 初始展开 = R₀ 恰等 · 连锁不越旗 · 批次语义序仲裁(cleared > stop > 其余)。
|
||||
# 实现 GDD:design/gdd/a2-minesweeper-rules.md(系统 ID A2——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name MinesweeperRules
|
||||
extends RefCounted
|
||||
|
||||
signal cell_revealed(evt: Dictionary)
|
||||
signal flag_judged(evt: Dictionary)
|
||||
signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(A2 外发;stopped 由 A3 直发)
|
||||
|
||||
const SUB_REASONS := ["not_adjacent", "target_revealed", "target_concealed", "flag_present", "flag_absent", "no_path", "out_of_catalog"]
|
||||
|
||||
var db
|
||||
var a3
|
||||
# 棋盘状态私有(E2 封装收口 2026-09-13):对外只暴露 current_board()/reveal_record() 等查询面,
|
||||
# 禁止跨模块直读/直写原始字典——先例教训 = board_view 绕过查询面直读 revealed。
|
||||
var _board: BoardData = null
|
||||
var _revealed := {} # Vector2i → {"kind": "safe"/"mine", "digit": int}
|
||||
var _flags := {} # Vector2i → true(棋盘单态;跨轮保留随图存续)
|
||||
var _hero := Vector2i.ZERO
|
||||
var _spawn: Dictionary = {}
|
||||
|
||||
|
||||
func _init(db_ref, a3_ref) -> void:
|
||||
db = db_ref
|
||||
a3 = a3_ref
|
||||
|
||||
|
||||
# ---- 板态 ----
|
||||
|
||||
func has_board() -> bool:
|
||||
return _board != null
|
||||
|
||||
|
||||
# 板产物只读出口(E2 收口——B1 快照 / 组合根 rebuild / 探针经此取板,禁再直读字段)
|
||||
func current_board() -> BoardData:
|
||||
return _board
|
||||
|
||||
|
||||
# 已开格记录查询面(cell → {kind, digit};未开 = {})——呈现层渲染消费面(board_view 徽章/四格态)
|
||||
func reveal_record(c: Vector2i) -> Dictionary:
|
||||
return _revealed.get(c, {})
|
||||
|
||||
|
||||
func hero_pos() -> Vector2i:
|
||||
return _hero
|
||||
|
||||
|
||||
func is_revealed(c: Vector2i) -> bool:
|
||||
return _revealed.has(c)
|
||||
|
||||
|
||||
func has_flag(c: Vector2i) -> bool:
|
||||
return _flags.has(c)
|
||||
|
||||
|
||||
func revealed_cells() -> Array:
|
||||
return _revealed.keys()
|
||||
|
||||
|
||||
func safe_remaining() -> int:
|
||||
var n := 0
|
||||
for c in _board.safe_cells():
|
||||
if not _revealed.has(c):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
func clear_map_cleared() -> bool:
|
||||
return safe_remaining() == 0
|
||||
|
||||
|
||||
# ---- install(编排边界相位 = A3 Idle/Waiting;Active 拒绝——防轮内换板)----
|
||||
|
||||
# during_round = 轮内刷新编排路径(2026-09-12 方案 A,B1 唯一调用方):Active 相位合法——
|
||||
# 玩家挖掘中立即换图,棋盘状态(revealed/flags)随 install 重置,初始展开事件照发(initial 通道)
|
||||
func install_board(b: BoardData, during_round := false) -> Dictionary:
|
||||
if a3.phase == a3.PHASE_ACTIVE and not during_round:
|
||||
return _outcome(false, "rejected", "phase", "")
|
||||
if a3.phase == a3.PHASE_IDLE and during_round:
|
||||
return _outcome(false, "rejected", "phase", "")
|
||||
if b == null:
|
||||
return _outcome(false, "rejected", "board_invalid", "out_of_catalog")
|
||||
var inv := _install_invariant_faults(b)
|
||||
if not inv.is_empty():
|
||||
push_error("[A2] install 不变式违例: %s" % str(inv))
|
||||
return _outcome(false, "rejected", "board_invalid", inv[0])
|
||||
_board = b
|
||||
_revealed.clear()
|
||||
_flags.clear()
|
||||
_spawn = {}
|
||||
for c in b.spawn_cells():
|
||||
_spawn[c] = true
|
||||
# 初始展开:O₀ = R₀ 恰等(免费/计入清图/无奖励/永不触雷触雾);板级一次跨轮不重放
|
||||
for c in _initial_expansion_order(b):
|
||||
_revealed[c] = {"kind": "safe", "digit": b.digit_of(c)}
|
||||
cell_revealed.emit({
|
||||
"cell": c, "kind": "safe", "digit": b.digit_of(c),
|
||||
"variant_ids": [], "channel": "initial", "reward_eligible": false,
|
||||
})
|
||||
_hero = b.spawn_center
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
# during_round = 轮内刷新编排路径(2026-09-12 方案 A):Active 相位合法——换图前退役旧板
|
||||
func flag_count() -> int:
|
||||
return _flags.size()
|
||||
|
||||
|
||||
func retire_board(during_round := false) -> Dictionary:
|
||||
if _board == null:
|
||||
return _outcome(true, "executed", "", "") # 幂等受理
|
||||
if a3.phase == a3.PHASE_ACTIVE and not during_round:
|
||||
return _outcome(false, "rejected", "phase", "")
|
||||
_board = null
|
||||
_revealed.clear()
|
||||
_flags.clear()
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
# 四不变式(诊断类别封闭枚举):inv1 出生区雷 / inv2a 出生区变异 / inv2b R₀ 变异 / inv3 数字恒真 / inv4 R₀=S
|
||||
func _install_invariant_faults(b: BoardData) -> Array:
|
||||
var faults: Array = []
|
||||
var r0 := {}
|
||||
for c in b.compute_r0():
|
||||
r0[c] = true
|
||||
for c in b.spawn_cells():
|
||||
if b.is_mine(c):
|
||||
faults.append("inv1_mine_in_spawn")
|
||||
if b.variant_ids(c).size() > 0:
|
||||
faults.append("inv2a_variant_in_spawn")
|
||||
for c in r0.keys():
|
||||
if b.variant_ids(c).size() > 0:
|
||||
faults.append("inv2b_variant_in_r0")
|
||||
for c in b.all_cells():
|
||||
if b.is_safe(c):
|
||||
var d := 0
|
||||
for n in b.neighbors8(c):
|
||||
if b.is_mine(n):
|
||||
d += 1
|
||||
if b.digit_of(c) != d:
|
||||
faults.append("inv3_digit_invariant")
|
||||
if r0.size() == b.safe_cells().size():
|
||||
faults.append("inv4_r0_equals_s")
|
||||
return faults
|
||||
|
||||
|
||||
# 初始展开规范序:触发格 BFS、8 邻行主序(≡ A1 F1 R₀ 算子;禁外扩环、禁只开 3×3)
|
||||
func _initial_expansion_order(b: BoardData) -> Array:
|
||||
var order: Array = b.spawn_cells().duplicate()
|
||||
var seen := {}
|
||||
for c in order:
|
||||
seen[c] = true
|
||||
var queue: Array = order.duplicate()
|
||||
while not queue.is_empty():
|
||||
var c: Vector2i = queue.pop_front()
|
||||
if b.is_safe(c) and b.digit_of(c) == 0 and b.variant_ids(c).is_empty():
|
||||
for n in b.neighbors8(c):
|
||||
if b.is_safe(n) and not seen.has(n):
|
||||
seen[n] = true
|
||||
order.append(n)
|
||||
queue.append(n)
|
||||
return order
|
||||
|
||||
|
||||
# ---- 批次结算(唯一提交入口;单意图批 = 平凡形 ≡ 便捷 API)----
|
||||
|
||||
func settle_batch(intents: Array) -> Array:
|
||||
var outcomes: Array = []
|
||||
if intents.is_empty():
|
||||
return outcomes
|
||||
if a3.phase != a3.PHASE_ACTIVE or _board == null:
|
||||
for intent in intents:
|
||||
outcomes.append(_outcome(false, "rejected", "phase", ""))
|
||||
return outcomes
|
||||
# F4 语义序仲裁:C_dig(真模拟清图)→ C_stop → C_other;类内稳定序 = 到达序
|
||||
var indexed: Array = []
|
||||
for i in intents.size():
|
||||
indexed.append({"i": i, "intent": intents[i], "cls": _classify(intents[i])})
|
||||
indexed.sort_custom(func(a, b):
|
||||
if a.cls != b.cls:
|
||||
return a.cls < b.cls
|
||||
return a.i < b.i)
|
||||
var round_over := false
|
||||
for item in indexed:
|
||||
if round_over:
|
||||
outcomes.append(_outcome(false, "voided", "", ""))
|
||||
continue
|
||||
var res := _execute(item.intent)
|
||||
outcomes.append(res)
|
||||
if a3.phase != a3.PHASE_ACTIVE:
|
||||
round_over = true
|
||||
return outcomes
|
||||
|
||||
|
||||
func _classify(intent: Dictionary) -> int:
|
||||
# 0 = C_dig(执行后清图——真模拟,禁计数器捷径);1 = C_stop;2 = C_other
|
||||
if intent.get("action", "") == "dig":
|
||||
var c: Vector2i = intent.get("cell", Vector2i(-9, -9))
|
||||
if _board_face_dig(c) == "" and not _board.is_mine(c):
|
||||
# 模拟 Chain(O, c) 后清图判定
|
||||
var chain := _chain_closure(c, {})
|
||||
var remaining := 0
|
||||
for sc in _board.safe_cells():
|
||||
if not _revealed.has(sc) and not chain.has(sc):
|
||||
remaining += 1
|
||||
if remaining == 0:
|
||||
return 0
|
||||
return 2
|
||||
if intent.get("action", "") == "stop":
|
||||
return 1
|
||||
return 2
|
||||
|
||||
|
||||
func _execute(intent: Dictionary) -> Dictionary:
|
||||
match intent.get("action", ""):
|
||||
"dig":
|
||||
return _do_dig(intent.get("cell", Vector2i(-9, -9)))
|
||||
"flag_place":
|
||||
return _do_flag_place(intent.get("cell", Vector2i(-9, -9)))
|
||||
"flag_pull":
|
||||
return _do_flag_pull(intent.get("cell", Vector2i(-9, -9)))
|
||||
"move":
|
||||
return _do_move(intent.get("cell", Vector2i(-9, -9)))
|
||||
"stop":
|
||||
return _do_stop()
|
||||
push_warning("[A2] 目录外意图: %s" % str(intent))
|
||||
return _outcome(false, "rejected", "board_face", "out_of_catalog")
|
||||
|
||||
|
||||
# ---- 挖掘 ----
|
||||
|
||||
func _do_dig(c: Vector2i) -> Dictionary:
|
||||
var sub := _board_face_dig(c)
|
||||
if sub != "":
|
||||
return _outcome(false, "rejected", "board_face", sub)
|
||||
# 真模拟连锁与清图(F2/F1——单一实现两名字)
|
||||
var chain := _chain_closure(c, _flags)
|
||||
var clears: bool = _cleared_after(chain)
|
||||
# F2 口径:chain_size = |O′ ∖ (O ∪ {c})|——闭包含已开集 O,只数新开格(不含首格)
|
||||
var chain_size := 0
|
||||
for cc in chain.keys():
|
||||
if cc != c and not _revealed.has(cc):
|
||||
chain_size += 1
|
||||
# 资源面/相位 + cleared 仲裁在 A3 事务内原子完成
|
||||
if not a3.request_dig(chain_size, clears):
|
||||
return _outcome(false, "rejected", "resource_face", "")
|
||||
# 事务内执行:首格 + 连锁新格揭示(BFS 规范序,含链开雾格——零特判);事件 = O′ ∖ O
|
||||
var reveal_order: Array = _chain_reveal_order(c, _flags)
|
||||
for rc in reveal_order:
|
||||
if _revealed.has(rc):
|
||||
continue # 闭包可穿过已开区——已开格零事件(cell_revealed × |O′ ∖ O|)
|
||||
var is_mine := _board.is_mine(rc)
|
||||
var channel := "dig" if rc == c else "chain"
|
||||
if is_mine:
|
||||
_revealed[rc] = {"kind": "mine", "digit": -1}
|
||||
cell_revealed.emit({
|
||||
"cell": rc, "kind": "mine", "digit": -1,
|
||||
"variant_ids": [], "channel": channel, "reward_eligible": false,
|
||||
})
|
||||
else:
|
||||
_revealed[rc] = {"kind": "safe", "digit": _board.digit_of(rc)}
|
||||
cell_revealed.emit({
|
||||
"cell": rc, "kind": "safe", "digit": _board.digit_of(rc),
|
||||
"variant_ids": _board.variant_ids(rc), "channel": channel, "reward_eligible": true,
|
||||
})
|
||||
if a3.phase != a3.PHASE_ACTIVE:
|
||||
_emit_round_end_flags_and_payload()
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
func _board_face_dig(c: Vector2i) -> String:
|
||||
if not _board.in_bounds(c):
|
||||
return "out_of_catalog"
|
||||
if not _adjacent_to_hero(c):
|
||||
return "not_adjacent"
|
||||
if is_revealed(c):
|
||||
return "target_revealed"
|
||||
if has_flag(c):
|
||||
return "flag_present"
|
||||
return ""
|
||||
|
||||
|
||||
# 连锁闭包:μX.[O∪{c} ⊆ X ∧ ∀x∈X:(x∈S ∧ num(x)=0 ⇒ N₈(x)∩S∖G ⊆ X)]——连锁不越旗
|
||||
func _chain_closure(start: Vector2i, flag_set: Dictionary) -> Dictionary:
|
||||
var chain := {start: true}
|
||||
var queue: Array = [start]
|
||||
while not queue.is_empty():
|
||||
var c: Vector2i = queue.pop_front()
|
||||
if _board.is_safe(c) and not flag_set.has(c) and _board.digit_of(c) == 0 and _board.variant_ids(c).is_empty():
|
||||
for n in _board.neighbors8(c):
|
||||
if _board.is_safe(n) and not flag_set.has(n) and not chain.has(n):
|
||||
chain[n] = true
|
||||
queue.append(n)
|
||||
return chain
|
||||
|
||||
|
||||
# 揭示规范序:首格 + 触发格 BFS、8 邻行主序
|
||||
func _chain_reveal_order(start: Vector2i, flag_set: Dictionary) -> Array:
|
||||
var order: Array = [start]
|
||||
var seen := {start: true}
|
||||
var queue: Array = [start]
|
||||
while not queue.is_empty():
|
||||
var c: Vector2i = queue.pop_front()
|
||||
if _board.is_safe(c) and not flag_set.has(c) and _board.digit_of(c) == 0 and _board.variant_ids(c).is_empty():
|
||||
for n in _board.neighbors8(c):
|
||||
if _board.is_safe(n) and not flag_set.has(n) and not seen.has(n):
|
||||
seen[n] = true
|
||||
order.append(n)
|
||||
queue.append(n)
|
||||
return order
|
||||
|
||||
|
||||
func _cleared_after(chain: Dictionary) -> bool:
|
||||
for c in _board.safe_cells():
|
||||
if not _revealed.has(c) and not chain.has(c):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# ---- 旗 ----
|
||||
|
||||
func _do_flag_place(c: Vector2i) -> Dictionary:
|
||||
var sub := ""
|
||||
if not _board.in_bounds(c):
|
||||
sub = "out_of_catalog"
|
||||
elif not _adjacent_to_hero(c):
|
||||
sub = "not_adjacent"
|
||||
elif is_revealed(c):
|
||||
sub = "target_revealed"
|
||||
elif has_flag(c):
|
||||
sub = "flag_present"
|
||||
if sub != "":
|
||||
return _outcome(false, "rejected", "board_face", sub)
|
||||
if not a3.try_spend("flag_place"):
|
||||
return _outcome(false, "rejected", "resource_face", "")
|
||||
_flags[c] = true # 旗动作零格子事件外发(发射面封闭;反馈经调用返回值命令式驱动)
|
||||
if a3.phase != a3.PHASE_ACTIVE:
|
||||
# 旗动作花完末点 AP 同样终结轮次(A3 三出口——AP 用完与动作种类无关;缺漏使 round_end
|
||||
# 永不发出、B1 的 Waiting 结算点永不到达 = 整局软锁。playtest 缺陷②,行为探针复现)
|
||||
_emit_round_end_flags_and_payload()
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
func _do_flag_pull(c: Vector2i) -> Dictionary:
|
||||
var sub := ""
|
||||
if not _board.in_bounds(c):
|
||||
sub = "out_of_catalog"
|
||||
elif not _adjacent_to_hero(c):
|
||||
sub = "not_adjacent"
|
||||
elif not has_flag(c):
|
||||
sub = "flag_absent"
|
||||
if sub != "":
|
||||
return _outcome(false, "rejected", "board_face", sub)
|
||||
if not a3.try_spend("flag_pull"):
|
||||
return _outcome(false, "rejected", "resource_face", "")
|
||||
_flags.erase(c)
|
||||
if a3.phase != a3.PHASE_ACTIVE:
|
||||
_emit_round_end_flags_and_payload() # 同 flag_place——末点 AP 轮末发射(playtest 缺陷②)
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
# ---- 移动(瞬移;免费不经账本;BFS 限已探明区含已引爆雷格)----
|
||||
|
||||
func _do_move(c: Vector2i) -> Dictionary:
|
||||
var sub := ""
|
||||
if not _board.in_bounds(c):
|
||||
sub = "out_of_catalog"
|
||||
elif not is_revealed(c):
|
||||
sub = "target_concealed"
|
||||
elif c != _hero and compute_move_path(c).is_empty():
|
||||
sub = "no_path"
|
||||
if sub != "":
|
||||
return _outcome(false, "rejected", "board_face", sub)
|
||||
_hero = c # 目标 = 当前格定义为零移动(幂等受理)
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
func compute_move_path(target: Vector2i) -> Array:
|
||||
if _board == null or not is_revealed(target):
|
||||
return []
|
||||
if target == _hero:
|
||||
return []
|
||||
# BFS(8 邻行主序)——路径 = 合法性载体 + D1 预览数据源
|
||||
var prev := {_hero: _hero}
|
||||
var queue: Array = [_hero]
|
||||
while not queue.is_empty():
|
||||
var c: Vector2i = queue.pop_front()
|
||||
if c == target:
|
||||
break
|
||||
for n in _board.neighbors8(c):
|
||||
if is_revealed(n) and not prev.has(n):
|
||||
prev[n] = c
|
||||
queue.append(n)
|
||||
if not prev.has(target):
|
||||
return []
|
||||
var path: Array = []
|
||||
var cur := target
|
||||
while cur != _hero:
|
||||
path.push_front(cur)
|
||||
cur = prev[cur]
|
||||
return path
|
||||
|
||||
|
||||
# ---- 停手(经批次入口委托 A3;先发旗判定批再委托——事件序契约 flag_judged < round_end 全路径统一)----
|
||||
|
||||
func _do_stop() -> Dictionary:
|
||||
_emit_flags_batch() # 基线 = 事务开始旗位(停手不改旗位)
|
||||
if not a3.request_stop():
|
||||
return _outcome(false, "rejected", "phase", "")
|
||||
# A3 直发 stopped 路径 round_end;A2 禁双发(双发射源纪律)
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
# ---- 轮末旗判定 + 棋盘路径 round_end 外发 ----
|
||||
|
||||
func _emit_flags_batch() -> void:
|
||||
# 判定基线 = 轮次结束事务开始时旗位;行主序枚举(确定性);被拔的不判;诚实重复上报(去重归 B1)
|
||||
var cells: Array = _flags.keys()
|
||||
cells.sort_custom(func(a, b): return a.y * 1000 + a.x < b.y * 1000 + b.x)
|
||||
for c in cells:
|
||||
flag_judged.emit({"cell": c, "hit": _board.is_mine(c)})
|
||||
|
||||
|
||||
func _emit_round_end_flags_and_payload() -> void:
|
||||
_emit_flags_batch()
|
||||
var payload: Dictionary = a3.last_round_end # 逐字段取自 A3(读取权 = 接口契约)
|
||||
round_end_board.emit({"path_tag": payload.get("path_tag", ""), "continuation_hint": payload.get("continuation_hint", "")})
|
||||
|
||||
|
||||
func _adjacent_to_hero(c: Vector2i) -> bool:
|
||||
var d := Vector2i(absi(c.x - _hero.x), absi(c.y - _hero.y))
|
||||
return maxi(d.x, d.y) == 1
|
||||
|
||||
|
||||
func _outcome(accepted: bool, outcome: String, reason: String, sub: String) -> Dictionary:
|
||||
return {"accepted": accepted, "outcome": outcome, "reject_reason": reason, "sub_reason": sub}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dlcnq7jcxrnwr
|
||||
@@ -0,0 +1,347 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 可选类供给能否按"格极稀 / 板≈0.78 / 战斗期望≈1(首件保证 + 指数递减)"三尺度被精确控制
|
||||
# Date: 2026-09-10
|
||||
# B4 奖励与道具经济:install 期身份预分配(roll_board 恰一次、消费 reward_roll 封闭流恰一 scope 批)·
|
||||
# 授予期零随机 · 库存关卡域(battle_over 全清)· 道具伤害恒定表值 · Ready 后只读。
|
||||
# 2026-09-11 可选类供给控制器批(用户拍板):格子池只剩即时类;可选类经 RewardSupplyController 注入
|
||||
# (战斗首件保证 + p0×decay^(j-1) 指数递减);旧 F5 格池刀保底(k_dmg)休眠;清图槽改一次性伤害。
|
||||
# 2026-09-14 伤害量值区间批(用户拍板②):区间行(power_min/max 可选配对)在身份预分配批内追加取号
|
||||
# (④ 步,既有取号序零扰动);快照 E14 口径不变——快照 power = roll 值,B1/呈现零改动;授予期零随机不变。
|
||||
# 切片登记:取号约定 = randi_range(0, W-1)(实现期 ADR 事实,切片内恒定)。
|
||||
# 实现 GDD:design/gdd/b4-reward-item-economy.md(系统 ID B4——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name RewardEconomy
|
||||
extends RefCounted
|
||||
|
||||
var db
|
||||
var rng
|
||||
var _supply: RewardSupplyController # 可选类供给状态机(组合件——战斗域计数)
|
||||
var _phase := "unrolled" # unrolled → ready → retired;clear_inventory 后 _inventory_cleared
|
||||
var _inventory_cleared := false
|
||||
var _eligible: Array = [] # [{cell, fog}] 行主序规范序
|
||||
var _mapping := {} # Vector2i → reward_id
|
||||
var _power_roll := {} # Vector2i → 区间行 roll 值(伤害量值区间批——身份批内追加取号)
|
||||
var _clear_power := -1 # 清图槽区间行 roll 值(-1 = 定值行)
|
||||
var _granted := {} # Vector2i → 快照(重复受理幂等返回)
|
||||
var _clear_identity := ""
|
||||
var _clear_granted := false
|
||||
var _clear_snapshot := {}
|
||||
var _inventory: Array = [] # 逐件实例 {item_id, snapshot, source_cell, round_index, status}
|
||||
|
||||
|
||||
func _init(db_ref, rng_ref) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
_supply = RewardSupplyController.new(db.reward_config)
|
||||
|
||||
|
||||
# ---- 战斗开始(B1.begin_battle 编排调用——B4 唯一调用方纪律不变)----
|
||||
# 供给控制器战斗域计数清零;库存(关卡域)不受影响——关内延续照旧
|
||||
func notify_battle_start() -> void:
|
||||
_supply.notify_battle_start()
|
||||
|
||||
|
||||
# ---- F1/F2/F5:install 期 roll(恰一次;Ready 相位再调拒绝)----
|
||||
|
||||
func roll_board(eligible: Array) -> bool:
|
||||
# 相位守卫:Ready 再调 = double-roll 拒绝(E5);Retired(换图后)= 合法新批重置(E6 roll 半)
|
||||
if _phase == "ready":
|
||||
push_warning("[B4] roll_board 相位守卫拒绝(E5——double-roll 编排违约显式暴露)")
|
||||
return false
|
||||
if eligible.is_empty():
|
||||
_fatal("roll_board 空资格清单(E1 兜底 fail-fast——A2 不变式④前置)")
|
||||
return false
|
||||
# 行主序规范化 + 去重(排序责任不外推给 B1)
|
||||
var seen := {}
|
||||
var list: Array = []
|
||||
for e in eligible:
|
||||
var c: Vector2i = e.cell
|
||||
if seen.has(c):
|
||||
push_warning("[B4] 资格清单重复格已去重: %s" % str(c))
|
||||
continue
|
||||
seen[c] = true
|
||||
list.append({"cell": c, "fog": bool(e.get("fog", false))})
|
||||
list.sort_custom(func(a, b): return a.cell.y * 1000 + a.cell.x < b.cell.y * 1000 + b.cell.x)
|
||||
_eligible = list
|
||||
_granted.clear() # 新批重置映射受理面(旧图快照消亡)
|
||||
_power_roll.clear()
|
||||
_clear_power = -1
|
||||
_clear_granted = false
|
||||
_clear_snapshot = {}
|
||||
var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(身份批 + 清图槽 + 注入判定/选格 + 区间行 power roll——追加式取号,既有序零扰动)
|
||||
var safe_rows: Array = _pool_rows("safe_cell")
|
||||
var clear_rows: Array = _pool_rows("clear_default")
|
||||
var inject_rows: Array = _pool_rows("active_inject")
|
||||
# ① 身份批:每资格格恰一即时类身份(可选类已退出格子池——供给控制器批;雾档过滤不变)
|
||||
var draft := {}
|
||||
for e in list:
|
||||
var pool := safe_rows.filter(func(row):
|
||||
if e.fog and int(row.tier) < int(db.reward_config.t_fog_min):
|
||||
return false # F2 雾档过滤
|
||||
return true)
|
||||
draft[e.cell] = weighted_pick(pool, stream.randi_range(0, _pool_weight(pool) - 1))
|
||||
# ② 清图槽:一次性高额伤害(当场兑付类——不入库、不占可选类供给;2026-09-11 用户拍板)
|
||||
var clear_rid := weighted_pick(clear_rows, stream.randi_range(0, _pool_weight(clear_rows) - 1))
|
||||
# ③ 可选类供给注入(替代旧 F5 刀保底):首件保证 / p0×decay^(j-1) 指数递减——判定恰一取号
|
||||
if _supply.should_inject(stream):
|
||||
var target = list[stream.randi_range(0, list.size() - 1)] # 均匀随机(拍板③——雾格不加权)
|
||||
draft[target.cell] = weighted_pick(inject_rows, stream.randi_range(0, _pool_weight(inject_rows) - 1))
|
||||
# ④ 伤害量值区间批(2026-09-14):身份定稿后逐格 roll power(区间行恰一取号,定值行零取号);
|
||||
# 授予期零随机纪律不变——快照 E14 口径,B1/呈现零改动
|
||||
for cell in draft.keys():
|
||||
var row: Dictionary = _row_by_id(draft[cell])
|
||||
if row.has("power_min"):
|
||||
_power_roll[cell] = stream.randi_range(int(row.power_min), int(row.power_max))
|
||||
var clear_row: Dictionary = _row_by_id(clear_rid)
|
||||
if clear_row.has("power_min"):
|
||||
_clear_power = stream.randi_range(int(clear_row.power_min), int(clear_row.power_max))
|
||||
_mapping = draft
|
||||
_clear_identity = clear_rid
|
||||
_phase = "ready"
|
||||
return true
|
||||
|
||||
|
||||
# ---- 关卡节点奖励(2026-09-11 关卡扩展批——线性奖励点,正式归属 B4/C1 修订)----
|
||||
# 奖励点 = 用户拍板的关卡节点类型:在本表全部可选类活行(damage_item / heal_pack,即"主动道具")
|
||||
# 中 roll 一件并直接入库。消耗 reward_roll 恰一 scope(与 board install 批同流异 scope——流集合封闭不变)。
|
||||
# cell=(-1,-1)/round_index=0 = 非格子来源标记(关卡节点授予,区别于安全格拾取的呈现面)。
|
||||
func grant_node_active(cell: Vector2i, round_index: int) -> Dictionary:
|
||||
var pool: Array = []
|
||||
for row in db.reward.rows:
|
||||
if int(row.weight) > 0 and (String(row.category) == "damage_item" or String(row.category) == "heal_pack"):
|
||||
pool.append(row)
|
||||
if pool.is_empty():
|
||||
_fatal("节点奖励池为空(damage_item/heal_pack 零活行)")
|
||||
return {}
|
||||
var stream: RandomNumberGenerator = rng.open_stream("reward_roll")
|
||||
var rid := weighted_pick(pool, stream.randi_range(0, _pool_weight(pool) - 1))
|
||||
var row: Dictionary = _row_by_id(rid)
|
||||
var rolled := -1
|
||||
if row.has("power_min"):
|
||||
rolled = stream.randi_range(int(row.power_min), int(row.power_max)) # 区间行恰一追加取号(伤害量值区间批)
|
||||
var inst = stock(_snapshot(row, rolled), cell, round_index)
|
||||
if inst is Dictionary:
|
||||
_supply.notify_granted() # 节点奖励 = 可选类实际发放——与 grant() 同一入账点(playtest-2026-09-12 报告 Bug#3:绕过供给计数修正)
|
||||
return inst
|
||||
|
||||
|
||||
# ---- 查询面(Ready 只读)----
|
||||
|
||||
func query_identity(cell: Vector2i):
|
||||
if _phase != "ready":
|
||||
_diag("query_identity 非 Ready 相位")
|
||||
return null
|
||||
var rid: String = _mapping.get(cell, "")
|
||||
if rid == "":
|
||||
_diag("query_identity 清单外坐标(E19——未分配非错误态)")
|
||||
return null
|
||||
var row: Dictionary = _row_by_id(rid)
|
||||
return {"reward_id": rid, "category": String(row.category), "tier": int(row.tier)}
|
||||
|
||||
|
||||
# 身份完整快照(轮内刷新批次隔离用——facts 缓冲期捕获,授予期 mapping 可能已换批):
|
||||
# 返回与 grant 相同结构的快照(display_name/power/effect_id 齐备),供 B1 冻结授予直通
|
||||
func query_full_snapshot(cell: Vector2i):
|
||||
if _phase != "ready":
|
||||
_diag("query_full_snapshot 非 Ready 相位")
|
||||
return null
|
||||
var rid: String = _mapping.get(cell, "")
|
||||
if rid == "":
|
||||
return null
|
||||
return _snapshot(_row_by_id(rid), int(_power_roll.get(cell, -1)))
|
||||
|
||||
|
||||
# ---- 受理面(B1 编排期调用;E6 收口 2026-09-13:失败统一返回 null——此前 false/null 混用)----
|
||||
|
||||
func grant(cell: Vector2i):
|
||||
if _phase != "ready":
|
||||
_diag("grant 非 Ready 相位(E4/E7)")
|
||||
return null
|
||||
if not _mapping.has(cell):
|
||||
_diag("grant 清单外坐标(E19)")
|
||||
return null
|
||||
if _granted.has(cell):
|
||||
_diag("grant 同格重复受理(E18 幂等兜底——受理恰一次归 B1)")
|
||||
return _granted[cell]
|
||||
var row: Dictionary = _row_by_id(_mapping[cell])
|
||||
var snap := _snapshot(row, int(_power_roll.get(cell, -1)))
|
||||
if String(snap.category) == "damage_item" or String(snap.category) == "heal_pack":
|
||||
_supply.notify_granted() # 发放即获取——首件保证/递减计数的唯一入账点(发放侧语义,满血拒"使用"不影响)
|
||||
_granted[cell] = snap
|
||||
return snap
|
||||
|
||||
|
||||
func grant_clear():
|
||||
if _phase != "ready":
|
||||
_diag("grant_clear 非 Ready 相位")
|
||||
return null
|
||||
if _clear_granted:
|
||||
_diag("grant_clear 同批次重调(E18 幂等兜底)")
|
||||
return _clear_snapshot
|
||||
var row: Dictionary = _row_by_id(_clear_identity)
|
||||
_clear_snapshot = _snapshot(row, _clear_power)
|
||||
_clear_granted = true
|
||||
return _clear_snapshot
|
||||
|
||||
|
||||
func stock(snapshot: Dictionary, source_cell: Vector2i, round_index: int):
|
||||
if _inventory_cleared:
|
||||
_diag("stock 战斗已清库(E7)")
|
||||
return null
|
||||
var cat: String = String(snapshot.get("category", ""))
|
||||
if cat != "damage_item" and cat != "heal_pack":
|
||||
_diag("stock 仅受理道具类(E20): %s" % cat)
|
||||
return null
|
||||
var inst := {
|
||||
"item_id": String(snapshot.get("reward_id", "")),
|
||||
"snapshot": snapshot,
|
||||
"source_cell": source_cell,
|
||||
"round_index": round_index,
|
||||
"status": "InStock",
|
||||
}
|
||||
_inventory.append(inst) # 数量无上限;枚举序 = stock 受理序(E21)
|
||||
return inst
|
||||
|
||||
|
||||
# 窗口选用消耗:零游戏态守卫(照常消耗——E10;满血/零效果守卫归 B1)
|
||||
func use(index: int):
|
||||
if _inventory_cleared:
|
||||
_diag("use 战斗已清库")
|
||||
return null
|
||||
if index < 0 or index >= _inventory.size():
|
||||
_diag("use 句柄越界")
|
||||
return null
|
||||
var inst: Dictionary = _inventory[index]
|
||||
if inst.status != "InStock":
|
||||
_diag("use 已消耗句柄(E9 Used 不可逆)")
|
||||
return null
|
||||
inst.status = "Used"
|
||||
return {
|
||||
"item_id": inst.item_id,
|
||||
"category": String(inst.snapshot.get("category", "")),
|
||||
"power": int(inst.snapshot.get("power", 0)), # 授予时快照值(E14——非表读值)
|
||||
"source_cell": inst.source_cell,
|
||||
"round_index": inst.round_index,
|
||||
}
|
||||
|
||||
|
||||
func query_inventory() -> Array:
|
||||
var out: Array = []
|
||||
for i in _inventory.size():
|
||||
var inst: Dictionary = _inventory[i]
|
||||
out.append({
|
||||
"index": i,
|
||||
"item_id": inst.item_id,
|
||||
"category": String(inst.snapshot.get("category", "")),
|
||||
"display_name": String(inst.snapshot.get("display_name", "")),
|
||||
"power": int(inst.snapshot.get("power", 0)),
|
||||
"source_cell": inst.source_cell,
|
||||
"round_index": inst.round_index,
|
||||
"status": inst.status,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
func in_stock_count() -> int:
|
||||
var n := 0
|
||||
for inst in _inventory:
|
||||
if inst.status == "InStock":
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
# 观测面(冒烟/调参工具——本战斗已实际获取的可选类件数)
|
||||
func supply_granted_count() -> int:
|
||||
return _supply.granted_count()
|
||||
|
||||
|
||||
func clear_inventory() -> bool:
|
||||
if _inventory_cleared:
|
||||
_diag("clear_inventory 重复调用(幂等守卫)")
|
||||
return false
|
||||
_inventory.clear() # battle_over 全清不跨战斗
|
||||
_inventory_cleared = true
|
||||
return true
|
||||
|
||||
|
||||
func retire_mapping() -> bool:
|
||||
if _phase != "ready":
|
||||
_diag("retire_mapping 非 Ready 相位(幂等受理族)")
|
||||
return false
|
||||
_phase = "retired"
|
||||
_mapping.clear()
|
||||
_power_roll.clear()
|
||||
_clear_power = -1
|
||||
_clear_identity = ""
|
||||
return true
|
||||
|
||||
|
||||
# ---- F6 当量供给(B1 条款①对照实物输入)----
|
||||
|
||||
func equivalence_supply() -> Dictionary:
|
||||
var live: Array = []
|
||||
var tier_min_buff := 99
|
||||
for row in db.reward.rows:
|
||||
if int(row.weight) <= 0:
|
||||
continue
|
||||
live.append(row.duplicate()) # E6:表行值拷贝出口——消费方禁改配置源
|
||||
if row.pool_tag == "safe_cell" and row.category == "buff":
|
||||
tier_min_buff = mini(tier_min_buff, int(row.tier))
|
||||
return {"live_rows": live, "tier_min_buff": tier_min_buff}
|
||||
|
||||
|
||||
# ---- F1 选行算子纯函数 ----
|
||||
|
||||
func weighted_pick(rows: Array, r: int) -> String:
|
||||
var total := _pool_weight(rows)
|
||||
assert(r >= 0 and r < total, "weighted_pick 取号越域(纯函数域守卫)")
|
||||
var acc := 0
|
||||
for row in rows:
|
||||
acc += int(row.weight)
|
||||
if r < acc:
|
||||
return String(row.reward_id)
|
||||
_fatal("weighted_pick 未命中(不应可达)")
|
||||
return ""
|
||||
|
||||
|
||||
func _pool_rows(pool_tag: String) -> Array:
|
||||
var out: Array = []
|
||||
for row in db.reward.rows:
|
||||
if row.pool_tag == pool_tag and int(row.weight) > 0:
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
func _pool_weight(rows: Array) -> int:
|
||||
var w := 0
|
||||
for row in rows:
|
||||
w += int(row.weight)
|
||||
return w
|
||||
|
||||
|
||||
func _row_by_id(rid: String) -> Dictionary:
|
||||
for row in db.reward.rows:
|
||||
if row.reward_id == rid:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
func _snapshot(row: Dictionary, rolled_power: int = -1) -> Dictionary:
|
||||
var snap := {
|
||||
"reward_id": String(row.reward_id),
|
||||
"category": String(row.category),
|
||||
"display_name": String(row.display_name),
|
||||
"power": int(row.power) if rolled_power < 0 else rolled_power, # 区间行 = 身份批 roll 值(E14 授予时快照口径)
|
||||
"tier": int(row.tier),
|
||||
}
|
||||
if String(row.get("effect_id", "")) != "":
|
||||
snap["effect_id"] = String(row.effect_id)
|
||||
return snap
|
||||
|
||||
|
||||
func _diag(msg: String) -> void:
|
||||
push_warning("[B4] " + msg)
|
||||
|
||||
|
||||
func _fatal(msg: String) -> void:
|
||||
push_error("[B4/fatal] " + msg)
|
||||
assert(false, msg)
|
||||
@@ -0,0 +1 @@
|
||||
uid://5vj3p6fmval7
|
||||
@@ -0,0 +1,43 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 可选类供给能否按"格极稀 <0.01 / 板≈0.78 / 战斗期望≈1(首件保证 + 指数递减)"三尺度被精确控制
|
||||
# Date: 2026-09-11
|
||||
# 可选类供给控制器(2026-09-11 用户拍板批——B4 私有组合件):
|
||||
# 格子权重池不再产出可选类;本控制器在 install 期决定"本板是否注入 1 件可选类"——
|
||||
# 战斗内尚未获取首件 → 本板必注入(首件保证);已获取 j 件 → 概率 p0×decay^(j-1)(指数下降、永不绝育)。
|
||||
# 行值快照(p0/decay 构造期快照——抗热重载,B2 同款);零自有随机数(判定取号经 B4 传入的 reward_roll
|
||||
# 封闭流实例——Rule 5);"获取"以实际发放入库为准(预分配未挖到不计数——授予侧经 B4.grant 挂钩上报)。
|
||||
# 实现 GDD:design/gdd/b4-reward-item-economy.md(系统 ID B4——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name RewardSupplyController
|
||||
extends RefCounted
|
||||
|
||||
var _granted := 0 # 本战斗内已实际获取的可选类件数(战斗域——notify_battle_start 清零)
|
||||
var _p0 := 0.4
|
||||
var _decay := 0.5
|
||||
|
||||
|
||||
func _init(reward_config) -> void:
|
||||
_p0 = clampf(float(reward_config.active_p0), 0.0, 1.0)
|
||||
_decay = clampf(float(reward_config.active_decay), 0.001, 1.0)
|
||||
|
||||
|
||||
# B1.begin_battle 编排调用(B4 转发)——战斗域计数重置;跨战斗库存延续不受影响
|
||||
func notify_battle_start() -> void:
|
||||
_granted = 0
|
||||
|
||||
|
||||
# install 期注入判定(B4.roll_board 在身份批 + 清图槽之后调用;取号 = 传入封闭流恰一次)
|
||||
func should_inject(stream: RandomNumberGenerator) -> bool:
|
||||
if _granted == 0:
|
||||
return true # 首件保证
|
||||
var p: float = _p0 * pow(_decay, float(_granted - 1))
|
||||
return stream.randf() < p
|
||||
|
||||
|
||||
# B4.grant 实际发放可选类时上报(发放即获取;满血拒"使用"不影响——那是消耗侧语义)
|
||||
func notify_granted() -> void:
|
||||
_granted += 1
|
||||
|
||||
|
||||
# 观测面(冒烟/调参工具)
|
||||
func granted_count() -> int:
|
||||
return _granted
|
||||
@@ -0,0 +1 @@
|
||||
uid://de40p45nneaw1
|
||||
@@ -0,0 +1,88 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 RngService autoload——封闭命名流单点(Rule 5)。玩法代码禁止 randomize()/自建 RandomNumberGenerator。
|
||||
# 实现 GDD:design/gdd/d2-data-config.md(系统 ID D2——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# R2 派生公式(黄金向量已预验证):derive = (SALT XOR wrap64(master + scope×GOLDEN)) AND 0x7FFF…
|
||||
# 取号语义:全新实例首个 open_stream = scope 0(B4 预推进术语的登记口径)。
|
||||
extends Node
|
||||
|
||||
const STREAM_NAMES := ["board_gen", "reward_roll", "encounter"] # 2026-09-12 增 encounter(遭遇随机化批,用户拍板——registry rng_service 条目同步)
|
||||
|
||||
# 分段表达式书写(D2 R2 实现级规约——禁 ≥2^63 完整字面量)
|
||||
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15
|
||||
const SALT_BOARD_GEN := (0xA0761D64 << 32) | 0x78BD642F # 0xA0761D6478BD642F
|
||||
const SALT_REWARD_ROLL := (0xE7037ED1 << 32) | 0xA0B428DB # 0xE7037ED1A0B428DB
|
||||
const SALT_ENCOUNTER := (0x5DEECE66D << 32) | 0x2545F491 # 0x5DEECE66D2545F491(遭遇骰——2026-09-12)
|
||||
const OUT_MASK := 0x7FFFFFFFFFFFFFFF
|
||||
const INT64_MIN := -9223372036854775808
|
||||
|
||||
# master_seed 只读出口(E5 封装收口 2026-09-13):写入只经 set_master_seed(计数器全零断言
|
||||
# 不可绕过);外部读 master_seed 供 D3 快照契约观测。
|
||||
var _master_seed: int = -1 # -1 = 未播种(取数即致命,EC7)
|
||||
var master_seed: int:
|
||||
get: return _master_seed
|
||||
var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0}
|
||||
|
||||
|
||||
func _salt_for(stream: String) -> int:
|
||||
match stream:
|
||||
"board_gen":
|
||||
return SALT_BOARD_GEN
|
||||
"reward_roll":
|
||||
return SALT_REWARD_ROLL
|
||||
"encounter":
|
||||
return SALT_ENCOUNTER
|
||||
push_error("[RngService] 未知名流: %s" % stream)
|
||||
assert(false, "未知名流(流集合封闭,EC12)")
|
||||
return 0
|
||||
|
||||
|
||||
# 纯函数(测试/预览入口,不推计数器)
|
||||
func derive_at(stream: String, scope: int) -> int:
|
||||
assert(scope >= 0, "负 scope 与 scope+2^63 经输出掩码别名(禁止)")
|
||||
var t: int = _master_seed + scope * GOLDEN_RATIO # wrap64 自然回绕——禁 float 中间量/禁 %/禁主动修复
|
||||
var seed_val: int = _salt_for(stream) ^ t
|
||||
return seed_val & OUT_MASK
|
||||
|
||||
|
||||
func set_master_seed(seed_val: int) -> void:
|
||||
if seed_val == INT64_MIN:
|
||||
push_error("[RngService] master_seed = INT64_MIN 致命(abs 恒等回绕,与 0 静默别名)")
|
||||
assert(false, "INT64_MIN 致命")
|
||||
return
|
||||
if seed_val < 0:
|
||||
seed_val = -seed_val
|
||||
push_warning("[RngService] master_seed 负输入 abs 归一化: %d" % seed_val)
|
||||
assert(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)")
|
||||
_master_seed = seed_val
|
||||
_counters["board_gen"] = 0
|
||||
_counters["reward_roll"] = 0
|
||||
|
||||
|
||||
# 切片登记:冒险开始重播种入口(正式侧调用权归 C2;切片主菜单 = 冒险起点)
|
||||
func new_adventure(seed_val: int) -> void:
|
||||
_counters["board_gen"] = 0
|
||||
_counters["reward_roll"] = 0
|
||||
_counters["encounter"] = 0
|
||||
set_master_seed(seed_val)
|
||||
|
||||
|
||||
# 有状态流门面:一次调用 = 一次 scope 取号(计数器读值 → 派生 → +1)
|
||||
func open_stream(stream: String) -> RandomNumberGenerator:
|
||||
if not STREAM_NAMES.has(stream):
|
||||
push_error("[RngService] 未知名流: %s" % stream)
|
||||
assert(false, "未知名流(EC12)")
|
||||
if _master_seed < 0:
|
||||
push_error("[RngService] 未设 master_seed 即取数(EC7——C2 未接线的显式暴露)")
|
||||
assert(false, "未播种流取数致命(EC7)")
|
||||
var scope: int = _counters[stream]
|
||||
_counters[stream] = scope + 1
|
||||
var rg := RandomNumberGenerator.new()
|
||||
rg.seed = derive_at(stream, scope)
|
||||
return rg
|
||||
|
||||
|
||||
# 只读计数器(D3 快照契约的观测面)
|
||||
func counter(stream: String) -> int:
|
||||
return _counters[stream]
|
||||
@@ -0,0 +1 @@
|
||||
uid://tcp13u2etw2n
|
||||
@@ -0,0 +1,137 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# B3 统一状态容器:三向效果同构实例 · 单一时限量纲 remaining_settlements(先兑付后衰减)·
|
||||
# 按行叠层(stack 授予不触碰 R)· 授予时数值快照抗热重载 · 零随机零信号。
|
||||
# 注:MVP 单敌假设经 B2 回验不成立(定多敌)——enemy 侧 side-wide 语义按 registry 改写执行。
|
||||
# 实现 GDD:design/gdd/b3-status-effect.md(系统 ID B3——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name StatusContainer
|
||||
extends RefCounted
|
||||
|
||||
var db
|
||||
var _open := false
|
||||
var _instances: Array = [] # 插入序;元素 = {effect_id, layers, remaining, source_class, origin_cell, round_index, snap_m, snap_unit, snap_display}
|
||||
|
||||
|
||||
func _init(db_ref) -> void:
|
||||
db = db_ref
|
||||
|
||||
|
||||
func notify_battle_start() -> bool:
|
||||
if _open:
|
||||
push_warning("[B3] Active 下重复 notify_battle_start(EC11)")
|
||||
return false
|
||||
_open = true
|
||||
_instances.clear()
|
||||
return true
|
||||
|
||||
|
||||
# 唯一授予入口(挖掘期零直写——B1 结算编排期统一调用)
|
||||
func apply_effect(effect_id: String, source: Dictionary) -> bool:
|
||||
if not _open:
|
||||
push_warning("[B3] 域闭授予(EC1)")
|
||||
return false
|
||||
var row: Dictionary = db.status_effect_row(effect_id)
|
||||
if row.is_empty():
|
||||
push_warning("[B3] 未知 effect_id(EC2): %s" % effect_id)
|
||||
return false
|
||||
var policy: String = row.stack_policy
|
||||
var dur: int = int(row.duration_settlements)
|
||||
var max_stack: int = int(row.max_stack)
|
||||
# 授予时快照(抗表重载——EC9)
|
||||
var snap_m: int = int(row.magnitude)
|
||||
var snap_unit: String = String(row.unit)
|
||||
var snap_display: String = String(row.display_name)
|
||||
match policy:
|
||||
"independent":
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
|
||||
return true
|
||||
"refresh":
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
inst.remaining = dur # R ← D(取实例快照 D——同表值);L 不变;空刷新恒 true
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
|
||||
return true
|
||||
"stack":
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
if inst.layers >= max_stack:
|
||||
return false # 满层拒绝原子——容器零写入(溢出行归 B1)
|
||||
inst.layers += 1 # stack 授予不触碰 R(spec 决议)
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
|
||||
return true
|
||||
push_warning("[B3] 未知 stack_policy")
|
||||
return false
|
||||
|
||||
|
||||
func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary, m: int, unit: String, display: String) -> Dictionary:
|
||||
return {
|
||||
"effect_id": effect_id,
|
||||
"layers": layers,
|
||||
"remaining": dur,
|
||||
"source_class": String(source.get("source_class", "")),
|
||||
"origin_cell": source.get("origin_cell", Vector2i(-1, -1)),
|
||||
"round_index": int(source.get("round_index", 0)),
|
||||
"snap_m": m,
|
||||
"snap_unit": unit,
|
||||
"snap_display": display,
|
||||
}
|
||||
|
||||
|
||||
# 只读投影(插入序稳定枚举;六归因字段 + display_name + 授予时快照)
|
||||
func query_active(side: String) -> Array:
|
||||
var out: Array = []
|
||||
for inst in _instances:
|
||||
var row: Dictionary = db.status_effect_row(inst.effect_id)
|
||||
if row.is_empty():
|
||||
continue
|
||||
if String(row.target_side) != side:
|
||||
continue
|
||||
out.append({
|
||||
"effect_id": inst.effect_id,
|
||||
"layers": inst.layers,
|
||||
"remaining_settlements": inst.remaining,
|
||||
"source_class": inst.source_class,
|
||||
"origin_cell": inst.origin_cell,
|
||||
"round_index": inst.round_index,
|
||||
"display_name": inst.snap_display,
|
||||
"snap_magnitude": inst.snap_m,
|
||||
"snap_unit": inst.snap_unit,
|
||||
"effective_value": inst.layers * inst.snap_m, # L×m 加算(percent 越域不 clamp——收口归 B1)
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
# 池层数读值(B1 战斗段伏击池 L 的消费面)
|
||||
func pool_layers(effect_id: String) -> int:
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
return inst.layers
|
||||
return 0
|
||||
|
||||
|
||||
# 轮末衰减:先兑付后衰减;每实例恰 −1;归 0 移除;返回移除数 k(恰一次执行由 B1 编排信任承载)
|
||||
func decay_round() -> int:
|
||||
if not _open:
|
||||
return 0 # EC13 域闭兜底读法——容器恒空,无独立分支不诊断
|
||||
var k := 0
|
||||
var survivors: Array = []
|
||||
for inst in _instances:
|
||||
inst.remaining -= 1
|
||||
if inst.remaining <= 0:
|
||||
k += 1
|
||||
else:
|
||||
survivors.append(inst)
|
||||
_instances = survivors
|
||||
return k
|
||||
|
||||
|
||||
func notify_battle_end() -> bool:
|
||||
if not _open:
|
||||
push_warning("[B3] BattleClosed 下重复 notify_battle_end(EC11)")
|
||||
return false
|
||||
_open = false
|
||||
_instances.clear() # 全清不跨战斗
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://dlck3xxei0t61
|
||||
@@ -0,0 +1,194 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# B2 战斗单位系统:hero/enemy 统一受伤死亡接口 · 行为环确定性轮转(死亡即不动手/先作用后推进)·
|
||||
# 行值快照抗热重载 · side-wide 伏击池读值由调用方注入(B2 无池写面)· 编排被动零事件。
|
||||
# 切片登记接口漂移:heal() 为切片新增(B1 治疗 clamp 需要写入口;B2 API 封闭枚举原本无治疗面——
|
||||
# 正式侧应走 B2 GDD 修订,切片以头注登记)。
|
||||
# 实现 GDD:design/gdd/b2-enemy-system.md(系统 ID B2——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name UnitContainer
|
||||
extends RefCounted
|
||||
|
||||
var db
|
||||
# 单位容器私有(E3 封装收口 2026-09-13):hero/enemies 原始数组不再外露——读取经 hero_unit()/
|
||||
# all_enemies() 投影面(死亡残位查询包含在内),写入只经 apply_damage/heal/advance/spawn/despawn。
|
||||
# 句柄语义不变:投影返回的 Dictionary 引用即单位句柄(同 query_alive 先例)。
|
||||
var _hero: Dictionary = {} # {hp, max_hp, atk, is_hero: true}
|
||||
var _enemies: Array = [] # 行主序生成序;元素 = 单位 Dictionary
|
||||
var _spawned := false
|
||||
var _despawned := false
|
||||
|
||||
|
||||
func _init(db_ref) -> void:
|
||||
db = db_ref
|
||||
|
||||
|
||||
# E4 收口(2026-09-13):加成值经参数显式传入(atk_override/max_hp_override,-1 = 表值兜底)——
|
||||
# 此前调用方写 db.hero 表值再由本类读表 = 共享可变单例传参(双写叠加 6→18→54 教训根源)。
|
||||
# 接口漂移登记:spawn 签名 +2 参数(B2 GDD API 封闭枚举——正式侧 GDD/registry 修订批随 ADR-0009)。
|
||||
# enemy_atk_override(2026-09-14 敌方攻击区间批):B1 遭遇骰同流的 spawn 期整战持久 roll 结果
|
||||
# (-1 = 表值定值路径;≥0 = 行值替换后再乘关卡 atk_mult——修饰语义不变)。B2 零随机纪律保持。
|
||||
func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override: int = -1,
|
||||
atk_override: int = -1, max_hp_override: int = -1, enemy_atk_override: int = -1) -> bool:
|
||||
if _spawned:
|
||||
push_warning("[B2] Alive 相位重复 spawn(相位守卫)")
|
||||
return false
|
||||
var row: Dictionary = db.resolve_enemy_rowset(level_index, node_depth)
|
||||
if row.is_empty():
|
||||
push_error("[B2] 遭遇行解析失败/越域 (%d,%d)" % [level_index, node_depth])
|
||||
assert(false, "遭遇行悬空(R1 前置守卫)")
|
||||
return false
|
||||
var eff_atk: int = atk_override if atk_override >= 1 else int(db.hero.atk)
|
||||
var eff_max_hp: int = max_hp_override if max_hp_override >= 1 else int(db.hero.max_hp)
|
||||
if hero_hp > eff_max_hp or hero_hp <= 0:
|
||||
push_warning("[B2] hero_hp 入参越界(EC16): %d" % hero_hp)
|
||||
return false
|
||||
# max_hp = 生效上限(非入参 hero_hp)——2026-09-11 关卡扩展批修复:跨战斗携带 HP 时上限不得压扁
|
||||
# (恢复点/治疗包的满血守卫与治疗上限随之失效——切片登记接口语义澄清)
|
||||
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": "探险者", "steps": 0, "pattern": [], "enemy_id": "hero"}
|
||||
_enemies.clear() # C3 DifficultyTable 修饰(2026-09-12 结构位兑现):施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10)
|
||||
var mod: Dictionary = db.resolve_difficulty_modifiers(level_index, node_depth)
|
||||
var hp_mult: float = float(mod.get("hp_mult", 1.0))
|
||||
var atk_mult: float = float(mod.get("atk_mult", 1.0))
|
||||
# unit_count_override ≥1 = B1 遭遇骰结果(B2 零随机纪律——收到即确定值;-1 = 行值定值路径)
|
||||
var count: int = unit_count_override if unit_count_override >= 1 else int(row.unit_count)
|
||||
if count < 1 or count > 3:
|
||||
push_warning("[B2] 实例化数量越域: %d" % count)
|
||||
return false
|
||||
for i in range(count):
|
||||
var eff_row_atk: int = enemy_atk_override if enemy_atk_override >= 0 else int(row.atk)
|
||||
_enemies.append({
|
||||
"handle_id": i,
|
||||
"enemy_id": String(row.enemy_id),
|
||||
"display_name": String(row.display_name),
|
||||
"hp": maxi(1, int(round(float(row.hp) * hp_mult))),
|
||||
"max_hp": maxi(1, int(round(float(row.hp) * hp_mult))),
|
||||
"atk": maxi(0, int(round(float(eff_row_atk) * atk_mult))),
|
||||
"heavy_multiplier": int(row.heavy_multiplier),
|
||||
"pattern": Array(row.pattern), # 行值快照(拷贝)
|
||||
"advantage_per_round_cap": int(row.advantage_per_round_cap),
|
||||
"armor_type": String(row.get("armor_type", "none")), # 护甲类型快照(克制矩阵消费面——B1 战斗段)
|
||||
"steps": 0,
|
||||
"alive": true,
|
||||
"is_hero": false,
|
||||
})
|
||||
_spawned = true
|
||||
_despawned = false
|
||||
return true
|
||||
|
||||
|
||||
func query_alive(side: String) -> Array:
|
||||
if _despawned:
|
||||
_diag("Despawned 句柄查询(域闭)")
|
||||
return []
|
||||
if side == "hero":
|
||||
return [] if (_hero.is_empty() or not _hero.alive) else [_hero]
|
||||
var out: Array = []
|
||||
for u in _enemies:
|
||||
if u.alive:
|
||||
out.append(u)
|
||||
return out
|
||||
|
||||
|
||||
# 当前拍(Dead 句柄保留只读可查——冻结不变式观测量;hero 句柄拒绝)
|
||||
func query_beat(u: Dictionary) -> String:
|
||||
if u.is_empty() or _despawned:
|
||||
_diag("query_beat 失效句柄")
|
||||
return ""
|
||||
if u.get("is_hero", false):
|
||||
_diag("hero 无行为环(环接口对主角句柄拒绝)")
|
||||
return ""
|
||||
var p: Array = u.pattern
|
||||
return String(p[u.steps % p.size()])
|
||||
|
||||
|
||||
# F2 分量供给面:{baseline, attach, total};L/m_amb 由调用方注入(判别探针,不校验游戏态域)
|
||||
func query_attack(u: Dictionary, L: int, m_amb: int) -> Dictionary:
|
||||
if u.is_empty() or _despawned or not u.alive:
|
||||
_diag("query_attack 失效/死亡句柄")
|
||||
return {"baseline": 0, "attach": 0, "total": 0}
|
||||
var beat := query_beat(u)
|
||||
if beat == "charge":
|
||||
return {"baseline": 0, "attach": 0, "total": 0} # charge 拍不读伏击池——空拍零兑付
|
||||
var baseline: int = u.atk
|
||||
if beat == "heavy":
|
||||
baseline = u.atk * int(u.heavy_multiplier)
|
||||
return {"baseline": baseline, "attach": L * m_amb, "total": baseline + L * m_amb}
|
||||
|
||||
|
||||
# 受击上报:hp 递减 floor 0 → Dead;n ≤ 0 拒绝
|
||||
func apply_damage(u: Dictionary, n: int) -> bool:
|
||||
if n <= 0:
|
||||
_diag("apply_damage n ≤ 0 非法")
|
||||
return false
|
||||
if u.is_empty() or _despawned or not u.alive:
|
||||
_diag("apply_damage 失效/死亡句柄")
|
||||
return false
|
||||
u.hp = maxi(0, u.hp - n)
|
||||
if u.hp == 0:
|
||||
u.alive = false # Dead 不可逆(死亡判定时机归 B1)
|
||||
return true
|
||||
|
||||
|
||||
# 治疗(切片登记接口漂移——B1 治疗 clamp 的写入口):返回实际回复量
|
||||
func heal(u: Dictionary, n: int) -> int:
|
||||
if n < 0 or u.is_empty() or _despawned or not u.alive:
|
||||
_diag("heal 非法/失效句柄")
|
||||
return 0
|
||||
var actual: int = mini(n, int(u.max_hp) - int(u.hp))
|
||||
u.hp += actual
|
||||
return actual
|
||||
|
||||
|
||||
# 推进:存活才推进;false 两级——预期冻结(无诊断)/ 失效句柄(诊断);hero 拒绝
|
||||
func advance(u: Dictionary) -> bool:
|
||||
if u.is_empty() or _despawned:
|
||||
_diag("advance 失效句柄")
|
||||
return false
|
||||
if u.get("is_hero", false):
|
||||
_diag("hero 不在推进域(主角无行为环)")
|
||||
return false
|
||||
if not u.alive:
|
||||
return false # 轮到前被杀——预期冻结(死亡即不动手),无诊断
|
||||
u.steps += 1
|
||||
return true
|
||||
|
||||
|
||||
# 固定顺位:行主序生成序最前存活敌;死亡后顺延;空 → null
|
||||
func default_target():
|
||||
for u in _enemies:
|
||||
if u.alive:
|
||||
return u
|
||||
return null
|
||||
|
||||
|
||||
# ---- 只读投影面(E3 封装收口:外部读单位经此,禁直读容器数组)----
|
||||
|
||||
# 主角句柄(未 spawn/despawn 后 = {})——句柄即 Dictionary 引用(同 query_alive 先例)
|
||||
func hero_unit() -> Dictionary:
|
||||
return _hero
|
||||
|
||||
|
||||
# 全体敌人句柄(含死亡残位——呈现层残位渲染 / 白盒断言的合法入口;数组结构拷贝、字典句柄同源)
|
||||
func all_enemies() -> Array:
|
||||
return _enemies.duplicate()
|
||||
|
||||
|
||||
# F3 机制算术纯函数(B1 预截流消费面):cap 判定在授予前;守恒 granted + overflow = mines_r
|
||||
func ambush_grant_plan(mines_r: int, cap: int) -> Dictionary:
|
||||
var granted: int = mini(mines_r, cap)
|
||||
return {"granted": granted, "overflow": mines_r - granted}
|
||||
|
||||
|
||||
func despawn_all() -> bool:
|
||||
if _despawned or not _spawned:
|
||||
_diag("重复/无效 despawn_all")
|
||||
return false
|
||||
_despawned = true
|
||||
_enemies.clear()
|
||||
_hero = {}
|
||||
return true
|
||||
|
||||
|
||||
func _diag(msg: String) -> void:
|
||||
push_warning("[B2] " + msg)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dek3bsxksloan
|
||||
@@ -0,0 +1,41 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 ActionEconomyTable schema(A3 回填值,单行全局;Rule 4 豁免)。
|
||||
# 数值域与跨字段校验依据 design/gdd/a3-action-economy.md "ActionEconomyTable Schema 定稿"。
|
||||
class_name ActionEconomyTable
|
||||
extends Resource
|
||||
|
||||
@export var ap_per_round: int = 8 # [1,32] knob
|
||||
@export var dig_ap_cost: int = 1 # [1,8] knob(域下限 1 = 零预算系统失效拒绝)
|
||||
@export var flag_place_ap_cost: int = 1 # [0,8] knob
|
||||
@export var flag_pull_ap_cost: int = 1 # [0,8] knob
|
||||
@export var rap_refresh_cost: int = 8 # [1,64] knob
|
||||
@export var refresh_max_per_round: int = 2 # [1,8] knob(2026-09-12 轮内刷新批——每轮刷新兜底上限,Active/Waiting 合计)
|
||||
@export var move_ap_cost: int = 0 # 冻结 0(P4 氛围承载)
|
||||
@export var chain_ap_cost: int = 0 # 冻结 0(v2 定稿)
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if ap_per_round < 1 or ap_per_round > 32:
|
||||
errs.append({"category": "domain", "message": "ap_per_round 越域 [1,32]: %d" % ap_per_round})
|
||||
if dig_ap_cost < 1 or dig_ap_cost > 8:
|
||||
errs.append({"category": "domain", "message": "dig_ap_cost 越域 [1,8]: %d" % dig_ap_cost})
|
||||
if flag_place_ap_cost < 0 or flag_place_ap_cost > 8:
|
||||
errs.append({"category": "domain", "message": "flag_place_ap_cost 越域 [0,8]: %d" % flag_place_ap_cost})
|
||||
if flag_pull_ap_cost < 0 or flag_pull_ap_cost > 8:
|
||||
errs.append({"category": "domain", "message": "flag_pull_ap_cost 越域 [0,8]: %d" % flag_pull_ap_cost})
|
||||
if rap_refresh_cost < 1 or rap_refresh_cost > 64:
|
||||
errs.append({"category": "domain", "message": "rap_refresh_cost 越域 [1,64]: %d" % rap_refresh_cost})
|
||||
if refresh_max_per_round < 1 or refresh_max_per_round > 8:
|
||||
errs.append({"category": "domain", "message": "refresh_max_per_round 越域 [1,8]: %d" % refresh_max_per_round})
|
||||
if move_ap_cost != 0:
|
||||
errs.append({"category": "frozen", "message": "move_ap_cost 结构冻结为 0: %d" % move_ap_cost})
|
||||
if chain_ap_cost != 0:
|
||||
errs.append({"category": "frozen", "message": "chain_ap_cost 结构冻结为 0: %d" % chain_ap_cost})
|
||||
# 跨字段(A3 三轮评审增补):任一动作成本 ≤ ap_per_round,违规加载期致命
|
||||
for pair in [["dig_ap_cost", dig_ap_cost], ["flag_place_ap_cost", flag_place_ap_cost], ["flag_pull_ap_cost", flag_pull_ap_cost]]:
|
||||
if pair[1] > ap_per_round:
|
||||
errs.append({"category": "cross_field", "message": "%s(%d) > ap_per_round(%d)——挖掘/旗动作永久不可用软锁" % [pair[0], pair[1], ap_per_round]})
|
||||
return errs
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtortb6o6p2gu
|
||||
@@ -0,0 +1,77 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 BoardGenTable schema(A1 回填值;键 (level_index, node_depth)——首个进度键表)。
|
||||
# 依据 design/gdd/a1-board-generation.md "BoardGenTable Schema 定稿" + F2/F2b/F2c 谓词。
|
||||
class_name BoardGenTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary]
|
||||
const SOLVER_RULES := ["single", "single_subset"]
|
||||
const FOG_RATIO_ABS_CAP := 0.25 # B1 修订——雾占比绝对上限
|
||||
const COST_BUDGET := 16384 # F2c provisional 量级护栏(禁墙钟逃生)
|
||||
const TARGET_ABS_FLOOR := 0.85 # B1 修订——target 域下限(P1 绝对锚)
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if rows.is_empty():
|
||||
errs.append({"category": "empty_table", "message": "BoardGenTable 为空"})
|
||||
return errs
|
||||
var levels := {}
|
||||
var anchor_ok := {}
|
||||
for row in rows:
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var key := "%d_%d" % [L, D]
|
||||
if levels.has(key):
|
||||
errs.append({"category": "dup_key", "message": "BoardGen 键重复: %s" % key})
|
||||
levels[key] = true
|
||||
if L < 1:
|
||||
errs.append({"category": "domain", "message": "level_index 须 ≥1"})
|
||||
if D < 0:
|
||||
errs.append({"category": "domain", "message": "node_depth 须 ≥0"})
|
||||
var w: int = int(row.get("board_width", 0))
|
||||
var h: int = int(row.get("board_height", 0))
|
||||
if w < 8 or w > 64 or h < 8 or h > 64:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) 尺寸越域 [8,64]" % [L, D]})
|
||||
var d: float = float(row.get("mine_density", 0.0))
|
||||
if d <= 0.0 or d > 0.5:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) mine_density 越域 (0,0.5]" % [L, D]})
|
||||
var t: float = float(row.get("inferability_target", 0.0))
|
||||
if t < TARGET_ABS_FLOOR or t > 1.0:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) inferability_target 越域 [0.85,1](P1 绝对锚)" % [L, D]})
|
||||
if not SOLVER_RULES.has(row.get("solver_rules", "")):
|
||||
errs.append({"category": "enum", "message": "(%d,%d) solver_rules 出域" % [L, D]})
|
||||
if row.get("solver_rules", "") != "single_subset":
|
||||
errs.append({"category": "frozen", "message": "(%d,%d) solver_rules 冻结 single_subset" % [L, D]})
|
||||
var ms: int = int(row.get("min_safe_count", 0))
|
||||
if ms < 1 or ms > 999:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) min_safe_count 越域 [1,999]" % [L, D]})
|
||||
var att: int = int(row.get("max_gen_attempts", 0))
|
||||
if att < 1 or att > 32:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) max_gen_attempts 越域 [1,32]" % [L, D]})
|
||||
var fc: int = int(row.get("fog_region_count", 0))
|
||||
var fr: float = float(row.get("fog_target_ratio", 0.0))
|
||||
# F2 规范合取形 + 雾占比绝对上限(B1 修订)
|
||||
var fog_ok := (fc == 0 and fr == 0.0) or (fc >= 1 and fr > 0.0 and fr <= minf(8.0 * d, FOG_RATIO_ABS_CAP) and fr < 1.0)
|
||||
if not fog_ok:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) fog_domain_ok 违例(count=%d ratio=%.3f)" % [L, D, fc, fr]})
|
||||
# 布雷数下限(R3)——零雷板拒绝
|
||||
var n_mines := int(round(d * float(w * h - 9)))
|
||||
if n_mines < 1:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) 布雷数下限违例 round(d×(W×H−9)) ≥ 1" % [L, D]})
|
||||
# F2b min_safe 可行性
|
||||
if w * h - n_mines < ms:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) min_safe_feasible 违例" % [L, D]})
|
||||
# F2c 成本可行性
|
||||
if att * w * h > COST_BUDGET:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) cost_feasible 违例 attempts×W×H ≤ %d" % [L, D, COST_BUDGET]})
|
||||
var biomes: Array = row.get("board_biomes", [])
|
||||
if biomes.is_empty():
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) board_biomes 非空域校验违例(A4⑮)" % [L, D]})
|
||||
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
|
||||
for L in anchor_ok.keys():
|
||||
if not anchor_ok[L]:
|
||||
errs.append({"category": "anchor", "message": "level %d 缺 depth-0 锚行(EC15)" % L})
|
||||
return errs
|
||||
@@ -0,0 +1 @@
|
||||
uid://ee2mfcg5542u
|
||||
@@ -0,0 +1,32 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-12
|
||||
# D2 DifficultyTable schema(C3 GDD Core Rule 1 兑现——B2 Rule 10 "接口未定 provisional" 的建表兑现,走 D2 接口漂移规则)。
|
||||
# 全局修饰结构位:按 (L,D) 精确匹配施加乘数于 EnemyTable 行值(B2.spawn 实例化时、行值快照前)。
|
||||
# MVP 空表(零行为——AC-10);首个使用场景 = 精英节点(C1 定稿联动,C3 OQ5);Boss 走 Dmax 直填行不走本表。
|
||||
class_name DifficultyTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary]:{level_index, node_depth, hp_mult, atk_mult}
|
||||
|
||||
const MULT_MIN := 0.1
|
||||
const MULT_MAX := 10.0
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
var seen := {}
|
||||
for row in rows:
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var key := "%d_%d" % [L, D]
|
||||
if seen.has(key):
|
||||
errs.append({"category": "dup_key", "message": "DifficultyTable 键重复: %s" % key})
|
||||
seen[key] = true
|
||||
if L < 1 or D < 0:
|
||||
errs.append({"category": "domain", "message": "难度键越域 (%d,%d)" % [L, D]})
|
||||
# 乘数越域 = warning 级(使用时 clamp——组合事故非配置事故,C3 GDD Edge Case 预登记)
|
||||
for k in ["hp_mult", "atk_mult"]:
|
||||
var v: float = float(row.get(k, 1.0))
|
||||
if v < MULT_MIN or v > MULT_MAX:
|
||||
errs.append({"category": "mult_oor_warning", "message": "%s (%s) 越域 [0.1,10] 使用时 clamp: %f" % [key, k, v]})
|
||||
return errs
|
||||
@@ -0,0 +1 @@
|
||||
uid://bskkax0l0f20
|
||||
@@ -0,0 +1,108 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 EnemyTable / HeroTable schema(B2 回填值)。EnemyTable 键 (L,D,enemy_id),每行集恰一敌种;
|
||||
# HeroTable 单行全局(Rule 4 豁免)。依据 design/gdd/b2-enemy-system.md Schema 节。
|
||||
class_name EnemyTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary]
|
||||
|
||||
const PATTERN_MVP := ["attack", "charge", "heavy"]
|
||||
const PATTERN_RESERVED := ["guard", "boost", "hinder", "summon"]
|
||||
# 护甲类型封闭枚举(伤害类型 × 护甲克制批——克制矩阵本体在 SettlementTable)
|
||||
const ARMOR_TYPES := ["none", "a", "b"]
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if rows.is_empty():
|
||||
errs.append({"category": "empty_table", "message": "EnemyTable 为空"})
|
||||
return errs
|
||||
var seen := {}
|
||||
var rowset_species := {}
|
||||
var anchor_ok := {}
|
||||
for row in rows:
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var eid: String = row.get("enemy_id", "")
|
||||
var key := "%d_%d_%s" % [L, D, eid]
|
||||
if seen.has(key):
|
||||
errs.append({"category": "dup_key", "message": "EnemyTable 键重复: %s" % key})
|
||||
seen[key] = true
|
||||
if L < 1 or D < 0:
|
||||
errs.append({"category": "domain", "message": "进度键越域 (%d,%d)" % [L, D]})
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "enemy_id 空"})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
|
||||
var uc: int = int(row.get("unit_count", 0))
|
||||
if uc < 1 or uc > 3:
|
||||
errs.append({"category": "domain", "message": "%s unit_count 越域 [1,3]" % eid})
|
||||
# unit_count_max(可省略——遭遇随机化批,C3 GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值
|
||||
if row.has("unit_count_max"):
|
||||
var ucm: int = int(row.get("unit_count_max", uc))
|
||||
if ucm < uc or ucm > 3:
|
||||
errs.append({"category": "cross_field", "message": "%s unit_count_max(%d) 越域 [unit_count(%d),3]" % [eid, ucm, uc]})
|
||||
var hp: int = int(row.get("hp", 0))
|
||||
if hp < 1 or hp > 999:
|
||||
errs.append({"category": "domain", "message": "%s hp 越域 [1,999]" % eid})
|
||||
var atk: int = int(row.get("atk", -1))
|
||||
if atk < 0 or atk > 999:
|
||||
errs.append({"category": "domain", "message": "%s atk 越域 [0,999]" % eid})
|
||||
# atk_min/atk_max(可省略配对——2026-09-14 敌方攻击区间批):遭遇骰同流 spawn 期整战持久 roll;
|
||||
# 中点锚 = A3 条款①同款期望当量纪律(调区间必同步调 atk)
|
||||
var has_amin: bool = row.has("atk_min")
|
||||
var has_amax: bool = row.has("atk_max")
|
||||
if has_amin != has_amax:
|
||||
errs.append({"category": "cross_field", "message": "%s atk_min/atk_max 必须成对出现" % eid})
|
||||
elif has_amin:
|
||||
var amin: int = int(row.get("atk_min", 0))
|
||||
var amax: int = int(row.get("atk_max", 0))
|
||||
if amin < 0 or amax > 999 or amin > amax:
|
||||
errs.append({"category": "domain", "message": "%s atk 区间越域 [0,999] 且 min≤max: %d..%d" % [eid, amin, amax]})
|
||||
if atk != int((amin + amax) / 2.0):
|
||||
errs.append({"category": "cross_field", "message": "%s atk(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [eid, atk, (amin + amax) / 2]})
|
||||
var hm: int = int(row.get("heavy_multiplier", 0))
|
||||
if hm < 1 or hm > 16:
|
||||
errs.append({"category": "domain", "message": "%s heavy_multiplier 越域 [1,16]" % eid})
|
||||
var pattern: Array = row.get("pattern", [])
|
||||
if pattern.size() < 1 or pattern.size() > 8:
|
||||
errs.append({"category": "domain", "message": "%s pattern 环长越域 [1,8]" % eid})
|
||||
for beat in pattern:
|
||||
if PATTERN_RESERVED.has(beat):
|
||||
errs.append({"category": "enum", "message": "%s pattern 预留值拒绝(MVP 校验域): %s" % [eid, beat]})
|
||||
elif not PATTERN_MVP.has(beat):
|
||||
errs.append({"category": "enum", "message": "%s pattern 未知枚举: %s" % [eid, beat]})
|
||||
var cap: int = int(row.get("advantage_per_round_cap", -1))
|
||||
if cap < 0 or cap > 16:
|
||||
errs.append({"category": "domain", "message": "%s advantage_per_round_cap 越域 [0,16]" % eid})
|
||||
# armor_type(可省略——缺省 none 向后兼容)
|
||||
var at: String = String(row.get("armor_type", "none"))
|
||||
if not ARMOR_TYPES.has(at):
|
||||
errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {none, a, b})" % [eid, at]})
|
||||
# 每行集恰一敌种(行集内 enemy_id 唯一)
|
||||
var rk := "%d_%d" % [L, D]
|
||||
if rowset_species.has(rk):
|
||||
errs.append({"category": "rowset_rule", "message": "(%d,%d) 行集出现第二敌种(每行集恰一敌种违例)" % [L, D]})
|
||||
rowset_species[rk] = eid
|
||||
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
|
||||
for L in anchor_ok.keys():
|
||||
if not anchor_ok[L]:
|
||||
errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 depth-0 锚行" % L})
|
||||
return errs
|
||||
|
||||
|
||||
# R1:进度键 floor + 行集恰一敌种(实体键无歧义,悬空致命由调用方处理)
|
||||
func resolve_rowset(L: int, D: int) -> Dictionary:
|
||||
var best: Dictionary = {}
|
||||
var best_l := -1
|
||||
var best_d := -1
|
||||
for row in rows:
|
||||
var rl: int = int(row.get("level_index", 0))
|
||||
var rd: int = int(row.get("node_depth", 0))
|
||||
if rl <= L and rd <= D and (rl > best_l or (rl == best_l and rd > best_d)):
|
||||
best = row
|
||||
best_l = rl
|
||||
best_d = rd
|
||||
return best
|
||||
@@ -0,0 +1 @@
|
||||
uid://cbqi1lvj5gjx1
|
||||
@@ -0,0 +1,19 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 HeroTable schema(B2 接口漂移新增;单行全局,Rule 4 豁免)。
|
||||
# atk = 基础攻击恒存在(2026-09-11 用户裁决):域 [1,999]、0 = 加载拒绝——零基线死锁 schema 层杜绝。
|
||||
class_name HeroTable
|
||||
extends Resource
|
||||
|
||||
@export var max_hp: int = 50 # [1,999] knob(provisional,溯源 = 原型锚)
|
||||
@export var atk: int = 6 # [1,999] knob——基础攻击(溯源 = 概念原型 HERO_BASE_ATK,provisional 归 C3);消费面 = B1 战斗段先手攻击
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if max_hp < 1 or max_hp > 999:
|
||||
errs.append({"category": "domain", "message": "max_hp 越域 [1,999]: %d" % max_hp})
|
||||
if atk < 1 or atk > 999:
|
||||
errs.append({"category": "domain", "message": "atk 越域 [1,999]: %d" % atk})
|
||||
return errs
|
||||
@@ -0,0 +1 @@
|
||||
uid://d4npdck7i4jw1
|
||||
@@ -0,0 +1,57 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证
|
||||
# Date: 2026-09-11
|
||||
# D2 LevelTable schema(provisional——C1 关卡节点地图未设计,本表为线性切片形态的结构位:
|
||||
# nodes 数组即未来节点链的数据承载面;路径分支/奖励点分布的定稿权归 C1/C3)。
|
||||
# 节点类型 closed enum:battle(战斗,须带 node_depth)/ reward(随机主动道具点)/ rest(血量恢复点)/ boss(关底 Boss,须带 node_depth)。
|
||||
class_name LevelTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary]
|
||||
|
||||
const NODE_TYPES := ["battle", "reward", "rest", "boss"]
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if rows.is_empty():
|
||||
errs.append({"category": "empty_table", "message": "LevelTable 为空"})
|
||||
return errs
|
||||
var seen := {}
|
||||
for row in rows:
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
if seen.has(L):
|
||||
errs.append({"category": "dup_key", "message": "LevelTable level_index 重复: %d" % L})
|
||||
seen[L] = true
|
||||
if L < 1:
|
||||
errs.append({"category": "domain", "message": "level_index 须 ≥1"})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "level %d display_name 空" % L})
|
||||
var heal: int = int(row.get("rest_heal_pct", -1))
|
||||
if heal < 0 or heal > 100:
|
||||
errs.append({"category": "domain", "message": "level %d rest_heal_pct 越域 [0,100]" % L})
|
||||
var nodes: Array = row.get("nodes", [])
|
||||
if nodes.is_empty():
|
||||
errs.append({"category": "domain", "message": "level %d 节点链为空" % L})
|
||||
continue
|
||||
var boss_count := 0
|
||||
var battle_count := 0
|
||||
for node in nodes:
|
||||
var t: String = String(node.get("type", ""))
|
||||
if not NODE_TYPES.has(t):
|
||||
errs.append({"category": "enum", "message": "level %d 未知节点类型: %s" % [L, t]})
|
||||
continue
|
||||
if t == "battle" or t == "boss":
|
||||
if int(node.get("node_depth", -1)) < 0:
|
||||
errs.append({"category": "domain", "message": "level %d %s 节点缺 node_depth" % [L, t]})
|
||||
battle_count += 1
|
||||
if t == "boss":
|
||||
boss_count += 1
|
||||
if boss_count != 1:
|
||||
errs.append({"category": "structural", "message": "level %d Boss 节点数 = %d(须恰一)" % [L, boss_count]})
|
||||
if battle_count < 2:
|
||||
errs.append({"category": "structural", "message": "level %d 战斗节点(含 Boss)= %d(≥2 才构成关卡)" % [L, battle_count]})
|
||||
# 线性切片纪律:末节点必须是 Boss(关底收尾——C2 关卡定义)
|
||||
if String(nodes[nodes.size() - 1].get("type", "")) != "boss":
|
||||
errs.append({"category": "structural", "message": "level %d 末节点非 Boss(线性关卡以 Boss 收尾)" % L})
|
||||
return errs
|
||||
@@ -0,0 +1 @@
|
||||
uid://ytfp75gipdwm
|
||||
@@ -0,0 +1,30 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 RewardConfigTable schema(B4 full 评审接口漂移新增;单行全局——B4 标量 knob 的 Rule 1 载体)。
|
||||
class_name RewardConfigTable
|
||||
extends Resource
|
||||
|
||||
@export var t_fog_min: int = 2 # [1,8] F2 雾档过滤门限
|
||||
@export var k_dmg: int = 0 # [0,999] 旧 F5 格池刀保底——2026-09-11 可选类供给控制器批置 0(语义由"战斗首件保证"取代)
|
||||
@export var n_draft: int = 32 # [1,999] 旧 F5 草稿重 roll 上限(随 k_dmg=0 休眠——字段保留)
|
||||
@export var p_min: int = 4 # [1,999] K≥1 供给闸刀力下界(随 k_dmg=0 休眠——字段保留)
|
||||
@export var active_p0: float = 0.4 # [0,1] 可选类供给:战斗首件之后,每张新板的注入基础概率(2026-09-11 用户拍板批)
|
||||
@export var active_decay: float = 0.5 # (0,1] 可选类供给:战斗内每多获取 1 件,注入概率乘一次衰减(指数下降、永不绝育——用户拍板④)
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if t_fog_min < 1 or t_fog_min > 8:
|
||||
errs.append({"category": "domain", "message": "t_fog_min 越域 [1,8]: %d" % t_fog_min})
|
||||
if k_dmg < 0 or k_dmg > 999:
|
||||
errs.append({"category": "domain", "message": "k_dmg 越域 [0,999]: %d" % k_dmg})
|
||||
if n_draft < 1 or n_draft > 999:
|
||||
errs.append({"category": "domain", "message": "n_draft 越域 [1,999]: %d" % n_draft})
|
||||
if p_min < 1 or p_min > 999:
|
||||
errs.append({"category": "domain", "message": "p_min 越域 [1,999]: %d" % p_min})
|
||||
if active_p0 < 0.0 or active_p0 > 1.0:
|
||||
errs.append({"category": "domain", "message": "active_p0 越域 [0,1]: %f" % active_p0})
|
||||
if active_decay <= 0.0 or active_decay > 1.0:
|
||||
errs.append({"category": "domain", "message": "active_decay 越域 (0,1]: %f" % active_decay})
|
||||
return errs
|
||||
@@ -0,0 +1 @@
|
||||
uid://dc14jo26wutra
|
||||
@@ -0,0 +1,73 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 RewardTable / RewardConfigTable schema(B4 回填值)。
|
||||
# RewardTable 行 = Dictionary,键 reward_id(Rule 4 豁免:定义表);跨字段校验族 + 池护栏。
|
||||
class_name RewardTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary]
|
||||
|
||||
const CATEGORIES := ["buff", "damage_item", "heal_pack", "instant_aoe", "instant_heal"] # 2026-09-11 两类奖励裁决五值:即时类 {buff, instant_aoe, instant_heal} / 可选类 {damage_item, heal_pack}
|
||||
const POOL_TAGS := ["safe_cell", "clear_default", "active_inject"] # active_inject = 可选类注入池(2026-09-11 供给控制器批——可选类退出格子权重池);clear_<biome> 命名空间预留拒绝
|
||||
const USE_TIMINGS := ["pre_settlement"] # MVP 单值承重
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if rows.is_empty():
|
||||
errs.append({"category": "empty_table", "message": "RewardTable 为空"})
|
||||
return errs
|
||||
var seen := {}
|
||||
for row in rows:
|
||||
var rid: String = row.get("reward_id", "")
|
||||
if rid == "":
|
||||
errs.append({"category": "domain", "message": "reward_id 空"})
|
||||
continue
|
||||
if seen.has(rid):
|
||||
errs.append({"category": "dup_key", "message": "reward_id 重复: %s" % rid})
|
||||
seen[rid] = true
|
||||
var cat: String = row.get("category", "")
|
||||
if not CATEGORIES.has(cat):
|
||||
errs.append({"category": "enum", "message": "%s category 出域/预留值拒绝: %s" % [rid, cat]})
|
||||
var pool: String = row.get("pool_tag", "")
|
||||
var pool_reserved: bool = pool.begins_with("clear_") and pool != "clear_default"
|
||||
if not POOL_TAGS.has(pool) and not pool_reserved:
|
||||
errs.append({"category": "enum", "message": "%s pool_tag 未知: %s" % [rid, pool]})
|
||||
elif pool_reserved:
|
||||
errs.append({"category": "enum", "message": "%s pool_tag clear_<biome> 命名空间预留拒绝: %s" % [rid, pool]})
|
||||
if not USE_TIMINGS.has(row.get("use_timing", "")):
|
||||
errs.append({"category": "enum", "message": "%s use_timing 出域: %s" % [rid, row.get("use_timing")]})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % rid})
|
||||
var power: int = int(row.get("power", 0))
|
||||
var weight: int = int(row.get("weight", -1))
|
||||
var tier: int = int(row.get("tier", 0))
|
||||
if weight < 0 or weight > 999:
|
||||
errs.append({"category": "domain", "message": "%s weight 越域 [0,999]" % rid})
|
||||
if tier < 1 or tier > 8:
|
||||
errs.append({"category": "domain", "message": "%s tier 越域 [1,8]" % rid})
|
||||
var eff: String = row.get("effect_id", "")
|
||||
# 跨字段:buff ⇒ effect_id 必填;非 buff ⇒ 恒空;item 类 power ≥ 1
|
||||
if cat == "buff" and eff == "":
|
||||
errs.append({"category": "cross_field", "message": "%s buff 行 effect_id 必填" % rid})
|
||||
if cat != "buff" and eff != "":
|
||||
errs.append({"category": "cross_field", "message": "%s 非 buff 行 effect_id 恒空" % rid})
|
||||
if cat != "buff" and power < 1: # 2026-09-11:即时类(aoe/heal)与道具类同 consumes power ≥ 1
|
||||
errs.append({"category": "cross_field", "message": "%s item 行 power ≥ 1" % rid})
|
||||
# 伤害量值区间批(2026-09-14):可选配对 power_min/power_max——缺省 = 定值行向后兼容(armor_type 先例)
|
||||
var has_min: bool = row.has("power_min")
|
||||
var has_max: bool = row.has("power_max")
|
||||
if has_min != has_max:
|
||||
errs.append({"category": "cross_field", "message": "%s power_min/power_max 必须成对出现" % rid})
|
||||
elif has_min:
|
||||
var pmin: int = int(row.get("power_min", 0))
|
||||
var pmax: int = int(row.get("power_max", 0))
|
||||
if pmin < 1 or pmax > 999 or pmin > pmax:
|
||||
errs.append({"category": "domain", "message": "%s power 域违例 [1,999] 且 min≤max: %d..%d" % [rid, pmin, pmax]})
|
||||
if cat == "buff":
|
||||
errs.append({"category": "cross_field", "message": "%s buff 行禁区间字段(power 恒 0)" % rid})
|
||||
if power != int((pmin + pmax) / 2.0):
|
||||
# 中点锚:power = 区间中点(A3 条款①期望当量锚——调区间必同步调 power)
|
||||
errs.append({"category": "cross_field", "message": "%s power(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [rid, power, (pmin + pmax) / 2]})
|
||||
return errs
|
||||
@@ -0,0 +1 @@
|
||||
uid://cm1mr66q2v8b4
|
||||
@@ -0,0 +1,57 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 SettlementTable schema(B1 回填值;单行全局,Rule 4 豁免)——八字段口径。
|
||||
# 三行致命 manifest:segorder_violation / poolmap_dangling / pperfect_oob。
|
||||
class_name SettlementTable
|
||||
extends Resource
|
||||
|
||||
@export var equivalence_ref_damage: int = 12 # [1,999] knob——当量参考敞口 D_ref
|
||||
@export var p_perfect: int = 200 # {100,200} percent——完美排雷第二份兑付触发(载体域)
|
||||
@export var segment_order: PackedStringArray = [] # 结构冻结——恒等拍板段序校验违例致命
|
||||
@export var source_label_map: Dictionary = {} # 结构冻结——source_class→文案键
|
||||
@export var effect_pool_map: Dictionary = {} # 结构冻结——effect_id→pool(悬空加载致命)
|
||||
@export var pool_clamp_lo: int = -100 # 结构冻结
|
||||
@export var pool_clamp_hi: int = 100 # 结构冻结
|
||||
@export var round_direction: String = "floor" # 结构冻结——单一取整规则
|
||||
# ---- 伤害类型 × 护甲克制批(2026-09-12 用户拍板结构;数值 provisional 待调参)----
|
||||
# 主角攻击 = 基础(atk×(1+attack_up),不吃克制)+ 开格充能×克制 + 标雷充能×克制(分量各自 floor 后相加)
|
||||
@export var damage_a_per_cell: int = 1 # 伤害A 充能:本轮每开 1 资格格(reward_eligible 安全格)+N
|
||||
@export var damage_a_cap: int = 8 # 伤害A 单轮充能上限(0 = 不封顶;8 与 ap_per_round 对齐)
|
||||
@export var damage_b_per_flag: int = 2 # 伤害B 充能:每颗首次标中雷 +N(沿用每雷实例一次去重——结构上限)
|
||||
@export var armor_effectiveness: Dictionary = {} # 克制矩阵 percent:{分量 {base|dmg_a|dmg_b}: {护甲 {none|a|b}: percent}}
|
||||
|
||||
const ARMOR_KEYS := ["none", "a", "b"]
|
||||
const COMPONENT_KEYS := ["base", "dmg_a", "dmg_b"]
|
||||
|
||||
const FROZEN_SEGMENT_ORDER := ["grant", "item_window", "buff_debuff", "item", "combat", "decay", "verdict"]
|
||||
const P_PERFECT_DOMAIN := [100, 200]
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if equivalence_ref_damage < 1 or equivalence_ref_damage > 999:
|
||||
errs.append({"category": "domain", "message": "equivalence_ref_damage 越域 [1,999]: %d" % equivalence_ref_damage})
|
||||
if not P_PERFECT_DOMAIN.has(p_perfect):
|
||||
errs.append({"category": "pperfect_oob", "message": "p_perfect 载体域 {100,200}: %d" % p_perfect})
|
||||
var seg := Array(segment_order)
|
||||
if seg != FROZEN_SEGMENT_ORDER:
|
||||
errs.append({"category": "segorder_violation", "message": "segment_order 违例——恒等拍板段序校验失败: %s" % str(seg)})
|
||||
if round_direction != "floor":
|
||||
errs.append({"category": "frozen", "message": "round_direction 结构冻结 floor: %s" % round_direction})
|
||||
if pool_clamp_lo != -100 or pool_clamp_hi != 100:
|
||||
errs.append({"category": "frozen", "message": "pool_clamp 域结构冻结 [-100,+100]"})
|
||||
# 伤害类型 × 护甲克制批新字段域校验
|
||||
if damage_a_per_cell < 0 or damage_a_per_cell > 999:
|
||||
errs.append({"category": "domain", "message": "damage_a_per_cell 越域 [0,999]: %d" % damage_a_per_cell})
|
||||
if damage_a_cap < 0 or damage_a_cap > 999:
|
||||
errs.append({"category": "domain", "message": "damage_a_cap 越域 [0,999](0 = 不封顶): %d" % damage_a_cap})
|
||||
if damage_b_per_flag < 0 or damage_b_per_flag > 999:
|
||||
errs.append({"category": "domain", "message": "damage_b_per_flag 越域 [0,999]: %d" % damage_b_per_flag})
|
||||
for comp in COMPONENT_KEYS:
|
||||
var row: Dictionary = armor_effectiveness.get(comp, {})
|
||||
for armor in ARMOR_KEYS:
|
||||
var pct: int = int(row.get(armor, -1))
|
||||
if pct < 0 or pct > 999:
|
||||
errs.append({"category": "domain", "message": "armor_effectiveness.%s.%s 越域 [0,999]: %d" % [comp, armor, pct]})
|
||||
return errs
|
||||
@@ -0,0 +1 @@
|
||||
uid://ctmgiqmejqe7e
|
||||
@@ -0,0 +1,58 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 StatusEffectTable schema(B3 回填值;行 = Dictionary,键 effect_id;Rule 4 豁免)。
|
||||
# 依据 design/gdd/b3-status-effect.md "StatusEffectTable Schema 定稿"。
|
||||
class_name StatusEffectTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary]
|
||||
|
||||
const TARGET_SIDES := ["hero", "enemy"]
|
||||
const UNITS := ["percent", "flat"]
|
||||
const STACK_POLICIES := ["stack", "refresh", "independent"]
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if rows.is_empty():
|
||||
errs.append({"category": "empty_table", "message": "StatusEffectTable 为空"})
|
||||
return errs
|
||||
var seen := {}
|
||||
for row in rows:
|
||||
var eid: String = row.get("effect_id", "")
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "effect_id 缺失/空"})
|
||||
continue
|
||||
if seen.has(eid):
|
||||
errs.append({"category": "dup_key", "message": "effect_id 重复: %s" % eid})
|
||||
seen[eid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
|
||||
if not TARGET_SIDES.has(row.get("target_side", "")):
|
||||
errs.append({"category": "enum", "message": "%s target_side 出域" % eid})
|
||||
var unit: String = row.get("unit", "")
|
||||
if not UNITS.has(unit):
|
||||
errs.append({"category": "enum", "message": "%s unit 出域" % eid})
|
||||
if not STACK_POLICIES.has(row.get("stack_policy", "")):
|
||||
errs.append({"category": "enum", "message": "%s stack_policy 出域" % eid})
|
||||
var m: int = int(row.get("magnitude", 0))
|
||||
# 跨字段:percent ∈ [-100,100] 且 ≠0 / flat ≥ 1
|
||||
if unit == "percent" and (m == 0 or m < -100 or m > 100):
|
||||
errs.append({"category": "cross_field", "message": "%s percent magnitude 域错: %d" % [eid, m]})
|
||||
if unit == "flat" and m < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s flat magnitude 须 ≥1: %d" % [eid, m]})
|
||||
var dur: int = int(row.get("duration_settlements", 0))
|
||||
if dur < 1 or dur > 99:
|
||||
errs.append({"category": "domain", "message": "%s duration_settlements 越域 [1,99]: %d" % [eid, dur]})
|
||||
var ms: int = int(row.get("max_stack", 0))
|
||||
if ms < 1 or ms > 16:
|
||||
errs.append({"category": "domain", "message": "%s max_stack 越域 [1,16]: %d" % [eid, ms]})
|
||||
return errs
|
||||
|
||||
|
||||
func row_by_id(effect_id: String) -> Dictionary:
|
||||
for row in rows:
|
||||
if row.get("effect_id", "") == effect_id:
|
||||
return row
|
||||
return {}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b4hsttg27d7o1
|
||||
@@ -0,0 +1,51 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 VariationTable schema(A4 回填值;行 = Dictionary,键 variant_id)。
|
||||
# 依据 design/gdd/a4-terrain-variation.md "VariationTable Schema 定稿"。
|
||||
class_name VariationTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary]
|
||||
|
||||
const SCOPES := ["region", "global"]
|
||||
const INFO_MASKS := ["none", "hide_numbers"]
|
||||
const CELL_CONSTRAINTS := ["any", "safe_nonzero"]
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if rows.is_empty():
|
||||
errs.append({"category": "empty_table", "message": "VariationTable 为空"})
|
||||
return errs
|
||||
var seen := {}
|
||||
for row in rows:
|
||||
var vid: String = row.get("variant_id", "")
|
||||
if vid == "":
|
||||
errs.append({"category": "domain", "message": "variant_id 缺失/空"})
|
||||
continue
|
||||
if seen.has(vid):
|
||||
errs.append({"category": "dup_key", "message": "variant_id 重复: %s" % vid})
|
||||
seen[vid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % vid})
|
||||
var ul: int = int(row.get("unlock_level", 0))
|
||||
if ul < 1 or ul > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_level 越域 [1,999]: %d" % [vid, ul]})
|
||||
if not SCOPES.has(row.get("scope", "")):
|
||||
errs.append({"category": "enum", "message": "%s scope 出域: %s" % [vid, row.get("scope")]})
|
||||
if not INFO_MASKS.has(row.get("info_mask", "")):
|
||||
errs.append({"category": "enum", "message": "%s info_mask 出域: %s" % [vid, row.get("info_mask")]})
|
||||
if not CELL_CONSTRAINTS.has(row.get("cell_constraint", "")):
|
||||
errs.append({"category": "enum", "message": "%s cell_constraint 出域: %s" % [vid, row.get("cell_constraint")]})
|
||||
# 跨字段(A4 re-review 增补,加载期致命):hide_numbers ⇒ safe_nonzero
|
||||
if row.get("info_mask", "") == "hide_numbers" and row.get("cell_constraint", "") != "safe_nonzero":
|
||||
errs.append({"category": "cross_field", "message": "%s hide_numbers ⇒ cell_constraint 必为 safe_nonzero" % vid})
|
||||
return errs
|
||||
|
||||
|
||||
func row_by_id(variant_id: String) -> Dictionary:
|
||||
for row in rows:
|
||||
if row.get("variant_id", "") == variant_id:
|
||||
return row
|
||||
return {}
|
||||
@@ -0,0 +1 @@
|
||||
uid://boin0gyrpqvwd
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_resource type="Resource" script_class="ActionEconomyTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/action_economy_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
ap_per_round = 8
|
||||
dig_ap_cost = 1
|
||||
flag_place_ap_cost = 1
|
||||
flag_pull_ap_cost = 1
|
||||
rap_refresh_cost = 8
|
||||
refresh_max_per_round = 2
|
||||
move_ap_cost = 0
|
||||
chain_ap_cost = 0
|
||||
@@ -0,0 +1,94 @@
|
||||
[gd_resource type="Resource" script_class="BoardGenTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/board_gen_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
rows = [{
|
||||
"level_index": 1,
|
||||
"node_depth": 0,
|
||||
"board_width": 10,
|
||||
"board_height": 10,
|
||||
"mine_density": 0.25,
|
||||
"inferability_target": 0.88,
|
||||
"solver_rules": "single_subset",
|
||||
"min_safe_count": 50,
|
||||
"max_gen_attempts": 16,
|
||||
"fog_region_count": 0,
|
||||
"fog_target_ratio": 0.0,
|
||||
"board_biomes": ["dungeon"]
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 1,
|
||||
"board_width": 10,
|
||||
"board_height": 10,
|
||||
"mine_density": 0.25,
|
||||
"inferability_target": 0.88,
|
||||
"solver_rules": "single_subset",
|
||||
"min_safe_count": 50,
|
||||
"max_gen_attempts": 16,
|
||||
"fog_region_count": 0,
|
||||
"fog_target_ratio": 0.0,
|
||||
"board_biomes": ["dungeon"]
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 2,
|
||||
"board_width": 12,
|
||||
"board_height": 12,
|
||||
"mine_density": 0.25,
|
||||
"inferability_target": 0.88,
|
||||
"solver_rules": "single_subset",
|
||||
"min_safe_count": 60,
|
||||
"max_gen_attempts": 24,
|
||||
"fog_region_count": 0,
|
||||
"fog_target_ratio": 0.0,
|
||||
"board_biomes": ["dungeon"]
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 3,
|
||||
"board_width": 12,
|
||||
"board_height": 12,
|
||||
"mine_density": 0.25,
|
||||
"inferability_target": 0.88,
|
||||
"solver_rules": "single_subset",
|
||||
"min_safe_count": 60,
|
||||
"max_gen_attempts": 24,
|
||||
"fog_region_count": 0,
|
||||
"fog_target_ratio": 0.0,
|
||||
"board_biomes": ["dungeon"]
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 4,
|
||||
"board_width": 12,
|
||||
"board_height": 12,
|
||||
"mine_density": 0.25,
|
||||
"inferability_target": 0.88,
|
||||
"solver_rules": "single_subset",
|
||||
"min_safe_count": 60,
|
||||
"max_gen_attempts": 24,
|
||||
"fog_region_count": 0,
|
||||
"fog_target_ratio": 0.0,
|
||||
"board_biomes": ["dungeon"]
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"node_depth": 0,
|
||||
"board_width": 12,
|
||||
"board_height": 12,
|
||||
"mine_density": 0.25,
|
||||
"inferability_target": 0.88,
|
||||
"solver_rules": "single_subset",
|
||||
"min_safe_count": 60,
|
||||
"max_gen_attempts": 24,
|
||||
"fog_region_count": 1,
|
||||
"fog_target_ratio": 0.10,
|
||||
"board_biomes": ["dungeon"]
|
||||
}]
|
||||
|
||||
; 切片调参登记(SLICE-TUNED provisional,调参所有权仍归 C3):
|
||||
; A1 GDD 参考行(d0.10/0.12)经"先布后删"环内移除后仅幸存 2-3 雷、R0/|S|约0.97、残余挖掘 2-4 格——
|
||||
; 掘近退化板(A2 反例 B 机制的低密度放大)。实验数据(8 种子均值,smoke/experiment_density.gd):
|
||||
; d0.25 → L1 幸存雷 11/残余 13/inf 0.925/att 2.9 · L2 雷 19/残余 21/inf 0.908/att 7.9(seed42 实测 core 0.889→target 定 0.88 首过),零降级告警。
|
||||
; target 收窄至 0.88/0.88(不低于 0.85 绝对锚)为密度上移的对价——B1 条款③⑤对账重跑义务随调参触发。
|
||||
; 2026-09-11 关卡扩展批:L1 增 D1–D4 行(Boss=Dmax=4)。L1 全程无雾——A4 迷雾 unlock_level=2(已锁事实),
|
||||
; Boss 板以 12×12 区分(d0.28/t0.87 实测达不到推理率门槛——best≈0.81 降级告警,回退 0.25/0.88;
|
||||
; 密度轴上调留给 C3 与求解器能力一起解)。
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_resource type="Resource" script_class="DifficultyTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/difficulty_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
rows = []
|
||||
|
||||
; C3 GDD Core Rule 1:全局修饰结构位——MVP 空表(零行为)。首个修饰行 = 精英节点上线(C1 联动)。
|
||||
@@ -0,0 +1,113 @@
|
||||
[gd_resource type="Resource" script_class="EnemyTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/enemy_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
rows = [{
|
||||
"level_index": 1,
|
||||
"node_depth": 0,
|
||||
"enemy_id": "goblin",
|
||||
"display_name": "地精",
|
||||
"unit_count": 1,
|
||||
"unit_count_max": 2,
|
||||
"hp": 24,
|
||||
"atk": 7,
|
||||
"atk_min": 4,
|
||||
"atk_max": 10,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "none"
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 1,
|
||||
"enemy_id": "goblin",
|
||||
"display_name": "地精一伙",
|
||||
"unit_count": 2,
|
||||
"unit_count_max": 3,
|
||||
"hp": 13,
|
||||
"atk": 5,
|
||||
"atk_min": 2,
|
||||
"atk_max": 8,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "none"
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 2,
|
||||
"enemy_id": "bugbear",
|
||||
"display_name": "熊地精",
|
||||
"unit_count": 1,
|
||||
"hp": 40,
|
||||
"atk": 9,
|
||||
"atk_min": 6,
|
||||
"atk_max": 12,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "charge", "heavy"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "a"
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 3,
|
||||
"enemy_id": "bugbear",
|
||||
"display_name": "熊地精双雄",
|
||||
"unit_count": 2,
|
||||
"unit_count_max": 3,
|
||||
"hp": 18,
|
||||
"atk": 6,
|
||||
"atk_min": 3,
|
||||
"atk_max": 9,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "charge", "heavy"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "a"
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 4,
|
||||
"enemy_id": "ogre_chief",
|
||||
"display_name": "食人魔酋长",
|
||||
"unit_count": 1,
|
||||
"hp": 150,
|
||||
"atk": 9,
|
||||
"atk_min": 6,
|
||||
"atk_max": 12,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "attack", "charge", "heavy"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "b"
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"node_depth": 0,
|
||||
"enemy_id": "bugbear",
|
||||
"display_name": "熊地精",
|
||||
"unit_count": 1,
|
||||
"hp": 30,
|
||||
"atk": 7,
|
||||
"atk_min": 4,
|
||||
"atk_max": 10,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "charge", "heavy"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "a"
|
||||
}]
|
||||
|
||||
; 切片调参登记 v3(2026-09-12 试玩批次 1 触发——F7 流程;C3 GDD F5 对照):
|
||||
; 试玩反馈 = 基线满血全通关(模拟 80%/真人 100%)——难度严重不足,主杠杆 = atk 曲线上调(承伤侧):
|
||||
; 4/4/6/4/7 → 7/5/9/6/9(约 +50%);HP 结构调整(D0 24→20 缩短战斗、Boss 130→150 用户拍板可上调)。
|
||||
; D0 注:20 HP 会被首轮盲挖粉尘秒杀(12+9)——回 24(4 轮战斗/承伤 42%,有过程有痛感)。
|
||||
; DND 命名(用户指令):goblin 地精(单拍杂鱼)→ bugbear 熊地精(三拍伏击环——DND 原型即伏击手)→
|
||||
; ogre_chief 食人魔酋长(经典低阶 Boss)。enemy_id 同步更名(内部键,无外部引用)。
|
||||
; L2 锚行同步 v3 语义(bugbear 30/7)。
|
||||
; 遭遇随机化批(2026-09-12 用户拍板):D0 [1,2]/D1 [2,3] hp13/D3 [2,3] hp18——数量区间骰(encounter 流,B1 掷骰);
|
||||
; D2/Boss 固定(新敌种首遇教学位不加骰——C3 GDD Core Rule 8)。期望总量 ≈304(+8%),F6 带内。
|
||||
;
|
||||
; 护甲类型批(2026-09-12 用户拍板:伤害类型 × 护甲类型克制——无甲/护甲A/护甲B):
|
||||
; armor_type ∈ {none, a, b}(缺省 = none 向后兼容);克制矩阵本体在 settlement.tres.armor_effectiveness。
|
||||
; 映射(教学梯度:无甲→甲A→甲B):goblin=none · bugbear=a · ogre_chief=b(L2 bugbear 同 a)——候选方案,
|
||||
; 数值与映射均 provisional 待用户调参。
|
||||
|
||||
; 敌方攻击区间批(2026-09-14 用户拍板②第二步——"上限掺运气"敌方侧:atk 上阶/下阶,中点恒等行值;
|
||||
; roll 时机 = 遭遇骰同流〔encounter〕spawn 期整战持久,每拍抖动否决〔数字可读性锚〕;B2 零随机纪律保持
|
||||
; ——收 override 即确定值。宽度 ±3 provisional 归 C3 测量迭代;验收 = 完美策略档 0 加成通关 ≤40%。
|
||||
@@ -0,0 +1,8 @@
|
||||
[gd_resource type="Resource" script_class="HeroTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/hero_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
max_hp = 50
|
||||
atk = 6
|
||||
@@ -0,0 +1,37 @@
|
||||
[gd_resource type="Resource" script_class="LevelTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/level_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
rows = [{
|
||||
"level_index": 1,
|
||||
"display_name": "第一层 · 苔藓回廊",
|
||||
"rest_heal_pct": 30,
|
||||
"nodes": [{
|
||||
"node_depth": 0,
|
||||
"type": "battle"
|
||||
}, {
|
||||
"node_depth": 1,
|
||||
"type": "battle"
|
||||
}, {
|
||||
"type": "reward"
|
||||
}, {
|
||||
"node_depth": 2,
|
||||
"type": "battle"
|
||||
}, {
|
||||
"node_depth": 3,
|
||||
"type": "battle"
|
||||
}, {
|
||||
"type": "rest"
|
||||
}, {
|
||||
"node_depth": 4,
|
||||
"type": "boss"
|
||||
}]
|
||||
}]
|
||||
|
||||
; 切片登记(provisional——C1/C3 未设计,结构位与数值均为 SLICE-TUNED):
|
||||
; 节点链 = 用户 2026-09-11 拍板的线性第一关(战×2 → 随机主动道具 → 战×2 → 血量恢复点 → Boss)。
|
||||
; node_depth 轴 = 节点深度(难度 = f(关卡序号, 节点深度)——game-concept 锁定口径);Boss = Dmax 行(A1 第三轴登记约定)。
|
||||
; rest_heal_pct = 30:恢复点回复 max_hp 的百分比(随生命天赋同步放大,防高生命档恢复失效)。
|
||||
; L1 全程无雾:A4 迷雾 unlock_level=2(已锁事实——迷雾自第二层解锁),Boss 板以 12×12 加密板区分。
|
||||
@@ -0,0 +1,112 @@
|
||||
[gd_resource type="Resource" script_class="RewardTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/reward_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
rows = [{
|
||||
"reward_id": "defense_up_basic",
|
||||
"category": "buff",
|
||||
"effect_id": "defense_up",
|
||||
"display_name": "石肤术",
|
||||
"power": 0,
|
||||
"weight": 16,
|
||||
"pool_tag": "safe_cell",
|
||||
"tier": 2,
|
||||
"use_timing": "pre_settlement"
|
||||
}, {
|
||||
"reward_id": "attack_up_basic",
|
||||
"category": "buff",
|
||||
"effect_id": "attack_up",
|
||||
"display_name": "增伤术",
|
||||
"power": 0,
|
||||
"weight": 20,
|
||||
"pool_tag": "safe_cell",
|
||||
"tier": 2,
|
||||
"use_timing": "pre_settlement"
|
||||
}, {
|
||||
"reward_id": "spark_aoe",
|
||||
"category": "instant_aoe",
|
||||
"effect_id": "",
|
||||
"display_name": "爆裂粉尘",
|
||||
"power": 4,
|
||||
"weight": 16,
|
||||
"pool_tag": "safe_cell",
|
||||
"tier": 1,
|
||||
"use_timing": "pre_settlement"
|
||||
}, {
|
||||
"reward_id": "heal_minor",
|
||||
"category": "instant_heal",
|
||||
"effect_id": "",
|
||||
"display_name": "急救敷料",
|
||||
"power": 4,
|
||||
"power_min": 2,
|
||||
"power_max": 6,
|
||||
"weight": 15,
|
||||
"pool_tag": "safe_cell",
|
||||
"tier": 1,
|
||||
"use_timing": "pre_settlement"
|
||||
}, {
|
||||
"reward_id": "knife_t1",
|
||||
"category": "damage_item",
|
||||
"effect_id": "",
|
||||
"display_name": "小刀",
|
||||
"power": 8,
|
||||
"power_min": 5,
|
||||
"power_max": 11,
|
||||
"weight": 60,
|
||||
"pool_tag": "active_inject",
|
||||
"tier": 1,
|
||||
"use_timing": "pre_settlement"
|
||||
}, {
|
||||
"reward_id": "knife_adv",
|
||||
"category": "damage_item",
|
||||
"effect_id": "",
|
||||
"display_name": "精工短刀",
|
||||
"power": 16,
|
||||
"power_min": 10,
|
||||
"power_max": 22,
|
||||
"weight": 30,
|
||||
"pool_tag": "active_inject",
|
||||
"tier": 2,
|
||||
"use_timing": "pre_settlement"
|
||||
}, {
|
||||
"reward_id": "heal_pack",
|
||||
"category": "heal_pack",
|
||||
"effect_id": "",
|
||||
"display_name": "治疗包",
|
||||
"power": 12,
|
||||
"power_min": 6,
|
||||
"power_max": 18,
|
||||
"weight": 10,
|
||||
"pool_tag": "active_inject",
|
||||
"tier": 2,
|
||||
"use_timing": "pre_settlement"
|
||||
}, {
|
||||
"reward_id": "clear_blast",
|
||||
"category": "instant_aoe",
|
||||
"effect_id": "",
|
||||
"display_name": "清障炸药",
|
||||
"power": 40,
|
||||
"power_min": 25,
|
||||
"power_max": 55,
|
||||
"weight": 1,
|
||||
"pool_tag": "clear_default",
|
||||
"tier": 2,
|
||||
"use_timing": "pre_settlement"
|
||||
}]
|
||||
|
||||
; 2026-09-11 可选类供给控制器批(用户拍板):
|
||||
; 可选类三行移出 safe_cell 权重池 → active_inject 注入池(小刀 60 / 精工短刀 30 / 治疗包 10——治疗资源宝贵),
|
||||
; 由 RewardSupplyController 在 install 期注入(首件保证 + 指数递减),不再走格子权重 roll。
|
||||
; 清图槽 clear_knife(入库刀)→ clear_blast(instant_aoe 一次性高额伤害,当场兑付全体存活敌,不入库不占可选类供给)。
|
||||
; 格子池现仅即时类四行(attack_up 20 / defense_up 16 / spark_aoe 16 / heal_minor 15,合计 67)。
|
||||
; 减伤墙调参批(2026-09-13,playtest-2026-09-12 报告 Q1 触发):defense_up 24 → 16——
|
||||
; 石肤自 32% 降至 23.9%,攻击成为池内最高行;与 settlement.tres pool_clamp_lo=-60 同批(免伤墙双杠杆)。
|
||||
|
||||
; 伤害量值区间批(2026-09-14 用户拍板②——"上限掺运气"第一步,奖励侧五行:上阶/下阶区间内随机;
|
||||
; 中点恒等旧定值 = A3 条款①期望当量锚不漂移;宽度 provisional 归 C3 测量迭代。验收口径 = 完美策略档
|
||||
; 0 加成通关率 ≤40%〔OQ7 完美档基线 100%〕)。roll 时机 = 身份预分配批(reward_roll 恰一 scope 批内
|
||||
; 追加取号,既有取号序零扰动——追加式);快照 E14 口径不变,B1/呈现零改动。区间:
|
||||
; heal_minor [2,6] · knife_t1 [5,11] · heal_pack [6,18] · knife_adv [10,22] · clear_blast [25,55]。
|
||||
; buff 两行(attack_up/defense_up)与 spark_aoe 不在本批(即时光效类按拍板面五行为准)。
|
||||
@@ -0,0 +1,16 @@
|
||||
[gd_resource type="Resource" script_class="RewardConfigTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/reward_config_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
t_fog_min = 2
|
||||
k_dmg = 0
|
||||
n_draft = 32
|
||||
p_min = 8
|
||||
active_p0 = 0.4
|
||||
active_decay = 0.5
|
||||
|
||||
; 2026-09-11 可选类供给控制器批(用户拍板):k_dmg 1→0(旧 F5 格池刀保底休眠,语义由"战斗首件保证"取代);
|
||||
; active_p0 = 0.4 / active_decay = 0.5(首件之后每张新板注入概率 0.4 → 0.2 → 0.1 → …指数下降、永不绝育;
|
||||
; provisional 起步值,标定目标 E[战斗获取] ≈ 1,level_monte_carlo 扫描验证后归 C3 定稿)。
|
||||
@@ -0,0 +1,61 @@
|
||||
[gd_resource type="Resource" script_class="SettlementTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/settlement_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
equivalence_ref_damage = 12
|
||||
p_perfect = 200
|
||||
segment_order = PackedStringArray("grant", "item_window", "buff_debuff", "item", "combat", "decay", "verdict")
|
||||
source_label_map = {
|
||||
"safe_cell": "安全格",
|
||||
"flag_hit": "标中旗",
|
||||
"mine_hit": "踩雷",
|
||||
"clear_reward": "清图奖励",
|
||||
"item": "道具",
|
||||
"enemy_action": "敌方行动",
|
||||
"hero_attack": "主角攻击",
|
||||
"flag_miss": "标旗未中"
|
||||
}
|
||||
effect_pool_map = {
|
||||
"attack_up": "hero_offense",
|
||||
"defense_up": "hero_defense",
|
||||
"weaken": "enemy_output",
|
||||
"ambush": "flat_attach",
|
||||
"enrage": "enemy_output",
|
||||
"off_guard": "hero_defense"
|
||||
}
|
||||
pool_clamp_lo = -100
|
||||
pool_clamp_hi = 100
|
||||
round_direction = "floor"
|
||||
damage_a_per_cell = 1
|
||||
damage_a_cap = 8
|
||||
damage_b_per_flag = 2
|
||||
armor_effectiveness = {
|
||||
"base": {
|
||||
"none": 100,
|
||||
"a": 100,
|
||||
"b": 100
|
||||
},
|
||||
"dmg_a": {
|
||||
"none": 100,
|
||||
"a": 150,
|
||||
"b": 50
|
||||
},
|
||||
"dmg_b": {
|
||||
"none": 100,
|
||||
"a": 50,
|
||||
"b": 150
|
||||
}
|
||||
}
|
||||
|
||||
; 伤害类型 × 护甲克制批(2026-09-12 用户拍板结构,数值 provisional 待用户/C3 调参):
|
||||
; 主角攻击 = 基础(atk×(1+attack_up),对全部护甲 100%)+ 伤害A(开格充能×克制)+ 伤害B(标雷充能×克制),
|
||||
; 三分量各自 floor 后相加;克制语义 = 特化甲 ×150 / 无甲 ×100 / 对位甲 ×50(穿甲弹/高爆弹式对策三角)。
|
||||
; damage_a_per_cell=1 · damage_a_cap=8(单轮充能封顶,防连锁轮失控;0 = 不封顶)· damage_b_per_flag=2
|
||||
; (每颗首次标中雷充能——沿用每雷实例一次去重,忘拔旗不重复充能)。
|
||||
; enrage(敌方暴走 +25% 进 enemy_output 池)/ off_guard(我方失衡 +25% 进 hero_defense 池)=
|
||||
; 标错惩罚池两行(第三项 = 复用 ambush +2 层);时效均 1 结算(用户拍板"短点,比如 1 回合")。
|
||||
; 减伤墙调参批(2026-09-13,playtest-2026-09-12 报告 Q1 触发):拟改 pool_clamp_lo=-60 被 schema
|
||||
; 冻结域拦截([-100,+100] 结构冻结——B1 F2 乘区结构决策),杠杆改走 status_effects.tres
|
||||
; defense_up magnitude -50 → -30(叠满两层 = -60 减伤帽,本表 clamp 不动);登记以 status_effects.tres 批注为准。
|
||||
@@ -0,0 +1,66 @@
|
||||
[gd_resource type="Resource" script_class="StatusEffectTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/status_effect_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
rows = [{
|
||||
"effect_id": "attack_up",
|
||||
"display_name": "增伤",
|
||||
"target_side": "hero",
|
||||
"magnitude": 50,
|
||||
"unit": "percent",
|
||||
"duration_settlements": 1,
|
||||
"stack_policy": "stack",
|
||||
"max_stack": 2
|
||||
}, {
|
||||
"effect_id": "defense_up",
|
||||
"display_name": "减伤",
|
||||
"target_side": "hero",
|
||||
"magnitude": -30,
|
||||
"unit": "percent",
|
||||
"duration_settlements": 1,
|
||||
"stack_policy": "stack",
|
||||
"max_stack": 2
|
||||
}, {
|
||||
"effect_id": "ambush",
|
||||
"display_name": "伏击",
|
||||
"target_side": "enemy",
|
||||
"magnitude": 3,
|
||||
"unit": "flat",
|
||||
"duration_settlements": 1,
|
||||
"stack_policy": "stack",
|
||||
"max_stack": 16
|
||||
}, {
|
||||
"effect_id": "weaken",
|
||||
"display_name": "削弱",
|
||||
"target_side": "enemy",
|
||||
"magnitude": -65,
|
||||
"unit": "percent",
|
||||
"duration_settlements": 1,
|
||||
"stack_policy": "stack",
|
||||
"max_stack": 16
|
||||
}, {
|
||||
"effect_id": "enrage",
|
||||
"display_name": "暴走",
|
||||
"target_side": "enemy",
|
||||
"magnitude": 25,
|
||||
"unit": "percent",
|
||||
"duration_settlements": 1,
|
||||
"stack_policy": "stack",
|
||||
"max_stack": 16
|
||||
}, {
|
||||
"effect_id": "off_guard",
|
||||
"display_name": "失衡",
|
||||
"target_side": "hero",
|
||||
"magnitude": 25,
|
||||
"unit": "percent",
|
||||
"duration_settlements": 1,
|
||||
"stack_policy": "stack",
|
||||
"max_stack": 16
|
||||
}]
|
||||
|
||||
; 减伤墙调参批(2026-09-13,playtest-2026-09-12 报告 Q1 触发——defense_scan 实测墙轮 39%、
|
||||
; 真人局 12/12 轮零承伤满血通关):defense_up magnitude -50 → -30——叠满两层 = -60 减伤帽
|
||||
; (原拟 pool_clamp_lo=-60 被 schema 冻结域拦截,杠杆移至本表);石肤权重 24→16 随批(reward.tres)。
|
||||
; 承伤带对 C3 F4(≤20%/普通场、≤50%/Boss 场)复验责任随批;B3 GDD "-50" 文字同步随下次 B3 修订批。
|
||||
@@ -0,0 +1,16 @@
|
||||
[gd_resource type="Resource" script_class="VariationTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/variation_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
rows = [{
|
||||
"variant_id": "fog",
|
||||
"display_name": "迷雾",
|
||||
"unlock_level": 2,
|
||||
"scope": "region",
|
||||
"info_mask": "hide_numbers",
|
||||
"cell_constraint": "safe_nonzero",
|
||||
"biome_tags": [],
|
||||
"counter_item_ids": []
|
||||
}]
|
||||
@@ -0,0 +1,74 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-12(code-review 修复批——自 main.gd 拆出,规范 1.1/1.3 文案拼装独立成装配器)
|
||||
# 战斗流程叙事文案装配器:组合根在模式流转点的玩家可见文案(棋盘日志横幅 / 节点行 / 终局文案 /
|
||||
# 相位指示)在此统一拼装——main.gd 头注"本类不拼呈现字符串"由本类兑现。静态无状态。
|
||||
# 数值口径注记:胜利文案携带的 HP/道具数 = 关卡载荷快照值(每关口径)。
|
||||
extends RefCounted
|
||||
|
||||
|
||||
# 节点进入横幅(棋盘日志——battle/reboss/reward/rest 共用区段样式)
|
||||
static func node_banner(runner, node: Dictionary, t: String) -> String:
|
||||
return "[color=#e0b860]── 节点 %d/%d · %s(深度 %d)──[/color]" % [
|
||||
runner.node_index + 1, runner.nodes().size(),
|
||||
"Boss 房" if t == "boss" else "战斗", int(node.get("node_depth", 0))]
|
||||
|
||||
|
||||
static func reward_line(snap: Dictionary) -> String:
|
||||
return "[color=#d0c090]关卡奖励:%s(+%d)入库[/color]" % [
|
||||
String(snap.get("display_name", "")), int(snap.get("power", 0))]
|
||||
|
||||
|
||||
static func rest_line(healed: int, hp: int, max_hp: int) -> String:
|
||||
return "[color=#7cc47c]恢复点:回复 %d HP(→ %d/%d)[/color]" % [healed, hp, max_hp]
|
||||
|
||||
|
||||
# 关卡开场行(种子/加成口径注记——面板加成是否生效由调用方传入)
|
||||
static func level_open_line(level_display: String, seed_val: int, atk: int, max_hp: int, boosted: bool) -> String:
|
||||
return "[color=#c8a860]第一关 · %s · 种子 %d · 攻击 %d / 生命 %d%s[/color]" % [
|
||||
level_display, seed_val, atk, max_hp, "(含面板加成)" if boosted else ""]
|
||||
|
||||
|
||||
# 调试开场行
|
||||
static func debug_open_line(seed_val: int, level: int, level_mode: bool) -> String:
|
||||
return "[color=#c8a860]种子 %d · 遭遇 %d 层%s[/color]" % [
|
||||
seed_val, level, "(关卡模式 · 库存延续)" if level_mode else ""]
|
||||
|
||||
|
||||
# 关卡终局文案(result = victory/defeat;载荷 = runner level_finished 快照)
|
||||
static func level_end_text(result: String, payload: Dictionary, effective_max_hp: int) -> String:
|
||||
if result == "victory":
|
||||
return "关卡通关!\n剩余道具 ×%d 随关卡结束清空 · 剩余 HP %d/%d" % [
|
||||
int(payload.get("remaining_items", 0)), int(payload.get("hp", 0)), effective_max_hp]
|
||||
return "关卡失败……探险者倒在了节点 %d(%s)。" % [
|
||||
int(payload.get("node_index", 0)) + 1, String(payload.get("node_type", ""))]
|
||||
|
||||
|
||||
# 调试战斗终局文案(level_end = 关内两层模式的关卡末;left = 清库剩余件数)
|
||||
static func battle_end_text(result: String, level_end: bool, left: int) -> String:
|
||||
if level_end:
|
||||
return "关卡通关!\n剩余道具 ×%d 随关卡结束清空" % left
|
||||
if result == "victory":
|
||||
return "战斗胜利!敌人全灭。"
|
||||
return "战斗失败……探险者倒下了。"
|
||||
|
||||
|
||||
# 相位指示文本(B 区轮次·阶段行)
|
||||
static func phase_text(round_index: int, battle_result: String, window_open: bool, waiting: bool, boss_active: bool, boss_name: String) -> String:
|
||||
var boss_tag := " · Boss:%s" % boss_name if boss_active else ""
|
||||
if battle_result != "":
|
||||
return "第 %d 轮 · 战斗结束%s" % [round_index, boss_tag]
|
||||
if window_open:
|
||||
return "第 %d 轮 · 道具窗口%s" % [round_index, boss_tag]
|
||||
if waiting:
|
||||
return "第 %d 轮 · 结算中%s" % [round_index, boss_tag]
|
||||
return "第 %d 轮 · 挖掘中%s" % [round_index, boss_tag]
|
||||
|
||||
|
||||
# Boss 名解析(runner Boss 节点 → enemy 表行名;缺省遗迹守卫)
|
||||
static func boss_name(runner, db) -> String:
|
||||
if runner != null:
|
||||
var node: Dictionary = runner.current_node()
|
||||
if String(node.get("type", "")) == "boss":
|
||||
var row: Dictionary = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)))
|
||||
return String(row.get("display_name", ""))
|
||||
return "遗迹守卫"
|
||||
@@ -0,0 +1 @@
|
||||
uid://cgpyvmn146x5w
|
||||
@@ -0,0 +1,102 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-13(封装审查 O1/O2 修复批——自 main.gd 拆出,规范 1.1 审视线 / 单一实现纪律)
|
||||
# 调试模式编排控制器:单场遭遇(第 1/2 层)与连续两层(库存与 HP 关内延续)的模式状态机与战斗生命周期。
|
||||
# 职责边界:层序/携带 HP/共享 B4 状态 + B1 生命周期编排(创建 / begin_battle / 关卡末 level_clear)。
|
||||
# 授权先例:与 tune_sim_controller 同款(code-review 2026-09-12 批)——控制器承接模式编排,
|
||||
# 组合根仍持有全部信号接线(battle_created 先于 begin_battle 发出,ADR-0001 接线唯一归组合根)
|
||||
# 与终局 UI 决策;玩家意图写调用(settle_batch / 窗口提交 / 刷新)不经本类。
|
||||
# E4 收口(2026-09-13):加成生效值经注入 Callable 供给(组合根从面板百分比现算)→ encounter
|
||||
# 显式传入战斗——零表写,"双写叠加/双基线同步"整类隐患随表写移除。
|
||||
# O2 收口:调试两层的"库存与 HP 延续"语义唯一实现点(此前与 main.gd 调试分支双实现漂移)。
|
||||
extends RefCounted
|
||||
|
||||
signal battle_created(b1, level: int, carry_hp: int) # begin_battle 前——组合根完成信号接线(ADR-0001)
|
||||
signal battle_ready(b1, level: int, carry_hp: int) # begin_battle 完成后——组合根 rebuild 棋盘 + 刷新呈现
|
||||
|
||||
var db
|
||||
var rng # /root/RngService(组合根注入)
|
||||
var _hero_override: Callable # 组合根生效值供给(→ {atk, max_hp};E4:经 encounter 传参,零表写)
|
||||
var _seed_value: Callable # 组合根种子读取(LineEdit)
|
||||
|
||||
var _level_mode := false
|
||||
var _level_battles: Array = []
|
||||
var _level_idx := 0
|
||||
var _carry_hp := 0
|
||||
var _shared_b4 = null
|
||||
var b1 = null # 当前战斗编排器(组合根在 battle_created 接线、battle_ready 接手)
|
||||
|
||||
|
||||
func _init(db_ref, rng_ref, hero_override: Callable, seed_value: Callable) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
_hero_override = hero_override
|
||||
_seed_value = seed_value
|
||||
|
||||
|
||||
func is_level_mode() -> bool:
|
||||
return _level_mode
|
||||
|
||||
|
||||
# 单场遭遇原语(2026-09-13 用户裁决:单场用户入口已删——本方法仅 ui_probe 雾板变体段使用;
|
||||
# 无库存延续、standalone 终局语义保留供探针战斗收尾)
|
||||
func start_single(level: int) -> bool:
|
||||
_level_mode = false
|
||||
_shared_b4 = null
|
||||
return _start_battle(level, -1)
|
||||
|
||||
|
||||
# 连续两层(调试入口——共享 B4 + HP 携带;首层满血开局)
|
||||
func start_debug_levels() -> bool:
|
||||
_level_mode = true
|
||||
_level_battles = [1, 2]
|
||||
_level_idx = 0
|
||||
_carry_hp = 0
|
||||
rng.new_adventure(_seed_value.call())
|
||||
_shared_b4 = RewardEconomy.new(db, rng)
|
||||
return _start_battle(_level_battles[0], -1)
|
||||
|
||||
|
||||
# 层间"进入下一层"(组合根过渡覆盖层确认后调用)
|
||||
func next_layer() -> bool:
|
||||
return _start_battle(_level_battles[_level_idx], _carry_hp)
|
||||
|
||||
|
||||
# 终局处置(组合根 battle_finished 处理器调用)→ 决策载荷:
|
||||
# {outcome: "next_layer"|"level_end"|"standalone_end", layer, carry_hp, items_left, remaining}
|
||||
func on_battle_finished(payload: Dictionary) -> Dictionary:
|
||||
var result: String = String(payload.get("result", ""))
|
||||
if result == "victory" and _level_mode and _level_idx < _level_battles.size() - 1:
|
||||
_carry_hp = maxi(1, int(payload.get("hero_hp", 0))) # despawn 前捕获值
|
||||
_level_idx += 1
|
||||
return {"outcome": "next_layer", "layer": _level_idx, "carry_hp": _carry_hp,
|
||||
"items_left": b1.stock_count()}
|
||||
var remaining := 0
|
||||
var is_level_end := result == "victory" and _level_mode
|
||||
if is_level_end:
|
||||
remaining = int(b1.level_clear()["remaining"]) # 库存随关卡结束清空(关内延续裁决③)
|
||||
elif _level_mode:
|
||||
b1.level_clear() # 失败同样清库(关卡域生命周期闭合——调试模式与 runner 模式同规)
|
||||
return {"outcome": "level_end" if is_level_end else "standalone_end",
|
||||
"layer": _level_idx, "carry_hp": _carry_hp, "items_left": 0, "remaining": remaining}
|
||||
|
||||
|
||||
# 会话复位(组合根终局出口"返回开始界面"调用)
|
||||
func reset() -> void:
|
||||
_level_mode = false
|
||||
_shared_b4 = null
|
||||
b1 = null
|
||||
|
||||
|
||||
func _start_battle(level: int, carry_hp: int) -> bool:
|
||||
if not _level_mode or _level_idx == 0:
|
||||
rng.new_adventure(_seed_value.call())
|
||||
var ov: Dictionary = _hero_override.call() # E4:生效值经 encounter 传入——零表写
|
||||
b1 = BattleOrchestrator.new(db, rng, _shared_b4 if _level_mode else null)
|
||||
battle_created.emit(b1, level, carry_hp)
|
||||
var hp: int = carry_hp if carry_hp > 0 else int(ov.max_hp)
|
||||
if not b1.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": hp,
|
||||
"hero_atk": int(ov.atk), "hero_max_hp": int(ov.max_hp)}):
|
||||
push_error("[DebugSession] 战斗启动失败(level %d)" % level)
|
||||
return false
|
||||
battle_ready.emit(b1, level, carry_hp)
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://dsfxx2nhu487l
|
||||
@@ -0,0 +1,208 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(五区呈现装配器)——读核心只读面(B1 呈现快照 / runner)→
|
||||
# 组装文案/投影 → 驱动各视图 setter。
|
||||
# 高内聚边界:"核心状态 → 呈现文案"的全部翻译集中于此;视图保持零核心依赖;
|
||||
# 静态无状态——呈现不持久化任何核心句柄。
|
||||
# E1 封装收口(2026-09-13):核心状态读取统一经 B1.presentation_snapshot() 成文快照面——
|
||||
# 禁再链式穿透 b1.b2.hero / b1.b2.enemies / b1.b3.query_active 一类路径(B1 协作对象已私有化,
|
||||
# 敌人含死亡残位的投影由快照 enemies[] 承载,含 alive/beat/ring 字段)。
|
||||
# 更新触发注记(battle.md Data Requirements):旗/拔/移动成功反馈零事件外发(A2 发射面封闭)——
|
||||
# 经 ActionOutcome 返回值命令式驱动;AP/RAP 无推送信号——组合根每次调用返回后重查;
|
||||
# B3 纯被动容器零信号——状态栏刷新绑 B1 结算编排完成时点;库存 B4 查询面——写路径回执后重查。
|
||||
# Date: 2026-09-12(五区改造批;基础 = 2026-09-11 HUD 装配器;2026-09-13 快照面改造)
|
||||
class_name HudPresenter
|
||||
extends RefCounted
|
||||
|
||||
|
||||
# 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配)
|
||||
static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView,
|
||||
inv: InventoryPanelView, b1, runner, level_active: bool, db, refresh_remaining: int) -> void:
|
||||
if b1 == null:
|
||||
_refresh_out_of_battle(stage, info, inv, runner, level_active, db)
|
||||
return
|
||||
var s: Dictionary = b1.presentation_snapshot()
|
||||
var hero: Dictionary = s.hero
|
||||
if hero.is_empty() or not bool(hero.get("alive", false)):
|
||||
_refresh_out_of_battle(stage, info, inv, runner, level_active, db)
|
||||
return
|
||||
_refresh_hero(stage, info, s)
|
||||
_refresh_economy(info, ap_bar, s, db, refresh_remaining)
|
||||
stage.set_enemies(s.enemies)
|
||||
info.set_status_effects(s.hero_effects)
|
||||
_fill_inventory(inv, s.inventory)
|
||||
|
||||
|
||||
# ---- B 区英雄卡 + C 区数据框 ----
|
||||
|
||||
static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Dictionary) -> void:
|
||||
var hero: Dictionary = s.hero
|
||||
var hp: int = int(hero.hp)
|
||||
var max_hp: int = int(hero.max_hp)
|
||||
var atk: int = int(hero.atk)
|
||||
# 修正 = attack_up 池有效值(hero_offense);来源角标 = 效果 display_name(快照 hero_effects 投影)
|
||||
var mod_total := 0
|
||||
var sources: Array = []
|
||||
for eff in s.hero_effects:
|
||||
if String(eff.effect_id) == "attack_up":
|
||||
mod_total += int(eff.effective_value)
|
||||
sources.append("%s×%d" % [String(eff.display_name), int(eff.layers)])
|
||||
var mod_text := ""
|
||||
if mod_total != 0:
|
||||
mod_text = "(%+d%% %s)" % [mod_total, "·".join(sources)]
|
||||
stage.hero_card.set_data(hp, max_hp, atk, mod_text, _hero_status_summary(s.hero_effects))
|
||||
info.set_hero(hp, max_hp)
|
||||
info.set_attack(atk, mod_text)
|
||||
|
||||
|
||||
static func _hero_status_summary(effects: Array) -> String:
|
||||
var parts: Array = []
|
||||
for eff in effects:
|
||||
parts.append("%s×%d(剩 %d)" % [String(eff.display_name), int(eff.layers), int(eff.remaining_settlements)])
|
||||
return " ".join(parts) if not parts.is_empty() else "状态:无"
|
||||
|
||||
|
||||
# ---- 行动经济(AP 点阵 / RAP / 弃图刷新按钮态) ----
|
||||
|
||||
static func _refresh_economy(info: HeroInfoPanelView, ap_bar: ApBarView, s: Dictionary, db, refresh_remaining: int) -> void:
|
||||
ap_bar.set_ap(int(s.ap), int(db.action_economy.ap_per_round))
|
||||
# RAP 活值投影(C 区点阵)——清零反馈分支(A3 D1 义务)依赖本处持续投喂才会可达
|
||||
info.set_rap(int(s.rap), int(db.action_economy.rap_refresh_cost))
|
||||
var digging := String(s.phase) == ActionEconomy.PHASE_ACTIVE
|
||||
ap_bar.set_refresh_state(refresh_remaining,
|
||||
bool(s.can_request_refresh) and digging) # Waiting 代理已随 OQ12 退役——UI 仅挖掘期受理
|
||||
ap_bar.set_stop_enabled(bool(s.hero.get("alive", false)) and digging)
|
||||
|
||||
|
||||
# ---- E 区库存常读列表(枚举序 = stock 受理序 E21;来源不呈现——2026-09-12 拍板) ----
|
||||
|
||||
static func _fill_inventory(inv: InventoryPanelView, inventory: Array) -> void:
|
||||
# 窗口态占屏(D2 原位窗口激活)——常读刷新不得覆盖窗口条目(窗口关闭后由下一轮刷新恢复常读面)
|
||||
if inv.is_window_open():
|
||||
return
|
||||
var rows: Array = []
|
||||
for inst in inventory:
|
||||
if inst.status != "InStock":
|
||||
continue
|
||||
rows.append(_inventory_row(inst.display_name, String(inst.category), int(inst.power), "可用"))
|
||||
if rows.is_empty():
|
||||
inv.show_empty()
|
||||
else:
|
||||
inv.show_inventory(rows)
|
||||
|
||||
|
||||
# 行值快照投影派生属性简述(伤害/回复/增益一句话)——来源 {cell, round} 不呈现
|
||||
static func _inventory_row(name_: String, category: String, power: int, status: String) -> Dictionary:
|
||||
var desc := ""
|
||||
var tooltip := ""
|
||||
match category:
|
||||
"damage_item":
|
||||
desc = "伤害 %d · 一次性" % power
|
||||
tooltip = "对最前敌造成 %d 点伤害(恒定表值,不吃修正)。触发:轮末道具窗口选用,道具段生效。" % power
|
||||
"heal_pack":
|
||||
desc = "回复 %d · 可用" % power
|
||||
tooltip = "回复 %d 点 HP(超量截断)。触发:轮末道具窗口选用;满血时禁选(不烧包)。" % power
|
||||
"buff":
|
||||
desc = "增益 · 结算期生效"
|
||||
tooltip = "状态效果增益。触发:轮末结算增益/减益段。"
|
||||
_:
|
||||
desc = "威力 %d" % power
|
||||
tooltip = "触发:轮末结算。"
|
||||
return {"display_name": name_, "category": category, "desc": desc, "tooltip": tooltip, "status": status}
|
||||
|
||||
|
||||
# 窗口条目投影(B1 window_opened 载荷 → E 区窗口态行;pos = 窗口条目位置——缺陷④契约)
|
||||
static func window_rows(items: Array, hero_hp: int, hero_max_hp: int) -> Array:
|
||||
var rows: Array = []
|
||||
for pos in items.size():
|
||||
var it: Dictionary = items[pos]
|
||||
var row := _inventory_row(String(it.display_name), String(it.category), int(it.power), "")
|
||||
row["pos"] = pos
|
||||
row["usable_now"] = bool(it.usable_now)
|
||||
row["reason"] = "HP 已满 · 禁选" if not bool(it.usable_now) else ""
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
# 战斗外(节点间隙/关卡结束)——HP 读 runner 携带值
|
||||
static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelView, inv: InventoryPanelView,
|
||||
runner, level_active: bool, db) -> void:
|
||||
if not level_active:
|
||||
return
|
||||
if runner != null:
|
||||
info.set_hero(runner.hero_hp, runner.effective_max_hp())
|
||||
info.set_attack(runner.effective_atk(), "(关卡推进中)")
|
||||
stage.hero_card.set_data(runner.hero_hp, runner.effective_max_hp(), runner.effective_atk(), "(关卡推进中)", "")
|
||||
stage.clear_enemies() # 节点间隙清残位敌卡(死亡瞬间的灰化呈现已在结算行驱动完成)
|
||||
info.set_rap(0, int(db.action_economy.rap_refresh_cost)) # battle_over RAP 清零反馈(A3 义务)
|
||||
var rows: Array = []
|
||||
for inst in runner.inventory():
|
||||
if inst.status == "InStock":
|
||||
rows.append(_inventory_row(inst.display_name, String(inst.category), int(inst.power), "可用"))
|
||||
if rows.is_empty():
|
||||
inv.show_empty()
|
||||
else:
|
||||
inv.show_inventory(rows)
|
||||
|
||||
|
||||
# 三源拒绝可感知(A2 reject_reason/sub_reason → 玩家文案;pattern 1 双源拒绝反馈)
|
||||
static func outcome_text(outcome: Dictionary) -> String:
|
||||
if outcome.accepted:
|
||||
return "执行完成。"
|
||||
match String(outcome.get("reject_reason", "")):
|
||||
"board_face":
|
||||
var sub := String(outcome.get("sub_reason", ""))
|
||||
var texts := {
|
||||
"not_adjacent": "不能执行:目标与小人不相邻",
|
||||
"target_revealed": "不能执行:目标已开",
|
||||
"target_concealed": "不能移动:目标未开",
|
||||
"flag_present": "不能挖:此格有旗——先右键拔旗",
|
||||
"flag_absent": "不能拔旗:此格无旗",
|
||||
"no_path": "不能移动:已探明区不连通",
|
||||
"out_of_catalog": "无效目标",
|
||||
}
|
||||
return texts.get(sub, "棋盘面拒绝: " + sub)
|
||||
"resource_face":
|
||||
return "AP 不足。"
|
||||
"phase":
|
||||
return "结算中,非行动阶段。"
|
||||
"board_invalid":
|
||||
return "棋盘数据非法(fail-fast)。"
|
||||
_:
|
||||
return "动作被拒。"
|
||||
|
||||
|
||||
# ---- 调整板批量模拟报告装配(文案归 presenter——视图只 set_text;code-review 2026-09-12 批) ----
|
||||
|
||||
static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, wins: int, dmg_all: Array, hp_left_win: Array, hp_boss: Array, rounds_all: Array, death_nodes: Dictionary, base_hp: int) -> String:
|
||||
var lines: Array = []
|
||||
lines.append("[b]模拟 N=%d(atk+%d%% / hp+%d%%)[/b]" % [n, atk_pct, hp_pct])
|
||||
lines.append("通关率 [color=#80d080]%.0f%%[/color](%d/%d)" % [100.0 * float(wins) / float(maxi(1, n)), wins, n])
|
||||
lines.append("整关承伤:均 %.1f · p90 %.0f(基础 HP %d)" % [_mean(dmg_all), _p90(dmg_all), base_hp])
|
||||
if not hp_left_win.is_empty():
|
||||
lines.append("通关剩余 HP 均 %.1f · 进 Boss HP 均 %.1f" % [_mean(hp_left_win), _mean(hp_boss)])
|
||||
lines.append("平均轮数 %.1f" % _mean(rounds_all))
|
||||
if death_nodes.is_empty():
|
||||
lines.append("败北节点:—")
|
||||
else:
|
||||
var parts: Array = []
|
||||
for k in death_nodes.keys():
|
||||
parts.append("%s×%d" % [k, death_nodes[k]])
|
||||
lines.append("败北节点:%s(0-3=战斗序,4=Boss)" % " ".join(parts))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
static func _mean(a: Array) -> float:
|
||||
if a.is_empty():
|
||||
return 0.0
|
||||
var t := 0.0
|
||||
for v in a:
|
||||
t += float(v)
|
||||
return t / a.size()
|
||||
|
||||
|
||||
static func _p90(a: Array) -> float:
|
||||
if a.is_empty():
|
||||
return 0.0
|
||||
var b := a.duplicate()
|
||||
b.sort()
|
||||
return float(b[mini(b.size() - 1, int(0.9 * b.size()))])
|
||||
@@ -0,0 +1 @@
|
||||
uid://d15kn31qh3ae
|
||||
@@ -0,0 +1,641 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证
|
||||
# 实现 GDD:design/ux/battle.md(战斗屏五区 A/B/C/D/E + 全屏 States + 遥测)。
|
||||
# Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 D1-lite 组合根;code-review 修复批重构)
|
||||
# 2026-09-13(封装审查修复批):核心状态读取收口至 B1/runner 成文只读面(presentation_snapshot /
|
||||
# current_board / current_battle 等——禁链式穿透 b1.b2.hero 一类路径);调试模式编排抽离
|
||||
# debug_session.gd(规范 1.1 审视线 + 调试语义单一实现)。
|
||||
# 组合根(composition root):唯一的模式流转与 core↔view 接线处。
|
||||
# 职责边界:① 构建五区视图并接线信号(节点构建归 ui_assembler.gd——规范 1.1,本类只接线)
|
||||
# ② 模式管理(完整关卡 C1 runner / 调试两层——后者编排归 debug_session.gd;单场原语仅探针使用)
|
||||
# ③ 核心写调用只在此处发生(a2.settle_batch / b1.submit_window_selection / 刷新编排入口 / runner 推进;
|
||||
# E4 收口:加成值零表写——面板百分比经 runner.set_hero_override / debug_session 生效值 Callable
|
||||
# 以 encounter 参数入战斗,db.hero 运行期只读)
|
||||
# ④ 全屏 States 状态机(挖掘/结算/道具窗口/终局)与一次性提示去重。
|
||||
# 渲染与文案归 views/,核心状态→文案装配归 hud_presenter.gd 与 battle_narrator.gd,节点构建归
|
||||
# ui_assembler.gd,模态层级归 modal_stack.gd,批量模拟编排归 tune_sim_controller.gd,调试模式编排归
|
||||
# debug_session.gd——本类不拼呈现字符串。
|
||||
# ADR-0001 纪律:呈现层永不直接订阅 A2/A3——只订 B1/runner 转发信号;相位读取仅用于驱动视图状态。
|
||||
extends Control
|
||||
|
||||
const UiAssembler := preload("res://presentation/ui_assembler.gd")
|
||||
const Narrator := preload("res://presentation/battle_narrator.gd")
|
||||
const ModalStack := preload("res://presentation/modal_stack.gd")
|
||||
const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
|
||||
const DebugSessionScript := preload("res://presentation/debug_session.gd")
|
||||
|
||||
var db
|
||||
var b1 = null # 当前战斗编排器(关卡与调试模式统一读它)
|
||||
var runner = null # C1 线性关卡推进器(完整关卡模式)
|
||||
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
|
||||
var _level_active := false
|
||||
var _seed_edit: LineEdit
|
||||
var _start_overlay: Control
|
||||
var _end_overlay: BattleEndOverlay
|
||||
var _inter_overlay: Control
|
||||
var _inter_label: Label
|
||||
|
||||
# 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI)
|
||||
var _debug = null
|
||||
|
||||
# 视图(views/)——battle.md 五区
|
||||
var strip_view: LevelStripView # A 区
|
||||
var stage: BattleStageView # B 区(含 log_view 明细带)
|
||||
var log_view: BattleLogView
|
||||
var info_panel: HeroInfoPanelView # C 区
|
||||
var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx)
|
||||
var board_view: BoardView # 探针兼容别名(= board_zone.board_view)
|
||||
var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar)
|
||||
var inventory_panel: InventoryPanelView # E 区(原位窗口态)
|
||||
# 模态 overlay(z 序 = 登记序;pattern 6 层级纪律)
|
||||
var legend_overlay: LegendOverlay
|
||||
var review_overlay: SettlementReviewOverlay
|
||||
var node_overlay: NodeOverlay
|
||||
var tune_panel: TunePanel
|
||||
|
||||
var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体)
|
||||
var _tune_sim = null # 调整板批量模拟控制器(tune_sim_controller.gd)
|
||||
var _hints_shown := {} # 一次性非模态提示去重(每 run 一次:fog_first/chord/no_adjacent)
|
||||
var _refresh_used_round := 0 # 本轮已受理刷新次数(遥测 round_refresh_index——每轮 round_started 归零)
|
||||
var _boss_active := false # Boss 遭遇变体(阶段指示附 Boss 名)
|
||||
var _window_payload: Array = [] # 窗口投影缓存(ux_window_choice 遥测读 usable_now/item_id)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
db = get_node("/root/ConfigDB")
|
||||
if not db.is_ready:
|
||||
var ok: bool = db.load_tables("res://data/tables", false)
|
||||
if not ok:
|
||||
_fatal_screen("数值表加载失败——见输出错误清单(fail-fast)")
|
||||
return
|
||||
_debug = DebugSessionScript.new(db, get_node("/root/RngService"),
|
||||
Callable(self, "_debug_hero_override"), Callable(self, "_seed_value"))
|
||||
_debug.battle_created.connect(_on_debug_battle_created)
|
||||
_debug.battle_ready.connect(_on_debug_battle_ready)
|
||||
_build_ui()
|
||||
_start_overlay.visible = true
|
||||
|
||||
|
||||
# ==================== 视图接线(battle.md 五区布局 1280×800;节点构建归 UiAssembler) ====================
|
||||
|
||||
func _build_ui() -> void:
|
||||
var root := VBoxContainer.new()
|
||||
add_child(root)
|
||||
var five: Dictionary = UiAssembler.build_five_zone(root, db)
|
||||
strip_view = five.strip_view
|
||||
stage = five.stage
|
||||
log_view = five.log_view
|
||||
info_panel = five.info_panel
|
||||
board_zone = five.board_zone
|
||||
board_view = five.board_view # 探针兼容别名(= board_zone.board_view)
|
||||
ap_bar = five.ap_bar # 探针兼容别名(= board_zone.ap_bar)
|
||||
inventory_panel = five.inventory_panel
|
||||
log_view.review_requested.connect(_open_review_from_band)
|
||||
board_zone.action_requested.connect(_on_board_action)
|
||||
board_zone.stop_pressed.connect(_on_stop_pressed)
|
||||
board_zone.refresh_confirmed.connect(_on_refresh_confirmed)
|
||||
board_zone.legend_toggled.connect(_on_legend_toggled)
|
||||
board_zone.hover_forbidden.connect(_on_hover_forbidden)
|
||||
board_zone.chord_attempted.connect(_on_chord_attempted)
|
||||
board_zone.fog_region_completed.connect(_on_fog_region_completed)
|
||||
inventory_panel.item_selected.connect(_on_window_item_selected)
|
||||
inventory_panel.skipped.connect(_on_window_skipped)
|
||||
inventory_panel.reject_reason.connect(_on_inventory_reject)
|
||||
|
||||
# 奖励徽章数据源:B4 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效)
|
||||
board_view.set_identity_resolver(_identity_lookup)
|
||||
|
||||
logger = PlaytestLogger.new()
|
||||
logger.ap_per_round = int(db.action_economy.ap_per_round)
|
||||
|
||||
# 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 终局 < 调整板 < 开始)
|
||||
var overlays: Dictionary = UiAssembler.build_overlays(self, db, int(db.hero.atk), int(db.hero.max_hp))
|
||||
legend_overlay = overlays.legend_overlay
|
||||
review_overlay = overlays.review_overlay
|
||||
node_overlay = overlays.node_overlay
|
||||
_end_overlay = overlays.end_overlay
|
||||
tune_panel = overlays.tune_panel
|
||||
legend_overlay.closed.connect(_on_modal_closed.bind(legend_overlay))
|
||||
review_overlay.closed.connect(_on_modal_closed.bind(review_overlay))
|
||||
node_overlay.continue_pressed.connect(_on_node_continue)
|
||||
_end_overlay.review_pressed.connect(_on_end_review)
|
||||
_end_overlay.continue_pressed.connect(_on_battle_end_continue)
|
||||
tune_panel.closed.connect(_close_tune_panel)
|
||||
tune_panel.apply_requested.connect(_on_tune_apply)
|
||||
tune_panel.simulate_requested.connect(_on_tune_simulate)
|
||||
_modals = ModalStack.new()
|
||||
for m: Control in [legend_overlay, review_overlay, node_overlay, _end_overlay, tune_panel]:
|
||||
_register_modal(m)
|
||||
_tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp))
|
||||
|
||||
var start: Dictionary = UiAssembler.build_start_overlay(self)
|
||||
_start_overlay = start.overlay
|
||||
_seed_edit = start.seed_edit
|
||||
start.lvbtn.pressed.connect(_start_full_level)
|
||||
start.tunebtn.pressed.connect(_show_tune_panel)
|
||||
start.dbgbtn.pressed.connect(_start_debug_levels)
|
||||
|
||||
var inter: Dictionary = UiAssembler.build_inter_overlay(self)
|
||||
_inter_overlay = inter.overlay
|
||||
_inter_label = inter.label
|
||||
inter.next_btn.pressed.connect(_on_inter_next_pressed)
|
||||
|
||||
var diag_btn := UiAssembler.build_diag_button(self)
|
||||
diag_btn.pressed.connect(_on_diagnose_pressed)
|
||||
|
||||
|
||||
func _fatal_screen(msg: String) -> void:
|
||||
var l := UiKit.label(msg, 20, true)
|
||||
l.add_theme_color_override("font_color", Color8(200, 60, 48))
|
||||
l.set_anchors_preset(Control.PRESET_CENTER)
|
||||
add_child(l)
|
||||
|
||||
|
||||
# ==================== 模态 overlay 层级管理(pattern 6 / 缺陷⑤——状态机归 modal_stack.gd) ====================
|
||||
|
||||
func _register_modal(m: Control) -> void:
|
||||
_modals.register(m)
|
||||
|
||||
|
||||
func _open_modal(m: Control) -> void:
|
||||
# 打开时显式隐藏树序在其上方的可见层(遮挡物隐藏),关闭恢复——单一模态栈
|
||||
_modals.open(m)
|
||||
|
||||
|
||||
func _close_modal(m: Control) -> void:
|
||||
_modals.close(m)
|
||||
|
||||
|
||||
func _on_modal_closed(m: Control) -> void:
|
||||
_close_modal(m)
|
||||
|
||||
|
||||
func _input_locked() -> bool:
|
||||
# 模态/非战斗期棋盘输入锁(各 overlay 自身可见即锁定)
|
||||
return legend_overlay.is_open() or review_overlay.is_open() or _end_overlay.is_open() \
|
||||
or node_overlay.visible or tune_panel.visible or _start_overlay.visible \
|
||||
or _inter_overlay.visible or b1 == null
|
||||
|
||||
|
||||
# ==================== 完整关卡模式(C1 线性推进器)====================
|
||||
|
||||
func _start_full_level() -> void:
|
||||
_start_overlay.visible = false
|
||||
# E4:加成经 runner.set_hero_override → encounter 参数入战斗(零表写——无双写叠加隐患)
|
||||
runner = LevelRunner.new(db, get_node("/root/RngService"))
|
||||
runner.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct())
|
||||
runner.step_row.connect(_on_step_row)
|
||||
runner.window_opened.connect(_on_window_opened)
|
||||
runner.round_started.connect(_on_round_started)
|
||||
runner.board_changed.connect(_on_board_changed)
|
||||
runner.node_entered.connect(_on_runner_node)
|
||||
runner.level_finished.connect(_on_level_finished)
|
||||
_level_active = true
|
||||
_reset_run_scope()
|
||||
log_view.clear()
|
||||
var seed_val := _seed_value()
|
||||
logger.start_session(seed_val) # 各战斗的 attach 在 _on_runner_node 内(b1 逐战创建)
|
||||
if not runner.start_level(1, seed_val):
|
||||
_fatal_screen("关卡启动失败——见输出日志")
|
||||
return
|
||||
log_view.append_bb(Narrator.level_open_line(String(runner.level_row.get("display_name", "")),
|
||||
seed_val, int(db.hero.atk), int(db.hero.max_hp),
|
||||
tune_panel.atk_pct() > 0 or tune_panel.hp_pct() > 0))
|
||||
|
||||
|
||||
func _reset_run_scope() -> void:
|
||||
# 每 run 一次的提示去重与轮内计数(弃图刷新遥测轮内序号)
|
||||
_hints_shown = {}
|
||||
_refresh_used_round = 0
|
||||
_boss_active = false
|
||||
inventory_panel.close_window()
|
||||
|
||||
|
||||
func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
|
||||
var t := String(node.get("type", ""))
|
||||
if t == "battle" or t == "boss":
|
||||
b1 = runner.current_battle() # 呈现层统一读 b1(与调试模式共用接线)
|
||||
logger.attach_battle(b1)
|
||||
_boss_active = t == "boss"
|
||||
_show_board_fx_for_node(t)
|
||||
log_view.append_bb(Narrator.node_banner(runner, node, t))
|
||||
elif t == "reward":
|
||||
var snap: Dictionary = info.get("snapshot", {})
|
||||
_open_modal(node_overlay)
|
||||
node_overlay.show_reward(snap)
|
||||
log_view.append_bb(Narrator.reward_line(snap))
|
||||
elif t == "rest":
|
||||
var healed: int = int(info.get("healed", 0))
|
||||
_open_modal(node_overlay)
|
||||
node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))
|
||||
log_view.append_bb(Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))))
|
||||
logger.log_node(node, info, runner.node_index)
|
||||
strip_view.refresh(runner)
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_level_finished(payload: Dictionary) -> void:
|
||||
_level_active = false
|
||||
var result: String = String(payload.get("result", ""))
|
||||
b1 = null
|
||||
_sync_settlement_visuals() # → 全关
|
||||
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(B3×4④)
|
||||
stage.gray_all_enemies() # 死亡兜底:卡体灰化残位(禁瞬消语义的战斗收尾面)
|
||||
if result == "victory":
|
||||
# 关卡结算:库存清空可见反馈(B4 D1×⑤)+ 胜利终局 overlay(金蜡封落于棋盘中央)
|
||||
var remaining: int = int(payload.get("remaining_items", 0))
|
||||
inventory_panel.play_cleared_feedback(remaining)
|
||||
_end_overlay.open_victory(Narrator.level_end_text("victory", payload, runner.effective_max_hp()),
|
||||
"返回冒险", board_view.get_global_rect())
|
||||
else:
|
||||
_end_overlay.open_defeat(Narrator.level_end_text("defeat", payload, 0),
|
||||
"返回", board_view.get_global_rect())
|
||||
logger.log_level_end(payload) # 试玩时间线:关卡终局
|
||||
runner.restore_hero_base() # 关卡结束——覆盖值归零(下次从面板重新应用)
|
||||
strip_view.refresh(runner)
|
||||
_open_modal(_end_overlay)
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_battle_end_continue() -> void:
|
||||
# 终局出口:返回开始界面(切片无下一关数据面——NodeOverlay/关卡结算屏归相邻域 spec)
|
||||
_close_modal(_end_overlay)
|
||||
_end_overlay.close()
|
||||
_debug.reset()
|
||||
if runner != null:
|
||||
runner.restore_hero_base()
|
||||
runner = null
|
||||
b1 = null
|
||||
tune_panel.reset_sliders()
|
||||
strip_view.refresh(null)
|
||||
_start_overlay.visible = true
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_end_review() -> void:
|
||||
review_overlay.open(log_view.parsed_text())
|
||||
|
||||
|
||||
# ==================== 调试模式(连续两层——编排归 debug_session.gd;
|
||||
# 单场遭遇用户入口已删(2026-09-13 用户裁决)——debug_session.start_single 原语仅探针使用)====================
|
||||
|
||||
func _start_debug_levels() -> void:
|
||||
tune_panel.reset_sliders()
|
||||
_start_overlay.visible = false
|
||||
log_view.clear()
|
||||
log_view.append_bb(Narrator.debug_open_line(_seed_value(), 1, true))
|
||||
_reset_run_scope()
|
||||
if not _debug.start_debug_levels():
|
||||
_fatal_screen("战斗启动失败——见输出日志")
|
||||
|
||||
|
||||
func _on_debug_battle_created(b1_ref, _level: int, _carry: int) -> void:
|
||||
# begin_battle 前接手 + 接线(ADR-0001:signal.connect 唯一归组合根)。b1 必须先于 begin_battle
|
||||
# 就位——begin 期间发出的 board_changed 要用新板重建棋盘并触发雾首遇提示(缺陷①同款时序义务)
|
||||
b1 = b1_ref
|
||||
_wire_battle(b1_ref)
|
||||
|
||||
|
||||
func _on_debug_battle_ready(b1_ref, _level: int, _carry: int) -> void:
|
||||
b1 = b1_ref
|
||||
board_view.rebuild(b1.a2.current_board(), b1.a2)
|
||||
_check_board_hints()
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_inter_next_pressed() -> void:
|
||||
_inter_overlay.visible = false
|
||||
if not _debug.next_layer():
|
||||
_fatal_screen("战斗启动失败——见输出日志")
|
||||
|
||||
|
||||
# E4:加成生效值供给(debug_session 注入 Callable——每次从面板百分比现算,经 encounter 传参零表写;
|
||||
# 原表写路径 _apply_hero_override/_scaled_override 已废除,双写叠加 6→18→54 整类隐患随之移除)
|
||||
func _debug_hero_override() -> Dictionary:
|
||||
return {"atk": _scaled(int(db.hero.atk), tune_panel.atk_pct()),
|
||||
"max_hp": _scaled(int(db.hero.max_hp), tune_panel.hp_pct())}
|
||||
|
||||
|
||||
func _scaled(base: int, pct: int) -> int:
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0)))
|
||||
|
||||
|
||||
# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——B4 Ready 域外零读、零诊断噪音)
|
||||
func _identity_lookup(c: Vector2i):
|
||||
if b1 == null or b1.a2.current_board() == null or b1.a3.phase != b1.a3.PHASE_ACTIVE:
|
||||
return null
|
||||
return b1.query_reward_identity(c)
|
||||
|
||||
|
||||
func _show_board_fx_for_node(node_type: String = "") -> void:
|
||||
# Boss 遭遇变体:紫赤影子爬棋盘一次性占位演出(§2.6)
|
||||
if node_type == "boss":
|
||||
board_zone.fx.play_boss_shadow()
|
||||
|
||||
|
||||
func _on_battle_finished(payload: Dictionary) -> void:
|
||||
# 仅调试模式接线(完整关卡的结束归 C1 runner._on_level_finished)
|
||||
var result: String = String(payload.get("result", ""))
|
||||
_sync_settlement_visuals() # → 全关(battle_over 反馈面)
|
||||
stage.gray_all_enemies() # 死亡兜底:卡体灰化残位(禁瞬消语义的战斗收尾面)
|
||||
info_panel.set_rap(0, int(db.action_economy.rap_refresh_cost)) # RAP 清零滚动 + 微文案
|
||||
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(B3×4④)
|
||||
var decision: Dictionary = _debug.on_battle_finished(payload)
|
||||
match String(decision.outcome):
|
||||
"next_layer":
|
||||
_inter_label.text = "第 %d 层通过!\n库存道具 ×%d · HP %d/%d 带入下一层" % [
|
||||
int(decision.layer), int(decision.items_left), int(decision.carry_hp), int(db.hero.max_hp)]
|
||||
_inter_overlay.visible = true
|
||||
_refresh_presentations()
|
||||
return
|
||||
"level_end":
|
||||
inventory_panel.play_cleared_feedback(int(decision.remaining))
|
||||
_end_overlay.open_victory(Narrator.battle_end_text("victory", true, int(decision.remaining)),
|
||||
"返回", board_view.get_global_rect())
|
||||
"standalone_end":
|
||||
if result == "victory":
|
||||
_end_overlay.open_victory(Narrator.battle_end_text("victory", false, 0),
|
||||
"回到遭遇选择", board_view.get_global_rect())
|
||||
else:
|
||||
_end_overlay.open_defeat(Narrator.battle_end_text("defeat", false, 0),
|
||||
"返回", board_view.get_global_rect())
|
||||
_open_modal(_end_overlay)
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
# ==================== core↔view 接线(战斗信号统一走这里)====================
|
||||
|
||||
func _wire_battle(b1_ref) -> void:
|
||||
b1_ref.step_row.connect(_on_step_row)
|
||||
b1_ref.window_opened.connect(_on_window_opened)
|
||||
b1_ref.battle_finished.connect(_on_battle_finished)
|
||||
b1_ref.round_started.connect(_on_round_started)
|
||||
b1_ref.board_changed.connect(_on_board_changed)
|
||||
|
||||
|
||||
func _on_step_row(row: Dictionary) -> void:
|
||||
log_view.append_row(row)
|
||||
stage.on_step_row_fx(row)
|
||||
# 高光演出(B1×3①② 占位):完美排雷横幅 / 标中旗加重弹道
|
||||
if String(row.get("type", "")) == "highlight":
|
||||
board_zone.fx.play_perfect_sweep()
|
||||
elif String(row.get("highlight", "")) == "flag_hit":
|
||||
board_zone.fx.play_flag_hit_projectile(true)
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_round_started(ri: int) -> void:
|
||||
log_view.finish_settlement_round() # 上轮摘要行(挖掘期 1 行——明细带 pattern 3)
|
||||
log_view.append_bb("[color=#e0b860]── 第 %d 轮 ──[/color]" % ri)
|
||||
_refresh_used_round = 0
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_board_changed() -> void:
|
||||
# 关卡模式下 board_changed 可能先于主场景接手 b1 到达(begin_battle 期间发)——先从 runner 同步
|
||||
if b1 == null and _level_active and runner != null:
|
||||
b1 = runner.current_battle()
|
||||
if b1 == null:
|
||||
return
|
||||
board_view.rebuild(b1.a2.current_board(), b1.a2)
|
||||
_check_board_hints()
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
# ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ----
|
||||
|
||||
func _check_board_hints() -> void:
|
||||
var board = b1.a2.current_board()
|
||||
if board == null:
|
||||
return
|
||||
# 零合法板(A1 EC5 降级)诚实提示——非静默(toast,非模态)
|
||||
if board.illegal_product:
|
||||
board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn")
|
||||
# 雾首遇(每 run 一次;必含三要点:无雷盲区不赌命 / 雾格不传播连锁 / 雾内插旗纯损——A4 D1×②)
|
||||
if not board.fog_cells().is_empty() and not _hints_shown.has("fog_first"):
|
||||
_hints_shown["fog_first"] = true
|
||||
_show_hint("迷雾=无雷盲区,不赌命开雾;雾格不传播连锁;雾内插旗纯损。", "fog_first")
|
||||
|
||||
|
||||
func _on_hover_forbidden(_cell: Vector2i) -> void:
|
||||
if not _hints_shown.has("no_adjacent"):
|
||||
_show_hint("需先走近(点击已开格移动,免费)", "no_adjacent")
|
||||
|
||||
|
||||
func _on_chord_attempted(_cell: Vector2i) -> void:
|
||||
if not _hints_shown.has("chord"):
|
||||
_show_hint("本作无双击快开——插旗后轮末自动判定标中;点数字格可移动小人", "chord")
|
||||
|
||||
|
||||
func _on_fog_region_completed(region_size: int) -> void:
|
||||
# 雾区全开一次性完成信号(视觉合并域 = 连通分量,禁实例级双响——EC11)
|
||||
board_zone.toast.show_toast("雾区已全开(%d 格)——迷雾域尽收眼底" % region_size, "gold")
|
||||
|
||||
|
||||
func _show_hint(text: String, hint_id: String) -> void:
|
||||
_hints_shown[hint_id] = true
|
||||
logger.log_ux("ux_hint_shown", {"hint_id": hint_id})
|
||||
board_zone.toast.show_toast(text, "info")
|
||||
|
||||
|
||||
# ---- 道具窗口(E 区原位激活——布局决策 D2 / pattern 2) ----
|
||||
|
||||
func _on_window_opened(items: Array) -> void:
|
||||
_window_payload = items
|
||||
var hero: Dictionary = b1.presentation_snapshot().hero # E1 收口:经呈现快照面读 HP
|
||||
var hp: int = int(hero.get("hp", 0)) if not hero.is_empty() else 0
|
||||
var max_hp: int = int(hero.get("max_hp", 0)) if not hero.is_empty() else 1
|
||||
inventory_panel.open_window(HudPresenter.window_rows(items, hp, max_hp))
|
||||
_sync_settlement_visuals() # 阶段指示 → 道具窗口
|
||||
|
||||
|
||||
func _on_window_item_selected(pos: int) -> void:
|
||||
# 选择语义 = 窗口条目位置(缺陷④契约——非库存句柄)
|
||||
if pos >= 0 and pos < _window_payload.size():
|
||||
var it: Dictionary = _window_payload[pos]
|
||||
logger.log_ux("ux_window_choice", {"selected_item_id": String(it.get("item_id", "")),
|
||||
"usable_now": true, "skip": false})
|
||||
inventory_panel.close_window()
|
||||
if b1 != null:
|
||||
b1.submit_window_selection([pos])
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_window_skipped() -> void:
|
||||
logger.log_ux("ux_window_choice", {"selected_item_id": "", "usable_now": false, "skip": true})
|
||||
inventory_panel.close_window()
|
||||
if b1 != null:
|
||||
b1.submit_window_selection([])
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_inventory_reject(text: String) -> void:
|
||||
board_zone.toast.show_toast(text, "warn")
|
||||
|
||||
|
||||
func _on_node_continue() -> void:
|
||||
_close_modal(node_overlay)
|
||||
if runner != null:
|
||||
runner.continue_after_node()
|
||||
|
||||
|
||||
# ==================== 玩家意图 → 核心写调用(唯一出口)====================
|
||||
|
||||
func _on_board_action(cell: Vector2i, kind: String) -> void:
|
||||
# 玩家动作 → 核心写调用的唯一通道(模态/窗口期/无战斗拒绝在此收口)
|
||||
if _input_locked() or inventory_panel.is_window_open():
|
||||
return
|
||||
var outcomes: Array = b1.a2.settle_batch([{"action": kind, "cell": cell}])
|
||||
var accepted: bool = not outcomes.is_empty() and bool(outcomes[0].accepted)
|
||||
logger.log_action(kind, cell, accepted)
|
||||
_handle_outcome_feedback(outcomes[0] if not outcomes.is_empty() else {})
|
||||
# settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 b1 已被
|
||||
# runner 置空,清图演出判定须先判 b1(基线既有重入隐患,2026-09-13 加固)
|
||||
if accepted and kind == "dig" and b1 != null and b1.a2.current_board() != null and b1.a2.clear_map_cleared():
|
||||
board_zone.fx.play_clear_burst()
|
||||
board_view.refresh()
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_stop_pressed() -> void:
|
||||
if _input_locked() or inventory_panel.is_window_open():
|
||||
return
|
||||
# stop 经批次提交(A2 D1×6①——不直调 A3)
|
||||
var outcomes: Array = b1.a2.settle_batch([{"action": "stop"}])
|
||||
logger.log_action("stop", Vector2i(-1, -1), not outcomes.is_empty() and bool(outcomes[0].accepted))
|
||||
if not outcomes.is_empty() and String(outcomes[0].get("outcome", "")) == "voided":
|
||||
# voided ≠ 拒绝——独立文案(pattern 1:防静默改判读感欺骗)
|
||||
board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn")
|
||||
logger.log_ux("ux_reject", {"source": "voided", "sub_reason": "stop_voided"})
|
||||
board_view.refresh()
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_refresh_confirmed() -> void:
|
||||
# 弃图刷新(挖掘期 Active 当场换图继续挖——D4;RAP−8 · 旧板旗作废,每轮上限 2 次)
|
||||
if _input_locked() or inventory_panel.is_window_open():
|
||||
return
|
||||
var ok: bool = false
|
||||
if _level_active and runner != null:
|
||||
ok = runner.player_request_refresh()
|
||||
elif b1 != null:
|
||||
ok = b1.player_request_refresh()
|
||||
logger.log_action("refresh", Vector2i(-1, -1), ok)
|
||||
if ok:
|
||||
_refresh_used_round += 1
|
||||
logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_round})
|
||||
board_zone.toast.show_toast("RAP−8 · 旧板旗作废——新图已就位,AP 保留继续挖", "gold")
|
||||
else:
|
||||
# 资源面/上限拒绝(EC13 合法拒绝可见;A3 无载荷——客户端按上下文推断)
|
||||
var rap: int = int(b1.a3.rap) if b1 != null else 0
|
||||
var cost: int = int(db.action_economy.rap_refresh_cost)
|
||||
var result := "rejected_rap" if rap < cost else "rejected_cap"
|
||||
logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_round + 1})
|
||||
if result == "rejected_rap":
|
||||
board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn")
|
||||
info_panel.pulse_rap()
|
||||
else:
|
||||
board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn")
|
||||
board_view.refresh()
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _handle_outcome_feedback(outcome: Dictionary) -> void:
|
||||
# 双源拒绝反馈(pattern 1):每次拒绝玩家可感知 + 来源可区分 + 返回值命令式驱动(无拒绝事件)
|
||||
if outcome.is_empty() or bool(outcome.get("accepted", false)):
|
||||
return
|
||||
logger.log_ux("ux_reject", {"source": String(outcome.get("reject_reason", "")),
|
||||
"sub_reason": String(outcome.get("sub_reason", ""))})
|
||||
board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn")
|
||||
if String(outcome.get("reject_reason", "")) == "resource_face":
|
||||
ap_bar.pulse_ap()
|
||||
|
||||
|
||||
# ==================== 全屏 States 状态机(挖掘/结算/窗口/终局)====================
|
||||
|
||||
func _refresh_presentations() -> void:
|
||||
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
|
||||
var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_max_per_round) - _refresh_used_round)
|
||||
HudPresenter.refresh(stage, info_panel, ap_bar, inventory_panel, b1, runner, _level_active, db, refresh_remaining)
|
||||
_sync_settlement_visuals()
|
||||
|
||||
|
||||
func _sync_settlement_visuals() -> void:
|
||||
# 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px
|
||||
var settling := _is_settling()
|
||||
board_view.set_desaturated(settling)
|
||||
board_view.set_dig_glow_enabled(not settling)
|
||||
stage.set_settle_focus(settling)
|
||||
log_view.set_mode("settle" if settling else "dig")
|
||||
stage.set_round_text(_phase_text())
|
||||
|
||||
|
||||
func _is_settling() -> bool:
|
||||
if b1 == null or not b1.is_battle_live():
|
||||
return false
|
||||
return b1.a3.phase == b1.a3.PHASE_WAITING
|
||||
|
||||
|
||||
func _phase_text() -> String:
|
||||
# 文案装配归 narrator(b1 缺位 = 非战斗态——组合根模式状态守卫)
|
||||
if b1 == null:
|
||||
return "第 — 轮 · —"
|
||||
return Narrator.phase_text(int(b1.round_index), String(b1.battle_result),
|
||||
inventory_panel.is_window_open(), b1.a3.phase == b1.a3.PHASE_WAITING,
|
||||
_boss_active, Narrator.boss_name(runner, db))
|
||||
|
||||
|
||||
# ==================== B 区明细带 / 图例(模态入口)====================
|
||||
|
||||
func _open_review_from_band() -> void:
|
||||
review_overlay.open(log_view.parsed_text())
|
||||
|
||||
|
||||
func _on_legend_toggled() -> void:
|
||||
if legend_overlay.is_open():
|
||||
_close_modal(legend_overlay)
|
||||
else:
|
||||
_open_modal(legend_overlay)
|
||||
|
||||
|
||||
# ==================== 诊断转储(卡死时点击——全量核心状态 + 环形缓冲)====================
|
||||
|
||||
func _on_diagnose_pressed() -> void:
|
||||
var ctx := {"mode": "level" if _level_active else "debug", "seed": _seed_value()}
|
||||
if _level_active and runner != null:
|
||||
ctx["runner"] = {"node_index": runner.node_index, "level_result": String(runner.level_result),
|
||||
"hero_hp": runner.hero_hp, "b1_is_null": runner.current_battle() == null}
|
||||
if b1 != null:
|
||||
ctx["b1"] = b1.diagnostic_snapshot() # 诊断观测面(私有字段零直读——code-review 2026-09-12)
|
||||
var path: String = logger.dump_diagnosis(ctx)
|
||||
board_zone.toast.show_toast("已转储诊断: " + path, "info")
|
||||
|
||||
|
||||
# ==================== 数值调整板(加成应用/批量模拟编排——组合根承接,视图纯 UI)====================
|
||||
|
||||
func _show_tune_panel() -> void:
|
||||
_start_overlay.visible = false
|
||||
tune_panel.visible = true
|
||||
|
||||
|
||||
func _close_tune_panel() -> void:
|
||||
tune_panel.visible = false
|
||||
_start_overlay.visible = true
|
||||
|
||||
|
||||
func _on_tune_apply(atk_pct: int, hp_pct: int) -> void:
|
||||
# E4:加成无持久态可"落"——各战斗开局经 _debug_hero_override/runner 现算生效(面板仅关闭回开始层)
|
||||
_close_tune_panel()
|
||||
|
||||
|
||||
func _on_tune_simulate(n: int) -> void:
|
||||
# 批量模拟编排 = 控制器承接(tune_sim_controller.gd);E4 后无表态需复位——模拟走 runner 覆盖参数
|
||||
_tune_sim.run(n)
|
||||
|
||||
|
||||
# ==================== 工具 ====================
|
||||
|
||||
func _seed_value() -> int:
|
||||
if _seed_edit.text.is_valid_int():
|
||||
return int(_seed_edit.text)
|
||||
return 42
|
||||
@@ -0,0 +1 @@
|
||||
uid://dcbnvi7tvlkun
|
||||
@@ -0,0 +1,12 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://slice0main0scene"]
|
||||
|
||||
[ext_resource type="Script" path="res://presentation/main.gd" id="1_main"]
|
||||
|
||||
[node name="Main" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_main")
|
||||
@@ -0,0 +1,34 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/interaction-patterns.md(pattern 6 模态 overlay 层级纪律)。
|
||||
# Date: 2026-09-12(code-review 修复批——自 main.gd 拆出,规范 1.1 状态装配独立成类)
|
||||
# 模态层级状态机:登记序 = 树序 = z 遮挡序;打开时显式隐藏树序在其上方的可见层、关闭恢复
|
||||
# (缺陷⑤纪律载体:功能存在 ≠ 功能可达——遮挡关系不得靠树序巧合)。
|
||||
# 纯逻辑零节点构建——组合根持有本类并喂入 Control;不持久化核心状态。
|
||||
extends RefCounted
|
||||
|
||||
var _list: Array = [] # 登记序 = z 遮挡序(低 → 高)
|
||||
var _occluded := {} # modal → 被其隐藏的上方层(关闭恢复)
|
||||
|
||||
|
||||
func register(m: Control) -> void:
|
||||
_list.append(m)
|
||||
|
||||
|
||||
func open(m: Control) -> void:
|
||||
var hidden: Array = []
|
||||
var idx := _list.find(m)
|
||||
for i in range(idx + 1, _list.size()):
|
||||
var higher: Control = _list[i]
|
||||
if is_instance_valid(higher) and higher.visible:
|
||||
higher.visible = false
|
||||
hidden.append(higher)
|
||||
_occluded[m] = hidden
|
||||
m.visible = true
|
||||
|
||||
|
||||
func close(m: Control) -> void:
|
||||
m.visible = false
|
||||
for h in _occluded.get(m, []):
|
||||
if is_instance_valid(h):
|
||||
h.visible = true
|
||||
_occluded.erase(m)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bk34o2kt2gfbq
|
||||
@@ -0,0 +1,165 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(§Events Fired 遥测义务——ux 行族)。
|
||||
# Date: 2026-09-12(五区改造批增 ux 遥测;基础 = 2026-09-12 日志器首版)
|
||||
# 试玩日志器(呈现层组件——订阅核心信号持久化 JSONL,供数值分析;卡死诊断转储)。
|
||||
# 职责边界:核心层只发 settlement_snapshot 信号(B1)/既有信号(battle_finished/node_entered/
|
||||
# level_finished/step_row);本组件负责订阅、缓冲与写文件——核心零日志感知(零 UI 依赖不变)。
|
||||
# 文件 = user://logs/playtest_<seed>_<时间戳>.jsonl(每行一次结算/终局/节点的全量状态 JSON)。
|
||||
# ux 行族(呈现层观测,不改 core):ux_round_input_ratio(原始输入动作数∶AP 消耗)、
|
||||
# ux_round_exit{path_tag}(快照 round_facts.path——呈现层只订 B1 的 ADR 纪律下唯一合法来源)、
|
||||
# ux_refresh_attempt / ux_window_choice / ux_reject / ux_hint_shown(组合根显式上报)。
|
||||
# 诊断转储 = 全量核心状态 + 最近结算/操作环形缓冲 → user://logs/diag_<时间戳>.json(卡死时点按钮)。
|
||||
class_name PlaytestLogger
|
||||
extends RefCounted
|
||||
|
||||
const RING_SIZE := 200
|
||||
|
||||
var _file: FileAccess = null
|
||||
var _path := ""
|
||||
var _seed := 0
|
||||
var _recent_steps: Array = [] # 环形缓冲:最近结算行
|
||||
var _recent_actions: Array = [] # 环形缓冲:最近玩家动作
|
||||
var _snap_count := 0
|
||||
var ap_per_round := 8 # 组合根注入(AP 消耗口径分母——A3 无推送信号)
|
||||
var _ux_inputs := 0 # 本结算轮原始输入动作数(log_action 计数,快照后归零)
|
||||
|
||||
|
||||
# 开启会话(每次 start_level/start_battle 调用——按种子分文件)。返回日志路径。
|
||||
func start_session(seed_val: int) -> String:
|
||||
_seed = seed_val
|
||||
_recent_steps.clear()
|
||||
_recent_actions.clear()
|
||||
_snap_count = 0
|
||||
DirAccess.make_dir_recursive_absolute("user://logs")
|
||||
var stamp: String = Time.get_datetime_string_from_system().replace(":", "").replace("-", "").replace(" ", "_")
|
||||
_path = "user://logs/playtest_%d_%s.jsonl" % [seed_val, stamp]
|
||||
if _file != null:
|
||||
_file.close()
|
||||
_file = FileAccess.open(_path, FileAccess.WRITE)
|
||||
_append({"kind": "session", "seed": seed_val, "ts": Time.get_datetime_string_from_system()})
|
||||
return _path
|
||||
|
||||
|
||||
func close_session() -> void:
|
||||
if _file != null:
|
||||
_file.close()
|
||||
_file = null
|
||||
|
||||
|
||||
func log_path() -> String:
|
||||
return _path
|
||||
|
||||
|
||||
func attach_battle(b1) -> void:
|
||||
b1.step_row.connect(_on_step_row)
|
||||
b1.settlement_snapshot.connect(_on_settlement_snapshot)
|
||||
b1.battle_finished.connect(func(p): _append({
|
||||
"kind": "battle_end", "result": String(p.get("result", "")),
|
||||
"hero_hp": int(p.get("hero_hp", 0)), "remaining_items": int(p.get("remaining_items", 0))}))
|
||||
|
||||
|
||||
# ---- ux 遥测(battle.md §Events Fired——呈现层观测行族,不改 core) ----
|
||||
|
||||
func log_ux(ux_kind: String, payload: Dictionary) -> void:
|
||||
var line := {"kind": "ux", "ux": ux_kind}
|
||||
for k in payload.keys():
|
||||
line[k] = payload[k]
|
||||
_append(line)
|
||||
|
||||
|
||||
func log_action(kind: String, cell: Vector2i, accepted: bool, extra: Dictionary = {}) -> void:
|
||||
_ux_inputs += 1 # 原始输入动作数(含被拒——P4 ≤2 护栏观测口径 = 原始输入)
|
||||
# 逐操作落盘(2026-09-12 用户要求:操作记录完整持久化——此前仅入环形缓冲、卡死诊断才可见)
|
||||
var line := {"kind": "action", "op": kind, "cell": [cell.x, cell.y], "accepted": accepted}
|
||||
for k in extra.keys():
|
||||
line[k] = extra[k]
|
||||
_append(line)
|
||||
_ring(_recent_actions, {"kind": kind, "cell": [cell.x, cell.y], "accepted": accepted}.merged(extra))
|
||||
|
||||
|
||||
func _on_settlement_snapshot(snap: Dictionary) -> void:
|
||||
_append(snap)
|
||||
# ux_round_exit{path_tag}:对齐上游封闭枚举 {stopped, ap_exhausted, cleared}
|
||||
var facts: Dictionary = snap.get("round_facts", {})
|
||||
log_ux("ux_round_exit", {"path_tag": String(facts.get("path", ""))})
|
||||
# ux_round_input_ratio:原始输入动作数∶AP 消耗(本轮 AP 配额 − 快照时剩余 = 消耗)
|
||||
var economy: Dictionary = snap.get("economy", {})
|
||||
var ap_left: int = int(economy.get("ap", 0))
|
||||
var consumed: int = clampi(ap_per_round - ap_left, 0, ap_per_round)
|
||||
log_ux("ux_round_input_ratio", {"inputs": _ux_inputs, "ap_consumed": consumed,
|
||||
"ratio": float(_ux_inputs) / float(maxi(consumed, 1))})
|
||||
_ux_inputs = 0
|
||||
|
||||
|
||||
func log_node(node: Dictionary, info: Dictionary, node_index: int) -> void:
|
||||
var t := String(node.get("type", ""))
|
||||
var line := {"kind": "node", "type": t, "index": node_index}
|
||||
if t == "battle" or t == "boss":
|
||||
line["depth"] = int(node.get("node_depth", 0))
|
||||
elif t == "reward":
|
||||
var snap: Dictionary = info.get("snapshot", {})
|
||||
line["reward_id"] = String(snap.get("reward_id", ""))
|
||||
line["power"] = int(snap.get("power", 0))
|
||||
elif t == "rest":
|
||||
line["healed"] = int(info.get("healed", 0))
|
||||
_append(line)
|
||||
|
||||
|
||||
# 批量模拟会话的局次标记(一文件多局——run_start 行携带该局种子/档位,供逐局切分)
|
||||
func log_run(meta: Dictionary) -> void:
|
||||
var line := {"kind": "run"}
|
||||
for k in meta.keys():
|
||||
line[k] = meta[k]
|
||||
_append(line)
|
||||
|
||||
|
||||
func log_level_end(payload: Dictionary) -> void:
|
||||
_append({"kind": "level_end", "result": String(payload.get("result", "")),
|
||||
"node_index": int(payload.get("node_index", -1)), "hp": int(payload.get("hp", 0)),
|
||||
"remaining_items": int(payload.get("remaining_items", -1))})
|
||||
|
||||
|
||||
func _on_step_row(row: Dictionary) -> void:
|
||||
_ring(_recent_steps, {"t": row.type, "src": row.source_class, "v": int(row.value), "r": int(row.round_index)})
|
||||
# 结算明细行逐行落盘(2026-09-12 用户要求——七段结算完整回放数据面)
|
||||
_append({"kind": "step", "type": row.type, "source": row.source_class,
|
||||
"cell": row.origin_cell, "round": int(row.round_index),
|
||||
"text": String(row.display_name), "value": int(row.value),
|
||||
"highlight": String(row.get("highlight", ""))})
|
||||
|
||||
|
||||
# ---- 诊断转储(卡死时点击——全量状态 + 环形缓冲)----
|
||||
|
||||
func dump_diagnosis(context: Dictionary) -> String:
|
||||
DirAccess.make_dir_recursive_absolute("user://logs")
|
||||
var stamp: String = Time.get_datetime_string_from_system().replace(":", "").replace("-", "").replace(" ", "_")
|
||||
var path := "user://logs/diag_%s.json" % stamp
|
||||
var dump := {
|
||||
"kind": "diagnosis", "ts": Time.get_datetime_string_from_system(), "seed": _seed,
|
||||
"session_log": _path, "snap_count": _snap_count,
|
||||
"recent_actions": _recent_actions.duplicate(),
|
||||
"recent_steps": _recent_steps.duplicate(),
|
||||
"core": context,
|
||||
}
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
if f != null:
|
||||
f.store_string(JSON.stringify(dump, " "))
|
||||
f.close()
|
||||
print("[playtest_logger] 诊断转储已写入: %s(项目日志目录见输出)" % path)
|
||||
print(" [diag] %s" % JSON.stringify(context))
|
||||
return path
|
||||
|
||||
|
||||
func _ring(buf: Array, item: Dictionary) -> void:
|
||||
buf.append(item)
|
||||
while buf.size() > RING_SIZE:
|
||||
buf.pop_front()
|
||||
|
||||
|
||||
func _append(dict: Dictionary) -> void:
|
||||
if dict.get("kind", "") == "settlement":
|
||||
_snap_count += 1
|
||||
if _file == null:
|
||||
return
|
||||
_file.store_line(JSON.stringify(dict))
|
||||
_file.flush()
|
||||
@@ -0,0 +1 @@
|
||||
uid://647qs6ujbugf
|
||||
@@ -0,0 +1,54 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-12(code-review 修复批——自 main.gd 拆出,规范 1.1/1.2 模拟编排独立成类)
|
||||
# 调整板批量模拟控制器:逐次驱动 LevelAutoplay(与 headless 扫描同口径)+ 进度/报告 setter 注入面板。
|
||||
# 职责边界:零核心写——表基线复位由组合根在移交前完成(_apply_hero_override(0,0)),
|
||||
# 加成施加 = LevelAutoplay 内部 runner.set_hero_override 单路径;报告文案装配归 HudPresenter。
|
||||
# 组合根只转发 simulate_requested 信号。await 依赖面板所在场景树(进度逐帧刷新)。
|
||||
extends RefCounted
|
||||
|
||||
const SEED_BASE := 4200 # 模拟种子基(与历史实现同源——同种子可回放)
|
||||
const LevelAutoplay := preload("res://smoke/level_autoplay.gd") # 与 headless 扫描同口径的自动策略(P3:preload 编译期路径校验)
|
||||
|
||||
var db
|
||||
var panel # TunePanel setter 面:atk_pct/hp_pct/set_sim_running/set_sim_progress/set_sim_report
|
||||
var _base_hp := 0 # 报告中的"基础 HP"口径注记值
|
||||
|
||||
|
||||
func _init(db_ref, panel_ref, base_hp: int) -> void:
|
||||
db = db_ref
|
||||
panel = panel_ref
|
||||
_base_hp = base_hp
|
||||
|
||||
|
||||
func run(n: int) -> void:
|
||||
# 共享日志会话(2026-09-12 用户要求:模拟运行与真人试玩同日志合规——操作/轮次/战斗/通关全量 JSONL;
|
||||
# 一文件多局,run 行携带种子切分局次)
|
||||
var logger = load("res://presentation/playtest_logger.gd").new()
|
||||
logger.ap_per_round = int(db.action_economy.ap_per_round)
|
||||
var log_path: String = logger.start_session(SEED_BASE)
|
||||
print("[tune_sim] 模拟日志: %s" % log_path)
|
||||
panel.set_sim_running(true)
|
||||
panel.set_sim_progress("模拟中:0/%d …" % n)
|
||||
var wins := 0
|
||||
var dmg_all: Array = []
|
||||
var hp_left_win: Array = []
|
||||
var hp_boss: Array = []
|
||||
var rounds_all: Array = []
|
||||
var death_nodes := {}
|
||||
for i in range(n):
|
||||
var st = LevelAutoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger)
|
||||
dmg_all.append(st.hero_dmg)
|
||||
rounds_all.append(st.rounds)
|
||||
if st.result == "victory":
|
||||
wins += 1
|
||||
hp_left_win.append(st.hp_left)
|
||||
hp_boss.append(st.hp_at_boss)
|
||||
elif st.result == "defeat":
|
||||
var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type]
|
||||
death_nodes[key] = int(death_nodes.get(key, 0)) + 1
|
||||
panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n])
|
||||
await panel.get_tree().process_frame
|
||||
logger.close_session()
|
||||
panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(),
|
||||
wins, dmg_all, hp_left_win, hp_boss, rounds_all, death_nodes, _base_hp))
|
||||
panel.set_sim_running(false)
|
||||
@@ -0,0 +1 @@
|
||||
uid://o0cee33g0yml
|
||||
@@ -0,0 +1,164 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(五区装配器——组合根的 UI 构建拆分载体)。
|
||||
# Date: 2026-09-12(code-review 修复批:main.gd 回涨 761 行触规范 1.1 审视线——构建拆出)
|
||||
# 高内聚边界(规范 1.1/1.3):视图构建装配器——只做 new / add_child / 布局,
|
||||
# 零信号连接、零核心写调用;返回句柄供组合根接线(ADR-0001:接线拓扑唯一归组合根)。
|
||||
# 静态无状态。z 序契约:调用方必须按 build 返回序 addChild(登记序 = 模态遮挡序——缺陷⑤纪律)。
|
||||
extends RefCounted
|
||||
|
||||
|
||||
# 战斗屏五区(A 进度条 / B 战斗画面 / C·D·E 三列)。返回句柄字典:
|
||||
# {strip_view, stage, log_view, info_panel, board_zone, board_view, ap_bar, inventory_panel}
|
||||
static func build_five_zone(root: Control, db) -> Dictionary:
|
||||
# set_anchors_and_offsets_preset(非 set_anchors_preset):后者会把控件重置回最小内容尺寸
|
||||
# (实测 927×613——五区缩顶、棋盘 45px 格 ×10 行超出 D 区 333px 高被裁,2026-09-13 用户报告回归)
|
||||
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
root.add_theme_constant_override("separation", 0)
|
||||
|
||||
# A 区:关卡进度条(全宽 × 28px 顶栏)
|
||||
var strip_panel := PanelContainer.new()
|
||||
strip_panel.custom_minimum_size = Vector2(0, 28)
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = Color8(30, 25, 20)
|
||||
sb.set_corner_radius_all(0)
|
||||
strip_panel.add_theme_stylebox_override("panel", sb)
|
||||
root.add_child(strip_panel)
|
||||
var strip_view := LevelStripView.new()
|
||||
strip_panel.add_child(strip_view)
|
||||
|
||||
# B 区:战斗画面(全宽 × 252px——常驻双舞台,布局决策 D1)
|
||||
var stage := BattleStageView.new(db)
|
||||
root.add_child(stage)
|
||||
|
||||
# C/D/E 三列(520px)
|
||||
var bottom := HBoxContainer.new()
|
||||
bottom.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
bottom.add_theme_constant_override("separation", 4)
|
||||
root.add_child(bottom)
|
||||
|
||||
var info_panel := HeroInfoPanelView.new()
|
||||
bottom.add_child(info_panel)
|
||||
|
||||
var board_zone := BoardZoneView.new()
|
||||
bottom.add_child(board_zone)
|
||||
|
||||
var inventory_panel := InventoryPanelView.new()
|
||||
bottom.add_child(inventory_panel)
|
||||
|
||||
return {
|
||||
"strip_view": strip_view,
|
||||
"stage": stage,
|
||||
"log_view": stage.log_view,
|
||||
"info_panel": info_panel,
|
||||
"board_zone": board_zone,
|
||||
"board_view": board_zone.board_view,
|
||||
"ap_bar": board_zone.ap_bar,
|
||||
"inventory_panel": inventory_panel,
|
||||
}
|
||||
|
||||
|
||||
# 模态覆盖层组(z 序 = 登记序:图例 < 明细回看 < 节点 < 终局 < 调整板)。
|
||||
# 返回 {legend_overlay, review_overlay, node_overlay, end_overlay, tune_panel};调用方按本序 addChild。
|
||||
static func build_overlays(host: Control, db, base_atk: int, base_hp: int) -> Dictionary:
|
||||
var legend_overlay := LegendOverlay.new()
|
||||
host.add_child(legend_overlay)
|
||||
|
||||
var review_overlay := SettlementReviewOverlay.new()
|
||||
host.add_child(review_overlay)
|
||||
|
||||
var node_overlay := NodeOverlay.new()
|
||||
host.add_child(node_overlay)
|
||||
|
||||
var end_overlay := BattleEndOverlay.new()
|
||||
host.add_child(end_overlay)
|
||||
|
||||
var tune_panel := TunePanel.new(db, base_atk, base_hp)
|
||||
tune_panel.visible = false # 显式初始化——原版依赖"被不透明开始层遮蔽"的树序巧合(缺陷⑤家族);模态锁输入以 visible 为准
|
||||
host.add_child(tune_panel)
|
||||
|
||||
return {
|
||||
"legend_overlay": legend_overlay,
|
||||
"review_overlay": review_overlay,
|
||||
"node_overlay": node_overlay,
|
||||
"end_overlay": end_overlay,
|
||||
"tune_panel": tune_panel,
|
||||
}
|
||||
|
||||
|
||||
# 开始覆盖层(返回 {overlay, box, seed_edit, lvbtn, tunebtn, dbgbtn}——按钮接线归组合根;
|
||||
# 单场遭遇用户入口已删(2026-09-13 用户裁决——单场原语仅探针经 debug_session.start_single 使用))
|
||||
static func build_start_overlay(host: Control) -> Dictionary:
|
||||
var overlay := overlay_bg(host, Color8(28, 22, 18))
|
||||
var box := overlay_box(overlay)
|
||||
var title := UiKit.label("Dungeon Sweeper", 34, true)
|
||||
title.add_theme_color_override("font_color", Color8(240, 226, 200))
|
||||
box.add_child(title)
|
||||
var sub := UiKit.label("战斗屏五区改造批 —— 扫雷的因,逐行兑现的果", 16, false)
|
||||
sub.add_theme_color_override("font_color", Color8(200, 188, 166))
|
||||
box.add_child(sub)
|
||||
box.add_child(HSeparator.new())
|
||||
|
||||
var seed_row := HBoxContainer.new()
|
||||
box.add_child(seed_row)
|
||||
seed_row.add_child(UiKit.label("随机种子:", 15, false))
|
||||
var seed_edit := LineEdit.new()
|
||||
seed_edit.text = "42"
|
||||
seed_edit.custom_minimum_size = Vector2(120, 0)
|
||||
seed_row.add_child(seed_edit)
|
||||
|
||||
var lvbtn := UiKit.button("▶ 第一关 · 苔藓回廊(战×2 → 道具 → 战×2 → 恢复 → Boss)")
|
||||
box.add_child(lvbtn)
|
||||
var tunebtn := UiKit.button("测试用 · 角色数值调整板(模拟天赋树属性提升)")
|
||||
box.add_child(tunebtn)
|
||||
var dbgbtn := UiKit.button("调试 · 连续关卡两层(库存与 HP 关内延续)")
|
||||
box.add_child(dbgbtn)
|
||||
|
||||
var help := UiKit.label("玩法:每轮 8 AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n安全格=拾取战利品(轮末结算生效)· 雷=敌方获得伏击 · 标中旗=削弱敌人。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−8 当场换图继续挖(每轮剩 2 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。", 13, false)
|
||||
help.add_theme_color_override("font_color", Color8(178, 166, 146))
|
||||
box.add_child(help)
|
||||
|
||||
return {"overlay": overlay, "box": box, "seed_edit": seed_edit,
|
||||
"lvbtn": lvbtn, "tunebtn": tunebtn, "dbgbtn": dbgbtn}
|
||||
|
||||
|
||||
# 调试两层模式的层间过渡覆盖层(历史入口专用)。返回 {overlay, label, next_btn}——接线归组合根
|
||||
static func build_inter_overlay(host: Control) -> Dictionary:
|
||||
var overlay := overlay_bg(host, Color(0, 0, 0, 0.6))
|
||||
var box := overlay_box(overlay)
|
||||
var label := UiKit.label("", 24, true)
|
||||
label.add_theme_color_override("font_color", Color8(240, 226, 200))
|
||||
box.add_child(label)
|
||||
var next_btn := UiKit.button("进入下一层")
|
||||
box.add_child(next_btn)
|
||||
overlay.visible = false
|
||||
return {"overlay": overlay, "label": label, "next_btn": next_btn}
|
||||
|
||||
|
||||
# Playtest 诊断按钮(调试变体——battle.md §States 允许;卡死转储)
|
||||
static func build_diag_button(host: Control) -> Button:
|
||||
var diag_btn := UiKit.button("诊断转储")
|
||||
diag_btn.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
|
||||
diag_btn.offset_left = 6.0
|
||||
diag_btn.offset_top = -30.0
|
||||
diag_btn.offset_right = 118.0
|
||||
diag_btn.offset_bottom = -6.0
|
||||
host.add_child(diag_btn)
|
||||
return diag_btn
|
||||
|
||||
|
||||
static func overlay_bg(host: Control, color: Color) -> ColorRect:
|
||||
var dim := ColorRect.new()
|
||||
dim.color = color
|
||||
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
host.add_child(dim)
|
||||
return dim
|
||||
|
||||
|
||||
static func overlay_box(overlay: Control) -> VBoxContainer:
|
||||
var center := CenterContainer.new()
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
overlay.add_child(center)
|
||||
var box := VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 12)
|
||||
center.add_child(box)
|
||||
return box
|
||||
@@ -0,0 +1 @@
|
||||
uid://c66hkrbcacd2s
|
||||
Loaded 100 of 317 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user