Files
whale-town-front-v2/scripts/test_world_npc_navigation.gd
xiangwang fc6c3c1bd3 feat: expand multiplayer, chat, and release support
- add network NPC synchronization and dialogue interactions
- add world bulletin publishing and display
- improve authentication, session refresh, and appearance sync
- synchronize player direction and movement animations
- improve input focus and progressive Web loading
- add macOS/Windows builds and deployment configuration
- include required fonts, shaders, and runtime assets
2026-09-08 21:37:43 +08:00

106 lines
4.5 KiB
GDScript

extends SceneTree
const MAP_SCENES := {
"whale_port": "res://scenes/Maps/square.tscn",
"work_zone": "res://scenes/Maps/work_zone.tscn",
"whale_cafe": "res://scenes/Maps/cafe_interior.tscn",
}
const MAX_NPC_FOOTPRINT := Vector2(60, 28)
const ROUTE_SAMPLE_DISTANCE := 8.0
var _failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var arguments := OS.get_cmdline_user_args()
assert(arguments.size() == 1, "pass the exported world NPC graph JSON path after --")
var graph := JSON.parse_string(FileAccess.get_file_as_string(arguments[0])) as Dictionary
assert(not graph.is_empty(), "world NPC graph JSON is invalid")
var locationsById := {}
for locationValue in graph.get("locations", []):
var location := locationValue as Dictionary
locationsById[str(location.get("id", ""))] = location
for mapId in MAP_SCENES:
await _validate_map(str(mapId), str(MAP_SCENES[mapId]), graph, locationsById)
if not _failures.is_empty():
for failure in _failures:
push_error(failure)
quit(1)
return
print("WORLD_NPC_NAVIGATION_OK")
quit()
func _validate_map(mapId: String, scenePath: String, graph: Dictionary, locationsById: Dictionary) -> void:
var mapScene := (load(scenePath) as PackedScene).instantiate() as Node2D
for uiName in ["ChatUI", "WorldBulletinPanel"]:
var uiNode := mapScene.find_child(uiName, true, false)
if uiNode != null:
uiNode.free()
root.add_child(mapScene)
await process_frame
for locationValue in graph.get("locations", []):
var location := locationValue as Dictionary
if str(location.get("mapId", "")) != mapId:
continue
for slotIndex in range((location.get("slots", []) as Array).size()):
var slot := (location.get("slots", []) as Array)[slotIndex] as Dictionary
_validate_clear_point(
mapScene, Vector2(float(slot.get("x", 0)), float(slot.get("y", 0))),
"%s slot %d" % [location.get("id", ""), slotIndex],
)
for edgeValue in graph.get("edges", []):
var edge := edgeValue as Dictionary
if str(edge.get("kind", "")) != "walk":
continue
var from := locationsById.get(str(edge.get("from", "")), {}) as Dictionary
var to := locationsById.get(str(edge.get("to", "")), {}) as Dictionary
if str(from.get("mapId", "")) != mapId or str(to.get("mapId", "")) != mapId:
continue
var fromCenter := Vector2(float(from.get("x", 0)), float(from.get("y", 0)))
var toCenter := Vector2(float(to.get("x", 0)), float(to.get("y", 0)))
var fromPoints: Array[Vector2] = [fromCenter]
var toPoints: Array[Vector2] = [toCenter]
for slotValue in from.get("slots", []):
var slot := slotValue as Dictionary
fromPoints.append(Vector2(float(slot.get("x", 0)), float(slot.get("y", 0))))
for slotValue in to.get("slots", []):
var slot := slotValue as Dictionary
toPoints.append(Vector2(float(slot.get("x", 0)), float(slot.get("y", 0))))
for fromPoint in fromPoints:
_validate_clear_segment(mapScene, fromPoint, toCenter, "%s -> %s" % [from.get("id", ""), to.get("id", "")])
for toPoint in toPoints:
_validate_clear_segment(mapScene, fromCenter, toPoint, "%s -> %s" % [from.get("id", ""), to.get("id", "")])
mapScene.queue_free()
await process_frame
func _validate_clear_segment(mapScene: Node2D, from: Vector2, to: Vector2, label: String) -> void:
var samples := maxi(1, int(ceil(from.distance_to(to) / ROUTE_SAMPLE_DISTANCE)))
for sampleIndex in range(samples + 1):
var point := from.lerp(to, float(sampleIndex) / float(samples))
if not _static_colliders_at(mapScene, point).is_empty():
_failures.append("route intersects static collision: %s at %s" % [label, point])
return
func _validate_clear_point(mapScene: Node2D, point: Vector2, label: String) -> void:
var colliders := _static_colliders_at(mapScene, point)
if not colliders.is_empty():
_failures.append("slot intersects static collision: %s at %s (%s)" % [label, point, ", ".join(colliders)])
func _static_colliders_at(mapScene: Node2D, point: Vector2) -> Array[String]:
var shape := RectangleShape2D.new()
shape.size = MAX_NPC_FOOTPRINT
var query := PhysicsShapeQueryParameters2D.new()
query.shape = shape
query.transform = Transform2D(0.0, point)
query.collision_mask = 1
query.collide_with_areas = false
query.collide_with_bodies = true
var colliders: Array[String] = []
for hit in mapScene.get_world_2d().direct_space_state.intersect_shape(query, 64):
var collider := hit.get("collider") as CollisionObject2D
if collider is StaticBody2D and not colliders.has(str(collider.get_path())):
colliders.append(str(collider.get_path()))
return colliders