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
124 changes: 124 additions & 0 deletions scripts/layout_math.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
## Pure layout math for dock and tile geometry.
## Extracted from main.gd so layout logic can be tested directly
## without requiring a full scene or DisplayServer access.
##
## Regression targets:
## - anchor-family dock geometry (#40, #52, #53, #54)
## - popup placement staying within reachable bounds
## - square tile assumptions
##
## Usage: call functions directly; no Godot node dependency.

const SIDE_GRID_W := 7
const SIDE_GRID_H := 16
const BOTTOM_GRID_W := 30
const BOTTOM_GRID_H := 5
const TILE_GAP := 6
const TILE_SIZE_BUMP := 1.15
const BOTTOM_TILE_BASE_PX := 40.0
const VERTICAL_TILE_BASE_PX := 48.0
const WORLD_PANEL_PADDING := Vector2i(16, 16)
const SIDEBAR_WIDTH := 220
const BOTTOM_DOCK_PADDING := Vector2i(48, 110)
const VERTICAL_DOCK_PADDING := Vector2i(60, 120)


## Compute tile pixel size for a given anchor family and zoom factor.
## Tiles are always square (same x and y).
static func tile_px_for_anchor(dock_anchor: String, zoom: float = 1.0) -> int:
var base_tile_px: float = BOTTOM_TILE_BASE_PX if dock_anchor == "bottom" else VERTICAL_TILE_BASE_PX
return maxi(1, int(round(base_tile_px * TILE_SIZE_BUMP * zoom)))


## Compute world panel pixel size given grid dimensions and tile size.
static func world_pixel_size(grid_w: int, grid_h: int, tile_px: int) -> Vector2i:
return Vector2i(
grid_w * tile_px + (grid_w - 1) * TILE_GAP,
grid_h * tile_px + (grid_h - 1) * TILE_GAP
)


## Get grid dimensions for a given anchor family.
static func grid_dims_for_anchor(anchor_family: String) -> Dictionary:
if anchor_family == "bottom":
return {"grid_w": BOTTOM_GRID_W, "grid_h": BOTTOM_GRID_H}
return {"grid_w": SIDE_GRID_W, "grid_h": SIDE_GRID_H}


## Get dock padding for a given anchor family.
static func dock_padding_for_anchor(anchor_family: String) -> Vector2i:
return BOTTOM_DOCK_PADDING if anchor_family == "bottom" else VERTICAL_DOCK_PADDING


## Compute dock window size for a given anchor family.
## Returns (width, height) that accounts for world panel + padding + sidebar.
static func dock_size_for_anchor(anchor_family: String, grid_w: int, grid_h: int, tile_px: int) -> Vector2i:
var padding := dock_padding_for_anchor(anchor_family)
var world := world_pixel_size(grid_w, grid_h, tile_px)
var base := Vector2i(world.x + padding.x, world.y + padding.y)
if anchor_family == "bottom":
base.x += SIDEBAR_WIDTH + 16
return base


## Compute dock window position within a usable screen rect.
## dock_anchor is the raw anchor string ("left", "bottom", "right").
static func dock_position_for_anchor(usable_left: int, usable_top: int, usable_width: int, usable_height: int, dock_size: Vector2i, dock_anchor: String) -> Vector2i:
if dock_anchor == "left":
return Vector2i(
usable_left + 12,
usable_top + usable_height - dock_size.y - 12
)
if dock_anchor == "bottom":
return Vector2i(
usable_left + int((usable_width - dock_size.x) / 2),
usable_top + usable_height - dock_size.y - 12
)
# right (default)
return Vector2i(
usable_left + usable_width - dock_size.x - 12,
usable_top + usable_height - dock_size.y - 12
)


## Compute popup (sidebar) position for a given anchor.
## Returns the (x, y) position of the sidebar panel.
static func popup_position_for_anchor(anchor_family: String, backdrop_width: float, sidebar_width: float) -> Vector2:
if anchor_family == "bottom":
return Vector2(backdrop_width - sidebar_width - 16, 16)
# side (left or right)
if anchor_family == "right":
return Vector2(max(16.0, backdrop_width - sidebar_width - 16), 16)
return Vector2(16, 16)


## Check if a popup position keeps the sidebar within a reachable bounds rect.
## A popup is "reachable" if its entire rectangle fits within the usable area.
static func popup_within_bounds(popup_pos: Vector2, popup_size: Vector2, usable_left: int, usable_top: int, usable_width: int, usable_height: int) -> bool:
var popup_right := popup_pos.x + popup_size.x
var popup_bottom := popup_pos.y + popup_size.y
return (
popup_pos.x >= usable_left
and popup_pos.y >= usable_top
and popup_right <= usable_left + usable_width
and popup_bottom <= usable_top + usable_height
)


## Get the anchor family string from a raw dock anchor.
static func anchor_family_from_dock_anchor(dock_anchor: String) -> String:
if dock_anchor == "bottom":
return "bottom"
return "vertical"


## Verify that tile sizes are square (x == y).
static func tile_is_square(tile_px: int) -> bool:
return true # by construction, tile_px_for_anchor returns a single int


