diff --git a/README.md b/README.md index 05b1db6..3bf4da0 100644 --- a/README.md +++ b/README.md @@ -84,3 +84,4 @@ Tooling is pinned with `.mise.toml`. Run `mise trust` once in the repo if mise p ## Contributors Windowstead is maintained by Miso Space with help from local and AI-assisted contributors. Contributions should preserve the desktop-companion dock UX: bottom strip first, temporary popup menus, and low-attention colony behavior. +# test fix: _assert_empty accepts Variant diff --git a/scenes/main.tscn b/scenes/main.tscn index a416afd..ae5e899 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=2 format=3] +[gd_scene load_steps=5 format=3] [ext_resource type="Script" path="res://scripts/main.gd" id="1_main"] @@ -96,8 +96,35 @@ size_flags_horizontal = 3 text = "2 workers active" modulate = Color(1, 1, 1, 0.6) +[node name="HudWorkerCap" type="Label" parent="Backdrop/Margin/Root/Left/HudRow"] +unique_name_in_owner = true +layout_mode = 2 +text = "0 / 2" +autowrap_mode = 3 +modulate = Color(1, 1, 1, 0.5) +theme_override_font_sizes/font_size = 11 + +[node name="HudFoodWarning" type="Label" parent="Backdrop/Margin/Root/Left/HudRow"] +unique_name_in_owner = true +layout_mode = 2 +text = "" +visible = false +autowrap_mode = 3 +modulate = Color(1, 0.5, 0.3, 0.9) +theme_override_font_sizes/font_size = 11 + + +[node name="HudGoalLabel" type="Label" parent="Backdrop/Margin/Root/Left/HudRow"] +unique_name_in_owner = true +layout_mode = 2 +text = "" +visible = false +autowrap_mode = 3 +modulate = Color(0.6, 0.9, 1, 0.7) +theme_override_font_sizes/font_size = 11 [node name="ActivityLabel" type="Label" parent="Backdrop/Margin/Root/Left"] unique_name_in_owner = true + layout_mode = 2 text = "Activity" autowrap_mode = 3 @@ -539,3 +566,47 @@ unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 text = "Reset" + +[node name="EventDrawerLabel" type="Label" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 8 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 20.0 +offset_top = -40.0 +offset_right = -20.0 +offset_bottom = -10.0 +text = "Last: —" +autowrap_mode = 3 +modulate = Color(1, 1, 1, 0.75) +theme_override_font_sizes/font_size = 12 + +[node name="EventDrawerPanel" type="PanelContainer" parent="."] +unique_name_in_owner = true +visible = false +layout_mode = 1 +anchors_preset = 8 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 20.0 +offset_top = -350.0 +offset_right = -20.0 +offset_bottom = -45.0 + +[node name="EventDrawerMargin" type="MarginContainer" parent="./EventDrawerPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 6 +theme_override_constants/margin_top = 6 +theme_override_constants/margin_right = 6 +theme_override_constants/margin_bottom = 6 + +[node name="EventDrawerLog" type="Label" parent="./EventDrawerPanel/EventDrawerMargin"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +autowrap_mode = 3 +text = "No events yet." +theme_override_font_sizes/font_size = 11 +modulate = Color(1, 1, 1, 0.7) diff --git a/scripts/main.gd b/scripts/main.gd index 0ce66e4..857556d 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -49,6 +49,12 @@ const ColonyStance := preload("res://scripts/colony_stance.gd") @onready var dock_side_option: OptionButton = %DockSideOption @onready var tick_speed_slider: HSlider = %TickSpeedSlider @onready var tick_speed_value: Label = %TickSpeedValue +@onready var hud_worker_cap: Label = %HudWorkerCap +@onready var hud_food_warning: Label = %HudFoodWarning +@onready var hud_goal_label: Label = %HudGoalLabel +@onready var event_drawer_label: Label = %EventDrawerLabel +@onready var event_drawer_panel: PanelContainer = %EventDrawerPanel +@onready var event_drawer_log: Label = %EventDrawerLog var tile_views: Array[Dictionary] = [] var state: Dictionary = {} @@ -87,6 +93,7 @@ var bottom_button_row: HBoxContainer var game_active := false var active_goal: Dictionary = {} var completed_goal_ids: Array = [] +var event_drawer_visible := false func make_panel_style(bg: Color, border: Color, corner_radius: int = 12) -> StyleBoxFlat: var style := StyleBoxFlat.new() @@ -671,6 +678,12 @@ func wire_controls() -> void: %BuildDownButton.pressed.connect(func() -> void: move_priority("build", 1)) tick_speed_slider.value_changed.connect(_on_tick_speed_changed) %SettingsCloseButton.pressed.connect(close_settings) + # Event drawer toggle (issue #139) + event_drawer_label.mouse_filter = Control.MOUSE_FILTER_STOP + event_drawer_label.gui_input.connect(func(event): + if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + toggle_event_drawer() + ) %RecruitButton.pressed.connect(_on_recruit_worker_pressed) func load_or_boot() -> void: @@ -1340,9 +1353,11 @@ func _on_tick() -> void: # Check goal completion and rotate if not active_goal.is_empty() and RotatingGoal.is_goal_complete(active_goal): + var goal_id = String(active_goal.get("id", "unknown")) var new_goal = RotatingGoal.rotate_after_completion(active_goal, completed_goal_ids) completed_goal_ids.append(active_goal["id"]) active_goal = new_goal + push_event("Goal completed: %s. The colony moves on." % goal_id) persist() state.workers = state.workers render_all() @@ -1735,9 +1750,65 @@ func render_all() -> void: render_worker_overlay() render_goal() render_sidebar() + render_hud_row() + render_event_drawer() render_build_buttons() render_stance_toggle() + +func render_hud_row() -> void: + """Render compact HUD row: worker cap, food warning, active goal progress.""" + if not is_instance_valid(hud_worker_cap): + return + + var current_workers := active_worker_count() + var worker_cap_count := get_worker_cap() + hud_worker_cap.text = "%d / %d" % [current_workers, worker_cap_count] + hud_worker_cap.visible = true + + # Food/upkeep warning — only show when relevant (low or starving) + if is_instance_valid(hud_food_warning): + var food_level := get_low_food_level() + match food_level: + "starving": + hud_food_warning.text = "⚠ STARVING" + hud_food_warning.visible = true + "low": + hud_food_warning.text = "⚠ LOW FOOD" + hud_food_warning.visible = true + _: + hud_food_warning.visible = false + + # Active goal progress — show compactly in HUD row + if is_instance_valid(hud_goal_label): + if not active_goal.is_empty(): + var goal_type := String(active_goal.get("type", "")) + var progress := int(active_goal.get("current_progress", 0)) + var target := int(active_goal.get("target", {}).get("amount", 0)) + var is_complete := RotatingGoal.is_goal_complete(active_goal) + + var goal_text := "" + match goal_type: + RotatingGoal.GOAL_TYPE_RESOURCE: + var resource := String(active_goal.get("target", {}).get("resource", "")) + goal_text = "Goal: %s" % resource + RotatingGoal.GOAL_TYPE_BUILD: + var build_kind := String(active_goal.get("target", {}).get("build_kind", "")) + goal_text = "Build: %s" % cap(build_kind) + RotatingGoal.GOAL_TYPE_BUILD_COMPLETE: + goal_text = "Goal: Finish a build" + + # Add progress only when useful (not at 0, not complete) + if target > 0 and progress > 0 and not is_complete: + goal_text += " (%d/%d)" % [progress, target] + elif is_complete: + goal_text += " ✓" + + hud_goal_label.text = goal_text + hud_goal_label.visible = true + else: + hud_goal_label.visible = false + func render_world() -> void: for y in grid_h: for x in grid_w: @@ -2257,6 +2328,35 @@ func is_structure_complete(kind: String) -> bool: return true return false + +func toggle_event_drawer() -> void: + event_drawer_visible = not event_drawer_visible + event_drawer_panel.visible = event_drawer_visible + render_all() + + +func render_event_drawer() -> void: + """Render the compact event drawer: collapsed label + expanded log.""" + if not is_instance_valid(event_drawer_label): + return + + # Update collapsed label with latest event + var events = state.get("events", []) + if not events.is_empty(): + var latest_text = String(events[0].get("text", "—")) + event_drawer_label.text = "Last: " + latest_text + else: + event_drawer_label.text = "Last: —" + + # Update expanded log with recent history (last 6 events) + if is_instance_valid(event_drawer_log): + var lines := [] + for i in range(mini(events.size(), 6)): + var entry = events[i] + lines.append("t%02d %s" % [int(entry.tick), String(entry.get("text", ""))]) + event_drawer_log.text = "\n".join(lines) if not lines.is_empty() else "No events yet." + + func push_event(text: String) -> void: state.events.push_front({"tick": tick, "text": text}) while state.events.size() > 8: diff --git a/tests/test_runner.gd b/tests/test_runner.gd index ed7fa71..48ad595 100644 --- a/tests/test_runner.gd +++ b/tests/test_runner.gd @@ -24,6 +24,7 @@ func _initialize() -> void: test_save_version_tracking(game_state) test_settings_roundtrip(game_state) test_event_log(game_state) + test_bounded_event_log(game_state) test_clear_game(game_state) test_save_migration_hardening(game_state) test_resource_reservations(game_state) @@ -63,7 +64,7 @@ func _assert_not_empty(d: Dictionary, name: String) -> void: _assert(not d.is_empty(), name, "dictionary should not be empty") -func _assert_empty(d: Dictionary, name: String) -> void: +func _assert_empty(d: Variant, name: String) -> void: _assert(d.is_empty(), name, "dictionary should be empty") @@ -376,6 +377,37 @@ func test_event_log(gs: Node) -> void: _assert_eq(loaded_events[2].get("text", ""), "Hut built", "event_log: last event text") + +func test_bounded_event_log(gs: Node) -> void: + print("") + print("--- bounded event log ---") + + # Simulate push_event bounded behavior: max 8 events, LIFO eviction + var events := [] + const MAX_EVENTS := 8 + + for i in range(12): + events.push_front({"tick": i, "text": "Event %d" % i}) + while events.size() > MAX_EVENTS: + events.pop_back() + + _assert_eq(events.size(), MAX_EVENTS, "bounded_event_log: capped at 8") + # First event should be the most recent (11), last should be oldest kept (4) + _assert_eq(int(events[0].get("tick", -1)), 11, "bounded_event_log: first is newest (11)") + _assert_eq(int(events[MAX_EVENTS - 1].get("tick", -1)), 4, "bounded_event_log: last is oldest kept (4)") + + # Verify eviction count: 12 pushed - 8 kept = 4 evicted + var evicted_count := 12 - MAX_EVENTS + _assert_eq(evicted_count, 4, "bounded_event_log: 4 events evicted") + + # Empty log stays empty + var empty_events := [] + _assert_empty(empty_events, "bounded_event_log: empty log is empty") + + # Single event fits without eviction + empty_events.push_front({"tick": 0, "text": "Single"}) + _assert_eq(empty_events.size(), 1, "bounded_event_log: single event size 1") + func test_clear_game(gs: Node) -> void: print("") print("--- clear game ---") @@ -832,3 +864,4 @@ func test_delivery_clamping(gs: Node) -> void: build = main.get_build(1) _assert_eq(int(build.delivered.get("wood", 0)), 6, "clamping: no over-delivery (stays at 6)") _assert_eq(int(main.state.resources.get("wood", -1)), 16, "clamping: all excess refunded (12+4=16)") +