Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ jobs:
run: |
set -euo pipefail
./.tools/Godot_v${{ steps.godot-config.outputs.version }}-${{ steps.godot-config.outputs.status }}_linux.x86_64 --headless --path . --script res://tests/test_reservations.gd > reservation-tests.log 2>&1
RES_EXIT=$?
cat reservation-tests.log
if [ $RES_EXIT -ne 0 ]; then
echo "::error::Reservation tests failed (exit code $RES_EXIT)"
exit 1
fi

macos-validation:
name: macOS validation
Expand Down
73 changes: 72 additions & 1 deletion scenes/main.tscn
Original file line number Diff line number Diff line change
@@ -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"]

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
100 changes: 100 additions & 0 deletions scripts/main.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_food_upkeep.gd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extends TestSuite
extends SceneTree
# Tests for food upkeep model (issue #147, links to #133).
# Validates: base workers no pressure, extra workers create pressure,
# low-food slowdown, starvation pause, and food-gathering bias.
Expand Down
4 changes: 2 additions & 2 deletions tests/test_recruit_worker.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ var test_fail := 0

func _initialize() -> void:
# Load main.gd and create an instance (no UI nodes needed for logic tests)
var main_script: GDScript = preload("res://scripts/main.gd")
var main_script: GDScript = load("res://scripts/main.gd") as GDScript
var main: Control = main_script.new()

test_can_recruit_with_capacity(main)
Expand Down Expand Up @@ -80,7 +80,7 @@ func test_recruit_adds_worker_to_state(main: Control) -> void:
{"name": "Jun", "task": {"kind": "", "data": {}}},
])
_assert(main.can_recruit_worker(), "precondition: can recruit")
var initial_count := main.state.workers.size()
var initial_count: int = main.state.workers.size()
main.recruit_worker()
_assert_eq(main.state.workers.size(), initial_count + 1, "recruit: state workers count increases by 1")

Expand Down
18 changes: 9 additions & 9 deletions tests/test_resource_trends.gd
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func _test_trend_stable_value() -> bool:

func _get_trend_mock(resource_name: String, current_val: int, previous_val: int = -1) -> String:
"""Simulate _get_trend by setting state.resources and prev_resources then calling the method."""
var main := Globals.get_node("/root/Main") as Node
var main := get_node("/root/Main") as Node
if main == null:
return {"ok": false, "msg": "Main autoload not found"}

Expand Down Expand Up @@ -171,7 +171,7 @@ func _test_get_trend_unknown_resource() -> bool:
# --- stockpile_summary_text arrow embedding tests ---

func _test_summary_contains_rising_arrow() -> bool:
var main := Globals.get_node("/root/Main") as Node
var main := get_node("/root/Main") as Node
if main == null:
return {"ok": false, "msg": "Main autoload not found"}

Expand All @@ -192,7 +192,7 @@ func _test_summary_contains_rising_arrow() -> bool:
return summary.find(rising_arrow) >= 0

func _test_summary_contains_stable_arrow() -> bool:
var main := Globals.get_node("/root/Main") as Node
var main := get_node("/root/Main") as Node
if main == null:
return {"ok": false, "msg": "Main autoload not found"}

Expand Down Expand Up @@ -220,7 +220,7 @@ func _test_summary_contains_stable_arrow() -> bool:
## Safe upper bound: ~35 characters for 280px sidebar, ~40 for 320px sidebar.

func _test_compact_summary_fits_bottom_dock() -> bool:
var main := Globals.get_node("/root/Main") as Node
var main := get_node("/root/Main") as Node
if main == null:
return {"ok": false, "msg": "Main autoload not found"}

Expand All @@ -247,7 +247,7 @@ func _test_compact_summary_fits_bottom_dock() -> bool:
return true

func _test_compact_summary_fits_side_dock() -> bool:
var main := Globals.get_node("/root/Main") as Node
var main := get_node("/root/Main") as Node
if main == null:
return {"ok": false, "msg": "Main autoload not found"}

Expand All @@ -273,7 +273,7 @@ func _test_compact_summary_fits_side_dock() -> bool:
return true

func _test_noncompact_first_line_fits() -> bool:
var main := Globals.get_node("/root/Main") as Node
var main := get_node("/root/Main") as Node
if main == null:
return {"ok": false, "msg": "Main autoload not found"}

Expand Down Expand Up @@ -304,7 +304,7 @@ func _test_noncompact_first_line_fits() -> bool:
return true

func _test_all_arrows_in_compact() -> bool:
var main := Globals.get_node("/root/Main") as Node
var main := get_node("/root/Main") as Node
if main == null:
return {"ok": false, "msg": "Main autoload not found"}

Expand Down Expand Up @@ -341,7 +341,7 @@ func _test_all_arrows_in_compact() -> bool:
return true

func _test_all_arrows_in_noncompact() -> bool:
var main := Globals.get_node("/root/Main") as Node
var main := get_node("/root/Main") as Node
if main == null:
return {"ok": false, "msg": "Main autoload not found"}

Expand Down Expand Up @@ -378,7 +378,7 @@ func _test_all_arrows_in_noncompact() -> bool:
return true

func _test_extreme_values_fit_compact() -> bool:
var main := Globals.get_node("/root/Main") as Node
var main := get_node("/root/Main") as Node
if main == null:
return {"ok": false, "msg": "Main autoload not found"}

Expand Down
Loading