Skip to content
Merged
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
92 changes: 92 additions & 0 deletions .github/labels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,95 @@
color: "0052cc"
description: Stable escalated-lane Saffron worker.
# Repo-specific labels can be appended below. Unknown labels are not pruned.
# Agents / Dispatch (mirrored from live repo labels, 2026-07-16)
- name: agent/foreman-coder
color: "ededed"
description: Foreman coding loop worker.
- name: agent/saffron
color: "ededed"
description: Saffron worker (unlaned).
- name: claimed
color: "85C653"
description: Issue is claimed by an active agent
- name: escalated
color: "FFA500"
description: Issue escalated to GPT review — not eligible for wishlist cron
- name: in-progress
color: "75C043"
description: Work has started
- name: needs-gpt
color: "9a3834"
description: This required gpt-5.5 model
# Areas
- name: area/ops
color: "5319E7"
description: Operations and release process
- name: area/product
color: "AFEEEE"
description: Product feature work
- name: area/security
color: "D93F0C"
description: Security related
- name: area/testing
color: "84B6E6"
description: Testing and QA
# Short-form priorities (legacy; prefer priority/*)
- name: p0
color: "FF0000"
description: Critical priority
- name: p1
color: "FBCA04"
description: High priority
- name: p2
color: "0E8A16"
description: Normal priority
# Types
- name: type/bug
color: "ededed"
description: Bug fix.
- name: type/chore
color: "ededed"
description: Chore or maintenance.
- name: type/digest
color: "ededed"
description: Digest or summary work.
- name: type/feature
color: "ededed"
description: Feature work.
- name: type/major
color: "ededed"
description: Major change.
- name: type/minor
color: "ededed"
description: Minor change.
- name: type/patch
color: "ededed"
description: Patch-level change.
- name: tech-debt
color: "7056ff"
description: Technical debt from audits.
# GitHub defaults / misc
- name: duplicate
color: "cfd3d7"
description: This issue or pull request already exists
- name: good first issue
color: "7057ff"
description: Good for newcomers
- name: help wanted
color: "008672"
description: Extra attention is needed
- name: invalid
color: "e4e669"
description: This doesn't seem right
- name: question
color: "d876e3"
description: Further information is requested
- name: wontfix
color: "ffffff"
description: This will not be worked on
- name: renovate
color: "6B7280"
description: Automated Renovate dashboard
- name: renovate/github-action
color: "ededed"
description: Renovate GitHub Action updates.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,3 @@ 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
80 changes: 16 additions & 64 deletions scripts/main.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const MilestoneManager := preload("res://scripts/milestone_manager.gd")
const ColonyStance := preload("res://scripts/colony_stance.gd")
const TileRender := preload("res://scripts/tile_render.gd")
const WorkerCapLogic := preload("res://scripts/worker_cap_logic.gd")
const WorkerRenderer := preload("res://scripts/worker_renderer.gd")
const ColonySim := preload("res://scripts/colony_sim.gd")


Expand Down Expand Up @@ -228,12 +229,21 @@ func apply_theme() -> void:
option.add_theme_stylebox_override("pressed", make_panel_style(Color(0.13, 0.18, 0.24, 1.0), Color(0.42, 0.58, 0.71, 0.95), 10))
option.add_theme_color_override("font_color", Color(0.95, 0.97, 1.0, 0.98))

# Button styleboxes are shared across every themed button — they are never
# mutated per-button, so one instance per state suffices (audit #279 item 4).
var _button_styles: Dictionary = {}

## Shared button styling for every themed Button in the dock UI.
func apply_button_theme(button: Button) -> void:
button.add_theme_stylebox_override("normal", make_panel_style(Color(0.18, 0.23, 0.3, 0.96), Color(0.36, 0.45, 0.55, 0.9), 10))
button.add_theme_stylebox_override("hover", make_panel_style(Color(0.23, 0.3, 0.39, 1.0), Color(0.49, 0.64, 0.78, 1.0), 10))
button.add_theme_stylebox_override("pressed", make_panel_style(Color(0.13, 0.18, 0.24, 1.0), Color(0.42, 0.58, 0.71, 0.95), 10))
button.add_theme_stylebox_override("disabled", make_panel_style(Color(0.12, 0.15, 0.19, 0.65), Color(0.24, 0.28, 0.33, 0.45), 10))
if _button_styles.is_empty():
_button_styles = {
"normal": make_panel_style(Color(0.18, 0.23, 0.3, 0.96), Color(0.36, 0.45, 0.55, 0.9), 10),
"hover": make_panel_style(Color(0.23, 0.3, 0.39, 1.0), Color(0.49, 0.64, 0.78, 1.0), 10),
"pressed": make_panel_style(Color(0.13, 0.18, 0.24, 1.0), Color(0.42, 0.58, 0.71, 0.95), 10),
"disabled": make_panel_style(Color(0.12, 0.15, 0.19, 0.65), Color(0.24, 0.28, 0.33, 0.45), 10),
}
for state in _button_styles:
button.add_theme_stylebox_override(state, _button_styles[state])
button.add_theme_color_override("font_color", Color(0.95, 0.97, 1.0, 0.98))
button.add_theme_color_override("font_disabled_color", Color(0.72, 0.76, 0.82, 0.6))

Expand Down Expand Up @@ -1473,18 +1483,8 @@ func render_worker_overlay() -> void:
sprite.position = draw_pos - sprite.custom_minimum_size * 0.5

func worker_collision_offset(slot: int, total: int) -> Vector2:
if total <= 1:
return Vector2.ZERO
var spacing := maxf(8.0, float(tile_size.x) * 0.18)
var offsets := [
Vector2(-spacing, -spacing),
Vector2(spacing, spacing),
Vector2(spacing, -spacing),
Vector2(-spacing, spacing),
Vector2(0, -spacing * 1.25),
Vector2(0, spacing * 1.25),
]
return offsets[slot % offsets.size()]
return WorkerRenderer.collision_offset(slot, total, spacing)

func tile_center(pos: Vector2i) -> Vector2:
var index := pos.y * grid_w + pos.x
Expand Down Expand Up @@ -1671,55 +1671,7 @@ func worker_texture(name: String, frame: int, carrying: String = "") -> Texture2
var cache_key := "%s:%d:%s" % [name, frame, carrying]
if worker_texture_cache.has(cache_key):
return worker_texture_cache[cache_key]
var accent: Color = Constants.WORKER_BADGE_COLORS.get(name, Color.WHITE)
var shadow := accent.darkened(0.45)
var skin := Color("#f2d0b1")
var image := Image.create(12, 14, false, Image.FORMAT_RGBA8)
image.fill(Color(0, 0, 0, 0))

# head
for y in range(0, 4):
for x in range(4, 8):
image.set_pixel(x, y, skin)

# body
for y in range(4, 10):
for x in range(3, 9):
image.set_pixel(x, y, accent)

# arms
for y in range(5, 9):
image.set_pixel(2, y, shadow)
image.set_pixel(9, y, shadow)

if not carrying.is_empty():
var cargo_color := Color("#9aa3aa")
if carrying == "wood":
cargo_color = Color("#8b5a2b")
elif carrying == "food":
cargo_color = Color("#6fbf73")
for y in range(5, 9):
for x in range(0, 3):
image.set_pixel(x, y, cargo_color)
image.set_pixel(1, 4, cargo_color.lightened(0.25))

# legs alternate per frame for a simple walk bob
if frame % 2 == 0:
image.set_pixel(4, 10, shadow)
image.set_pixel(4, 11, shadow)
image.set_pixel(7, 10, shadow)
image.set_pixel(7, 11, shadow)
else:
image.set_pixel(5, 10, shadow)
image.set_pixel(4, 11, shadow)
image.set_pixel(6, 10, shadow)
image.set_pixel(7, 11, shadow)

# feet
image.set_pixel(3, 13, shadow)
image.set_pixel(7, 13, shadow)