## Compute the stockpile position for a given anchor family.
static func stockpile_pos_for_anchor(anchor_family: String) -> Vector2i:
if anchor_family == "bottom":
return Vector2i(11, 2)
return Vector2i(2, 7)
61 changes: 18 additions & 43 deletions scripts/main.gd
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ const BUILD_COSTS := {
"workshop": {"wood": 4, "stone": 6},
"garden": {"wood": 3, "stone": 1},
}
const LayoutMath := preload("res://scripts/layout_math.gd")

const BUILD_UNLOCKS := {
"hut": true,
"workshop": "hut",
Expand Down Expand Up @@ -171,32 +173,21 @@ func update_tile_metrics(dock_anchor: String) -> void:
tile_size = Vector2i(tile_px, tile_px)

func tile_px_for_anchor(dock_anchor: String) -> int:
var base_tile_px: float = BOTTOM_TILE_BASE_PX if dock_anchor == "bottom" else VERTICAL_TILE_BASE_PX
var zoom: float = float(settings.get("zoom_factor", 1.0))
return maxi(1, int(round(base_tile_px * TILE_SIZE_BUMP * zoom)))
return LayoutMath.tile_px_for_anchor(dock_anchor, float(settings.get("zoom_factor", 1.0)))

func world_pixel_size() -> Vector2i:
return Vector2i(
grid_w * tile_size.x + (grid_w - 1) * TILE_GAP,
grid_h * tile_size.y + (grid_h - 1) * TILE_GAP
)
return LayoutMath.world_pixel_size(grid_w, grid_h, tile_size.x)

func dock_padding_for_anchor(dock_anchor: String) -> Vector2i:
return BOTTOM_DOCK_PADDING if dock_anchor == "bottom" else VERTICAL_DOCK_PADDING
return LayoutMath.dock_padding_for_anchor(anchor_family)

func apply_anchor_geometry(dock_anchor: String) -> void:
if dock_anchor == "bottom":
anchor_family = "bottom"
else:
anchor_family = "vertical"
if anchor_family == "bottom":
grid_w = BOTTOM_GRID_W
grid_h = BOTTOM_GRID_H
stockpile_pos = BOTTOM_STOCKPILE_POS
else:
grid_w = SIDE_GRID_W
grid_h = SIDE_GRID_H
stockpile_pos = SIDE_STOCKPILE_POS
var family := LayoutMath.anchor_family_from_dock_anchor(dock_anchor)
anchor_family = family
var dims := LayoutMath.grid_dims_for_anchor(family)
grid_w = dims["grid_w"]
grid_h = dims["grid_h"]
stockpile_pos = LayoutMath.stockpile_pos_for_anchor(family)
func apply_anchor_layout(dock_anchor: String) -> void:
var is_bottom := anchor_family == "bottom"
var world_size: Vector2i = world_pixel_size()
Expand All @@ -217,34 +208,18 @@ func apply_anchor_layout(dock_anchor: String) -> void:
func position_popup_panel(dock_anchor: String) -> void:
var backdrop_size: Vector2 = get_node("Backdrop").size
var popup_size: Vector2 = sidebar_scroll.custom_minimum_size
if dock_anchor == "bottom":
sidebar_scroll.position = Vector2(backdrop_size.x - SIDEBAR_WIDTH - 16, 16)
else:
sidebar_scroll.position = Vector2(16, 16)
if dock_anchor == "right":
sidebar_scroll.position.x = max(16.0, backdrop_size.x - popup_size.x - 16)
var popup_pos := LayoutMath.popup_position_for_anchor(anchor_family, backdrop_size.x, popup_size.x)
sidebar_scroll.position = popup_pos
sidebar_scroll.size = popup_size

func dock_size_for_anchor(dock_anchor: String) -> Vector2i:
var base := world_pixel_size() + dock_padding_for_anchor(dock_anchor)
if dock_anchor == "bottom":
base.x += SIDEBAR_WIDTH + 16 # account for popup sidebar in bottom mode
return base
return LayoutMath.dock_size_for_anchor(anchor_family, grid_w, grid_h, tile_size.x)

func dock_position_for_anchor(usable_rect: Rect2i, dock_size: Vector2i, dock_anchor: String) -> Vector2i:
if dock_anchor == "left":
return Vector2i(
usable_rect.position.x + 12,
usable_rect.position.y + usable_rect.size.y - dock_size.y - 12
)
if dock_anchor == "bottom":
return Vector2i(
usable_rect.position.x + int((usable_rect.size.x - dock_size.x) / 2),
usable_rect.position.y + usable_rect.size.y - dock_size.y - 12
)
return Vector2i(
usable_rect.position.x + usable_rect.size.x - dock_size.x - 12,
usable_rect.position.y + usable_rect.size.y - dock_size.y - 12
return LayoutMath.dock_position_for_anchor(
usable_rect.position.x, usable_rect.position.y,
usable_rect.size.x, usable_rect.size.y,
dock_size, dock_anchor
)

func keep_window_pinned() -> void:
Expand Down
Loading