Save The Tree implementation and acceptance work in progress

Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
This commit is contained in:
Takeshi
2026-10-11 15:58:00 +09:00
parent 39f6068461
commit b84bade045
235 changed files with 495831 additions and 312 deletions
+123 -5
View File
@@ -82,6 +82,9 @@ var _drag_button := MOUSE_BUTTON_NONE
var _runtime: Variant = null
var _hovered_cell: Variant = null
var _preview_topology_key: Array = []
var _connection_cache_key: Array = []
var _connection_cache_report: Dictionary = {}
var _last_preview_report: Dictionary = INVALID_PREVIEW.duplicate(true)
var _last_excavation_preview_report: Dictionary = INVALID_EXCAVATION_PREVIEW.duplicate(true)
@@ -112,6 +115,8 @@ func _ready() -> void:
func set_runtime(value: Variant) -> void:
_connection_cache_key.clear()
_connection_cache_report.clear()
_disconnect_runtime()
_runtime = value
_actor_tracks.clear()
@@ -201,7 +206,11 @@ func get_build_preview_report(cell: Variant = null) -> Dictionary:
build_rotation
)
if report != null and report.has_method(&"to_dictionary"):
return report.call(&"to_dictionary") as Dictionary
var result: Dictionary = report.call(&"to_dictionary")
result[&"connection"] = _build_connection_report(
_cells_from_serialized(result.get(&"cells", [])),
Vector3i(target.x, target.y, layer_mode))
return result
return {
&"can_build": _fallback_cells_are_available(Vector3i(target.x, target.y, layer_mode)),
&"error_codes": [],
@@ -338,6 +347,9 @@ func _process(delta: float) -> void:
lens_material.set_shader_parameter("extent", size)
lens_material.set_shader_parameter("progress", _layer_transition_elapsed / LAYER_TRANSITION_SECONDS)
_layer_lens.visible = _layer_transition_elapsed < LAYER_TRANSITION_SECONDS
if build_definition != null and _hovered_cell is Vector3i and _current_preview_topology() != _preview_topology_key:
_update_build_preview()
queue_redraw()
var clock := get_node_or_null("/root/GameClock")
if clock != null and bool(clock.get("paused")):
return
@@ -421,7 +433,13 @@ func _gui_input(event: InputEvent) -> void:
accept_event()
return
if event.pressed and event.button_index in [MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN]:
set_zoom(zoom_level + (0.5 if event.button_index == MOUSE_BUTTON_WHEEL_UP else -0.5), event.position)
if pan_mode and not event.ctrl_pressed:
var direction := 1.0 if event.button_index == MOUSE_BUTTON_WHEEL_UP else -1.0
pan_offset.y += direction * _cell_dimensions().y * maxf(event.factor, 0.0)
_clamp_pan()
queue_redraw()
else:
set_zoom(zoom_level + (0.5 if event.button_index == MOUSE_BUTTON_WHEEL_UP else -0.5), event.position)
accept_event()
return
if event is InputEventMouseMotion and _dragging:
@@ -657,6 +675,16 @@ func _draw_congestion_overlay(bounds: Dictionary) -> void:
draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), color, 2.0)
## Visual-only level comes from the existing live FacilityInstance, not the
## construction footprint dictionary. No upgrade or simulation value is written.
func _facility_visual_level(instance_id: StringName) -> int:
var service: Variant = _service(&"facility_service")
if service == null or not service.has_method(&"get_instance"):
return 1
var instance: Variant = service.get_instance(instance_id)
return int(instance.level) if instance != null else 1
func _completed_facilities_for_draw() -> Array:
if _drawing and _draw_cache.has(&"completed_facilities"):
return _draw_cache[&"completed_facilities"]
@@ -685,7 +713,8 @@ func _other_layer_rooms() -> Array[Dictionary]:
if cells.is_empty(): continue
var rect := _cell_bounds_rect(cells)
if rect.intersects(Rect2(Vector2.ZERO, size)):
rooms.append({&"id": facility[&"instance_id"], &"rect": rect, &"definition_id": facility[&"definition_id"], &"origin_x": cells[0].x})
rooms.append({&"id": facility[&"instance_id"], &"rect": rect, &"definition_id": facility[&"definition_id"], &"origin_x": cells[0].x,
&"level": _facility_visual_level(StringName(facility[&"instance_id"]))})
for job: ConstructionJob in _construction_jobs_for_draw():
if job.layer == layer_mode or job.state in [ConstructionJob.State.COMPLETED, ConstructionJob.State.CANCELLED]: continue
if job.reserved_cells.is_empty(): continue
@@ -748,7 +777,7 @@ func _draw_world_entity_icons() -> void:
if not rect.intersects(Rect2(Vector2.ZERO,size)):
continue
var id := StringName(facility.get(&"definition_id",&""))
PixelArt.room(self,rect,id,p,dungeon,town,layer_mode==LayerMode.BACK,cells[0].x, _room_presentations.get(StringName(facility.get(&"instance_id", &"")), {}))
PixelArt.room(self,rect,id,p,dungeon,town,layer_mode==LayerMode.BACK,cells[0].x, _room_presentations.get(StringName(facility.get(&"instance_id", &"")), {}), _facility_visual_level(StringName(facility.get(&"instance_id", &""))))
var instance_id := StringName(facility.get(&"instance_id",&""))
# Thin status lamp conveys operational state without repainting a room.
draw_rect(Rect2(rect.end.x-4*p,rect.position.y+2*p,2*p,2*p),_facility_color(instance_id))
@@ -1255,7 +1284,7 @@ func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, re
if staff and state == StaffRuntime.State.INCAPACITATED:
mode = &"collapsed"
elif receiving and activity in [&"standard_room", &"premium_room"]:
mode = &"sleep"
mode = &"standard_room_sleep" if activity == &"standard_room" else &"sleep"
elif moving:
mode = &"carry_walk" if carrying else &"walk"
elif carrying:
@@ -1322,6 +1351,7 @@ func _draw_build_preview() -> void:
draw_rect(rect, fill)
draw_rect(rect, edge, false, 2.0)
_draw_build_connection(_last_preview_report.get(&"connection", {}))
func _draw_excavation_preview() -> void:
if not excavation_mode or not _hovered_cell is Vector3i:
@@ -1434,6 +1464,7 @@ func _set_hovered_cell(value: Variant) -> void:
func _update_build_preview() -> void:
_preview_topology_key = _current_preview_topology()
_last_preview_report = get_build_preview_report()
build_preview_changed.emit(_last_preview_report.duplicate(true))
@@ -1451,6 +1482,8 @@ func _on_mouse_exited() -> void:
func _on_snapshot_published(_snapshot: Dictionary = {}, _digest: String = "") -> void:
_connection_cache_key.clear()
_connection_cache_report.clear()
queue_redraw()
_update_build_preview()
_update_excavation_preview()
@@ -1567,3 +1600,88 @@ func _can_drop_data(_position: Vector2, data: Variant) -> bool:
func _drop_data(position: Vector2, data: Variant) -> void:
if _can_drop_data(position, data):
staff_assignment_dropped.emit(data, facility_at_canvas(position))
func _current_preview_topology() -> Array:
var world: Variant = _service(&"world_service")
var path: Variant = _service(&"path_service")
return [world.revision if world != null else -1,
path.graph_revision if path != null else -1,
path.traffic_revision if path != null else -1]
## Read-only connection preview. Shares ordinary facility access candidates and
## accessible graph edges, but does not issue live path queries or change routing.
## The displayed path proves connectivity; it is not a cheapest-route promise.
func _build_connection_report(cells: Array[Vector3i], origin: Vector3i) -> Dictionary:
var path: Variant = _service(&"path_service")
if path == null or _runtime == null or cells.is_empty():
return {&"status": &"UNAVAILABLE", &"reachable": false, &"path": []}
var entrance: Variant = _runtime.call(&"_operations_entrance_cell")
var key: Array = [origin, cells, _current_preview_topology(), entrance]
if key == _connection_cache_key:
return _connection_cache_report.duplicate(true)
var result := {&"status": &"UNAVAILABLE", &"reachable": false,
&"entrance": entrance, &"origin": origin, &"path": [], &"access_points": []}
if entrance is Vector3i:
var future := FacilityInstance.new()
future.origin = origin
future.occupied_cells.assign(cells)
var candidates: Array = _runtime.call(&"_facility_access_candidates", future)
result[&"access_points"] = candidates
var targets: Dictionary = {}
for cell: Vector3i in candidates: targets[cell] = true
var queue: Array[Vector3i] = [entrance]
var parents: Dictionary = {}
parents[entrance] = entrance
var cursor := 0
var target: Variant = null
while cursor < queue.size():
var current := queue[cursor]
cursor += 1
if targets.has(current):
target = current
break
for edge: Dictionary in path.get_neighbors(current):
var next: Vector3i = edge[&"to"]
if parents.has(next) or path.get_edge_report(current, next).is_empty():
continue
parents[next] = current
queue.append(next)
var route: Array[Vector3i] = []
if target is Vector3i:
var current: Vector3i = target
while current != entrance:
route.push_front(current)
current = parents[current]
route.push_front(entrance)
result[&"reachable"] = not route.is_empty()
result[&"status"] = &"CONNECTED" if result[&"reachable"] else &"DISCONNECTED"
result[&"path"] = route
_connection_cache_key = key.duplicate(true)
_connection_cache_report = result.duplicate(true)
return result
func _draw_build_connection(report: Dictionary) -> void:
if report.is_empty() or not _hovered_cell is Vector3i: return
var status: StringName = report.get(&"status", &"UNAVAILABLE")
var color := Color("8ff0ab") if status == &"CONNECTED" else Color("ffd080")
var target := cell_to_canvas(_hovered_cell)
var route: Array = report.get(&"path", [])
if status == &"CONNECTED":
for i in range(1, route.size()):
var from: Vector3i = route[i-1]
var to: Vector3i = route[i]
if from.z == layer_mode and to.z == layer_mode:
draw_line(cell_to_canvas(from), cell_to_canvas(to), color, 2.0)
if not route.is_empty() and (route.back() as Vector3i).z == layer_mode:
draw_line(cell_to_canvas(route.back()), target, color, 2.0)
elif report.get(&"entrance") is Vector3i:
# Dashed straight guide is a warning, never a traversable-path claim.
draw_dashed_line(cell_to_canvas(report[&"entrance"]), target, color, 2.0, 8.0)
draw_line(target-Vector2(6,6),target+Vector2(6,6),color,2.0)
draw_line(target+Vector2(-6,6),target+Vector2(6,-6),color,2.0)
var message := "入口への経路接続済" if status == &"CONNECTED" else "入口への経路未接続" if status == &"DISCONNECTED" else "入口・経路を確認できません"
draw_string(ThemeDB.fallback_font, target+Vector2(8,-12), message,
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, color)