var texture := ImageTexture.create_from_image(image)
var texture := WorkerRenderer.create_texture(name, frame, carrying)
worker_texture_cache[cache_key] = texture
return texture

Expand Down
69 changes: 69 additions & 0 deletions scripts/worker_renderer.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
## Pure worker sprite rendering — pixel-art texture generation and crowd
## offsets. No scene references; main.gd owns the texture cache and calls in.
class_name WorkerRenderer

const Constants := preload("res://scripts/constants.gd")


## Build the 12x14 pixel-art texture for a worker. Deterministic per
## (name, frame, carrying) — callers should cache by that key.
static func create_texture(worker_name: String, frame: int, carrying: String = "") -> Texture2D:
var accent: Color = Constants.WORKER_BADGE_COLORS.get(worker_name, Color.WHITE)
var shadow := accent.darkened(0.45)
var skin := Color("#f2d0b1")
var image := Image.create(12, 14, false, Image.FORMAT_RGBA8)
image.fill(Color(0, 0, 0, 0))

# head
for y in range(0, 4):
for x in range(4, 8):
image.set_pixel(x, y, skin)

# body
for y in range(4, 10):
for x in range(3, 9):
image.set_pixel(x, y, accent)

# arms
for y in range(5, 9):
image.set_pixel(2, y, shadow)
image.set_pixel(9, y, shadow)

if not carrying.is_empty():
var cargo_color := Color("#9aa3aa")
if carrying == "wood":
cargo_color = Color("#8b5a2b")
elif carrying == "food":
cargo_color = Color("#6fbf73")
for y in range(5, 9):
for x in range(0, 3):
image.set_pixel(x, y, cargo_color)
image.set_pixel(1, 4, cargo_color.lightened(0.25))

# legs alternate per frame for a simple walk bob
if frame % 2 == 0:
image.set_pixel(4, 10, shadow)
image.set_pixel(4, 11, shadow)
image.set_pixel(7, 10, shadow)
image.set_pixel(7, 11, shadow)
else:
image.set_pixel(5, 10, shadow)
image.set_pixel(4, 11, shadow)
image.set_pixel(6, 10, shadow)
image.set_pixel(7, 11, shadow)

# feet
image.set_pixel(3, 13, shadow)
image.set_pixel(7, 13, shadow)

return ImageTexture.create_from_image(image)


## Offset for the slot-th of total workers sharing one tile: occupants are
## spread evenly around a ring, so any crowd size stays visually distinct
## (the old fixed six-offset table wrapped and overlapped above six).
static func collision_offset(slot: int, total: int, spacing: float) -> Vector2:
if total <= 1:
return Vector2.ZERO
var angle := TAU * float(slot % total) / float(total)
return Vector2(cos(angle), sin(angle)) * spacing * 1.25
107 changes: 107 additions & 0 deletions tests/test_dock_layout.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
extends "res://tests/test_case.gd"

# ── Dock anchor layout orchestration tests (#279 item 11) ─────────────────────
# apply_anchor_layout() reparents the header labels, status column, and HUD
# row between the bottom-strip and side-dock families — the largest UX
# surface in the dock, previously untested. main.gd is instantiated WITHOUT
# entering the tree (so _ready never runs and no autoloads are needed); the
# node references it @onready-resolves in production are assigned manually.

func run_tests() -> void:
var main = load("res://scripts/main.gd").new()
var refs := _build_layout_harness(main)

# ── Bottom family ──
main.apply_anchor_geometry("bottom")
main.apply_anchor_layout("bottom")
assert_eq(main.anchor_family, "bottom", "bottom: anchor family applied")
assert_true(main.bottom_header_row != null, "bottom: header row created")
assert_eq(main.bottom_header_row.get_parent(), refs.left_column, "bottom: header row lives in left column")
assert_eq(refs.resource_label.get_parent(), main.bottom_header_row, "bottom: resource label in bottom header")
assert_eq(refs.status_label.get_parent(), main.bottom_status_column, "bottom: status label in bottom status column")
assert_eq(refs.hud_row.get_parent(), main.bottom_button_row, "bottom: HUD row in bottom button row")
assert_true(main.bottom_header_row.visible, "bottom: bottom header visible")
assert_eq(refs.status_label.horizontal_alignment, HORIZONTAL_ALIGNMENT_RIGHT, "bottom: status right-aligned")
assert_eq(refs.world_grid.columns, main.grid_w, "bottom: grid columns match bottom dims")

