forked from xiangwang25/whale-town-front-v2
- 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
106 lines
4.5 KiB
GDScript
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
|