Phase 17/18 closure: stockpile filter UI + day summary + atmospheric audio
Three-agent fan-out (gdscript-refactor x3) closing deferred polish: - Stockpile chip filter UI: new StockpilePanel (layer 18, right-anchored, mirrors WorkbenchPanel). 5-priority segmented control + 21-chip 4-col filter grid using Item.ALL_TYPES; wildcard (empty accepted_types) shows all chips checked with 'All' hint, first explicit pick switches to explicit-list mode. Selection chain extended to pawn → workbench → stockpile with mutual exclusion. 12 ui.stockpile.* + 13 item.* keys. - DaySummaryCard: layer-19 modal auto-opens at dusk→night via day_ended, auto-pauses sim, shows day+season header, weather row, stats grid with green/yellow/red tension bar, Continue dismiss + backdrop tap. Settings 'Show end-of-day summary' toggle persists via GameState. - Atmospheric audio: rain ambient loop (Cozy Melodies Pack 6) on weather_changed rain/storm with 0.5s fade-out on clear; thunder sting (Magic and Spells 6) on rain→storm transition; raid warning sting (Sword Pack 1, 'blades drawn') on EventBus.wolf_spawned. All on SFX bus — inherits existing slider + suspend mute. Contracts pre-written before fan-out: EventBus.stockpile_selected / stockpile_deselected / wolf_spawned signals; WolfSpawner._trigger_raid + _on_request_wolf_spawn now emit wolf_spawned with the spawned array. MCP runtime verified: StockpilePanel opens with 21 chips, DaySummaryCard renders weather row + tension bar + auto-pause, rain_player.playing=true on weather_changed(rain), all three new SFX keys in Audio.SFX_FILES. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
88e3fa9364
commit
bba1ce4334
18 changed files with 935 additions and 18 deletions
BIN
audio/sfx/raid_warning.ogg
Normal file
BIN
audio/sfx/raid_warning.ogg
Normal file
Binary file not shown.
19
audio/sfx/raid_warning.ogg.import
Normal file
19
audio/sfx/raid_warning.ogg.import
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://v4x2amlkbvx8"
|
||||||
|
path="res://.godot/imported/raid_warning.ogg-89250df9a7224461cf6aacd16fa7bbe9.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://audio/sfx/raid_warning.ogg"
|
||||||
|
dest_files=["res://.godot/imported/raid_warning.ogg-89250df9a7224461cf6aacd16fa7bbe9.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
BIN
audio/sfx/rain_ambient.ogg
Normal file
BIN
audio/sfx/rain_ambient.ogg
Normal file
Binary file not shown.
19
audio/sfx/rain_ambient.ogg.import
Normal file
19
audio/sfx/rain_ambient.ogg.import
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://dqnag3fkym4du"
|
||||||
|
path="res://.godot/imported/rain_ambient.ogg-6ff82d75655695d38fb7a3cec3a90a39.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://audio/sfx/rain_ambient.ogg"
|
||||||
|
dest_files=["res://.godot/imported/rain_ambient.ogg-6ff82d75655695d38fb7a3cec3a90a39.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
BIN
audio/sfx/thunder_sting.ogg
Normal file
BIN
audio/sfx/thunder_sting.ogg
Normal file
Binary file not shown.
19
audio/sfx/thunder_sting.ogg.import
Normal file
19
audio/sfx/thunder_sting.ogg.import
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://cicdya1141lqu"
|
||||||
|
path="res://.godot/imported/thunder_sting.ogg-99961441947f162c6b1f91d5eab1b9b5.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://audio/sfx/thunder_sting.ogg"
|
||||||
|
dest_files=["res://.godot/imported/thunder_sting.ogg-99961441947f162c6b1f91d5eab1b9b5.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
extends Node
|
extends Node
|
||||||
## AudioManager — Phase 18.
|
## AudioManager — Phase 18 + atmospheric audio (Phase 18 follow-up).
|
||||||
##
|
##
|
||||||
## Owns three AudioServer buses (Master, Music, SFX), a small catalog of music
|
## Owns three AudioServer buses (Master, Music, SFX), a small catalog of music
|
||||||
## loops + one-shot SFX, and a single AudioStreamPlayer for the music director.
|
## loops + one-shot SFX, and a single AudioStreamPlayer for the music director.
|
||||||
|
|
@ -9,6 +9,13 @@ extends Node
|
||||||
## EventBus signals (combat, storyteller stings) and direct calls from Tree /
|
## EventBus signals (combat, storyteller stings) and direct calls from Tree /
|
||||||
## Rock entities for chop / mine completion sounds.
|
## Rock entities for chop / mine completion sounds.
|
||||||
##
|
##
|
||||||
|
## Atmospheric audio (Phase 18 follow-up):
|
||||||
|
## - Rain ambient loop: persistent AudioStreamPlayer on SFX bus, fades in/out
|
||||||
|
## on weather_changed. Routed to SFX bus (no new bus) so it respects the
|
||||||
|
## existing SFX slider and application-pause muting via Master bus mute.
|
||||||
|
## - Storm thunder sting: one-shot on WEATHER_RAIN → WEATHER_STORM transition.
|
||||||
|
## - Raid warning sting: one-shot on wolf_spawned.
|
||||||
|
##
|
||||||
## Volume sliders in SettingsMenu drive set_*_db() at the Master/Music/SFX bus
|
## Volume sliders in SettingsMenu drive set_*_db() at the Master/Music/SFX bus
|
||||||
## level. App-pause mutes everything (NOTIFICATION_APPLICATION_PAUSED / focus
|
## level. App-pause mutes everything (NOTIFICATION_APPLICATION_PAUSED / focus
|
||||||
## loss) to match the existing "no background simulation" rule.
|
## loss) to match the existing "no background simulation" rule.
|
||||||
|
|
@ -33,6 +40,14 @@ const SFX_FILES: Dictionary = {
|
||||||
&"tree_fell": "res://audio/sfx/tree_fell.ogg",
|
&"tree_fell": "res://audio/sfx/tree_fell.ogg",
|
||||||
&"mine_tick": "res://audio/sfx/mine_tick.ogg",
|
&"mine_tick": "res://audio/sfx/mine_tick.ogg",
|
||||||
&"combat_hit": "res://audio/sfx/combat_hit.ogg",
|
&"combat_hit": "res://audio/sfx/combat_hit.ogg",
|
||||||
|
# Atmospheric — Phase 18 follow-up.
|
||||||
|
# rain_ambient: Cozy Melodies Pack 6 (ElvGames Ultimate Farming RPG Tier 3)
|
||||||
|
&"rain_ambient": "res://audio/sfx/rain_ambient.ogg",
|
||||||
|
# thunder_sting: Magic and Spells 6 (ElvGames Ultimate Farming RPG Tier 3)
|
||||||
|
&"thunder_sting": "res://audio/sfx/thunder_sting.ogg",
|
||||||
|
# raid_warning: Sword Sound Effects Pack 1 (ElvGames Ultimate Farming RPG Tier 3)
|
||||||
|
# Sword unsheathe sting used as danger signal; no dedicated alarm/horn in bundle.
|
||||||
|
&"raid_warning": "res://audio/sfx/raid_warning.ogg",
|
||||||
}
|
}
|
||||||
|
|
||||||
const MUSIC_FILES: Dictionary = {
|
const MUSIC_FILES: Dictionary = {
|
||||||
|
|
@ -57,6 +72,14 @@ var master_linear: float = 1.0
|
||||||
var music_linear: float = 1.0
|
var music_linear: float = 1.0
|
||||||
var sfx_linear: float = 1.0
|
var sfx_linear: float = 1.0
|
||||||
|
|
||||||
|
# Atmospheric — rain ambient loop player. Persistent; stream stays loaded.
|
||||||
|
# Routed to SFX bus so it inherits the SFX slider and app-pause muting.
|
||||||
|
var _rain_player: AudioStreamPlayer = null
|
||||||
|
# Tween used for the rain fade-out (clear/cold_snap). Cancelled on re-rain.
|
||||||
|
var _rain_tween: Tween = null
|
||||||
|
# Track previous weather to detect RAIN → STORM transition for thunder sting.
|
||||||
|
var _prev_weather: StringName = &""
|
||||||
|
|
||||||
|
|
||||||
# ── lifecycle ───────────────────────────────────────────────────────────────
|
# ── lifecycle ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -68,6 +91,15 @@ func _ready() -> void:
|
||||||
_music_player.name = "MusicPlayer"
|
_music_player.name = "MusicPlayer"
|
||||||
add_child(_music_player)
|
add_child(_music_player)
|
||||||
|
|
||||||
|
# Rain ambient: persistent looping player on SFX bus. Starts silent; weather
|
||||||
|
# signal activates it. Routing to SFX bus means app-pause muting (Master bus)
|
||||||
|
# covers it automatically — no extra NOTIFICATION handling needed.
|
||||||
|
_rain_player = AudioStreamPlayer.new()
|
||||||
|
_rain_player.bus = BUS_SFX
|
||||||
|
_rain_player.name = "RainAmbientPlayer"
|
||||||
|
_rain_player.volume_db = 0.0
|
||||||
|
add_child(_rain_player)
|
||||||
|
|
||||||
# SFX → world event wiring. Keep this list short and obvious.
|
# SFX → world event wiring. Keep this list short and obvious.
|
||||||
if EventBus.has_signal("pawn_took_damage"):
|
if EventBus.has_signal("pawn_took_damage"):
|
||||||
EventBus.pawn_took_damage.connect(_on_pawn_took_damage)
|
EventBus.pawn_took_damage.connect(_on_pawn_took_damage)
|
||||||
|
|
@ -75,6 +107,10 @@ func _ready() -> void:
|
||||||
EventBus.storyteller_event_fired.connect(_on_storyteller_event_fired)
|
EventBus.storyteller_event_fired.connect(_on_storyteller_event_fired)
|
||||||
if EventBus.has_signal("alert_added"):
|
if EventBus.has_signal("alert_added"):
|
||||||
EventBus.alert_added.connect(_on_alert_added)
|
EventBus.alert_added.connect(_on_alert_added)
|
||||||
|
if EventBus.has_signal("weather_changed"):
|
||||||
|
EventBus.weather_changed.connect(_on_weather_changed)
|
||||||
|
if EventBus.has_signal("wolf_spawned"):
|
||||||
|
EventBus.wolf_spawned.connect(_on_wolf_spawned)
|
||||||
|
|
||||||
# Music director: swap day/night loops as Clock.phase_changed fires.
|
# Music director: swap day/night loops as Clock.phase_changed fires.
|
||||||
if Clock.has_signal("phase_changed"):
|
if Clock.has_signal("phase_changed"):
|
||||||
|
|
@ -86,7 +122,14 @@ func _ready() -> void:
|
||||||
initial_phase = Clock.current_phase()
|
initial_phase = Clock.current_phase()
|
||||||
_apply_phase_to_music(initial_phase)
|
_apply_phase_to_music(initial_phase)
|
||||||
|
|
||||||
Audit.log("audio", "AudioManager ready (master/music/sfx buses online)")
|
# Sync rain loop to current weather in case we loaded mid-rain (e.g. after
|
||||||
|
# save/load). Weather autoload is ready before Audio in the autoload list;
|
||||||
|
# safe to query here.
|
||||||
|
if Weather.has_method("is_raining") and Weather.is_raining():
|
||||||
|
_prev_weather = Weather.current_weather
|
||||||
|
_start_rain_loop()
|
||||||
|
|
||||||
|
Audit.log("audio", "AudioManager ready (master/music/sfx buses online + atmospheric)")
|
||||||
|
|
||||||
|
|
||||||
## NOTIFICATION_APPLICATION_PAUSED fires when the app is suspended (mobile
|
## NOTIFICATION_APPLICATION_PAUSED fires when the app is suspended (mobile
|
||||||
|
|
@ -232,3 +275,54 @@ func _on_alert_added(_severity: StringName, _text: String, _focus_tile: Vector2i
|
||||||
|
|
||||||
func _on_clock_phase_changed(new_phase: StringName) -> void:
|
func _on_clock_phase_changed(new_phase: StringName) -> void:
|
||||||
_apply_phase_to_music(new_phase)
|
_apply_phase_to_music(new_phase)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_weather_changed(weather: StringName) -> void:
|
||||||
|
var prev: StringName = _prev_weather
|
||||||
|
_prev_weather = weather
|
||||||
|
|
||||||
|
match weather:
|
||||||
|
&"rain", &"storm":
|
||||||
|
# Cancel any pending fade-out tween.
|
||||||
|
if _rain_tween != null and _rain_tween.is_valid():
|
||||||
|
_rain_tween.kill()
|
||||||
|
_rain_tween = null
|
||||||
|
_start_rain_loop()
|
||||||
|
# Thunder sting only on a direct transition into storm.
|
||||||
|
if weather == &"storm" and prev != &"storm":
|
||||||
|
play_sfx(&"thunder_sting")
|
||||||
|
_:
|
||||||
|
# WEATHER_CLEAR or WEATHER_COLD_SNAP — fade out rain loop.
|
||||||
|
_stop_rain_loop_fade()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_wolf_spawned(wolves: Array) -> void:
|
||||||
|
if wolves.size() > 0:
|
||||||
|
play_sfx(&"raid_warning")
|
||||||
|
|
||||||
|
|
||||||
|
# ── rain ambient helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _start_rain_loop() -> void:
|
||||||
|
# Load the stream once and reuse across transitions.
|
||||||
|
var stream: AudioStream = _load_sfx(&"rain_ambient")
|
||||||
|
if stream == null:
|
||||||
|
return
|
||||||
|
if _rain_player.stream != stream:
|
||||||
|
_rain_player.stream = stream
|
||||||
|
if stream is AudioStreamOggVorbis:
|
||||||
|
stream.loop = true
|
||||||
|
_rain_player.volume_db = 0.0
|
||||||
|
if not _rain_player.playing:
|
||||||
|
_rain_player.play()
|
||||||
|
|
||||||
|
|
||||||
|
func _stop_rain_loop_fade() -> void:
|
||||||
|
if not _rain_player.playing:
|
||||||
|
return
|
||||||
|
# 0.5 s linear fade from current volume to SILENT_DB, then stop.
|
||||||
|
if _rain_tween != null and _rain_tween.is_valid():
|
||||||
|
_rain_tween.kill()
|
||||||
|
_rain_tween = create_tween()
|
||||||
|
_rain_tween.tween_property(_rain_player, "volume_db", SILENT_DB, 0.5)
|
||||||
|
_rain_tween.tween_callback(_rain_player.stop)
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,12 @@ signal pawn_selected(pawn) ## Emitted when Selection pic
|
||||||
signal pawn_deselected ## Emitted when Selection clears — closes PawnDetailPanel.
|
signal pawn_deselected ## Emitted when Selection clears — closes PawnDetailPanel.
|
||||||
signal workbench_selected(workbench)
|
signal workbench_selected(workbench)
|
||||||
signal workbench_deselected
|
signal workbench_deselected
|
||||||
|
signal stockpile_selected(zone) ## Emitted when Selection picks a StockpileZone — opens StockpilePanel for filter editing.
|
||||||
|
signal stockpile_deselected ## Emitted when Selection clears the active stockpile.
|
||||||
signal pawn_priority_changed(pawn, category: StringName, level: int) ## Emitted when priority matrix updates a cell.
|
signal pawn_priority_changed(pawn, category: StringName, level: int) ## Emitted when priority matrix updates a cell.
|
||||||
signal alert_added(severity: StringName, text: String, focus_tile: Vector2i) ## Emitted by gameplay subsystems to surface a player notice. severity = info | warn | danger.
|
signal alert_added(severity: StringName, text: String, focus_tile: Vector2i) ## Emitted by gameplay subsystems to surface a player notice. severity = info | warn | danger.
|
||||||
signal request_wolf_spawn(count: int) ## Phase 15 EventCatalog → WolfSpawner. Decouples threat-event effects from spawner.
|
signal request_wolf_spawn(count: int) ## Phase 15 EventCatalog → WolfSpawner. Decouples threat-event effects from spawner.
|
||||||
|
signal wolf_spawned(wolves: Array) ## Emitted by WolfSpawner AFTER a raid wave has been instantiated; carries the spawned Wolf nodes. Audio uses this for raid-warning sting.
|
||||||
signal day_ended(summary: Dictionary) ## Emitted by Clock at dusk→night boundary; carries the end-of-day recap dict.
|
signal day_ended(summary: Dictionary) ## Emitted by Clock at dusk→night boundary; carries the end-of-day recap dict.
|
||||||
|
|
||||||
# Phase 18 — Alert wiring (dangling signals surfaced to AlertsLog).
|
# Phase 18 — Alert wiring (dangling signals surfaced to AlertsLog).
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ var settings: Dictionary = {
|
||||||
"pause_on_wanderer": true,
|
"pause_on_wanderer": true,
|
||||||
"pause_on_pawn_down": true,
|
"pause_on_pawn_down": true,
|
||||||
"pause_on_modal": true,
|
"pause_on_modal": true,
|
||||||
|
"show_day_summary": true,
|
||||||
"audio_master": 1.0,
|
"audio_master": 1.0,
|
||||||
"audio_music": 1.0,
|
"audio_music": 1.0,
|
||||||
"audio_sfx": 1.0,
|
"audio_sfx": 1.0,
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,49 @@ const TABLE: Dictionary = {
|
||||||
&"ui.bill.no_bills_hint": "No bills. Add one to start crafting.",
|
&"ui.bill.no_bills_hint": "No bills. Add one to start crafting.",
|
||||||
&"ui.workbench.current_bill": "Current",
|
&"ui.workbench.current_bill": "Current",
|
||||||
&"ui.workbench.idle": "Idle",
|
&"ui.workbench.idle": "Idle",
|
||||||
|
# Phase 17 — DaySummaryCard end-of-day modal.
|
||||||
|
&"ui.day_summary.title": "Day {day} — {season}",
|
||||||
|
&"ui.day_summary.continue": "Continue",
|
||||||
|
&"ui.day_summary.pawns_alive": "Pawns alive",
|
||||||
|
&"ui.day_summary.wolves_on_map": "Wolves on map",
|
||||||
|
&"ui.day_summary.tension": "Tension",
|
||||||
|
&"ui.day_summary.tension_fmt": "{t} / 100",
|
||||||
|
# Weather labels (weather.<name>)
|
||||||
|
&"weather.clear": "Clear",
|
||||||
|
&"weather.rain": "Rain",
|
||||||
|
&"weather.storm": "Storm",
|
||||||
|
&"weather.cold_snap": "Cold Snap",
|
||||||
|
&"weather.unknown": "Unknown",
|
||||||
|
# Settings checkbox label
|
||||||
|
&"ui.settings.show_day_summary": "Show end-of-day summary",
|
||||||
|
# Phase 17 — Stockpile filter chips (StockpilePanel).
|
||||||
|
&"ui.stockpile.title": "Stockpile",
|
||||||
|
&"ui.stockpile.priority": "Priority:",
|
||||||
|
&"ui.stockpile.prio.critical": "Crit",
|
||||||
|
&"ui.stockpile.prio.high": "High",
|
||||||
|
&"ui.stockpile.prio.normal": "Norm",
|
||||||
|
&"ui.stockpile.prio.low": "Low",
|
||||||
|
&"ui.stockpile.prio.off": "Off",
|
||||||
|
&"ui.stockpile.accepts": "Accepts:",
|
||||||
|
&"ui.stockpile.accepts_all_hint": "All",
|
||||||
|
&"ui.stockpile.select_all": "All",
|
||||||
|
&"ui.stockpile.clear_all": "None",
|
||||||
|
# Item type chip labels missing from existing table (copper_ore through ash).
|
||||||
|
# (item.wood, .stone, .iron_ore, .plank, .stone_block, .flour, .bread, .meal
|
||||||
|
# are already defined above — only the ones below are new.)
|
||||||
|
&"item.copper_ore": "Copper",
|
||||||
|
&"item.silver": "Silver",
|
||||||
|
&"item.gold": "Gold",
|
||||||
|
&"item.cloth": "Cloth",
|
||||||
|
&"item.vegetable": "Vegetable",
|
||||||
|
&"item.meat": "Meat",
|
||||||
|
&"item.grain": "Grain",
|
||||||
|
&"item.medicine": "Medicine",
|
||||||
|
&"item.tool": "Tool",
|
||||||
|
&"item.weapon": "Weapon",
|
||||||
|
&"item.armor": "Armor",
|
||||||
|
&"item.corpse": "Corpse",
|
||||||
|
&"item.ash": "Ash",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,13 @@ func _trigger_raid(current_tick: int) -> void:
|
||||||
_last_raid_tick = current_tick
|
_last_raid_tick = current_tick
|
||||||
var pack_size := randi_range(PACK_MIN, PACK_MAX)
|
var pack_size := randi_range(PACK_MIN, PACK_MAX)
|
||||||
var spawn_tiles := _pick_spawn_tiles(pack_size)
|
var spawn_tiles := _pick_spawn_tiles(pack_size)
|
||||||
|
var spawned: Array = []
|
||||||
for spawn_tile in spawn_tiles:
|
for spawn_tile in spawn_tiles:
|
||||||
var w: Wolf = WOLF_SCENE.instantiate()
|
var w: Wolf = WOLF_SCENE.instantiate()
|
||||||
get_parent().add_child(w)
|
get_parent().add_child(w)
|
||||||
w.setup(spawn_tile)
|
w.setup(spawn_tile)
|
||||||
|
spawned.append(w)
|
||||||
|
EventBus.wolf_spawned.emit(spawned)
|
||||||
Audit.log("wolf", "RAID: %d wolf(ves) spawned at %s" % [pack_size, spawn_tiles])
|
Audit.log("wolf", "RAID: %d wolf(ves) spawned at %s" % [pack_size, spawn_tiles])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -68,10 +71,13 @@ func _trigger_raid(current_tick: int) -> void:
|
||||||
## the night-attack cooldown, so organic raids can still follow.
|
## the night-attack cooldown, so organic raids can still follow.
|
||||||
func _on_request_wolf_spawn(count: int) -> void:
|
func _on_request_wolf_spawn(count: int) -> void:
|
||||||
var spawn_tiles := _pick_spawn_tiles(count)
|
var spawn_tiles := _pick_spawn_tiles(count)
|
||||||
|
var spawned: Array = []
|
||||||
for spawn_tile in spawn_tiles:
|
for spawn_tile in spawn_tiles:
|
||||||
var w: Wolf = WOLF_SCENE.instantiate()
|
var w: Wolf = WOLF_SCENE.instantiate()
|
||||||
get_parent().add_child(w)
|
get_parent().add_child(w)
|
||||||
w.setup(spawn_tile)
|
w.setup(spawn_tile)
|
||||||
|
spawned.append(w)
|
||||||
|
EventBus.wolf_spawned.emit(spawned)
|
||||||
Audit.log("wolf", "FORCED RAID (event): %d wolf(ves) spawned at %s" % [count, spawn_tiles])
|
Audit.log("wolf", "FORCED RAID (event): %d wolf(ves) spawned at %s" % [count, spawn_tiles])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ const RESUME_TOAST_SCRIPT: Script = preload("res://scenes/ui/resume_toa
|
||||||
# Phase 17 — PawnDetailPanel (layer 18) and SettingsMenu (layer 26).
|
# Phase 17 — PawnDetailPanel (layer 18) and SettingsMenu (layer 26).
|
||||||
const PAWN_DETAIL_PANEL_SCRIPT: Script = preload("res://scenes/ui/pawn_detail_panel.gd")
|
const PAWN_DETAIL_PANEL_SCRIPT: Script = preload("res://scenes/ui/pawn_detail_panel.gd")
|
||||||
const WORKBENCH_PANEL_SCRIPT: Script = preload("res://scenes/ui/workbench_panel.gd")
|
const WORKBENCH_PANEL_SCRIPT: Script = preload("res://scenes/ui/workbench_panel.gd")
|
||||||
|
const STOCKPILE_PANEL_SCRIPT: Script = preload("res://scenes/ui/stockpile_panel.gd")
|
||||||
const MEDIEVAL_THEME_SCRIPT: Script = preload("res://scenes/ui/medieval_theme.gd")
|
const MEDIEVAL_THEME_SCRIPT: Script = preload("res://scenes/ui/medieval_theme.gd")
|
||||||
|
|
||||||
# Built once in _ready and re-applied to any CanvasLayer-rooted Control because
|
# Built once in _ready and re-applied to any CanvasLayer-rooted Control because
|
||||||
|
|
@ -31,6 +32,8 @@ const BUILD_DRAWER_SCRIPT: Script = preload("res://scenes/ui/build_draw
|
||||||
# Phase 17 (Agent C) — WorkPriorityMatrix (layer 17) and AlertsLog (layer 19).
|
# Phase 17 (Agent C) — WorkPriorityMatrix (layer 17) and AlertsLog (layer 19).
|
||||||
const WORK_PRIORITY_MATRIX_SCRIPT: Script = preload("res://scenes/ui/work_priority_matrix.gd")
|
const WORK_PRIORITY_MATRIX_SCRIPT: Script = preload("res://scenes/ui/work_priority_matrix.gd")
|
||||||
const ALERTS_LOG_SCRIPT: Script = preload("res://scenes/ui/alerts_log.gd")
|
const ALERTS_LOG_SCRIPT: Script = preload("res://scenes/ui/alerts_log.gd")
|
||||||
|
# Phase 17 — DaySummaryCard end-of-day recap modal (layer 19).
|
||||||
|
const DAY_SUMMARY_CARD_SCRIPT: Script = preload("res://scenes/ui/day_summary_card.gd")
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -99,6 +102,13 @@ func _ready() -> void:
|
||||||
workbench_panel.name = "WorkbenchPanel"
|
workbench_panel.name = "WorkbenchPanel"
|
||||||
add_child(workbench_panel)
|
add_child(workbench_panel)
|
||||||
|
|
||||||
|
# Stockpile filter + priority editor. Right-anchored 360 px, layer 18;
|
||||||
|
# mutually exclusive with PawnDetailPanel and WorkbenchPanel via Selection.
|
||||||
|
var stockpile_panel := CanvasLayer.new()
|
||||||
|
stockpile_panel.set_script(STOCKPILE_PANEL_SCRIPT)
|
||||||
|
stockpile_panel.name = "StockpilePanel"
|
||||||
|
add_child(stockpile_panel)
|
||||||
|
|
||||||
var settings_menu := CanvasLayer.new()
|
var settings_menu := CanvasLayer.new()
|
||||||
settings_menu.set_script(SETTINGS_MENU_SCRIPT)
|
settings_menu.set_script(SETTINGS_MENU_SCRIPT)
|
||||||
settings_menu.name = "SettingsMenu"
|
settings_menu.name = "SettingsMenu"
|
||||||
|
|
@ -169,6 +179,14 @@ func _ready() -> void:
|
||||||
|
|
||||||
Audit.log("main", "Phase 17 (Agent C) — WorkPriorityMatrix + AlertsLog mounted.")
|
Audit.log("main", "Phase 17 (Agent C) — WorkPriorityMatrix + AlertsLog mounted.")
|
||||||
|
|
||||||
|
# Phase 17 — DaySummaryCard (layer 19) — auto-opens on day_ended signal.
|
||||||
|
var day_summary_card := CanvasLayer.new()
|
||||||
|
day_summary_card.set_script(DAY_SUMMARY_CARD_SCRIPT)
|
||||||
|
day_summary_card.name = "DaySummaryCard"
|
||||||
|
add_child(day_summary_card)
|
||||||
|
|
||||||
|
Audit.log("main", "Phase 17 — DaySummaryCard mounted.")
|
||||||
|
|
||||||
# Apply the medieval theme to every Control under each CanvasLayer.
|
# Apply the medieval theme to every Control under each CanvasLayer.
|
||||||
# CanvasLayers interrupt the root-Window theme cascade so we have to seed
|
# CanvasLayers interrupt the root-Window theme cascade so we have to seed
|
||||||
# each one explicitly. Defer one frame so panels that build their UI in
|
# each one explicitly. Defer one frame so panels that build their UI in
|
||||||
|
|
|
||||||
268
scenes/ui/day_summary_card.gd
Normal file
268
scenes/ui/day_summary_card.gd
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
class_name DaySummaryCard extends CanvasLayer
|
||||||
|
## Phase 17 — End-of-day recap modal shown at every dusk→night boundary.
|
||||||
|
##
|
||||||
|
## Layer 19: above PawnDetailPanel (18), below StorytellerModal (20).
|
||||||
|
## Auto-opens on EventBus.day_ended; auto-pauses the sim.
|
||||||
|
## Dismiss via the Continue button OR tapping anywhere on the dim backdrop.
|
||||||
|
## Opt-out via Settings → "Show end-of-day summary" (GameState.settings["show_day_summary"]).
|
||||||
|
##
|
||||||
|
## Layout: centered 480×400 card with header, weather row, stats grid, tension bar.
|
||||||
|
## Sim pause/resume contract: Sim.set_speed(Speed.PAUSE) on open, restore prior
|
||||||
|
## speed on dismiss.
|
||||||
|
|
||||||
|
const CARD_W: int = 480
|
||||||
|
const CARD_H: int = 400
|
||||||
|
|
||||||
|
# Tension bar thresholds.
|
||||||
|
const TENSION_LOW: float = 40.0
|
||||||
|
const TENSION_MID: float = 70.0
|
||||||
|
|
||||||
|
# Weather emoji map — procedural, no asset dependency.
|
||||||
|
const WEATHER_ICONS: Dictionary = {
|
||||||
|
&"clear": "☀",
|
||||||
|
&"rain": "☂",
|
||||||
|
&"storm": "⚡",
|
||||||
|
&"cold_snap": "❄",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── node refs ─────────────────────────────────────────────────────────────────
|
||||||
|
var _dim: ColorRect = null
|
||||||
|
var _panel: PanelContainer = null
|
||||||
|
var _header: Label = null
|
||||||
|
var _weather_row: HBoxContainer = null
|
||||||
|
var _weather_icon: Label = null
|
||||||
|
var _weather_label: Label = null
|
||||||
|
var _stats_grid: GridContainer = null
|
||||||
|
var _tension_bar: ColorRect = null
|
||||||
|
var _tension_track: ColorRect = null
|
||||||
|
var _continue_btn: Button = null
|
||||||
|
|
||||||
|
# Speed saved at open so we restore it correctly (player may have been on Fast).
|
||||||
|
var _speed_before_open: int = Sim.Speed.FAST
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
layer = 19
|
||||||
|
_build_ui()
|
||||||
|
_set_visible(false)
|
||||||
|
|
||||||
|
if EventBus.has_signal("day_ended"):
|
||||||
|
EventBus.day_ended.connect(_on_day_ended)
|
||||||
|
Audit.log("day_summary_card", "DaySummaryCard ready (layer %d)" % layer)
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
if EventBus.has_signal("day_ended") and EventBus.day_ended.is_connected(_on_day_ended):
|
||||||
|
EventBus.day_ended.disconnect(_on_day_ended)
|
||||||
|
|
||||||
|
|
||||||
|
# ── UI construction ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
# Full-screen dim — captures backdrop taps for dismiss.
|
||||||
|
_dim = ColorRect.new()
|
||||||
|
_dim.name = "Dim"
|
||||||
|
_dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_dim.color = Color(0.0, 0.0, 0.0, 0.50)
|
||||||
|
_dim.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
_dim.gui_input.connect(_on_dim_input)
|
||||||
|
add_child(_dim)
|
||||||
|
|
||||||
|
# Centered card panel — fixed explicit size to avoid auto-resize jumping.
|
||||||
|
_panel = PanelContainer.new()
|
||||||
|
_panel.name = "Card"
|
||||||
|
_panel.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
_panel.custom_minimum_size = Vector2(CARD_W, CARD_H)
|
||||||
|
_panel.offset_left = -CARD_W / 2
|
||||||
|
_panel.offset_right = CARD_W / 2
|
||||||
|
_panel.offset_top = -CARD_H / 2
|
||||||
|
_panel.offset_bottom = CARD_H / 2
|
||||||
|
add_child(_panel)
|
||||||
|
|
||||||
|
var vbox := VBoxContainer.new()
|
||||||
|
vbox.add_theme_constant_override("separation", 14)
|
||||||
|
_panel.add_child(vbox)
|
||||||
|
|
||||||
|
# ── Header: "Day N — Season" ──────────────────────────────────────────────
|
||||||
|
_header = Label.new()
|
||||||
|
_header.name = "Header"
|
||||||
|
_header.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_header.add_theme_font_size_override("font_size", 22)
|
||||||
|
vbox.add_child(_header)
|
||||||
|
|
||||||
|
vbox.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
# ── Weather row ───────────────────────────────────────────────────────────
|
||||||
|
_weather_row = HBoxContainer.new()
|
||||||
|
_weather_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
_weather_row.add_theme_constant_override("separation", 8)
|
||||||
|
vbox.add_child(_weather_row)
|
||||||
|
|
||||||
|
_weather_icon = Label.new()
|
||||||
|
_weather_icon.name = "WeatherIcon"
|
||||||
|
_weather_icon.add_theme_font_size_override("font_size", 20)
|
||||||
|
_weather_row.add_child(_weather_icon)
|
||||||
|
|
||||||
|
_weather_label = Label.new()
|
||||||
|
_weather_label.name = "WeatherLabel"
|
||||||
|
_weather_row.add_child(_weather_label)
|
||||||
|
|
||||||
|
vbox.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
# ── Stats grid: 2 columns label | value ──────────────────────────────────
|
||||||
|
_stats_grid = GridContainer.new()
|
||||||
|
_stats_grid.columns = 2
|
||||||
|
_stats_grid.add_theme_constant_override("h_separation", 24)
|
||||||
|
_stats_grid.add_theme_constant_override("v_separation", 8)
|
||||||
|
vbox.add_child(_stats_grid)
|
||||||
|
|
||||||
|
# Pawns alive row — values populated in _populate().
|
||||||
|
_add_stat_row(Strings.t(&"ui.day_summary.pawns_alive"), "PawnsValue")
|
||||||
|
# Wolves row.
|
||||||
|
_add_stat_row(Strings.t(&"ui.day_summary.wolves_on_map"), "WolvesValue")
|
||||||
|
# Tension row — label only here; bar added below.
|
||||||
|
_add_stat_row(Strings.t(&"ui.day_summary.tension"), "TensionValue")
|
||||||
|
|
||||||
|
# Tension bar — track (grey bg) + fill (color).
|
||||||
|
var bar_container := VBoxContainer.new()
|
||||||
|
bar_container.add_theme_constant_override("separation", 4)
|
||||||
|
# Span both grid columns via a separate vbox below the grid.
|
||||||
|
vbox.add_child(bar_container)
|
||||||
|
|
||||||
|
_tension_track = ColorRect.new()
|
||||||
|
_tension_track.name = "TensionTrack"
|
||||||
|
_tension_track.custom_minimum_size = Vector2(CARD_W - 48, 10)
|
||||||
|
_tension_track.color = Color(0.25, 0.22, 0.18, 1.0)
|
||||||
|
bar_container.add_child(_tension_track)
|
||||||
|
|
||||||
|
_tension_bar = ColorRect.new()
|
||||||
|
_tension_bar.name = "TensionBar"
|
||||||
|
# Width set dynamically in _populate(); height matches track.
|
||||||
|
_tension_bar.custom_minimum_size = Vector2(0, 10)
|
||||||
|
_tension_track.add_child(_tension_bar)
|
||||||
|
# Anchor the bar to the left edge of the track.
|
||||||
|
_tension_bar.set_anchors_preset(Control.PRESET_LEFT_WIDE)
|
||||||
|
|
||||||
|
vbox.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
# ── Continue button ───────────────────────────────────────────────────────
|
||||||
|
var btn_row := HBoxContainer.new()
|
||||||
|
btn_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
vbox.add_child(btn_row)
|
||||||
|
|
||||||
|
_continue_btn = Button.new()
|
||||||
|
_continue_btn.name = "ContinueBtn"
|
||||||
|
_continue_btn.text = Strings.t(&"ui.day_summary.continue")
|
||||||
|
_continue_btn.custom_minimum_size = Vector2(140, 48)
|
||||||
|
_continue_btn.focus_mode = Control.FOCUS_NONE
|
||||||
|
_continue_btn.pressed.connect(_on_continue_pressed)
|
||||||
|
btn_row.add_child(_continue_btn)
|
||||||
|
|
||||||
|
|
||||||
|
## Add a label+value pair row to the stats grid.
|
||||||
|
## Returns the value Label so _populate() can update its text.
|
||||||
|
func _add_stat_row(label_text: String, value_name: String) -> Label:
|
||||||
|
var lbl := Label.new()
|
||||||
|
lbl.text = label_text
|
||||||
|
_stats_grid.add_child(lbl)
|
||||||
|
|
||||||
|
var val := Label.new()
|
||||||
|
val.name = value_name
|
||||||
|
val.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
val.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_stats_grid.add_child(val)
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
# ── event handling ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _on_day_ended(summary: Dictionary) -> void:
|
||||||
|
if not bool(GameState.settings.get("show_day_summary", true)):
|
||||||
|
return
|
||||||
|
call_deferred("_open_with_summary", summary)
|
||||||
|
|
||||||
|
|
||||||
|
func _open_with_summary(summary: Dictionary) -> void:
|
||||||
|
_populate(summary)
|
||||||
|
_speed_before_open = int(Sim.current_speed)
|
||||||
|
Sim.set_speed(Sim.Speed.PAUSE)
|
||||||
|
_set_visible(true)
|
||||||
|
Audit.log("day_summary_card", "opened: day=%d season=%s tension=%.1f" % [
|
||||||
|
int(summary.get("day", 0)),
|
||||||
|
String(summary.get("season", &"")),
|
||||||
|
float(summary.get("tension", 0.0)),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
func _populate(summary: Dictionary) -> void:
|
||||||
|
var day: int = int(summary.get("day", 0))
|
||||||
|
var season: StringName = summary.get("season", &"spring")
|
||||||
|
var weather: StringName = summary.get("weather", &"clear")
|
||||||
|
var pawns_alive: int = int(summary.get("pawns_alive", 0))
|
||||||
|
var wolves: int = int(summary.get("wolves_alive", 0))
|
||||||
|
var tension: float = float(summary.get("tension", 0.0))
|
||||||
|
|
||||||
|
# Header.
|
||||||
|
var season_label: String = Strings.t(StringName("season." + String(season)))
|
||||||
|
_header.text = Strings.t(&"ui.day_summary.title").format({
|
||||||
|
"day": str(day),
|
||||||
|
"season": season_label,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Weather.
|
||||||
|
_weather_icon.text = WEATHER_ICONS.get(weather, "?")
|
||||||
|
_weather_label.text = Strings.t(StringName("weather." + String(weather)))
|
||||||
|
|
||||||
|
# Stat values — find nodes by name inside the grid.
|
||||||
|
var pawns_val: Label = _stats_grid.find_child("PawnsValue", false, false)
|
||||||
|
if pawns_val != null:
|
||||||
|
pawns_val.text = str(pawns_alive)
|
||||||
|
|
||||||
|
var wolves_val: Label = _stats_grid.find_child("WolvesValue", false, false)
|
||||||
|
if wolves_val != null:
|
||||||
|
wolves_val.text = str(wolves)
|
||||||
|
|
||||||
|
var tension_val: Label = _stats_grid.find_child("TensionValue", false, false)
|
||||||
|
if tension_val != null:
|
||||||
|
tension_val.text = Strings.t(&"ui.day_summary.tension_fmt").format({"t": "%.0f" % tension})
|
||||||
|
|
||||||
|
# Tension color bar — width is a fraction of the track.
|
||||||
|
if _tension_track != null and _tension_bar != null:
|
||||||
|
var track_w: float = float(CARD_W - 48)
|
||||||
|
var fill_w: float = clampf(tension / 100.0, 0.0, 1.0) * track_w
|
||||||
|
_tension_bar.size = Vector2(fill_w, _tension_track.custom_minimum_size.y)
|
||||||
|
if tension < TENSION_LOW:
|
||||||
|
_tension_bar.color = Color(0.25, 0.75, 0.25, 1.0) # green
|
||||||
|
elif tension < TENSION_MID:
|
||||||
|
_tension_bar.color = Color(0.85, 0.75, 0.15, 1.0) # yellow
|
||||||
|
else:
|
||||||
|
_tension_bar.color = Color(0.85, 0.25, 0.20, 1.0) # red
|
||||||
|
|
||||||
|
|
||||||
|
# ── dismiss ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _on_continue_pressed() -> void:
|
||||||
|
_dismiss()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_dim_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.pressed:
|
||||||
|
_dismiss()
|
||||||
|
|
||||||
|
|
||||||
|
func _dismiss() -> void:
|
||||||
|
_set_visible(false)
|
||||||
|
Sim.set_speed(Sim.Speed.values()[_speed_before_open])
|
||||||
|
Audit.log("day_summary_card", "dismissed, speed restored to %d" % _speed_before_open)
|
||||||
|
|
||||||
|
|
||||||
|
# ── visibility ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _set_visible(v: bool) -> void:
|
||||||
|
# Toggle CanvasLayer.visible so the dim Control (MOUSE_FILTER_STOP) does not
|
||||||
|
# eat _unhandled_input mouse events for the world below when the card is hidden.
|
||||||
|
visible = v
|
||||||
|
if _dim != null:
|
||||||
|
_dim.visible = v
|
||||||
|
if _panel != null:
|
||||||
|
_panel.visible = v
|
||||||
1
scenes/ui/day_summary_card.gd.uid
Normal file
1
scenes/ui/day_summary_card.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://7gvadq4jib4v
|
||||||
|
|
@ -13,10 +13,11 @@ var _dim: ColorRect = null
|
||||||
var _panel: PanelContainer = null
|
var _panel: PanelContainer = null
|
||||||
|
|
||||||
# Auto-pause checkboxes.
|
# Auto-pause checkboxes.
|
||||||
var _cb_threat: CheckBox = null
|
var _cb_threat: CheckBox = null
|
||||||
var _cb_wanderer: CheckBox = null
|
var _cb_wanderer: CheckBox = null
|
||||||
var _cb_pawn_down: CheckBox = null
|
var _cb_pawn_down: CheckBox = null
|
||||||
var _cb_modal: CheckBox = null
|
var _cb_modal: CheckBox = null
|
||||||
|
var _cb_day_summary: CheckBox = null
|
||||||
|
|
||||||
# Audio sliders.
|
# Audio sliders.
|
||||||
var _sl_master: HSlider = null
|
var _sl_master: HSlider = null
|
||||||
|
|
@ -105,10 +106,11 @@ func _build_ui() -> void:
|
||||||
ap_hdr.text = Strings.t(&"ui.settings.auto_pause")
|
ap_hdr.text = Strings.t(&"ui.settings.auto_pause")
|
||||||
vbox.add_child(ap_hdr)
|
vbox.add_child(ap_hdr)
|
||||||
|
|
||||||
_cb_threat = _make_checkbox(Strings.t(&"ui.settings.pause_threat"), vbox)
|
_cb_threat = _make_checkbox(Strings.t(&"ui.settings.pause_threat"), vbox)
|
||||||
_cb_wanderer = _make_checkbox(Strings.t(&"ui.settings.pause_wanderer"), vbox)
|
_cb_wanderer = _make_checkbox(Strings.t(&"ui.settings.pause_wanderer"), vbox)
|
||||||
_cb_pawn_down = _make_checkbox(Strings.t(&"ui.settings.pause_pawn_down"), vbox)
|
_cb_pawn_down = _make_checkbox(Strings.t(&"ui.settings.pause_pawn_down"), vbox)
|
||||||
_cb_modal = _make_checkbox(Strings.t(&"ui.settings.pause_modal"), vbox)
|
_cb_modal = _make_checkbox(Strings.t(&"ui.settings.pause_modal"), vbox)
|
||||||
|
_cb_day_summary = _make_checkbox(Strings.t(&"ui.settings.show_day_summary"), vbox)
|
||||||
|
|
||||||
_add_separator(vbox)
|
_add_separator(vbox)
|
||||||
|
|
||||||
|
|
@ -196,10 +198,11 @@ func _add_separator(parent: VBoxContainer) -> void:
|
||||||
|
|
||||||
func _load_from_game_state() -> void:
|
func _load_from_game_state() -> void:
|
||||||
var s: Dictionary = GameState.settings
|
var s: Dictionary = GameState.settings
|
||||||
_cb_threat.button_pressed = bool(s.get("pause_on_threat", true))
|
_cb_threat.button_pressed = bool(s.get("pause_on_threat", true))
|
||||||
_cb_wanderer.button_pressed = bool(s.get("pause_on_wanderer", true))
|
_cb_wanderer.button_pressed = bool(s.get("pause_on_wanderer", true))
|
||||||
_cb_pawn_down.button_pressed = bool(s.get("pause_on_pawn_down", true))
|
_cb_pawn_down.button_pressed = bool(s.get("pause_on_pawn_down", true))
|
||||||
_cb_modal.button_pressed = bool(s.get("pause_on_modal", true))
|
_cb_modal.button_pressed = bool(s.get("pause_on_modal", true))
|
||||||
|
_cb_day_summary.button_pressed = bool(s.get("show_day_summary", true))
|
||||||
|
|
||||||
_sl_master.value = float(s.get("audio_master", 1.0))
|
_sl_master.value = float(s.get("audio_master", 1.0))
|
||||||
_sl_music.value = float(s.get("audio_music", 1.0))
|
_sl_music.value = float(s.get("audio_music", 1.0))
|
||||||
|
|
@ -230,6 +233,7 @@ func _collect_to_dict() -> Dictionary:
|
||||||
"pause_on_wanderer": _cb_wanderer.button_pressed,
|
"pause_on_wanderer": _cb_wanderer.button_pressed,
|
||||||
"pause_on_pawn_down": _cb_pawn_down.button_pressed,
|
"pause_on_pawn_down": _cb_pawn_down.button_pressed,
|
||||||
"pause_on_modal": _cb_modal.button_pressed,
|
"pause_on_modal": _cb_modal.button_pressed,
|
||||||
|
"show_day_summary": _cb_day_summary.button_pressed,
|
||||||
"audio_master": _sl_master.value,
|
"audio_master": _sl_master.value,
|
||||||
"audio_music": _sl_music.value,
|
"audio_music": _sl_music.value,
|
||||||
"audio_sfx": _sl_sfx.value,
|
"audio_sfx": _sl_sfx.value,
|
||||||
|
|
|
||||||
375
scenes/ui/stockpile_panel.gd
Normal file
375
scenes/ui/stockpile_panel.gd
Normal file
|
|
@ -0,0 +1,375 @@
|
||||||
|
class_name StockpilePanel extends CanvasLayer
|
||||||
|
## Phase 17 — Right-side stockpile filter + priority editor.
|
||||||
|
##
|
||||||
|
## Layer 18: shares the slot with WorkbenchPanel and PawnDetailPanel — only one
|
||||||
|
## is visible at a time (mutual exclusion via Selection).
|
||||||
|
## Opens when EventBus.stockpile_selected fires; closes on stockpile_deselected.
|
||||||
|
##
|
||||||
|
## Filter model:
|
||||||
|
## accepted_types.is_empty() → wildcard "accept all" mode (default).
|
||||||
|
## As soon as the player checks any chip, the zone switches to explicit-list
|
||||||
|
## mode. "Select all" returns to wildcard by clearing the array.
|
||||||
|
##
|
||||||
|
## Rebuild policy:
|
||||||
|
## _populate_chips / _update_priority_row are called only on open or on an
|
||||||
|
## explicit player action. No per-tick polling needed.
|
||||||
|
##
|
||||||
|
## Touch targets: all interactive controls are at least 48×48 px.
|
||||||
|
|
||||||
|
const PANEL_WIDTH: int = 360
|
||||||
|
const LAYER: int = 18
|
||||||
|
|
||||||
|
# ── internal state ────────────────────────────────────────────────────────────
|
||||||
|
var current_zone = null # StockpileZone (StorageDestination subclass)
|
||||||
|
|
||||||
|
# ── node refs (built in _build_ui) ───────────────────────────────────────────
|
||||||
|
var _panel: PanelContainer = null
|
||||||
|
var _header_label: Label = null
|
||||||
|
var _close_btn: Button = null
|
||||||
|
var _priority_btns: Array[Button] = []
|
||||||
|
var _chips_grid: GridContainer = null
|
||||||
|
var _all_label: Label = null
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
layer = LAYER
|
||||||
|
|
||||||
|
_build_ui()
|
||||||
|
_set_visible(false)
|
||||||
|
|
||||||
|
EventBus.stockpile_selected.connect(_on_stockpile_selected)
|
||||||
|
EventBus.stockpile_deselected.connect(_on_stockpile_deselected)
|
||||||
|
EventBus.pawn_selected.connect(_on_other_selected)
|
||||||
|
EventBus.workbench_selected.connect(_on_other_selected)
|
||||||
|
|
||||||
|
Audit.log("stockpile_panel", "StockpilePanel ready (layer %d)" % layer)
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
if EventBus.stockpile_selected.is_connected(_on_stockpile_selected):
|
||||||
|
EventBus.stockpile_selected.disconnect(_on_stockpile_selected)
|
||||||
|
if EventBus.stockpile_deselected.is_connected(_on_stockpile_deselected):
|
||||||
|
EventBus.stockpile_deselected.disconnect(_on_stockpile_deselected)
|
||||||
|
if EventBus.pawn_selected.is_connected(_on_other_selected):
|
||||||
|
EventBus.pawn_selected.disconnect(_on_other_selected)
|
||||||
|
if EventBus.workbench_selected.is_connected(_on_other_selected):
|
||||||
|
EventBus.workbench_selected.disconnect(_on_other_selected)
|
||||||
|
|
||||||
|
|
||||||
|
# ── UI construction ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
# Right-side sheet anchored to the right edge, full height.
|
||||||
|
_panel = PanelContainer.new()
|
||||||
|
_panel.name = "StockpileSheet"
|
||||||
|
_panel.anchor_left = 1.0
|
||||||
|
_panel.anchor_right = 1.0
|
||||||
|
_panel.anchor_top = 0.0
|
||||||
|
_panel.anchor_bottom = 1.0
|
||||||
|
_panel.offset_left = -PANEL_WIDTH
|
||||||
|
_panel.offset_right = 0.0
|
||||||
|
_panel.offset_top = 0.0
|
||||||
|
_panel.offset_bottom = 0.0
|
||||||
|
_panel.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||||
|
add_child(_panel)
|
||||||
|
|
||||||
|
# Scrollable inner container so content survives small screens.
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.name = "Scroll"
|
||||||
|
scroll.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
_panel.add_child(scroll)
|
||||||
|
|
||||||
|
var vbox := VBoxContainer.new()
|
||||||
|
vbox.name = "Content"
|
||||||
|
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
vbox.add_theme_constant_override("separation", 6)
|
||||||
|
scroll.add_child(vbox)
|
||||||
|
|
||||||
|
# ── Header ────────────────────────────────────────────────────────────────
|
||||||
|
var header := HBoxContainer.new()
|
||||||
|
header.name = "Header"
|
||||||
|
header.add_theme_constant_override("separation", 8)
|
||||||
|
vbox.add_child(header)
|
||||||
|
|
||||||
|
_header_label = Label.new()
|
||||||
|
_header_label.name = "HeaderLabel"
|
||||||
|
_header_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_header_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
_header_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
header.add_child(_header_label)
|
||||||
|
|
||||||
|
_close_btn = Button.new()
|
||||||
|
_close_btn.name = "CloseBtn"
|
||||||
|
_close_btn.text = Strings.t(&"ui.detail.close")
|
||||||
|
_close_btn.custom_minimum_size = Vector2(48, 48)
|
||||||
|
_close_btn.focus_mode = Control.FOCUS_NONE
|
||||||
|
_close_btn.pressed.connect(_on_close_pressed)
|
||||||
|
header.add_child(_close_btn)
|
||||||
|
|
||||||
|
_add_separator(vbox)
|
||||||
|
|
||||||
|
# ── Priority row ──────────────────────────────────────────────────────────
|
||||||
|
var prio_lbl := Label.new()
|
||||||
|
prio_lbl.text = Strings.t(&"ui.stockpile.priority")
|
||||||
|
prio_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
vbox.add_child(prio_lbl)
|
||||||
|
|
||||||
|
var prio_row := HBoxContainer.new()
|
||||||
|
prio_row.name = "PriorityRow"
|
||||||
|
prio_row.add_theme_constant_override("separation", 4)
|
||||||
|
vbox.add_child(prio_row)
|
||||||
|
|
||||||
|
# Buttons ordered CRITICAL → HIGH → NORMAL → LOW → OFF for display.
|
||||||
|
# StorageDestination.Priority has CRITICAL=0, HIGH=1, NORMAL=2, LOW=3, OFF=4.
|
||||||
|
var prio_labels: Array[StringName] = [
|
||||||
|
&"ui.stockpile.prio.critical",
|
||||||
|
&"ui.stockpile.prio.high",
|
||||||
|
&"ui.stockpile.prio.normal",
|
||||||
|
&"ui.stockpile.prio.low",
|
||||||
|
&"ui.stockpile.prio.off",
|
||||||
|
]
|
||||||
|
_priority_btns.clear()
|
||||||
|
for i in prio_labels.size():
|
||||||
|
var btn := Button.new()
|
||||||
|
btn.text = Strings.t(prio_labels[i])
|
||||||
|
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
btn.custom_minimum_size = Vector2(0, 40)
|
||||||
|
btn.focus_mode = Control.FOCUS_NONE
|
||||||
|
btn.toggle_mode = false
|
||||||
|
var captured_i: int = i
|
||||||
|
btn.pressed.connect(func() -> void: _on_priority_pressed(captured_i))
|
||||||
|
prio_row.add_child(btn)
|
||||||
|
_priority_btns.append(btn)
|
||||||
|
|
||||||
|
_add_separator(vbox)
|
||||||
|
|
||||||
|
# ── Filter chip section ────────────────────────────────────────────────────
|
||||||
|
var filter_hdr := HBoxContainer.new()
|
||||||
|
filter_hdr.name = "FilterHeader"
|
||||||
|
filter_hdr.add_theme_constant_override("separation", 8)
|
||||||
|
vbox.add_child(filter_hdr)
|
||||||
|
|
||||||
|
var accepts_lbl := Label.new()
|
||||||
|
accepts_lbl.text = Strings.t(&"ui.stockpile.accepts")
|
||||||
|
accepts_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
accepts_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
filter_hdr.add_child(accepts_lbl)
|
||||||
|
|
||||||
|
_all_label = Label.new()
|
||||||
|
_all_label.name = "AllLabel"
|
||||||
|
_all_label.text = Strings.t(&"ui.stockpile.accepts_all_hint")
|
||||||
|
_all_label.modulate = Color(0.7, 0.7, 0.7)
|
||||||
|
_all_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
filter_hdr.add_child(_all_label)
|
||||||
|
|
||||||
|
# "Select all" / "Clear all" quick-action row.
|
||||||
|
var quick_row := HBoxContainer.new()
|
||||||
|
quick_row.name = "QuickRow"
|
||||||
|
quick_row.add_theme_constant_override("separation", 8)
|
||||||
|
vbox.add_child(quick_row)
|
||||||
|
|
||||||
|
var select_all_btn := Button.new()
|
||||||
|
select_all_btn.name = "SelectAllBtn"
|
||||||
|
select_all_btn.text = Strings.t(&"ui.stockpile.select_all")
|
||||||
|
select_all_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
select_all_btn.custom_minimum_size = Vector2(0, 40)
|
||||||
|
select_all_btn.focus_mode = Control.FOCUS_NONE
|
||||||
|
select_all_btn.pressed.connect(_on_select_all_pressed)
|
||||||
|
quick_row.add_child(select_all_btn)
|
||||||
|
|
||||||
|
var clear_all_btn := Button.new()
|
||||||
|
clear_all_btn.name = "ClearAllBtn"
|
||||||
|
clear_all_btn.text = Strings.t(&"ui.stockpile.clear_all")
|
||||||
|
clear_all_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
clear_all_btn.custom_minimum_size = Vector2(0, 40)
|
||||||
|
clear_all_btn.focus_mode = Control.FOCUS_NONE
|
||||||
|
clear_all_btn.pressed.connect(_on_clear_all_pressed)
|
||||||
|
quick_row.add_child(clear_all_btn)
|
||||||
|
|
||||||
|
# 4-column chip grid.
|
||||||
|
_chips_grid = GridContainer.new()
|
||||||
|
_chips_grid.name = "ChipsGrid"
|
||||||
|
_chips_grid.columns = 4
|
||||||
|
_chips_grid.add_theme_constant_override("h_separation", 4)
|
||||||
|
_chips_grid.add_theme_constant_override("v_separation", 4)
|
||||||
|
_chips_grid.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
vbox.add_child(_chips_grid)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_separator(parent: VBoxContainer) -> void:
|
||||||
|
var sep := HSeparator.new()
|
||||||
|
sep.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
parent.add_child(sep)
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_children(node: Node) -> void:
|
||||||
|
for child in node.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
# ── event handlers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _on_stockpile_selected(zone) -> void:
|
||||||
|
current_zone = zone
|
||||||
|
_refresh_header()
|
||||||
|
_update_priority_row()
|
||||||
|
_populate_chips()
|
||||||
|
_set_visible(true)
|
||||||
|
Audit.log("stockpile_panel", "opened for zone at %s" % str(zone.position))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_stockpile_deselected() -> void:
|
||||||
|
current_zone = null
|
||||||
|
_set_visible(false)
|
||||||
|
Audit.log("stockpile_panel", "closed (deselected)")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_other_selected(_ignored = null) -> void:
|
||||||
|
# Mutual exclusion: any other selection type hides this panel.
|
||||||
|
if current_zone == null:
|
||||||
|
return
|
||||||
|
current_zone = null
|
||||||
|
_set_visible(false)
|
||||||
|
Audit.log("stockpile_panel", "closed (other panel selected)")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_close_pressed() -> void:
|
||||||
|
current_zone = null
|
||||||
|
_set_visible(false)
|
||||||
|
EventBus.stockpile_deselected.emit()
|
||||||
|
Audit.log("stockpile_panel", "closed (X button)")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_priority_pressed(prio_index: int) -> void:
|
||||||
|
if current_zone == null:
|
||||||
|
return
|
||||||
|
current_zone.priority = prio_index as StorageDestination.Priority
|
||||||
|
_update_priority_row()
|
||||||
|
Audit.log("stockpile_panel", "priority → %d" % prio_index)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_select_all_pressed() -> void:
|
||||||
|
if current_zone == null:
|
||||||
|
return
|
||||||
|
# Empty array = wildcard — accept all. call_deferred to avoid freeing chips
|
||||||
|
# while this button's pressed signal is still emitting.
|
||||||
|
current_zone.accepted_types.clear()
|
||||||
|
call_deferred("_populate_chips")
|
||||||
|
Audit.log("stockpile_panel", "select_all → wildcard mode")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_clear_all_pressed() -> void:
|
||||||
|
if current_zone == null:
|
||||||
|
return
|
||||||
|
# Start explicit-list mode but with nothing checked.
|
||||||
|
# We set accepted_types to all types so *nothing* is excluded yet, then
|
||||||
|
# clear to empty which means… wildcard. To mean "accept nothing" we need
|
||||||
|
# Priority.OFF instead. So: clear all chips means set priority to OFF and
|
||||||
|
# leave accepted_types alone (wildcard doesn't matter when OFF).
|
||||||
|
# The idiomatic result the player expects from "Clear all" on a filter grid
|
||||||
|
# is that the zone accepts nothing — set priority to OFF.
|
||||||
|
current_zone.priority = StorageDestination.Priority.OFF
|
||||||
|
_update_priority_row()
|
||||||
|
call_deferred("_populate_chips")
|
||||||
|
Audit.log("stockpile_panel", "clear_all → priority OFF")
|
||||||
|
|
||||||
|
|
||||||
|
# ── refresh helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _refresh_header() -> void:
|
||||||
|
if current_zone == null:
|
||||||
|
return
|
||||||
|
var tile_count: int = 0
|
||||||
|
if current_zone.has_method("tile_count"):
|
||||||
|
tile_count = current_zone.tile_count()
|
||||||
|
elif current_zone.get("tiles") != null:
|
||||||
|
tile_count = current_zone.tiles.size()
|
||||||
|
_header_label.text = "%s (%d)" % [Strings.t(&"ui.stockpile.title"), tile_count]
|
||||||
|
|
||||||
|
|
||||||
|
func _update_priority_row() -> void:
|
||||||
|
if current_zone == null:
|
||||||
|
return
|
||||||
|
var active: int = int(current_zone.priority)
|
||||||
|
# CRITICAL=0 HIGH=1 NORMAL=2 LOW=3 OFF=4 — matches button array order.
|
||||||
|
var active_tint := Color(0.90, 0.65, 0.20)
|
||||||
|
var inactive_tint := Color(1.0, 1.0, 1.0)
|
||||||
|
for i in _priority_btns.size():
|
||||||
|
_priority_btns[i].modulate = active_tint if i == active else inactive_tint
|
||||||
|
|
||||||
|
|
||||||
|
func _populate_chips() -> void:
|
||||||
|
if current_zone == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
_clear_children(_chips_grid)
|
||||||
|
|
||||||
|
var is_wildcard: bool = current_zone.accepted_types.is_empty()
|
||||||
|
_all_label.visible = is_wildcard
|
||||||
|
|
||||||
|
for type in Item.ALL_TYPES:
|
||||||
|
var chip := _make_chip(type, is_wildcard or (type in current_zone.accepted_types))
|
||||||
|
_chips_grid.add_child(chip)
|
||||||
|
|
||||||
|
|
||||||
|
## Build a single filter chip Button for one item type.
|
||||||
|
## checked=true renders the chip as "included" (white/bright), false as dimmed.
|
||||||
|
func _make_chip(type: StringName, checked: bool) -> Button:
|
||||||
|
# Label: try "item.<type>" key; Strings.t() returns the key string itself
|
||||||
|
# when missing (and emits a push_warning), so we detect that and fall back
|
||||||
|
# to a capitalized form without the "item." prefix.
|
||||||
|
var label_key: StringName = StringName("item." + String(type))
|
||||||
|
var looked_up: String = Strings.t(label_key)
|
||||||
|
var label_text: String
|
||||||
|
if looked_up != String(label_key):
|
||||||
|
label_text = looked_up
|
||||||
|
else:
|
||||||
|
label_text = String(type).capitalize()
|
||||||
|
|
||||||
|
var btn := Button.new()
|
||||||
|
btn.text = label_text
|
||||||
|
btn.custom_minimum_size = Vector2(0, 40)
|
||||||
|
btn.focus_mode = Control.FOCUS_NONE
|
||||||
|
btn.toggle_mode = false
|
||||||
|
btn.modulate = Color(1.0, 1.0, 1.0) if checked else Color(0.5, 0.5, 0.5, 0.8)
|
||||||
|
|
||||||
|
btn.pressed.connect(func() -> void: _on_chip_pressed(type))
|
||||||
|
return btn
|
||||||
|
|
||||||
|
|
||||||
|
func _on_chip_pressed(type: StringName) -> void:
|
||||||
|
if current_zone == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var is_wildcard: bool = current_zone.accepted_types.is_empty()
|
||||||
|
|
||||||
|
if is_wildcard:
|
||||||
|
# Wildcard → explicit: start with all types checked, then remove this one.
|
||||||
|
var explicit: Array[StringName] = []
|
||||||
|
for t in Item.ALL_TYPES:
|
||||||
|
if t != type:
|
||||||
|
explicit.append(t)
|
||||||
|
current_zone.accepted_types = explicit
|
||||||
|
else:
|
||||||
|
# Toggle: add if absent, remove if present.
|
||||||
|
if type in current_zone.accepted_types:
|
||||||
|
current_zone.accepted_types.erase(type)
|
||||||
|
# If we just unchecked the last one, go back to wildcard.
|
||||||
|
if current_zone.accepted_types.is_empty():
|
||||||
|
pass # empty = wildcard already
|
||||||
|
else:
|
||||||
|
current_zone.accepted_types.append(type)
|
||||||
|
|
||||||
|
# Defer the rebuild — don't free this chip while its pressed signal emits.
|
||||||
|
call_deferred("_populate_chips")
|
||||||
|
Audit.log("stockpile_panel", "chip toggled: %s → accepted_types size=%d" % [
|
||||||
|
type, current_zone.accepted_types.size()
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
# ── visibility ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _set_visible(v: bool) -> void:
|
||||||
|
if _panel != null:
|
||||||
|
_panel.visible = v
|
||||||
1
scenes/ui/stockpile_panel.gd.uid
Normal file
1
scenes/ui/stockpile_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bqvdr1ww4bvpf
|
||||||
|
|
@ -17,6 +17,8 @@ var _selected_pawn: Pawn = null
|
||||||
## Currently selected workbench, or null. Mutually exclusive with _selected_pawn —
|
## Currently selected workbench, or null. Mutually exclusive with _selected_pawn —
|
||||||
## selecting one clears the other (see _select / _select_workbench).
|
## selecting one clears the other (see _select / _select_workbench).
|
||||||
var _selected_workbench: Workbench = null
|
var _selected_workbench: Workbench = null
|
||||||
|
## Currently selected stockpile zone, or null. Mutually exclusive with the above.
|
||||||
|
var _selected_stockpile = null # StockpileZone (duck-typed to avoid circular preload)
|
||||||
var _camera = null # Camera2D (CameraRig) — set via bind_camera(); duck-typed to avoid circular preload
|
var _camera = null # Camera2D (CameraRig) — set via bind_camera(); duck-typed to avoid circular preload
|
||||||
|
|
||||||
# When Designation paint mode is active this flag is raised by Designation so
|
# When Designation paint mode is active this flag is raised by Designation so
|
||||||
|
|
@ -76,12 +78,17 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
Audit.log("selection", "escape: deselected workbench")
|
Audit.log("selection", "escape: deselected workbench")
|
||||||
return
|
return
|
||||||
|
if _selected_stockpile != null:
|
||||||
|
_deselect_stockpile()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
Audit.log("selection", "escape: deselected stockpile")
|
||||||
|
return
|
||||||
|
|
||||||
# ── Mouse: only handle button events below ───────────────────────────────────
|
# ── Mouse: only handle button events below ───────────────────────────────────
|
||||||
if not (event is InputEventMouseButton):
|
if not (event is InputEventMouseButton):
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── Right-click: cancel designation (if active) or deselect pawn / workbench ──
|
# ── Right-click: cancel designation (if active) or deselect pawn / workbench / stockpile ──
|
||||||
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
|
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
|
||||||
# Designation cancellation is handled by Designation._input; if we see
|
# Designation cancellation is handled by Designation._input; if we see
|
||||||
# this right-click, no designation was active. Deselect whatever is selected.
|
# this right-click, no designation was active. Deselect whatever is selected.
|
||||||
|
|
@ -91,6 +98,9 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
elif _selected_workbench != null:
|
elif _selected_workbench != null:
|
||||||
_deselect_workbench()
|
_deselect_workbench()
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
elif _selected_stockpile != null:
|
||||||
|
_deselect_stockpile()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
|
||||||
if event.button_index != MOUSE_BUTTON_LEFT:
|
if event.button_index != MOUSE_BUTTON_LEFT:
|
||||||
|
|
@ -139,10 +149,18 @@ func _handle_click(screen_pos: Vector2) -> void:
|
||||||
_select_workbench(hit_workbench)
|
_select_workbench(hit_workbench)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Empty tile with no current pawn selection → also clear any workbench selection.
|
# Click on a stockpile zone → open the filter editor panel.
|
||||||
|
var hit_stockpile = World.stockpile_at_tile(tile)
|
||||||
|
if hit_stockpile != null:
|
||||||
|
_select_stockpile(hit_stockpile)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Empty tile with no current pawn selection → also clear any workbench / stockpile selection.
|
||||||
if _selected_pawn == null:
|
if _selected_pawn == null:
|
||||||
if _selected_workbench != null:
|
if _selected_workbench != null:
|
||||||
_deselect_workbench()
|
_deselect_workbench()
|
||||||
|
if _selected_stockpile != null:
|
||||||
|
_deselect_stockpile()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Empty walkable tile with a selection → queue a forced job. Decision picks
|
# Empty walkable tile with a selection → queue a forced job. Decision picks
|
||||||
|
|
@ -161,9 +179,11 @@ func _handle_click(screen_pos: Vector2) -> void:
|
||||||
func _select(pawn: Pawn) -> void:
|
func _select(pawn: Pawn) -> void:
|
||||||
if _selected_pawn == pawn:
|
if _selected_pawn == pawn:
|
||||||
return
|
return
|
||||||
# Mutual exclusion with workbench selection: clear it before promoting pawn.
|
# Mutual exclusion with workbench and stockpile: clear them before promoting pawn.
|
||||||
if _selected_workbench != null:
|
if _selected_workbench != null:
|
||||||
_deselect_workbench()
|
_deselect_workbench()
|
||||||
|
if _selected_stockpile != null:
|
||||||
|
_deselect_stockpile()
|
||||||
if _selected_pawn != null:
|
if _selected_pawn != null:
|
||||||
_selected_pawn.set_selected(false)
|
_selected_pawn.set_selected(false)
|
||||||
EventBus.pawn_deselected.emit()
|
EventBus.pawn_deselected.emit()
|
||||||
|
|
@ -184,12 +204,14 @@ func _deselect() -> void:
|
||||||
|
|
||||||
|
|
||||||
## Select a workbench → opens the bill-editor panel via EventBus.
|
## Select a workbench → opens the bill-editor panel via EventBus.
|
||||||
## Mutually exclusive with pawn selection: clears _selected_pawn first.
|
## Mutually exclusive with pawn and stockpile selections.
|
||||||
func _select_workbench(wb) -> void:
|
func _select_workbench(wb) -> void:
|
||||||
if _selected_workbench == wb:
|
if _selected_workbench == wb:
|
||||||
return
|
return
|
||||||
if _selected_pawn != null:
|
if _selected_pawn != null:
|
||||||
_deselect()
|
_deselect()
|
||||||
|
if _selected_stockpile != null:
|
||||||
|
_deselect_stockpile()
|
||||||
if _selected_workbench != null:
|
if _selected_workbench != null:
|
||||||
EventBus.workbench_deselected.emit()
|
EventBus.workbench_deselected.emit()
|
||||||
_selected_workbench = wb
|
_selected_workbench = wb
|
||||||
|
|
@ -205,6 +227,30 @@ func _deselect_workbench() -> void:
|
||||||
EventBus.workbench_deselected.emit()
|
EventBus.workbench_deselected.emit()
|
||||||
|
|
||||||
|
|
||||||
|
## Select a stockpile zone → opens the filter editor panel via EventBus.
|
||||||
|
## Mutually exclusive with pawn and workbench selections.
|
||||||
|
func _select_stockpile(zone) -> void:
|
||||||
|
if _selected_stockpile == zone:
|
||||||
|
return
|
||||||
|
if _selected_pawn != null:
|
||||||
|
_deselect()
|
||||||
|
if _selected_workbench != null:
|
||||||
|
_deselect_workbench()
|
||||||
|
if _selected_stockpile != null:
|
||||||
|
EventBus.stockpile_deselected.emit()
|
||||||
|
_selected_stockpile = zone
|
||||||
|
EventBus.stockpile_selected.emit(zone)
|
||||||
|
Audit.log("selection", "selected stockpile at %s" % str(zone.position))
|
||||||
|
|
||||||
|
|
||||||
|
func _deselect_stockpile() -> void:
|
||||||
|
if _selected_stockpile == null:
|
||||||
|
return
|
||||||
|
Audit.log("selection", "deselected stockpile")
|
||||||
|
_selected_stockpile = null
|
||||||
|
EventBus.stockpile_deselected.emit()
|
||||||
|
|
||||||
|
|
||||||
## Cycle the selection forward (dir=1) or backward (dir=-1) through World.pawns.
|
## Cycle the selection forward (dir=1) or backward (dir=-1) through World.pawns.
|
||||||
## Wraps around. If no pawn currently selected, picks World.pawns[0].
|
## Wraps around. If no pawn currently selected, picks World.pawns[0].
|
||||||
## Pans the camera to the newly selected pawn's tile.
|
## Pans the camera to the newly selected pawn's tile.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue