Detailed graphics pass: authored textures, humanoid NPCs, HUD feedback, asset pipeline
- tools/generate_textures.py (deterministic PIL) produces all authored PNGs: brick facade, concrete, asphalt, awning, skyline, vignette, HUD icons, access-card art (all <=1024px, power-of-two) - WorldBuilder: textured facade/glass door/sidewalk/asphalt, striped awning with 'STEP LINE', generated skyline backdrop planes - GameManager: soft sun shadows (4-split parallel); guard in 'guard' group - NPC avatars: humanoid rig (torso/head/limbs) with archetype props (camera, delivery box, performer hat, protest sign, tourist backpack), procedural walk bob, head-turns-toward-guard attention cue - HUD: inspection artifact cards, mode icons (flashlight/door/wand), per-escalation vignette, body-camera overlay (corner brackets + REC + clock) - assets/ import convention documented; test_asset_textures_present_and_pow2 guards the pipeline (57 tests pass) - README tech stack updated for Gitea Actions + asset pipeline; screenshot gallery regenerated (day + night)
This commit is contained in:
1 parent
ac3bb7dcd8
commit
031503e161
31 files changed
+636
-57
No files matched your search
+157
-4
@@ -5,6 +5,23 @@ extends CanvasLayer
|
||||
|
||||
const Rules := preload("res://src/core/rules.gd")
|
||||
const Strings := preload("res://src/core/strings.gd")
|
||||
const T_VIGNETTE := preload("res://assets/textures/vignette.png")
|
||||
const T_ICON_FLASH := preload("res://assets/textures/icons/flashlight_on.png")
|
||||
const T_ICON_DOOR := preload("res://assets/textures/icons/door_locked.png")
|
||||
const T_ICON_WAND := preload("res://assets/textures/icons/wand.png")
|
||||
const T_ICON_REC := preload("res://assets/textures/icons/rec_dot.png")
|
||||
const T_CARD_ID := preload("res://assets/textures/cards/id_card.png")
|
||||
const T_CARD_WRIST := preload("res://assets/textures/cards/wristband.png")
|
||||
const T_CARD_TICKET := preload("res://assets/textures/cards/ticket.png")
|
||||
const T_CARD_CRED := preload("res://assets/textures/cards/credential.png")
|
||||
const T_CARD_GUEST := preload("res://assets/textures/cards/guest_list.png")
|
||||
const CARD_TEXTURES := {
|
||||
Rules.AccessKind.ID: T_CARD_ID,
|
||||
Rules.AccessKind.WRISTBAND: T_CARD_WRIST,
|
||||
Rules.AccessKind.TICKET: T_CARD_TICKET,
|
||||
Rules.AccessKind.CREDENTIAL: T_CARD_CRED,
|
||||
Rules.AccessKind.GUEST_LIST: T_CARD_GUEST,
|
||||
}
|
||||
|
||||
var top_label: Label
|
||||
var time_label: Label
|
||||
@@ -17,6 +34,12 @@ var inspect_text: Label
|
||||
var inspect_meta: Label
|
||||
var radio_cooldown_label: Label
|
||||
var mode_label: Label
|
||||
var vignette_rect: TextureRect
|
||||
var cam_clock: Label
|
||||
var icon_row: HBoxContainer
|
||||
var flashlight_icon: TextureRect
|
||||
var door_icon: TextureRect
|
||||
var wand_icon: TextureRect
|
||||
var wand_panel: PanelContainer
|
||||
var wand_text: Label
|
||||
var wand_meta: Label
|
||||
@@ -105,6 +128,57 @@ func _init() -> void:
|
||||
escalation_label.modulate = Color(0.6, 0.9, 0.6)
|
||||
vbox.add_child(escalation_label)
|
||||
|
||||
# --- escalation vignette (screen-space tint) ---
|
||||
vignette_rect = TextureRect.new()
|
||||
vignette_rect.texture = T_VIGNETTE
|
||||
vignette_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
vignette_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
vignette_rect.modulate = Color(1, 1, 1, 0)
|
||||
vignette_rect.show_behind_parent = false
|
||||
add_child(vignette_rect)
|
||||
|
||||
# --- body-camera overlay (REC + shift clock + corner brackets, top right) ---
|
||||
var cam_box := VBoxContainer.new()
|
||||
cam_box.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
cam_box.position = Vector2(-180, 12)
|
||||
cam_box.alignment = BoxContainer.ALIGNMENT_END
|
||||
add_child(cam_box)
|
||||
var rec_row := HBoxContainer.new()
|
||||
rec_row.alignment = BoxContainer.ALIGNMENT_END
|
||||
cam_box.add_child(rec_row)
|
||||
var rec_icon := TextureRect.new()
|
||||
rec_icon.texture = T_ICON_REC
|
||||
rec_icon.custom_minimum_size = Vector2(22, 22)
|
||||
rec_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
rec_row.add_child(rec_icon)
|
||||
var rec_label := Label.new()
|
||||
rec_label.text = "REC"
|
||||
rec_label.add_theme_font_size_override("font_size", 15)
|
||||
rec_label.modulate = Color(0.95, 0.35, 0.35)
|
||||
rec_row.add_child(rec_label)
|
||||
cam_clock = Label.new()
|
||||
cam_clock.text = "SHIFT 00:00:00"
|
||||
cam_clock.add_theme_font_size_override("font_size", 12)
|
||||
cam_clock.modulate = Color(0.95, 0.95, 0.95)
|
||||
cam_clock.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
cam_box.add_child(cam_clock)
|
||||
# corner brackets
|
||||
var bracket_color := Color(0.9, 0.9, 0.92, 0.55)
|
||||
_add_corner_brackets(bracket_color)
|
||||
|
||||
# --- mode icons next to the door/light label ---
|
||||
icon_row = HBoxContainer.new()
|
||||
vbox.add_child(icon_row)
|
||||
flashlight_icon = _make_icon(T_ICON_FLASH)
|
||||
flashlight_icon.visible = false
|
||||
icon_row.add_child(flashlight_icon)
|
||||
door_icon = _make_icon(T_ICON_DOOR)
|
||||
door_icon.visible = false
|
||||
icon_row.add_child(door_icon)
|
||||
wand_icon = _make_icon(T_ICON_WAND)
|
||||
wand_icon.visible = false
|
||||
icon_row.add_child(wand_icon)
|
||||
|
||||
# --- inspect panel (popup center) ---
|
||||
inspect_panel = PanelContainer.new()
|
||||
inspect_panel.visible = false
|
||||
@@ -163,6 +237,9 @@ func set_shift_state(coordinator: Object, scenario: Resource) -> void:
|
||||
coordinator.reputation.rating,
|
||||
coordinator.crowd.mood * 100.0,
|
||||
]
|
||||
if cam_clock:
|
||||
var total := int(coordinator.elapsed_s)
|
||||
cam_clock.text = "SHIFT %02d:%02d:%02d" % [total / 3600, (total / 60) % 60, total % 60]
|
||||
_update_radio_cooldown(coordinator)
|
||||
|
||||
func _update_radio_cooldown(coordinator: Object) -> void:
|
||||
@@ -188,6 +265,47 @@ func set_flashlight(on: bool) -> void:
|
||||
|
||||
func _refresh_mode_label() -> void:
|
||||
mode_label.text = "Door: %s · Light: %s" % ["LOCKED" if _door_locked else "open", "on" if _flashlight else "off"]
|
||||
if icon_row:
|
||||
flashlight_icon.visible = _flashlight
|
||||
door_icon.visible = _door_locked
|
||||
|
||||
func set_wand_icon(visible: bool) -> void:
|
||||
if wand_icon:
|
||||
wand_icon.visible = visible
|
||||
|
||||
func _make_icon(texture: Texture2D) -> TextureRect:
|
||||
var tr := TextureRect.new()
|
||||
tr.texture = texture
|
||||
tr.custom_minimum_size = Vector2(20, 20)
|
||||
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
tr.modulate = Color(1, 1, 1, 0.9)
|
||||
return tr
|
||||
|
||||
func _add_corner_brackets(color: Color) -> void:
|
||||
const LEN := 34.0
|
||||
const TH := 3.0
|
||||
const OFF := 10.0
|
||||
var viewport := get_viewport()
|
||||
var vp: Vector2 = viewport.get_visible_rect().size if viewport else Vector2(1280, 720)
|
||||
var corners := [
|
||||
Vector2(OFF, OFF), Vector2(vp.x - OFF, OFF), Vector2(OFF, vp.y - OFF), Vector2(vp.x - OFF, vp.y - OFF),
|
||||
]
|
||||
for i in 4:
|
||||
var c: Vector2 = corners[i]
|
||||
var flip_x: bool = (i % 2) == 1
|
||||
var flip_y: bool = i >= 2
|
||||
var h := ColorRect.new()
|
||||
h.color = color
|
||||
h.size = Vector2(LEN, TH)
|
||||
h.position = Vector2(c.x - (LEN if flip_x else 0.0), c.y - (TH if flip_y else 0.0))
|
||||
h.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(h)
|
||||
var v := ColorRect.new()
|
||||
v.color = color
|
||||
v.size = Vector2(TH, LEN)
|
||||
v.position = Vector2(c.x - (TH if flip_x else 0.0), c.y - (LEN if flip_y else 0.0))
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(v)
|
||||
|
||||
func set_action_hint(text: String) -> void:
|
||||
hint_label.text = text
|
||||
@@ -199,15 +317,20 @@ func set_action_hint(text: String) -> void:
|
||||
func set_escalation(level: int) -> void:
|
||||
var name: String = Rules.LEVEL_NAMES.get(level, "Observation")
|
||||
escalation_label.text = Strings.fmt("hud.escalation_prefix", [name])
|
||||
var tint := Color(0.6, 0.9, 0.6)
|
||||
match level:
|
||||
Rules.Level.OBSERVATION, Rules.Level.SIMPLE_DETERRENCE:
|
||||
escalation_label.modulate = Color(0.6, 0.9, 0.6)
|
||||
tint = Color(0.6, 0.9, 0.6)
|
||||
Rules.Level.FORMAL_WARNING:
|
||||
escalation_label.modulate = Color(0.95, 0.9, 0.4)
|
||||
tint = Color(0.95, 0.9, 0.4)
|
||||
Rules.Level.BLOCKING:
|
||||
escalation_label.modulate = Color(1.0, 0.65, 0.3)
|
||||
tint = Color(1.0, 0.65, 0.3)
|
||||
Rules.Level.RADIO_ASSIST, Rules.Level.EMERGENCY:
|
||||
escalation_label.modulate = Color(1.0, 0.35, 0.3)
|
||||
tint = Color(1.0, 0.35, 0.3)
|
||||
escalation_label.modulate = tint
|
||||
# Screen-space vignette follows the escalation color.
|
||||
if vignette_rect:
|
||||
vignette_rect.modulate = Color(tint.r, tint.g, tint.b, 0.05 + 0.2 * float(level) / 5.0)
|
||||
|
||||
func log(text: String) -> void:
|
||||
_log_lines.append(text)
|
||||
@@ -240,6 +363,26 @@ func show_inspection(controller: Object, artifacts: Array, result: Dictionary) -
|
||||
var npc_display: String = (profile.display_name if profile else "Person")
|
||||
var granted: bool = result.get("granted", false)
|
||||
inspect_text.text = "Access check — %s\nGranted: %s" % [npc_display, "YES" if granted else "NO"]
|
||||
_clear_card_row()
|
||||
var card_row := HBoxContainer.new()
|
||||
card_row.name = "INSPECT_CARD_ROW"
|
||||
card_row.add_theme_constant_override("separation", 6)
|
||||
var added_card := false
|
||||
for artifact in artifacts:
|
||||
var tex: Texture2D = CARD_TEXTURES.get(artifact.kind, null) as Texture2D
|
||||
if tex == null:
|
||||
continue
|
||||
var card := TextureRect.new()
|
||||
card.texture = tex
|
||||
card.custom_minimum_size = Vector2(92, 58)
|
||||
card.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
card.tooltip_text = artifact.summary()
|
||||
if artifact.attributes.size() > 0:
|
||||
card.tooltip_text += "\n" + str(artifact.attributes)
|
||||
card_row.add_child(card)
|
||||
added_card = true
|
||||
if added_card:
|
||||
inspect_panel.add_child(card_row)
|
||||
var lines: Array[String] = []
|
||||
for artifact in artifacts:
|
||||
lines.append(" • " + artifact.summary())
|
||||
@@ -251,8 +394,18 @@ func show_inspection(controller: Object, artifacts: Array, result: Dictionary) -
|
||||
inspect_panel.visible = true
|
||||
|
||||
func hide_inspection() -> void:
|
||||
_clear_card_row()
|
||||
inspect_panel.visible = false
|
||||
|
||||
## Remove previously added artifact cards so reopening rebuilds them.
|
||||
func _clear_card_row() -> void:
|
||||
if inspect_panel == null:
|
||||
return
|
||||
var row := inspect_panel.find_child("INSPECT_CARD_ROW", true, false) as Control
|
||||
if row:
|
||||
inspect_panel.remove_child(row)
|
||||
row.queue_free()
|
||||
|
||||
func show_wand(controller: Object, result: Dictionary) -> void:
|
||||
var profile: Object = controller.profile
|
||||
var npc_display: String = (profile.display_name if profile else "Person")
|
||||
|
||||
Reference in new issue
Block a user