feat(@projects/@magic-civilization): ✨ prologue wanderers visible + tile-dominant unit/city sprites
- Reveal the prologue spawn box in recalculate_vision: the player owns no units during the turn -1/0/1 cold-open, so unit-derived fog left the whole map black and the wanderers invisible. Reveal the local player's box (LOS from centroid + each wanderer hex) whenever a prologue is active. - Prologue overlay: draw the real dwarf_wanderer / dwarf_tribe sprites, centered on the hex (+ hex_center, was drawing at the corner), scaled to match in-game units. Marker glyph kept only as a missing-asset fallback. - Scale all unit sprites to 0.6 of hex width and city sprites to 0.75 via DrawHelpers.scaled_sprite_size — Civ-like tile dominance instead of the tiny native 64px blit that read as unreadable tokens. - Add toggleable leveled Log autoload (dev/info/warn/error via MC_LOG_LEVEL, default info); route prologue diagnostics through it with durable tags (prologue / prologue-overlay), not objective ids. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
74c0868059
commit
fd24254a7a
8 changed files with 212 additions and 26 deletions
|
|
@ -322,7 +322,7 @@ func _start_game() -> void:
|
|||
# `get_data("setup")` returns the per-id dict which is empty for this
|
||||
# file shape — caught the hard way in the first apricot smoke run.
|
||||
var start_turn: int = _read_start_turn_from_setup()
|
||||
print("[p0-34] world_map._start_game: start_turn=%d" % start_turn)
|
||||
Log.info("world_map._start_game: start_turn=%d" % start_turn, "prologue")
|
||||
if start_turn == -1:
|
||||
_bootstrap_prologue(game_map)
|
||||
else:
|
||||
|
|
@ -428,8 +428,8 @@ func _bootstrap_prologue(game_map: RefCounted) -> void:
|
|||
|
||||
var mode: String = _read_prologue_mode_from_setup()
|
||||
var radius: int = _read_spawn_box_radius_from_setup()
|
||||
print("[p0-34] _bootstrap_prologue: mode=%s radius=%d players=%d" %
|
||||
[mode, radius, GameState.players.size()])
|
||||
Log.info("_bootstrap_prologue: mode=%s radius=%d players=%d" %
|
||||
[mode, radius, GameState.players.size()], "prologue")
|
||||
|
||||
var registered_count: int = 0
|
||||
for p: Variant in GameState.players:
|
||||
|
|
@ -446,6 +446,8 @@ func _bootstrap_prologue(game_map: RefCounted) -> void:
|
|||
pid, start_pos.x, start_pos.y, mode, radius, grid
|
||||
):
|
||||
registered_count += 1
|
||||
var wflat: PackedInt32Array = (driver as PrologueDriverScript).wanderers_for(pid)
|
||||
Log.dev("player %d registered: wanderers=%d" % [pid, wflat.size() / 2], "prologue")
|
||||
|
||||
if registered_count == 0:
|
||||
push_warning("WorldMap: no players registered with prologue; reverting to legacy")
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ extends RefCounted
|
|||
|
||||
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
|
||||
const PathfinderScript: GDScript = preload("res://engine/src/map/pathfinder.gd")
|
||||
const PrologueDriverScript: GDScript = preload(
|
||||
"res://engine/src/modules/management/prologue_driver.gd"
|
||||
)
|
||||
|
||||
## Tri-state visibility values stored in tile.visibility[player_index].
|
||||
const VIS_UNSEEN: int = 0 ## Never seen — full black fog.
|
||||
|
|
@ -33,6 +36,10 @@ const SIGHT_RANGE_SCOUT: int = 3 ## Fast explorer — sees 3 hexes out.
|
|||
const SIGHT_RANGE_WARRIOR: int = 2 ## Standard military — 2-hex awareness.
|
||||
const SIGHT_RANGE_CITY: int = 2 ## Cities act as watchtowers at same range.
|
||||
|
||||
## p0-34: how far around the spawn-box centroid to lift fog during the
|
||||
## tribe-founding prologue. Box radius defaults to 3; +1 margin shows its edge.
|
||||
const PROLOGUE_REVEAL_RANGE: int = 4
|
||||
|
||||
|
||||
static func recalculate_vision(player: RefCounted, game_map: RefCounted) -> void:
|
||||
## Reset all tiles from visible(2) to explored(1), then recompute from units.
|
||||
|
|
@ -53,6 +60,39 @@ static func recalculate_vision(player: RefCounted, game_map: RefCounted) -> void
|
|||
if tile != null:
|
||||
tile.set_visibility(player.index, 2)
|
||||
|
||||
# p0-34: during the tribe-founding prologue the player owns no units, so the
|
||||
# loop above leaves the whole map fogged and the wanderer glyphs invisible.
|
||||
# Reveal the player's spawn box directly so the opening is visible (design:
|
||||
# "fog partially lifted so the player sees the whole box").
|
||||
if TurnManager.prologue != null and TurnManager.prologue.is_available():
|
||||
_reveal_prologue_box(player, game_map)
|
||||
|
||||
|
||||
## p0-34: reveal the local player's prologue spawn box — LOS from the centroid
|
||||
## plus each wanderer's exact hex (LOS-independent, so no glyph hides behind
|
||||
## blocking terrain). No-op once the prologue clears (centroid == ZERO).
|
||||
static func _reveal_prologue_box(player: RefCounted, game_map: RefCounted) -> void:
|
||||
var driver: PrologueDriverScript = TurnManager.prologue as PrologueDriverScript
|
||||
if driver == null:
|
||||
return
|
||||
var pid: int = int(player.index)
|
||||
var centroid: Vector2i = driver.centroid(pid)
|
||||
if centroid != Vector2i.ZERO:
|
||||
var box: Array[Vector2i] = PathfinderScript.visible_hexes(
|
||||
game_map, centroid, PROLOGUE_REVEAL_RANGE
|
||||
)
|
||||
for pos: Vector2i in box:
|
||||
var tile: Resource = game_map.get_tile(pos) as Resource
|
||||
if tile != null:
|
||||
tile.set_visibility(player.index, VIS_VISIBLE)
|
||||
var flat: PackedInt32Array = driver.wanderers_for(pid)
|
||||
var i: int = 0
|
||||
while i + 1 < flat.size():
|
||||
var wtile: Resource = game_map.get_tile(Vector2i(flat[i], flat[i + 1])) as Resource
|
||||
if wtile != null:
|
||||
wtile.set_visibility(player.index, VIS_VISIBLE)
|
||||
i += 2
|
||||
|
||||
|
||||
static func record_observations(player: RefCounted, game_map: RefCounted) -> void:
|
||||
## Record currently-visible tiles into the observation store for climate history.
|
||||
|
|
|
|||
76
src/game/engine/src/autoloads/log.gd
Normal file
76
src/game/engine/src/autoloads/log.gd
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
extends Node
|
||||
## Leveled, toggleable console logger. Severity order: DEV < INFO < WARN < ERROR.
|
||||
## A message emits only when its level is >= the active threshold, which is read
|
||||
## from the `MC_LOG_LEVEL` env var (dev|info|warn|error; default "info") at boot.
|
||||
## DEV is silent in normal runs and turned on with `MC_LOG_LEVEL=dev` for
|
||||
## debugging; flip back by unsetting the var.
|
||||
##
|
||||
## WARN routes through push_warning and ERROR through push_error so they also
|
||||
## surface in Godot's error stream and CI logs; DEV and INFO print to stdout.
|
||||
##
|
||||
## Distinct from GameLogger: that is the structured flight-recorder event stream
|
||||
## (HTTP batches, typed events). This is plain severity-leveled console logging
|
||||
## for development and diagnostics.
|
||||
|
||||
enum Level { DEV = 0, INFO = 1, WARN = 2, ERROR = 3 }
|
||||
|
||||
const LEVEL_NAMES: Dictionary = {
|
||||
Level.DEV: "DEV",
|
||||
Level.INFO: "INFO",
|
||||
Level.WARN: "WARN",
|
||||
Level.ERROR: "ERROR",
|
||||
}
|
||||
|
||||
const _NAME_TO_LEVEL: Dictionary = {
|
||||
"dev": Level.DEV,
|
||||
"info": Level.INFO,
|
||||
"warn": Level.WARN,
|
||||
"error": Level.ERROR,
|
||||
}
|
||||
|
||||
var _threshold: int = Level.INFO
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var configured: String = EnvConfig.get_var("MC_LOG_LEVEL", "info").to_lower()
|
||||
if _NAME_TO_LEVEL.has(configured):
|
||||
_threshold = _NAME_TO_LEVEL[configured]
|
||||
else:
|
||||
push_warning("Log: unknown MC_LOG_LEVEL '%s' — defaulting to info" % configured)
|
||||
|
||||
|
||||
## Override the threshold at runtime (e.g. from a debug console).
|
||||
func set_threshold(level: Level) -> void:
|
||||
_threshold = level
|
||||
|
||||
|
||||
func dev(message: String, tag: String = "") -> void:
|
||||
_emit(Level.DEV, message, tag)
|
||||
|
||||
|
||||
func info(message: String, tag: String = "") -> void:
|
||||
_emit(Level.INFO, message, tag)
|
||||
|
||||
|
||||
func warn(message: String, tag: String = "") -> void:
|
||||
_emit(Level.WARN, message, tag)
|
||||
|
||||
|
||||
func error(message: String, tag: String = "") -> void:
|
||||
_emit(Level.ERROR, message, tag)
|
||||
|
||||
|
||||
func _emit(level: Level, message: String, tag: String) -> void:
|
||||
if level < _threshold:
|
||||
return
|
||||
var prefix: String = "[%s]" % LEVEL_NAMES[level]
|
||||
if not tag.is_empty():
|
||||
prefix += "[%s]" % tag
|
||||
var line: String = "%s %s" % [prefix, message]
|
||||
match level:
|
||||
Level.WARN:
|
||||
push_warning(line)
|
||||
Level.ERROR:
|
||||
push_error(line)
|
||||
_:
|
||||
print(line)
|
||||
|
|
@ -6,6 +6,11 @@ extends Node2D
|
|||
const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
|
||||
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
|
||||
const CityScript: GDScript = preload("res://engine/src/entities/city.gd")
|
||||
const DrawHelpers: GDScript = preload("res://engine/src/rendering/unit_renderer_draw.gd")
|
||||
|
||||
## City sprite size as a fraction of hex width (aspect preserved). Cities are the
|
||||
## tile's focal structure, so slightly larger than units (UNIT_SPRITE_TILE_FRACTION).
|
||||
const CITY_SPRITE_TILE_FRACTION: float = 0.75
|
||||
|
||||
## Visual constants
|
||||
const CITY_RADIUS: float = 24.0
|
||||
|
|
@ -157,8 +162,8 @@ func _draw_city_sprite(pixel: Vector2, city: CityScript, color: Color) -> void:
|
|||
# Optional overlay: draw sprite on top when available
|
||||
var sprite: Texture2D = _get_city_sprite(city)
|
||||
if sprite != null:
|
||||
var tex_size: Vector2 = sprite.get_size()
|
||||
draw_texture(sprite, pixel - tex_size * 0.5)
|
||||
var draw_size: Vector2 = DrawHelpers.scaled_sprite_size(sprite, CITY_SPRITE_TILE_FRACTION)
|
||||
draw_texture_rect(sprite, Rect2(pixel - draw_size * 0.5, draw_size), false)
|
||||
|
||||
|
||||
func _draw_city_label(pixel: Vector2, city: CityScript) -> void:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
class_name PrologueOverlayRenderer
|
||||
extends Node2D
|
||||
## Draw-first overlay for the Freepeople tribe-founding prologue (p0-34).
|
||||
## Paints a circle + letter glyph for each wanderer ('W') and for the
|
||||
## convergence centroid (when relevant). Matches the placeholder pattern
|
||||
## introduced by p0-23 ahead of asset-sprite's real sprite authoring.
|
||||
## Draw-first overlay for the Freepeople tribe-founding prologue.
|
||||
## Paints the real dwarf-wanderer sprite for each wanderer and the dwarf-tribe
|
||||
## sprite at the convergence centroid, scaled to match in-game units. Falls back
|
||||
## to a marker glyph (circle + letter) only when a sprite asset is missing.
|
||||
##
|
||||
## Reads state from TurnManager.prologue (a PrologueDriver). Hidden when
|
||||
## the prologue has resolved (state == Normal) or when the driver is null.
|
||||
|
|
@ -12,29 +12,48 @@ const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
|
|||
const PrologueDriverScript: GDScript = preload(
|
||||
"res://engine/src/modules/management/prologue_driver.gd"
|
||||
)
|
||||
const DrawHelpers: GDScript = preload("res://engine/src/rendering/unit_renderer_draw.gd")
|
||||
|
||||
const WANDERER_RADIUS: float = 10.0
|
||||
const TRIBE_RADIUS: float = 16.0
|
||||
const WANDERER_COLOR: Color = Color(0.85, 0.75, 0.55, 0.9)
|
||||
const WANDERER_RADIUS: float = 24.0
|
||||
const TRIBE_RADIUS: float = 30.0
|
||||
const WANDERER_COLOR: Color = Color(0.95, 0.80, 0.40, 0.95)
|
||||
## Real wanderer sprite (copyleft asset); the marker glyph below is only the
|
||||
## missing-asset fallback.
|
||||
const WANDERER_SPRITE_PATH: String = "sprites/units/dwarf_wanderer.png"
|
||||
const TRIBE_SPRITE_PATH: String = "sprites/units/dwarf_tribe.png"
|
||||
## Sprite size as a fraction of hex width — mirror of unit_renderer.gd
|
||||
## UNIT_SPRITE_TILE_FRACTION so prologue units match in-game units. Keep in sync.
|
||||
const SPRITE_TILE_FRACTION: float = 0.6
|
||||
const WANDERER_BORDER: Color = Color(0.25, 0.18, 0.08, 1.0)
|
||||
const TRIBE_COLOR: Color = Color(0.9, 0.5, 0.2, 0.95)
|
||||
const TRIBE_BORDER: Color = Color(0.2, 0.12, 0.05, 1.0)
|
||||
const GLYPH_COLOR: Color = Color.BLACK
|
||||
|
||||
var _font: Font = null
|
||||
var _wanderer_tex: Texture2D = null
|
||||
var _tribe_tex: Texture2D = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
z_index = 12 # above hex + fog, below modal overlays
|
||||
_font = ThemeDB.fallback_font
|
||||
_wanderer_tex = ThemeAssets.load_sprite(WANDERER_SPRITE_PATH, false)
|
||||
_tribe_tex = ThemeAssets.load_sprite(TRIBE_SPRITE_PATH, false)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if TurnManager.prologue == null:
|
||||
Log.dev("overlay _draw skipped: TurnManager.prologue is null", "prologue-overlay")
|
||||
return
|
||||
var driver: PrologueDriverScript = TurnManager.prologue as PrologueDriverScript
|
||||
if not driver.is_available():
|
||||
Log.dev("overlay _draw skipped: driver not available", "prologue-overlay")
|
||||
return
|
||||
Log.dev(
|
||||
"overlay _draw running: players=%d visible=%s global_pos=%s"
|
||||
% [GameState.players.size(), str(visible), str(global_position)],
|
||||
"prologue-overlay",
|
||||
)
|
||||
# Drawing happens even on Normal so the capital-founded transition looks
|
||||
# clean (empty overlay). Wanderers + tribes are queried per known player.
|
||||
for player_var: Variant in GameState.players:
|
||||
|
|
@ -47,16 +66,31 @@ func _draw() -> void:
|
|||
|
||||
func _draw_wanderers(driver: PrologueDriverScript, player_id: int) -> void:
|
||||
var flat: PackedInt32Array = driver.wanderers_for(player_id)
|
||||
if flat.size() >= 2:
|
||||
var first_px: Vector2 = HexUtilsScript.axial_to_pixel(Vector2i(flat[0], flat[1]))
|
||||
Log.dev(
|
||||
"draw_wanderers p%d: count=%d first axial=(%d,%d) px=%s"
|
||||
% [player_id, flat.size() / 2, flat[0], flat[1], str(first_px)],
|
||||
"prologue-overlay",
|
||||
)
|
||||
else:
|
||||
Log.dev("draw_wanderers p%d: count=0 (empty)" % player_id, "prologue-overlay")
|
||||
var i: int = 0
|
||||
while i + 1 < flat.size():
|
||||
var q: int = flat[i]
|
||||
var r: int = flat[i + 1]
|
||||
var pixel: Vector2 = HexUtilsScript.axial_to_pixel(Vector2i(q, r))
|
||||
draw_circle(pixel, WANDERER_RADIUS, WANDERER_COLOR)
|
||||
draw_arc(pixel, WANDERER_RADIUS, 0.0, TAU, 24, WANDERER_BORDER, 1.5)
|
||||
if _font != null:
|
||||
var offset: Vector2 = Vector2(-4.0, 5.0)
|
||||
draw_string(_font, pixel + offset, "W", HORIZONTAL_ALIGNMENT_LEFT, -1, 14, GLYPH_COLOR)
|
||||
var pixel: Vector2 = (
|
||||
HexUtilsScript.axial_to_pixel(Vector2i(q, r)) + HexUtilsScript.hex_center
|
||||
)
|
||||
if _wanderer_tex != null:
|
||||
_draw_unit_sprite(_wanderer_tex, pixel)
|
||||
else:
|
||||
# Missing-asset fallback: the placeholder marker glyph.
|
||||
draw_circle(pixel, WANDERER_RADIUS, WANDERER_COLOR)
|
||||
draw_arc(pixel, WANDERER_RADIUS, 0.0, TAU, 24, WANDERER_BORDER, 3.0)
|
||||
if _font != null:
|
||||
var offset: Vector2 = Vector2(-7.0, 7.0)
|
||||
draw_string(_font, pixel + offset, "W", HORIZONTAL_ALIGNMENT_LEFT, -1, 20, GLYPH_COLOR)
|
||||
i += 2
|
||||
|
||||
|
||||
|
|
@ -66,9 +100,22 @@ func _draw_tribe_marker(driver: PrologueDriverScript, player_id: int) -> void:
|
|||
return
|
||||
var q: int = int(tribe.get("q", 0))
|
||||
var r: int = int(tribe.get("r", 0))
|
||||
var pixel: Vector2 = HexUtilsScript.axial_to_pixel(Vector2i(q, r))
|
||||
draw_circle(pixel, TRIBE_RADIUS, TRIBE_COLOR)
|
||||
draw_arc(pixel, TRIBE_RADIUS, 0.0, TAU, 28, TRIBE_BORDER, 2.0)
|
||||
if _font != null:
|
||||
var offset: Vector2 = Vector2(-5.0, 6.0)
|
||||
draw_string(_font, pixel + offset, "T", HORIZONTAL_ALIGNMENT_LEFT, -1, 18, GLYPH_COLOR)
|
||||
var pixel: Vector2 = (
|
||||
HexUtilsScript.axial_to_pixel(Vector2i(q, r)) + HexUtilsScript.hex_center
|
||||
)
|
||||
if _tribe_tex != null:
|
||||
_draw_unit_sprite(_tribe_tex, pixel)
|
||||
else:
|
||||
# Missing-asset fallback: the placeholder marker glyph.
|
||||
draw_circle(pixel, TRIBE_RADIUS, TRIBE_COLOR)
|
||||
draw_arc(pixel, TRIBE_RADIUS, 0.0, TAU, 28, TRIBE_BORDER, 3.0)
|
||||
if _font != null:
|
||||
var offset: Vector2 = Vector2(-8.0, 8.0)
|
||||
draw_string(_font, pixel + offset, "T", HORIZONTAL_ALIGNMENT_LEFT, -1, 24, GLYPH_COLOR)
|
||||
|
||||
|
||||
## Draw a unit sprite scaled to SPRITE_TILE_FRACTION of the hex width (aspect
|
||||
## preserved), centered on `pixel` — matches unit_renderer's in-game unit size.
|
||||
func _draw_unit_sprite(tex: Texture2D, pixel: Vector2) -> void:
|
||||
var draw_size: Vector2 = DrawHelpers.scaled_sprite_size(tex, SPRITE_TILE_FRACTION)
|
||||
draw_texture_rect(tex, Rect2(pixel - draw_size * 0.5, draw_size), false)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ const COMBAT_TYPE_LABELS: Dictionary = {
|
|||
const SPRITE_LOOKUP_RACE_SEX_FORMAT: String = "sprites/units/%s_%s_%s.png"
|
||||
const SPRITE_LOOKUP_GENERIC_FORMAT: String = "sprites/units/%s.png"
|
||||
|
||||
## Unit sprite size as a fraction of hex width (aspect preserved) — tile-dominant
|
||||
## per 4X convention. Mirror of prologue_overlay_renderer.gd SPRITE_TILE_FRACTION.
|
||||
const UNIT_SPRITE_TILE_FRACTION: float = 0.6
|
||||
|
||||
## Formation outline colors and geometry.
|
||||
const FORMATION_LINE_COLOR: Color = Color(0.85, 0.75, 0.25, 0.6)
|
||||
const FORMATION_LINE_WIDTH: float = 3.0
|
||||
|
|
@ -288,8 +292,8 @@ func _draw() -> void:
|
|||
var sex: String = data.get("sex", "")
|
||||
var sprite: Texture2D = _get_unit_sprite(type_id, race_id, sex)
|
||||
if sprite != null:
|
||||
var tex_size: Vector2 = sprite.get_size()
|
||||
draw_texture(sprite, pixel - tex_size * 0.5)
|
||||
var draw_size: Vector2 = DrawHelpers.scaled_sprite_size(sprite, UNIT_SPRITE_TILE_FRACTION)
|
||||
draw_texture_rect(sprite, Rect2(pixel - draw_size * 0.5, draw_size), false)
|
||||
|
||||
DrawHelpers.draw_hp_bar(
|
||||
self, pixel, data, HP_BAR_WIDTH, HP_BAR_HEIGHT, HP_BAR_OFFSET_Y, HP_BAR_BG_COLOR
|
||||
|
|
|
|||
|
|
@ -4,6 +4,17 @@ extends RefCounted
|
|||
|
||||
const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
|
||||
|
||||
## Scale a unit sprite to `fraction` of the hex width, preserving aspect ratio.
|
||||
## Source sprites are small (64px); this upsizes them to a tile-dominant size so
|
||||
## units read clearly (4X convention) instead of as tiny tokens.
|
||||
static func scaled_sprite_size(sprite: Texture2D, fraction: float) -> Vector2:
|
||||
var tex: Vector2 = sprite.get_size()
|
||||
var target_w: float = HexUtilsScript.HEX_WIDTH * fraction
|
||||
if tex.x <= 0.0:
|
||||
return Vector2(target_w, target_w)
|
||||
return tex * (target_w / tex.x)
|
||||
|
||||
|
||||
## Draws formation outlines between adjacent member units above movement range.
|
||||
static func draw_formation_outlines(
|
||||
canvas: CanvasItem,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ config/features=PackedStringArray("4.3", "GL Compatibility")
|
|||
[autoload]
|
||||
|
||||
EnvConfig="*res://engine/src/autoloads/env_config.gd"
|
||||
Log="*res://engine/src/autoloads/log.gd"
|
||||
SettingsManager="*res://engine/src/autoloads/settings_manager.gd"
|
||||
EventBus="*res://engine/src/autoloads/event_bus.gd"
|
||||
DataLoader="*res://engine/src/autoloads/data_loader.gd"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue