NEO-99: wire district GLB overlay and migrate tab targets to PrototypeNpcRegistry.
Add visual catalog/art scripts, instance environment art in main, spawn GLB interactable and NPC prop meshes at server anchors, and replace legacy prototype_target_* ids with prototype_npc_elite/melee/ranged for Tab lock.pull/134/head
parent
e19ecdb390
commit
fa3d48a6aa
|
|
@ -20,6 +20,8 @@
|
|||
[ext_resource type="Script" path="res://scripts/craft_recipe_panel.gd" id="19_craft_panel"]
|
||||
[ext_resource type="Script" path="res://scripts/prototype_economy_hud_section.gd" id="20_economy_hud"]
|
||||
[ext_resource type="Script" path="res://scripts/gig_progression_client.gd" id="21_gig_prog"]
|
||||
[ext_resource type="PackedScene" path="res://assets/district/prototype_district_environment.glb" id="22_district_env"]
|
||||
[ext_resource type="Script" path="res://scripts/prototype_district_art.gd" id="23_district_art"]
|
||||
|
||||
[sub_resource type="NavigationMesh" id="NavigationMesh_district"]
|
||||
geometry_collision_mask = 1
|
||||
|
|
@ -265,6 +267,9 @@ shape = SubResource("BoxShape3D_floor")
|
|||
|
||||
[node name="InteractablesRoot" type="Node3D" parent="World/NavigationRegion3D" unique_id=1700011]
|
||||
|
||||
[node name="DistrictArt" parent="World/NavigationRegion3D" instance=ExtResource("22_district_env")]
|
||||
script = ExtResource("23_district_art")
|
||||
|
||||
[node name="MoveRejectPedestal" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=1900021 groups=["walkable"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.5, 0, -6.5)
|
||||
collision_mask = 3
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ extends Object
|
|||
## NEO-25: spawns walkable interactable props + glow marker pairs from server descriptor dicts.
|
||||
## Returns glow groups for [method InteractionRadiusIndicators.setup_glow_groups].
|
||||
|
||||
const PrototypeDistrictVisualCatalog := preload(
|
||||
"res://scripts/prototype_district_visual_catalog.gd"
|
||||
)
|
||||
|
||||
const _PROP_SIZE := Vector3(0.9, 1.0, 0.4)
|
||||
const _MARKER_MESH_SIZE := Vector3(0.22, 0.35, 0.22)
|
||||
const _MARKER_OFFSET_A := Vector3(1.1, 0.25, 0.9)
|
||||
|
|
@ -35,18 +39,7 @@ static func build_from_catalog(
|
|||
interactables_root.add_child(body)
|
||||
body.global_position = anchor
|
||||
|
||||
var prop_mesh := MeshInstance3D.new()
|
||||
var box_mesh := BoxMesh.new()
|
||||
box_mesh.size = _PROP_SIZE
|
||||
prop_mesh.mesh = box_mesh
|
||||
prop_mesh.position = Vector3.ZERO
|
||||
var prop_mat := StandardMaterial3D.new()
|
||||
if kind == "resource_node":
|
||||
prop_mat.albedo_color = Color(0.18, 0.55, 0.32, 1.0)
|
||||
else:
|
||||
prop_mat.albedo_color = Color(0.28, 0.38, 0.45, 1.0)
|
||||
prop_mesh.material_override = prop_mat
|
||||
body.add_child(prop_mesh)
|
||||
_add_prop_visual(body, id, kind, anchor.y)
|
||||
|
||||
var col := CollisionShape3D.new()
|
||||
var box_shape := BoxShape3D.new()
|
||||
|
|
@ -99,3 +92,29 @@ static func _make_marker_mesh() -> MeshInstance3D:
|
|||
m.size = _MARKER_MESH_SIZE
|
||||
mi.mesh = m
|
||||
return mi
|
||||
|
||||
|
||||
static func _add_prop_visual(
|
||||
body: StaticBody3D, interactable_id: String, kind: String, anchor_y: float
|
||||
) -> void:
|
||||
var packed: Variant = PrototypeDistrictVisualCatalog.INTERACTABLE_PROPS.get(interactable_id, null)
|
||||
if packed is PackedScene:
|
||||
var visual: Node3D = PrototypeDistrictVisualCatalog.instantiate_prop(
|
||||
packed as PackedScene, anchor_y
|
||||
)
|
||||
visual.name = "PropVisual"
|
||||
body.add_child(visual)
|
||||
return
|
||||
|
||||
var prop_mesh := MeshInstance3D.new()
|
||||
var box_mesh := BoxMesh.new()
|
||||
box_mesh.size = _PROP_SIZE
|
||||
prop_mesh.mesh = box_mesh
|
||||
prop_mesh.position = Vector3.ZERO
|
||||
var prop_mat := StandardMaterial3D.new()
|
||||
if kind == "resource_node":
|
||||
prop_mat.albedo_color = Color(0.18, 0.55, 0.32, 1.0)
|
||||
else:
|
||||
prop_mat.albedo_color = Color(0.28, 0.38, 0.45, 1.0)
|
||||
prop_mesh.material_override = prop_mat
|
||||
body.add_child(prop_mesh)
|
||||
|
|
|
|||
|
|
@ -970,7 +970,7 @@ func _render_combat_target_hp_label() -> void:
|
|||
return
|
||||
var lock_id := _locked_target_id_from_state(_last_target_state)
|
||||
if lock_id.is_empty():
|
||||
_combat_target_hp_label.text = ("Target HP: — (Tab → orange/purple dummy near spawn)")
|
||||
_combat_target_hp_label.text = ("Target HP: — (Tab → elite/melee/ranged near spawn)")
|
||||
return
|
||||
var row: Dictionary = {}
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
extends Node3D
|
||||
|
||||
## Visual overlay for the prototype district (`prototype_district_environment.glb`).
|
||||
## Hides placeholder box meshes under the navigation region while keeping collision shapes.
|
||||
## Blender export mirrored +Z relative to `main.tscn`; flip art so visuals line up with colliders.
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
scale = Vector3(1.0, 1.0, -1.0)
|
||||
var nav: NavigationRegion3D = get_parent() as NavigationRegion3D
|
||||
if nav == null:
|
||||
return
|
||||
_hide_placeholder_meshes(nav)
|
||||
|
||||
|
||||
static func _hide_placeholder_meshes(nav: NavigationRegion3D) -> void:
|
||||
for child in nav.get_children():
|
||||
if child is Node3D and child.name == "DistrictArt":
|
||||
continue
|
||||
if child.name == "InteractablesRoot":
|
||||
continue
|
||||
_hide_mesh_instances(child)
|
||||
|
||||
|
||||
static func _hide_mesh_instances(node: Node) -> void:
|
||||
if node is MeshInstance3D:
|
||||
(node as MeshInstance3D).visible = false
|
||||
for child in node.get_children():
|
||||
_hide_mesh_instances(child)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://brx4j9m2n8k6v
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
extends Object
|
||||
|
||||
## Blender-exported prototype district art (visual-only). Collision/nav stay on `main.tscn` boxes.
|
||||
## Prop GLBs import as a wrapper root plus one child mesh node that still carries baked
|
||||
## world transforms from Blender — see [method normalize_prop_visual].
|
||||
|
||||
const ENVIRONMENT: PackedScene = preload("res://assets/district/prototype_district_environment.glb")
|
||||
|
||||
const INTERACTABLE_PROPS: Dictionary = {
|
||||
"prototype_terminal": preload("res://assets/district/props/terminal.glb"),
|
||||
"prototype_resource_node_alpha": preload("res://assets/district/props/salvage_alpha.glb"),
|
||||
"prototype_subsurface_vein_beta": preload("res://assets/district/props/subsurface_beta.glb"),
|
||||
"prototype_bio_mat_gamma": preload("res://assets/district/props/bio_gamma.glb"),
|
||||
"prototype_urban_bulk_delta": preload("res://assets/district/props/urban_delta.glb"),
|
||||
}
|
||||
|
||||
const TARGET_PROPS: Dictionary = {
|
||||
"prototype_npc_melee": preload("res://assets/district/props/target_alpha.glb"),
|
||||
"prototype_npc_ranged": preload("res://assets/district/props/target_beta.glb"),
|
||||
"prototype_npc_elite": preload("res://assets/district/props/target_alpha.glb"),
|
||||
}
|
||||
|
||||
|
||||
static func instantiate_prop(packed: PackedScene, anchor_y: float = 0.5) -> Node3D:
|
||||
var visual: Node3D = packed.instantiate() as Node3D
|
||||
normalize_prop_visual(visual, anchor_y)
|
||||
return visual
|
||||
|
||||
|
||||
static func normalize_prop_visual(visual: Node3D, anchor_y: float) -> void:
|
||||
_zero_node3d_tree(visual)
|
||||
var meshes: Array = _collect_mesh_instances(visual)
|
||||
if meshes.is_empty():
|
||||
visual.position = Vector3.ZERO
|
||||
return
|
||||
var min_y: float = INF
|
||||
for mesh_variant: Variant in meshes:
|
||||
var mesh: MeshInstance3D = mesh_variant as MeshInstance3D
|
||||
min_y = min(min_y, mesh.get_aabb().position.y)
|
||||
if min_y == INF:
|
||||
visual.position = Vector3.ZERO
|
||||
return
|
||||
visual.position = Vector3(0.0, -anchor_y - min_y, 0.0)
|
||||
|
||||
|
||||
static func _zero_node3d_tree(node: Node3D) -> void:
|
||||
node.position = Vector3.ZERO
|
||||
node.rotation = Vector3.ZERO
|
||||
node.scale = Vector3.ONE
|
||||
for child in node.get_children():
|
||||
if child is Node3D:
|
||||
_zero_node3d_tree(child as Node3D)
|
||||
|
||||
|
||||
static func _collect_mesh_instances(node: Node) -> Array:
|
||||
var meshes: Array = []
|
||||
if node is MeshInstance3D:
|
||||
meshes.append(node)
|
||||
for child in node.get_children():
|
||||
meshes.append_array(_collect_mesh_instances(child))
|
||||
return meshes
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dqw8h2k5m3n7p
|
||||
|
|
@ -1,33 +1,41 @@
|
|||
extends Object
|
||||
|
||||
## NEO-24: prototype combat target ids in **tab cycle order** — must match
|
||||
## `server/NeonSprawl.Server/Game/Targeting/PrototypeTargetRegistry.cs`
|
||||
## (`PrototypeTargetAlphaId`, `PrototypeTargetBetaId`; ascending id order).
|
||||
## Anchors + radii are a **display-only** mirror of the C# registry so the scene markers
|
||||
## NEO-24 / NEO-91: prototype combat NPC ids in **tab cycle order** — must match
|
||||
## `server/NeonSprawl.Server/Game/Npc/PrototypeNpcRegistry.cs`
|
||||
## (`GetInstanceIdsInOrder()` ascending id order).
|
||||
## Anchors + radii are a **display-only** mirror of the C# registry so scene markers
|
||||
## (`prototype_target_markers.gd`) and HUD distance readouts match server-authoritative
|
||||
## range decisions. If the server registry moves, update this file in the same commit —
|
||||
## the two must stay in lock-step to avoid confusing "ok at 10 m / denied at 5 m" bugs.
|
||||
## Static accessors only (no `class_name`; headless / CI loads before editor import — see client
|
||||
## README Godot CLI section).
|
||||
|
||||
const ORDERED_IDS: Array[String] = ["prototype_target_alpha", "prototype_target_beta"]
|
||||
const ORDERED_IDS: Array[String] = [
|
||||
"prototype_npc_elite",
|
||||
"prototype_npc_melee",
|
||||
"prototype_npc_ranged",
|
||||
]
|
||||
|
||||
## All prototype targets share a single radius (`PrototypeTargetRegistry.SharedLockRadius`
|
||||
## on the server). Per-target radii will come back with real combat design (E5.M1).
|
||||
## All prototype NPCs share a single radius (`PrototypeNpcRegistry.SharedLockRadius`
|
||||
## on the server). Per-instance radii will come back with real combat design (E5.M1).
|
||||
const SHARED_LOCK_RADIUS: float = 6.0
|
||||
|
||||
## Keys match [constant ORDERED_IDS]; values mirror `PrototypeTargetRegistry.cs` anchors
|
||||
## (world space) and horizontal lock radii (XZ meters). Anchors are placed so the two rings
|
||||
## overlap at origin (~3.5 m wide), letting Tab flip between targets without locomotion.
|
||||
## Keep in lock-step with the C# registry.
|
||||
## Keys match [constant ORDERED_IDS]; values mirror `PrototypeNpcRegistry.cs` anchors
|
||||
## (world space) and horizontal lock radii (XZ meters).
|
||||
const ANCHORS: Dictionary = {
|
||||
"prototype_target_alpha":
|
||||
"prototype_npc_elite":
|
||||
{
|
||||
"position": Vector3(0.0, 0.5, 0.0),
|
||||
"radius": SHARED_LOCK_RADIUS,
|
||||
"color": Color(1.0, 0.85, 0.2, 1.0),
|
||||
},
|
||||
"prototype_npc_melee":
|
||||
{
|
||||
"position": Vector3(-3.0, 0.5, -3.0),
|
||||
"radius": SHARED_LOCK_RADIUS,
|
||||
"color": Color(1.0, 0.55, 0.2, 1.0),
|
||||
},
|
||||
"prototype_target_beta":
|
||||
"prototype_npc_ranged":
|
||||
{
|
||||
"position": Vector3(3.0, 0.0, 3.0),
|
||||
"radius": SHARED_LOCK_RADIUS,
|
||||
|
|
@ -118,10 +126,12 @@ static func next_in_range_id_after(current: Variant, world: Vector3) -> String:
|
|||
## Player-facing short label for world [Label3D] markers (NEO-85 combat UX).
|
||||
static func display_short_name(target_id: String) -> String:
|
||||
match target_id:
|
||||
"prototype_target_alpha":
|
||||
return "Dummy α"
|
||||
"prototype_target_beta":
|
||||
return "Dummy β"
|
||||
"prototype_npc_elite":
|
||||
return "Elite"
|
||||
"prototype_npc_melee":
|
||||
return "Melee"
|
||||
"prototype_npc_ranged":
|
||||
return "Ranged"
|
||||
_:
|
||||
return target_id.strip_edges()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
extends Node3D
|
||||
|
||||
## NEO-24 UX: combat dummy anchors (`prototype_target_alpha`, `prototype_target_beta`) need
|
||||
## obvious world markers — a capsule body, lock ring, and [Label3D] name per
|
||||
## `PrototypeTargetConstants.ANCHORS`. NEO-85: highlight the server-locked dummy via
|
||||
## NEO-24 / NEO-91 UX: prototype NPC anchors need obvious world markers — imported
|
||||
## prop mesh (or capsule fallback), lock ring, and [Label3D] name per
|
||||
## `PrototypeTargetConstants.ANCHORS`. NEO-85: highlight the server-locked NPC via
|
||||
## [method set_locked_target].
|
||||
##
|
||||
## Display-only; no game logic hangs off these nodes. If the server registry moves,
|
||||
## update `prototype_target_constants.gd` — markers respawn on next scene load.
|
||||
|
||||
const PrototypeTargetConstants := preload("res://scripts/prototype_target_constants.gd")
|
||||
const PrototypeDistrictVisualCatalog := preload(
|
||||
"res://scripts/prototype_district_visual_catalog.gd"
|
||||
)
|
||||
|
||||
## Visual tuning — readable from the default isometric camera at spawn.
|
||||
const _DUMMY_CAPSULE_HEIGHT: float = 1.75
|
||||
|
|
@ -46,17 +49,29 @@ func _spawn_marker(target_id: String) -> void:
|
|||
group.position = anchor
|
||||
add_child(group)
|
||||
|
||||
var body := MeshInstance3D.new()
|
||||
var body := Node3D.new()
|
||||
body.name = "DummyBody"
|
||||
var capsule := CapsuleMesh.new()
|
||||
capsule.radius = _DUMMY_CAPSULE_RADIUS
|
||||
capsule.height = _DUMMY_CAPSULE_HEIGHT
|
||||
body.mesh = capsule
|
||||
body.position = Vector3(0.0, _DUMMY_CAPSULE_HEIGHT * 0.5, 0.0)
|
||||
var body_mat := _make_unshaded_material(tint, 0.88)
|
||||
body.material_override = body_mat
|
||||
group.add_child(body)
|
||||
|
||||
var body_mat: StandardMaterial3D = null
|
||||
var packed: Variant = PrototypeDistrictVisualCatalog.TARGET_PROPS.get(target_id, null)
|
||||
if packed is PackedScene:
|
||||
var visual: Node3D = PrototypeDistrictVisualCatalog.instantiate_prop(
|
||||
packed as PackedScene, anchor.y
|
||||
)
|
||||
visual.name = "PropVisual"
|
||||
body.add_child(visual)
|
||||
else:
|
||||
var fallback := MeshInstance3D.new()
|
||||
var capsule := CapsuleMesh.new()
|
||||
capsule.radius = _DUMMY_CAPSULE_RADIUS
|
||||
capsule.height = _DUMMY_CAPSULE_HEIGHT
|
||||
fallback.mesh = capsule
|
||||
fallback.position = Vector3(0.0, _DUMMY_CAPSULE_HEIGHT * 0.5, 0.0)
|
||||
body_mat = _make_unshaded_material(tint, 0.88)
|
||||
fallback.material_override = body_mat
|
||||
body.add_child(fallback)
|
||||
|
||||
var ring: MeshInstance3D = _make_radius_ring(radius, tint)
|
||||
ring.name = "RadiusRing"
|
||||
ring.position = Vector3(0.0, _RING_Y - anchor.y, 0.0)
|
||||
|
|
@ -86,16 +101,19 @@ func _apply_highlight_states() -> void:
|
|||
var target_id: String = id_variant as String
|
||||
var entry: Dictionary = _markers_by_id[target_id]
|
||||
var tint: Color = entry["tint"]
|
||||
var body_mat: StandardMaterial3D = entry["body_mat"]
|
||||
var body_mat: Variant = entry.get("body_mat")
|
||||
var ring_mat: StandardMaterial3D = entry["ring_mat"]
|
||||
var highlighted: bool = target_id == _locked_target_id and not _locked_target_id.is_empty()
|
||||
if body_mat is StandardMaterial3D:
|
||||
if highlighted:
|
||||
(body_mat as StandardMaterial3D).albedo_color = tint.lightened(0.25)
|
||||
else:
|
||||
(body_mat as StandardMaterial3D).albedo_color = Color(tint.r, tint.g, tint.b, 0.88)
|
||||
if highlighted:
|
||||
body_mat.albedo_color = tint.lightened(0.25)
|
||||
var ring_c := tint
|
||||
ring_c.a = 0.78
|
||||
ring_mat.albedo_color = ring_c
|
||||
else:
|
||||
body_mat.albedo_color = Color(tint.r, tint.g, tint.b, 0.88)
|
||||
var ring_c2 := tint
|
||||
ring_c2.a = 0.45
|
||||
ring_mat.albedo_color = ring_c2
|
||||
|
|
|
|||
Loading…
Reference in New Issue