style(client): gdformat all 52 GDScript files — zero format warnings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,12 +40,25 @@ const OUTLINE_SHADER_PATH := BASE_PATH + "shaders/outline.gdshader"
|
||||
## All body segment names in assembly order. D-160: 14 base + 2 swappable torso + 2 face = 18.
|
||||
## torso_upper is loaded and hidden by default; clothing coverage reveals it.
|
||||
const ALL_SEGMENTS: Array[String] = [
|
||||
"head", "neck", "torso_upper", "torso", "hips",
|
||||
"arm_upper_l", "arm_upper_r", "arm_lower_l", "arm_lower_r",
|
||||
"hand_l", "hand_r",
|
||||
"leg_upper_l", "leg_upper_r", "leg_lower_l", "leg_lower_r",
|
||||
"foot_l", "foot_r",
|
||||
"eyes", "eyebrows",
|
||||
"head",
|
||||
"neck",
|
||||
"torso_upper",
|
||||
"torso",
|
||||
"hips",
|
||||
"arm_upper_l",
|
||||
"arm_upper_r",
|
||||
"arm_lower_l",
|
||||
"arm_lower_r",
|
||||
"hand_l",
|
||||
"hand_r",
|
||||
"leg_upper_l",
|
||||
"leg_upper_r",
|
||||
"leg_lower_l",
|
||||
"leg_lower_r",
|
||||
"foot_l",
|
||||
"foot_r",
|
||||
"eyes",
|
||||
"eyebrows",
|
||||
]
|
||||
|
||||
## Segments that do NOT receive the skin tone shader — preserve original embedded material.
|
||||
@@ -107,14 +120,14 @@ var _overhead_attachment: BoneAttachment3D = null
|
||||
|
||||
# Inspectable state for tests
|
||||
var _active_torso_variant: String = "full"
|
||||
var _active_coverages: Dictionary = {} # item_id -> coverage dict
|
||||
var _loaded_slots: Array[String] = [] # "hair", "facial_hair", etc.
|
||||
var _active_coverages: Dictionary = {} # item_id -> coverage dict
|
||||
var _loaded_slots: Array[String] = [] # "hair", "facial_hair", etc.
|
||||
|
||||
var _toon_shader: Shader = null
|
||||
var _toon_masked_shader: Shader = null
|
||||
var _outline_shader: Shader = null
|
||||
var _white_mask: ImageTexture = null # 1x1 white pixel — forces full-body tinting
|
||||
var _iris_mask: Texture2D = null # iris-only mask from T_Eye_Split.png
|
||||
var _iris_mask: Texture2D = null # iris-only mask from T_Eye_Split.png
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -140,6 +153,7 @@ func _load_shaders() -> void:
|
||||
# Public API
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## Rebuild the full character from a CharacterVisualDescriptor.
|
||||
func load_descriptor(descriptor: CharacterVisualDescriptor) -> void:
|
||||
_clear()
|
||||
@@ -168,8 +182,14 @@ func set_facing(direction: Variant) -> void:
|
||||
rotation.y = -atan2((direction as Vector2).x, (direction as Vector2).y)
|
||||
elif direction is String:
|
||||
var angles: Dictionary = {
|
||||
"south": 0.0, "southwest": 45.0, "west": 90.0, "northwest": 135.0,
|
||||
"north": 180.0, "northeast": -135.0, "east": -90.0, "southeast": -45.0,
|
||||
"south": 0.0,
|
||||
"southwest": 45.0,
|
||||
"west": 90.0,
|
||||
"northwest": 135.0,
|
||||
"north": 180.0,
|
||||
"northeast": -135.0,
|
||||
"east": -90.0,
|
||||
"southeast": -45.0,
|
||||
}
|
||||
rotation_degrees.y = angles.get((direction as String).to_lower(), 0.0)
|
||||
|
||||
@@ -232,6 +252,7 @@ func get_overhead_anchor() -> Marker3D:
|
||||
# Internal — teardown
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _clear() -> void:
|
||||
# Outline nodes are duplicates of body/clothing meshes — must be freed FIRST,
|
||||
# before the source meshes are removed. _rebuild_outlines() is not called here
|
||||
@@ -287,6 +308,7 @@ func _clear() -> void:
|
||||
# Internal — skeleton
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _load_skeleton() -> void:
|
||||
if not ResourceLoader.exists(SKELETON_PATH):
|
||||
push_error("CharacterVisual: skeleton not found at %s" % SKELETON_PATH)
|
||||
@@ -317,7 +339,12 @@ func _validate_slot_bones() -> void:
|
||||
for slot: String in SLOT_TO_BONE:
|
||||
var bone_name: String = SLOT_TO_BONE[slot]
|
||||
if _skeleton.find_bone(bone_name) == -1:
|
||||
push_warning("CharacterVisual: SLOT_TO_BONE['%s'] = '%s' — bone not found in skeleton" % [slot, bone_name])
|
||||
push_warning(
|
||||
(
|
||||
"CharacterVisual: SLOT_TO_BONE['%s'] = '%s' — bone not found in skeleton"
|
||||
% [slot, bone_name]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
## #712: Create a Marker3D anchored ~0.3m above the Head bone via BoneAttachment3D.
|
||||
@@ -344,6 +371,7 @@ func _create_overhead_anchor() -> void:
|
||||
# Internal — body segments (D-160)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _load_body_segments(desc: CharacterVisualDescriptor) -> void:
|
||||
var body_dir := BASE_PATH + "bodies/%s/" % desc.body_type_key()
|
||||
var tone := SKIN_TONES[clampi(desc.skin_tone, 0, SKIN_TONES.size() - 1)]
|
||||
@@ -420,6 +448,7 @@ func _apply_body_shader(mi: MeshInstance3D, seg_name: String, tone: Dictionary)
|
||||
# Internal — head, hair, facial hair, eyebrows (BoneAttachment3D) (D-161)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _load_head(desc: CharacterVisualDescriptor) -> void:
|
||||
if desc.head_id.is_empty():
|
||||
return
|
||||
@@ -452,7 +481,8 @@ func _load_eyebrows(_desc: CharacterVisualDescriptor) -> void:
|
||||
|
||||
|
||||
func _attach_to_bone(
|
||||
path: String, bone_name: String, tint: Color = Color.WHITE, _render_priority: int = 0) -> BoneAttachment3D:
|
||||
path: String, bone_name: String, tint: Color = Color.WHITE, _render_priority: int = 0
|
||||
) -> BoneAttachment3D:
|
||||
if _skeleton == null:
|
||||
return null
|
||||
if not ResourceLoader.exists(path):
|
||||
@@ -517,6 +547,7 @@ func _attach_to_bone(
|
||||
# Internal — clothing (D-162): coverage.json → segment hiding + torso variant
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _load_clothing(desc: CharacterVisualDescriptor) -> void:
|
||||
if _skeleton == null or desc.clothing_slots.is_empty():
|
||||
return
|
||||
@@ -566,7 +597,6 @@ func _load_clothing(desc: CharacterVisualDescriptor) -> void:
|
||||
# Hiding is reserved for amputation/prosthetics via the debug tab or game logic.
|
||||
|
||||
|
||||
|
||||
func _read_coverage(path: String) -> Dictionary:
|
||||
if not FileAccess.file_exists(path):
|
||||
return {}
|
||||
@@ -608,6 +638,7 @@ func _apply_clothing_shader(mi: MeshInstance3D, tints: Array, mask_tex: Texture2
|
||||
# Internal — accessories (BoneAttachment3D per slot→bone)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _load_accessories(desc: CharacterVisualDescriptor) -> void:
|
||||
for slot: String in desc.accessory_slots:
|
||||
var item_id: String = desc.accessory_slots[slot]
|
||||
@@ -629,8 +660,10 @@ func _load_accessories(desc: CharacterVisualDescriptor) -> void:
|
||||
# Internal — tinting (hair, accessories, bone-attached assets with tint)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _apply_tinted_shader(
|
||||
mi: MeshInstance3D, tint: Color, mask_tex: Texture2D = null, _render_priority: int = 0) -> void:
|
||||
mi: MeshInstance3D, tint: Color, mask_tex: Texture2D = null, _render_priority: int = 0
|
||||
) -> void:
|
||||
if mi.mesh == null:
|
||||
return
|
||||
for surf in range(mi.mesh.get_surface_count()):
|
||||
@@ -657,6 +690,7 @@ func _apply_tinted_shader(
|
||||
# Internal — outline pass (inverted hull, cull_front)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _rebuild_outlines() -> void:
|
||||
for node in _outline_nodes:
|
||||
if is_instance_valid(node):
|
||||
@@ -693,6 +727,7 @@ func _rebuild_outlines() -> void:
|
||||
# Animation
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _load_animations() -> void:
|
||||
if _skeleton == null:
|
||||
return
|
||||
@@ -764,6 +799,7 @@ func stop_animation() -> void:
|
||||
# Static helpers
|
||||
# =============================================================================
|
||||
|
||||
|
||||
static func _find_skeleton(root: Node) -> Skeleton3D:
|
||||
if root is Skeleton3D:
|
||||
return root as Skeleton3D
|
||||
@@ -791,7 +827,9 @@ static func _get_albedo_texture(mat: Material) -> Texture2D:
|
||||
if mat is BaseMaterial3D:
|
||||
return (mat as BaseMaterial3D).albedo_texture
|
||||
if mat is ShaderMaterial:
|
||||
for p: String in ["albedo_tex", "albedo_texture", "texture_albedo", "Hair_Texture", "BaseColor"]:
|
||||
for p: String in [
|
||||
"albedo_tex", "albedo_texture", "texture_albedo", "Hair_Texture", "BaseColor"
|
||||
]:
|
||||
var val: Variant = (mat as ShaderMaterial).get_shader_parameter(p)
|
||||
if val is Texture2D:
|
||||
return val as Texture2D
|
||||
|
||||
@@ -166,6 +166,7 @@ func to_dict() -> Dictionary:
|
||||
# -- Color serialization helpers --
|
||||
# Wire format: [r, g, b, a] float array (rmp_serde serializes Color as tuple).
|
||||
|
||||
|
||||
static func _decode_color(value: Variant, fallback: Color) -> Color:
|
||||
if value is Array and value.size() >= 3:
|
||||
return Color(value[0], value[1], value[2], value[3] if value.size() >= 4 else 1.0)
|
||||
@@ -198,5 +199,3 @@ static func _encode_tint_map(data: Dictionary) -> Dictionary:
|
||||
encoded.append(_encode_color(c))
|
||||
result[key] = encoded
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ func _process(delta: float) -> void:
|
||||
|
||||
# --- Hover detection (automatic, from GameState.visible_entities) ---
|
||||
|
||||
|
||||
func _detect_hover() -> void:
|
||||
var prev_id := hovered_entity_id
|
||||
|
||||
@@ -115,7 +116,7 @@ func _find_nearest_entity() -> Dictionary:
|
||||
var mouse_world: Vector2 = xform.affine_inverse() * mouse_screen
|
||||
|
||||
var best_dist := INF
|
||||
var result := { id = -1, kind = "", color = COLOR_DEFAULT, offset = Vector2.ZERO }
|
||||
var result := {id = -1, kind = "", color = COLOR_DEFAULT, offset = Vector2.ZERO}
|
||||
|
||||
for entity in GameState.visible_entities:
|
||||
if not entity.has("entity_id") or not entity.has("x") or not entity.has("y"):
|
||||
@@ -138,6 +139,7 @@ func _find_nearest_entity() -> Dictionary:
|
||||
|
||||
# --- State transitions ---
|
||||
|
||||
|
||||
func _set_target(new_state: State) -> void:
|
||||
if new_state == _target:
|
||||
return
|
||||
@@ -154,29 +156,75 @@ func _set_target(new_state: State) -> void:
|
||||
|
||||
|
||||
func _snapshot() -> Dictionary:
|
||||
return { gap = _gap, len = _len, rot = _rot, thick = _thick,
|
||||
color = _color, bloom = _bloom, bracket_a = _bracket_a, alpha = _alpha }
|
||||
return {
|
||||
gap = _gap,
|
||||
len = _len,
|
||||
rot = _rot,
|
||||
thick = _thick,
|
||||
color = _color,
|
||||
bloom = _bloom,
|
||||
bracket_a = _bracket_a,
|
||||
alpha = _alpha
|
||||
}
|
||||
|
||||
|
||||
func _params_for(state: State) -> Dictionary:
|
||||
match state:
|
||||
State.ENTITY_HOVER:
|
||||
return { gap = 10.0, len = 6.0, rot = 0.0, thick = 1.0,
|
||||
color = _hover_color, bloom = 0.5, bracket_a = 1.0, alpha = 1.0 }
|
||||
return {
|
||||
gap = 10.0,
|
||||
len = 6.0,
|
||||
rot = 0.0,
|
||||
thick = 1.0,
|
||||
color = _hover_color,
|
||||
bloom = 0.5,
|
||||
bracket_a = 1.0,
|
||||
alpha = 1.0
|
||||
}
|
||||
State.OBJECT_HOVER:
|
||||
return { gap = 4.0, len = 6.0, rot = PI / 4.0, thick = 1.0,
|
||||
color = _hover_color, bloom = 0.3, bracket_a = 0.0, alpha = 0.8 }
|
||||
return {
|
||||
gap = 4.0,
|
||||
len = 6.0,
|
||||
rot = PI / 4.0,
|
||||
thick = 1.0,
|
||||
color = _hover_color,
|
||||
bloom = 0.3,
|
||||
bracket_a = 0.0,
|
||||
alpha = 0.8
|
||||
}
|
||||
State.WEAPON_AIM:
|
||||
return { gap = 12.0, len = 9.0, rot = 0.0, thick = 2.0,
|
||||
color = COLOR_WEAPON, bloom = 0.0, bracket_a = 0.0, alpha = 1.0 }
|
||||
return {
|
||||
gap = 12.0,
|
||||
len = 9.0,
|
||||
rot = 0.0,
|
||||
thick = 2.0,
|
||||
color = COLOR_WEAPON,
|
||||
bloom = 0.0,
|
||||
bracket_a = 0.0,
|
||||
alpha = 1.0
|
||||
}
|
||||
_:
|
||||
return { gap = 4.0, len = 6.0, rot = 0.0, thick = 1.0,
|
||||
color = COLOR_DEFAULT, bloom = 0.4, bracket_a = 0.0, alpha = 0.45 }
|
||||
return {
|
||||
gap = 4.0,
|
||||
len = 6.0,
|
||||
rot = 0.0,
|
||||
thick = 1.0,
|
||||
color = COLOR_DEFAULT,
|
||||
bloom = 0.4,
|
||||
bracket_a = 0.0,
|
||||
alpha = 0.45
|
||||
}
|
||||
|
||||
|
||||
func _apply_params(p: Dictionary) -> void:
|
||||
_gap = p.gap; _len = p.len; _rot = p.rot; _thick = p.thick
|
||||
_color = p.color; _bloom = p.bloom; _bracket_a = p.bracket_a; _alpha = p.alpha
|
||||
_gap = p.gap
|
||||
_len = p.len
|
||||
_rot = p.rot
|
||||
_thick = p.thick
|
||||
_color = p.color
|
||||
_bloom = p.bloom
|
||||
_bracket_a = p.bracket_a
|
||||
_alpha = p.alpha
|
||||
|
||||
|
||||
func _interpolate() -> void:
|
||||
@@ -193,6 +241,7 @@ func _interpolate() -> void:
|
||||
|
||||
# --- Drawing ---
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
# _hover_offset is computed in _detect_hover() during _process(), not recalculated
|
||||
# here. This is safe because Camera2D (tree sibling, earlier in scene order) applies
|
||||
@@ -233,13 +282,19 @@ func _draw_brackets(offset: Vector2, color: Color) -> void:
|
||||
|
||||
# --- Test-friendly API (expected by test_cursor_states.gd) ---
|
||||
|
||||
|
||||
func get_state() -> String:
|
||||
match current_state:
|
||||
State.DEFAULT: return "Default"
|
||||
State.ENTITY_HOVER: return "EntityHover"
|
||||
State.OBJECT_HOVER: return "ObjectHover"
|
||||
State.WEAPON_AIM: return "WeaponAim"
|
||||
_: return "Default"
|
||||
State.DEFAULT:
|
||||
return "Default"
|
||||
State.ENTITY_HOVER:
|
||||
return "EntityHover"
|
||||
State.OBJECT_HOVER:
|
||||
return "ObjectHover"
|
||||
State.WEAPON_AIM:
|
||||
return "WeaponAim"
|
||||
_:
|
||||
return "Default"
|
||||
|
||||
|
||||
func set_hover_target(data: Dictionary) -> void:
|
||||
@@ -252,10 +307,14 @@ func set_hover_target(data: Dictionary) -> void:
|
||||
if kind == "Npc":
|
||||
var rel: String = data.get("relationship", "Unknown")
|
||||
match rel:
|
||||
"Friendly": _hover_color = Constants.ENTITY_COLOR_FRIENDLY
|
||||
"PersonOfInterest": _hover_color = Constants.ENTITY_COLOR_POI
|
||||
"Hostile": _hover_color = Constants.ENTITY_COLOR_HOSTILE
|
||||
_: _hover_color = Constants.ENTITY_COLOR_UNKNOWN
|
||||
"Friendly":
|
||||
_hover_color = Constants.ENTITY_COLOR_FRIENDLY
|
||||
"PersonOfInterest":
|
||||
_hover_color = Constants.ENTITY_COLOR_POI
|
||||
"Hostile":
|
||||
_hover_color = Constants.ENTITY_COLOR_HOSTILE
|
||||
_:
|
||||
_hover_color = Constants.ENTITY_COLOR_UNKNOWN
|
||||
_target = State.ENTITY_HOVER
|
||||
_t = 1.0
|
||||
_apply_params(_params_for(State.ENTITY_HOVER))
|
||||
|
||||
@@ -18,7 +18,7 @@ const TILE_SIZE: int = Constants.TILE_SIZE
|
||||
# D-044: 24x32 entity footprint within 32x32 visual tile (64x64 source at 0.5 scale = 32px runtime)
|
||||
const ENTITY_WIDTH: int = 24
|
||||
const ENTITY_HEIGHT: int = 32
|
||||
const ENTITY_OFFSET_X: float = 0.0 # sprite fills tile width at 0.5 scale
|
||||
const ENTITY_OFFSET_X: float = 0.0 # sprite fills tile width at 0.5 scale
|
||||
const ENTITY_OFFSET_Y: float = TILE_SIZE - ENTITY_HEIGHT # feet-anchored for correct y-sort with D-019 tilt
|
||||
|
||||
# Lerp speed — framerate-independent exponential smoothing.
|
||||
@@ -28,12 +28,13 @@ const LERP_SPEED: float = 12.0
|
||||
# #521: Color transition duration in seconds (D-033: "0.5s fade")
|
||||
const COLOR_FADE_DURATION: float = 0.5
|
||||
|
||||
var entity_nodes: Dictionary = {} # entity_id -> Node2D
|
||||
var _entity_targets: Dictionary = {} # entity_id -> Vector2 (target pixel position)
|
||||
var entity_nodes: Dictionary = {} # entity_id -> Node2D
|
||||
var _entity_targets: Dictionary = {} # entity_id -> Vector2 (target pixel position)
|
||||
var _entity_relationships: Dictionary = {} # #521: entity_id -> String (last relationship)
|
||||
var _entity_tweens: Dictionary = {} # #521: entity_id -> {from: Color, target: Color, elapsed: float}
|
||||
var _entity_facing: Dictionary = {} # #540: entity_id -> String ("north"/"east"/"south"/"west")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
print("EntityRenderer: Initialized")
|
||||
|
||||
@@ -111,7 +112,11 @@ func _create_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
||||
var tex := _load_sprite_texture(direction)
|
||||
if tex == null:
|
||||
push_error(
|
||||
"EntityRenderer: no texture for entity %d direction '%s' — entity will be invisible" % [entity_id, direction])
|
||||
(
|
||||
"EntityRenderer: no texture for entity %d direction '%s' — entity will be invisible"
|
||||
% [entity_id, direction]
|
||||
)
|
||||
)
|
||||
entity_node.texture = tex
|
||||
|
||||
# D-033: self_modulate for relationship tinting; modulate.a is reserved for D-015 dimming.
|
||||
@@ -220,10 +225,14 @@ func _entity_direction(entity_id: int, _entity_data: Dictionary) -> String:
|
||||
# N/NW → north, NE/E → east, SE/S → south, SW/W → west
|
||||
static func _octant_to_direction(octant: String) -> String:
|
||||
match octant:
|
||||
"North", "Northwest": return "north"
|
||||
"Northeast", "East": return "east"
|
||||
"Southeast", "South": return "south"
|
||||
"Southwest", "West": return "west"
|
||||
"North", "Northwest":
|
||||
return "north"
|
||||
"Northeast", "East":
|
||||
return "east"
|
||||
"Southeast", "South":
|
||||
return "south"
|
||||
"Southwest", "West":
|
||||
return "west"
|
||||
_:
|
||||
push_warning("EntityRenderer: unrecognised octant '%s' — defaulting to south" % octant)
|
||||
return "south"
|
||||
@@ -247,11 +256,13 @@ func _add_facing_indicator(parent_node: Node2D) -> void:
|
||||
var s := Constants.FACING_INDICATOR_SIZE
|
||||
var offset := Constants.FACING_INDICATOR_OFFSET
|
||||
# Triangle pointing up (North), offset from center. Rotates around (0,0).
|
||||
indicator.polygon = PackedVector2Array([
|
||||
Vector2(0, -offset - s),
|
||||
Vector2(-s * 0.6, -offset + s * 0.4),
|
||||
Vector2(s * 0.6, -offset + s * 0.4),
|
||||
])
|
||||
indicator.polygon = PackedVector2Array(
|
||||
[
|
||||
Vector2(0, -offset - s),
|
||||
Vector2(-s * 0.6, -offset + s * 0.4),
|
||||
Vector2(s * 0.6, -offset + s * 0.4),
|
||||
]
|
||||
)
|
||||
indicator.color = Constants.ENTITY_COLOR_PLAYER
|
||||
# Sprite2D local space: 64px texture at scale 0.5 → center of visible sprite at (32,32).
|
||||
# Indicator rotates around this point to track player facing direction.
|
||||
|
||||
@@ -17,25 +17,25 @@ extends Node2D
|
||||
const TILE_SIZE: int = Constants.TILE_SIZE
|
||||
|
||||
# D-059: Fog entity colors
|
||||
const COLOR_UNRECOGNIZED := Color("#555566") # Neutral grey blob
|
||||
const COLOR_UNRECOGNIZED := Color("#555566") # Neutral grey blob
|
||||
# Intentionally matches Constants.INSERT_COLOR_TEXT / cursor default (#c8d0e0).
|
||||
# Sonar pings are insert-generated — same visual language as cursor and HUD overlays.
|
||||
const COLOR_PING := Color("#c8d0e0") # Insert white-blue (D-048/D-056)
|
||||
const COLOR_PING := Color("#c8d0e0") # Insert white-blue (D-048/D-056)
|
||||
|
||||
# Animation timing
|
||||
const PULSE_PERIOD: float = 0.8 # Breathing pulse cycle (seconds)
|
||||
const PULSE_MIN_ALPHA: float = 0.4 # Min alpha during breathing
|
||||
const PULSE_MAX_ALPHA: float = 0.8 # Max alpha during breathing
|
||||
const PING_DURATION: float = 1.5 # Sound ping expand + fade (seconds)
|
||||
const PING_MAX_RADIUS: float = 24.0 # Max ring expand radius (pixels)
|
||||
const PING_RING_COUNT: int = 3 # Concentric rings per ping
|
||||
const PING_RING_WIDTH: float = 1.5 # Ring line width (pixels)
|
||||
const DRIFT_RANGE: float = 0.5 # ±0.5 tile position drift (D-059)
|
||||
const DRIFT_PERIOD: float = 2.0 # Seconds per drift wander
|
||||
const PULSE_PERIOD: float = 0.8 # Breathing pulse cycle (seconds)
|
||||
const PULSE_MIN_ALPHA: float = 0.4 # Min alpha during breathing
|
||||
const PULSE_MAX_ALPHA: float = 0.8 # Max alpha during breathing
|
||||
const PING_DURATION: float = 1.5 # Sound ping expand + fade (seconds)
|
||||
const PING_MAX_RADIUS: float = 24.0 # Max ring expand radius (pixels)
|
||||
const PING_RING_COUNT: int = 3 # Concentric rings per ping
|
||||
const PING_RING_WIDTH: float = 1.5 # Ring line width (pixels)
|
||||
const DRIFT_RANGE: float = 0.5 # ±0.5 tile position drift (D-059)
|
||||
const DRIFT_PERIOD: float = 2.0 # Seconds per drift wander
|
||||
|
||||
# Entity blob rendering
|
||||
const BLOB_RADIUS: float = 10.0
|
||||
const SILHOUETTE_SCALE: float = 1.3 # Silhouette slightly larger than blob
|
||||
const SILHOUETTE_SCALE: float = 1.3 # Silhouette slightly larger than blob
|
||||
# Transition thresholds: recognition progress mapped from remaining_ticks/total_delay_ticks.
|
||||
# Color transition starts at 50% to compress the visual change into ~0.3s (D-060).
|
||||
const COLOR_TRANSITION_START: float = 0.5
|
||||
@@ -45,8 +45,8 @@ const SILHOUETTE_APPEAR_THRESHOLD: float = 0.3
|
||||
const SILHOUETTE_SIZE := Vector2(6.0, 10.0)
|
||||
|
||||
# Internal state — no child nodes, pure data + _draw()
|
||||
var _entities: Dictionary = {} # entity_id -> {pos, drift_offset, drift_target, drift_timer, progress}
|
||||
var _pings: Array = [] # [{pos: Vector2, elapsed: float}]
|
||||
var _entities: Dictionary = {} # entity_id -> {pos, drift_offset, drift_target, drift_timer, progress}
|
||||
var _pings: Array = [] # [{pos: Vector2, elapsed: float}]
|
||||
var _time: float = 0.0
|
||||
|
||||
|
||||
@@ -121,8 +121,7 @@ func update_from_state() -> void:
|
||||
func _draw() -> void:
|
||||
# Breathing pulse — shared across all entities
|
||||
var pulse_t := fmod(_time, PULSE_PERIOD) / PULSE_PERIOD
|
||||
var pulse_alpha := lerpf(PULSE_MIN_ALPHA, PULSE_MAX_ALPHA,
|
||||
0.5 + 0.5 * sin(pulse_t * TAU))
|
||||
var pulse_alpha := lerpf(PULSE_MIN_ALPHA, PULSE_MAX_ALPHA, 0.5 + 0.5 * sin(pulse_t * TAU))
|
||||
|
||||
# Draw entity blobs
|
||||
for eid in _entities.keys():
|
||||
@@ -132,7 +131,9 @@ func _draw() -> void:
|
||||
# Color transition: grey → D-033 teal based on recognition progress
|
||||
# Transition begins at 50% progress (D-060: ~0.3s visual transition within delay window)
|
||||
var progress: float = e.progress
|
||||
var color_t: float = clampf((progress - COLOR_TRANSITION_START) / (1.0 - COLOR_TRANSITION_START), 0.0, 1.0)
|
||||
var color_t: float = clampf(
|
||||
(progress - COLOR_TRANSITION_START) / (1.0 - COLOR_TRANSITION_START), 0.0, 1.0
|
||||
)
|
||||
var blob_color: Color = COLOR_UNRECOGNIZED.lerp(Constants.ENTITY_COLOR_UNKNOWN, color_t)
|
||||
blob_color.a = pulse_alpha
|
||||
|
||||
@@ -172,7 +173,4 @@ func _draw() -> void:
|
||||
|
||||
|
||||
func _random_drift() -> Vector2:
|
||||
return Vector2(
|
||||
randf_range(-DRIFT_RANGE, DRIFT_RANGE),
|
||||
randf_range(-DRIFT_RANGE, DRIFT_RANGE)
|
||||
)
|
||||
return Vector2(randf_range(-DRIFT_RANGE, DRIFT_RANGE), randf_range(-DRIFT_RANGE, DRIFT_RANGE))
|
||||
|
||||
@@ -41,7 +41,11 @@ func _ready() -> void:
|
||||
noise_tex.width = 256
|
||||
noise_tex.height = 256
|
||||
noise_tex.seamless = true
|
||||
noise_tex.changed.connect(func(): _noise_ready = true; fog_noise_ready.emit())
|
||||
noise_tex.changed.connect(
|
||||
func():
|
||||
_noise_ready = true
|
||||
fog_noise_ready.emit()
|
||||
)
|
||||
_shader_mat.set_shader_parameter("noise_tex", noise_tex)
|
||||
_shader_mat.set_shader_parameter("tile_size", TILE_SIZE)
|
||||
|
||||
@@ -74,6 +78,8 @@ func update_fog() -> void:
|
||||
_shader_mat.set_shader_parameter("rect_sz", _fog_rect.size)
|
||||
_shader_mat.set_shader_parameter("map_offset", Vector2(FogState.map_bounds.position))
|
||||
_shader_mat.set_shader_parameter("map_size", Vector2(FogState.map_bounds.size))
|
||||
var t: float = FogState.override_time if FogState.override_time >= 0.0 else Time.get_ticks_msec() / 1000.0
|
||||
var t: float = (
|
||||
FogState.override_time if FogState.override_time >= 0.0 else Time.get_ticks_msec() / 1000.0
|
||||
)
|
||||
_shader_mat.set_shader_parameter("time", t)
|
||||
_shader_mat.set_shader_parameter("debug_exploration", FogState.debug_exploration)
|
||||
|
||||
@@ -20,20 +20,21 @@ extends Node2D
|
||||
const TILE_SIZE: int = Constants.TILE_SIZE
|
||||
|
||||
# Visual parameters
|
||||
const INDICATOR_LIFETIME: float = 3.5 # Total seconds visible
|
||||
const FADE_DURATION: float = 0.6 # Fade-out window at end
|
||||
const EDGE_INSET: float = 20.0 # Pixels inward from viewport edge
|
||||
const ARROW_HALF: float = 7.0 # Half-width of arrowhead base
|
||||
const ARROW_LEN: float = 12.0 # Length from tip to base
|
||||
const INDICATOR_LIFETIME: float = 3.5 # Total seconds visible
|
||||
const FADE_DURATION: float = 0.6 # Fade-out window at end
|
||||
const EDGE_INSET: float = 20.0 # Pixels inward from viewport edge
|
||||
const ARROW_HALF: float = 7.0 # Half-width of arrowhead base
|
||||
const ARROW_LEN: float = 12.0 # Length from tip to base
|
||||
|
||||
# D-018/D-069 colors — sourced from Constants to prevent palette drift
|
||||
const COLOR_NEUTRAL: Color = Constants.INSERT_COLOR_TEXT # Generic / footstep
|
||||
const COLOR_VOICE: Color = Constants.ENTITY_COLOR_POI # Speech / conversation
|
||||
const COLOR_NEUTRAL: Color = Constants.INSERT_COLOR_TEXT # Generic / footstep
|
||||
const COLOR_VOICE: Color = Constants.ENTITY_COLOR_POI # Speech / conversation
|
||||
const COLOR_DANGER: Color = Constants.ENTITY_COLOR_HOSTILE # Alert / threat / gunshot
|
||||
|
||||
# Indicators: [{x, y, event_type, elapsed}]
|
||||
var _indicators: Array = []
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _indicators.is_empty():
|
||||
return
|
||||
@@ -68,12 +69,17 @@ func update_sound_events(events: Array) -> void:
|
||||
found = true
|
||||
break
|
||||
if not found:
|
||||
_indicators.append({
|
||||
"x": ex,
|
||||
"y": ey,
|
||||
"event_type": evt.get("event_type", ""),
|
||||
"elapsed": 0.0,
|
||||
})
|
||||
(
|
||||
_indicators
|
||||
. append(
|
||||
{
|
||||
"x": ex,
|
||||
"y": ey,
|
||||
"event_type": evt.get("event_type", ""),
|
||||
"elapsed": 0.0,
|
||||
}
|
||||
)
|
||||
)
|
||||
if not _indicators.is_empty():
|
||||
queue_redraw()
|
||||
|
||||
@@ -144,9 +150,19 @@ func _draw_arrow(pos: Vector2, dir: Vector2, color: Color) -> void:
|
||||
## Map event type string → D-018 color category.
|
||||
func color_for_type(event_type: String) -> Color:
|
||||
var et := event_type.to_lower()
|
||||
if et.contains("voice") or et.contains("speech") or et.contains("convers") or et.contains("talk"):
|
||||
if (
|
||||
et.contains("voice")
|
||||
or et.contains("speech")
|
||||
or et.contains("convers")
|
||||
or et.contains("talk")
|
||||
):
|
||||
return COLOR_VOICE
|
||||
if et.contains("danger") or et.contains("gunshot") or et.contains("explosion") \
|
||||
or et.contains("alert") or et.contains("threat"):
|
||||
if (
|
||||
et.contains("danger")
|
||||
or et.contains("gunshot")
|
||||
or et.contains("explosion")
|
||||
or et.contains("alert")
|
||||
or et.contains("threat")
|
||||
):
|
||||
return COLOR_DANGER
|
||||
return COLOR_NEUTRAL
|
||||
|
||||
@@ -27,11 +27,13 @@ const TILE_TYPE_MAP: Dictionary = {
|
||||
|
||||
var _initialized: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_setup_tileset()
|
||||
_initialized = true
|
||||
print("TileRenderer: Initialized")
|
||||
|
||||
|
||||
# Build a programmatic TileSet with colored placeholder tiles
|
||||
func _setup_tileset() -> void:
|
||||
var ts := TileSet.new()
|
||||
@@ -63,6 +65,7 @@ func _setup_tileset() -> void:
|
||||
var source_id := ts.add_source(source)
|
||||
tile_set = ts
|
||||
|
||||
|
||||
# Update tiles from snapshot data
|
||||
# tiles: Array of {x: int, y: int, z: int, type: String, visibility: String (optional)}
|
||||
# z here is the server-side FLOOR LEVEL (0 = ground, 1 = first floor, etc.),
|
||||
@@ -93,19 +96,25 @@ func update_tiles(tiles: Array) -> void:
|
||||
|
||||
var tile_type_str: String = tile_data.type
|
||||
if not TILE_TYPE_MAP.has(tile_type_str):
|
||||
push_warning("TileRenderer: unknown tile type '%s' at (%d, %d)" % [
|
||||
tile_type_str, tile_data.x, tile_data.y])
|
||||
push_warning(
|
||||
(
|
||||
"TileRenderer: unknown tile type '%s' at (%d, %d)"
|
||||
% [tile_type_str, tile_data.x, tile_data.y]
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
var atlas_x: int = TILE_TYPE_MAP[tile_type_str]
|
||||
var coords := Vector2i(tile_data.x, tile_data.y)
|
||||
set_cell(coords, 0, Vector2i(atlas_x, 0))
|
||||
|
||||
|
||||
# Fill a tile region with a solid color
|
||||
func _fill_tile(img: Image, tile_index: int, color: Color) -> void:
|
||||
var rect := Rect2i(tile_index * TILE_SIZE, 0, TILE_SIZE, TILE_SIZE)
|
||||
img.fill_rect(rect, color)
|
||||
|
||||
|
||||
# Fill a tile region with a color and a 1px border
|
||||
func _fill_tile_with_border(img: Image, tile_index: int, fill: Color, border: Color) -> void:
|
||||
var x_offset := tile_index * TILE_SIZE
|
||||
|
||||
@@ -20,9 +20,11 @@ var _last_tick: int = -1
|
||||
@onready var entity_renderer = $FogGroup/YSortGroup/Entities
|
||||
@onready var sound_indicator_renderer = $SoundIndicators # #126 D-018 medium-range indicators
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
print("WorldRenderer: Initialized (D-049 z-stack)")
|
||||
|
||||
|
||||
# Called each frame to update visuals from game state.
|
||||
# Uses tick-based invalidation — re-renders all layers when a new snapshot arrives.
|
||||
func update_from_state() -> void:
|
||||
|
||||
Reference in New Issue
Block a user