# ── Switch to side family ──
main.apply_anchor_geometry("left")
main.apply_anchor_layout("left")
assert_eq(main.anchor_family, "vertical", "side: anchor family applied")
assert_true(main.side_header_row != null, "side: header row created")
assert_eq(main.side_header_row.get_parent(), refs.left_column, "side: header row lives in left column")
assert_eq(refs.resource_label.get_parent(), main.side_header_row, "side: resource label in side header")
assert_eq(refs.status_label.get_parent(), main.side_status_column, "side: status label in side status column")
assert_eq(refs.hud_row.get_parent(), main.side_button_row, "side: HUD row in side button row")
assert_false(main.bottom_header_row.visible, "side: bottom header hidden")
assert_true(main.side_header_row.visible, "side: side header visible")
assert_eq(refs.status_label.horizontal_alignment, HORIZONTAL_ALIGNMENT_LEFT, "side: status left-aligned")
assert_eq(refs.world_grid.columns, main.grid_w, "side: grid columns match side dims")

# ── And back — parentage must be fully restored ──
main.apply_anchor_geometry("bottom")
main.apply_anchor_layout("bottom")
assert_eq(refs.resource_label.get_parent(), main.bottom_header_row, "return: resource label back in bottom header")
assert_eq(refs.status_label.get_parent(), main.bottom_status_column, "return: status label back in bottom column")
assert_eq(refs.hud_row.get_parent(), main.bottom_button_row, "return: HUD row back in bottom button row")
assert_true(main.bottom_header_row.visible, "return: bottom header visible again")
assert_false(main.side_header_row.visible, "return: side header hidden again")
# HUD row internal ordering is normalized by the shared tail.
assert_eq(refs.hud_row.get_child(0), refs.menu_button, "return: menu button first in HUD row")
assert_eq(refs.hud_row.get_child(2), refs.menu_hint, "return: menu hint last in HUD row")

main.free()


## Wire up the minimal node graph apply_anchor_layout touches, returning the
## nodes the assertions need. Mirrors the scene structure without loading it.
func _build_layout_harness(main: Control) -> Dictionary:
var backdrop := Panel.new()
backdrop.name = "Backdrop"
main.add_child(backdrop)

var root_box := BoxContainer.new()
backdrop.add_child(root_box)
var left_column := VBoxContainer.new()
root_box.add_child(left_column)
var world_panel := PanelContainer.new()
left_column.add_child(world_panel)
var world_grid := GridContainer.new()
world_panel.add_child(world_grid)
var sidebar_scroll := ScrollContainer.new()
root_box.add_child(sidebar_scroll)

var resource_label := Label.new()
left_column.add_child(resource_label)
var status_label := Label.new()
left_column.add_child(status_label)

var hud_row := HBoxContainer.new()
left_column.add_child(hud_row)
var menu_button := Button.new()
hud_row.add_child(menu_button)
var build_mode_button := Button.new()
hud_row.add_child(build_mode_button)
var menu_hint := Label.new()
hud_row.add_child(menu_hint)

main.root_box = root_box
main.left_column = left_column
main.world_panel = world_panel
main.world_grid = world_grid
main.sidebar_scroll = sidebar_scroll
main.resource_label = resource_label
main.status_label = status_label
main.menu_button = menu_button
main.build_mode_button = build_mode_button
main.menu_hint = menu_hint

return {
"left_column": left_column,
"world_grid": world_grid,
"resource_label": resource_label,
"status_label": status_label,
"hud_row": hud_row,
"menu_button": menu_button,
"menu_hint": menu_hint,
}
Loading