P23-03: 生活の観察

This commit is contained in:
Agent Control Runner
2026-10-15 18:01:00 +09:00
parent 49445d4a4d
commit 19fc3f50c7
8 changed files with 1888 additions and 71 deletions
+252 -24
View File
@@ -79,6 +79,8 @@ var _queue_hidden: Dictionary = {}
var _landing_badges: Array[Dictionary] = []
var _transport_report: Dictionary = {}
var _life_observation: Dictionary = {}
var _life_overlay: Node2D
var _life_observation_serial := 0
var _dragging := false
var _drag_button := MOUSE_BUTTON_NONE
@@ -104,6 +106,10 @@ func _ready() -> void:
_inactive_layer_art = InactiveLayerRooms.new()
_inactive_layer_art.name = "InactiveLayerRooms"
add_child(_inactive_layer_art)
_life_overlay = Node2D.new()
_life_overlay.name = "LifeOverlay"
_life_overlay.draw.connect(_draw_life_overlay)
add_child(_life_overlay)
_layer_lens = ColorRect.new()
_layer_lens.name = "LayerLens"
_layer_lens.mouse_filter = Control.MOUSE_FILTER_IGNORE
@@ -513,6 +519,8 @@ func _draw() -> void:
func _draw_contents() -> void:
_life_observation = {}
if _life_overlay != null:
_life_overlay.queue_redraw()
if _inactive_layer_art != null:
_inactive_layer_art.visible = false
_draw_background()
@@ -524,6 +532,9 @@ func _draw_contents() -> void:
if map_rect.size == Vector2.ZERO:
return
_life_observation = _build_life_observation()
_life_observation_serial += 1
_life_observation[&"observation_serial"] = _life_observation_serial
_life_observation[&"overlay_consumed_serial"] = -1
_room_presentations = _life_observation[&"rooms"]
_carried_patients = _life_observation[&"carried_patients"]
_treated_patients = _life_observation[&"treated_patients"]
@@ -540,11 +551,9 @@ func _draw_contents() -> void:
_draw_legacy_stairs()
_draw_elevators()
_draw_other_layer()
_draw_congestion_overlay(bounds)
_draw_life_badges()
_draw_construction_labels()
_draw_facility_labels()
_draw_actors()
_draw_excavation_jobs()
_draw_build_preview()
_draw_excavation_preview()
@@ -599,6 +608,7 @@ func _build_life_observation() -> Dictionary:
model[&"guests"] = guests
model[&"staff"] = staff
model[&"tasks"] = tasks
_fit_life_actors(model)
var bridge: Variant = _service(&"supply_logistics")
var goods: Dictionary = bridge.get_report() if bridge != null else {&"requests": [], &"deliveries": []}
model[&"goods"] = goods
@@ -638,7 +648,14 @@ func _build_life_observation() -> Dictionary:
var cell := Vector3i(x, y, layer_mode)
var row: Dictionary = path.get_cell_traffic(cell)
if not row.is_empty():
traffic.append({&"cell": cell, &"traffic": row, &"rect": get_cell_rect(cell).grow(-2.0)})
var cell_rect := get_cell_rect(cell)
var rect := cell_rect.grow(-2.0)
var world_pixel := _cell_dimensions().x / 16.0
var thickness := clampf(rect.size.y * 0.16, 2.0, 5.0)
var band := Rect2(Vector2(rect.position.x + rect.size.x * 0.12,
cell_rect.end.y - 2 * world_pixel - thickness * 0.5),
Vector2(rect.size.x * 0.76, thickness))
traffic.append({&"cell": cell, &"traffic": row, &"rect": rect, &"band_rect": band})
model[&"traffic"] = traffic
return model
@@ -758,9 +775,27 @@ func _draw_leaf_cluster(center: Vector2, p: float, variation: int) -> void:
draw_rect(Rect2(c+offset,Vector2(3+i%2,2)*p),Color("95ad69") if i%3==0 else Color("648957"))
func _draw_congestion_overlay(_bounds: Dictionary) -> void:
func _draw_life_overlay() -> void:
if _life_observation.is_empty():
return
# These are actual child indices and the passes executed by this callback.
_life_observation[&"overlay_child_indices"] = [_inactive_layer_art.get_index(), _life_overlay.get_index(), _layer_lens.get_index()]
_life_observation[&"overlay_passes"] = []
_life_observation[&"overlay_passes"].append(&"traffic")
_draw_congestion_overlay(_life_overlay)
_life_observation[&"overlay_passes"].append(&"actors")
for cabin: Dictionary in _life_observation.get(&"cabins", []):
for actor: Dictionary in cabin[&"passengers"]:
_draw_observed_actor(actor, _life_overlay)
_draw_actors(_life_overlay)
for badge: Dictionary in _life_observation.get(&"actor_aggregations", []):
_draw_readable_badge(badge[&"at"], badge[&"text"], _life_overlay)
_life_observation[&"overlay_consumed_serial"] = _life_observation[&"observation_serial"]
func _draw_congestion_overlay(canvas: CanvasItem) -> void:
for row: Dictionary in _life_observation.get(&"traffic", []):
var rect: Rect2 = row[&"rect"]
var band: Rect2 = row[&"band_rect"]
var traffic: Dictionary = row[&"traffic"]
var density := int(traffic[&"current_density"])
var blocked := bool(traffic[&"temporary_block"])
@@ -768,13 +803,11 @@ func _draw_congestion_overlay(_bounds: Dictionary) -> void:
continue
var ratio := float(density) / maxi(int(traffic[&"base_capacity"]), 1)
var color := Color("e34332") if blocked or ratio > 1.0 else Color("e3a62f") if ratio >= 0.65 else Color("55c96b")
var center := rect.get_center()
draw_line(Vector2(rect.position.x + rect.size.x * 0.12, center.y),
Vector2(rect.end.x - rect.size.x * 0.12, center.y),
Color(color, 0.90 if show_grid else 0.28), clampf(rect.size.y * 0.16, 2.0, 5.0))
canvas.draw_rect(band, Color(color, 0.90))
if blocked:
draw_line(rect.position, rect.end, Color(color, 0.90 if show_grid else 0.28), 2.0)
draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), Color(color, 0.90 if show_grid else 0.28), 2.0)
var cross := band.grow(-1.0)
canvas.draw_line(cross.position, cross.end, Color(color, 0.90), 2.0)
canvas.draw_line(Vector2(cross.end.x, cross.position.y), Vector2(cross.position.x, cross.end.y), Color(color, 0.90), 2.0)
## Visual-only level comes from the existing live FacilityInstance, not the
@@ -887,13 +920,15 @@ func _draw_world_entity_icons() -> void:
draw_rect(rect.grow(-p),Color("f5d582"),false,2*p)
func _draw_readable_badge(at: Vector2, text: String) -> void:
func _draw_readable_badge(at: Vector2, text: String, canvas: CanvasItem = null) -> void:
if canvas == null:
canvas = self
var width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x+6
var rect := Rect2(at, Vector2(width, 14))
if not rect.intersects(Rect2(Vector2.ZERO, size)):
return
draw_rect(rect, Color("34413b", 0.94))
draw_string(ThemeDB.fallback_font, at+Vector2(3, 11), text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color("fff2ce"))
canvas.draw_rect(rect, Color("34413b", 0.94))
canvas.draw_string(ThemeDB.fallback_font, at+Vector2(3, 11), text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color("fff2ce"))
func _prepare_room_presentations() -> void:
@@ -1146,8 +1181,6 @@ func _draw_facility_labels() -> void:
func _draw_elevators() -> void:
for row: Dictionary in _life_observation.get(&"cabins", []):
PixelArt.elevator_cabin(self, row[&"rect"], row[&"pixel"], layer_mode == LayerMode.BACK, row[&"operational"])
for actor: Dictionary in row[&"passengers"]:
_draw_observed_actor(actor)
PixelArt.elevator_gate(self, row[&"rect"], row[&"pixel"], row[&"doors_open"])
_draw_readable_badge(row[&"badge_at"], row[&"badge"])
for badge: Dictionary in _life_observation.get(&"landing_badges", []):
@@ -1188,8 +1221,7 @@ func _observe_elevators(observation: Dictionary) -> Array[Dictionary]:
var doors_open := operational and phase in [&"IDLE", &"BOARDING", &"ALIGHTING"]
var passengers_drawn: Array[Dictionary] = []
var seat_columns := maxi(int((cabin.size.x / p - 4.0) / 8.0), 1)
var visible_count := mini(passengers.size(), seat_columns*2)
for index: int in range(visible_count):
for index: int in range(passengers.size()):
var actor_key := _elevator_actor_key(String(passengers[index]))
var is_staff := actor_key.begins_with("staff/")
var person: Variant = null
@@ -1200,7 +1232,7 @@ func _observe_elevators(observation: Dictionary) -> Array[Dictionary]:
var carrying := is_staff and person != null and not String(person.carried_inventory_id).is_empty()
var state := int(person.state) if person != null else 0
var x := cabin.position.x + (2.0 + (index % seat_columns + 0.5)*8.0)*p
var feet := cabin.end.y - (2.0 + int(index / seat_columns)*9.0)*p
var feet := cabin.end.y - (2.0 + (int(index / seat_columns) % 2)*9.0)*p
var center := Vector2(x, feet+3*p-_cell_dimensions().y*0.5)
passengers_drawn.append(_actor_drawing(center,actor_key,is_staff,state,false,carrying,&"",true,observation))
cabins.append({&"shaft_id": shaft_id, &"rect": cabin, &"pixel": p,
@@ -1240,9 +1272,9 @@ func _tracked_elevator_floor(shaft_id: StringName, current_floor: float, observa
return lerpf(float(track[&"from"]),current_floor,t)
func _draw_actors() -> void:
func _draw_actors(canvas: CanvasItem = null) -> void:
for row: Dictionary in _life_observation.get(&"actor_drawings", []):
_draw_observed_actor(row)
_draw_observed_actor(row, canvas)
func _observe_actors(observation: Dictionary) -> Array[Dictionary]:
@@ -1291,6 +1323,9 @@ func _observe_actors(observation: Dictionary) -> Array[Dictionary]:
continue
var position := _tracked_actor_position(key,staff.current_cell,observation)
if staff.current_cell.z==layer_mode:
if _queue_positions.has(key):
rows.append(_actor_drawing(_queue_positions[key],key,true,int(staff.state),false,not String(staff.carried_inventory_id).is_empty(),_staff_work_animation(staff),false,observation))
continue
if _carried_patients.has(staff.staff_id):
var carrier: Variant = staff_service.get_staff(_carried_patients[staff.staff_id])
if carrier != null:
@@ -1412,11 +1447,13 @@ func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, re
_draw_observed_actor(_actor_drawing(center, key, staff, state, receiving, carrying, activity, riding))
func _draw_observed_actor(row: Dictionary) -> void:
func _draw_observed_actor(row: Dictionary, canvas: CanvasItem = null) -> void:
if canvas == null:
canvas = self
var at: Vector2 = row[&"at"]
var p: float = row[&"scale"]
draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2))
People.draw(self, at, p, row[&"mode"], row[&"frame"], row[&"variant"], row[&"staff"], row[&"flip"])
canvas.draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2))
People.draw(canvas, at, p, row[&"mode"], row[&"frame"], row[&"variant"], row[&"staff"], row[&"flip"])
func _actor_drawing(center: Vector2, key: String, staff: bool, state: int, receiving: bool, carrying: bool, activity: StringName = &"", riding := false, observation: Dictionary = {}) -> Dictionary:
@@ -1454,6 +1491,197 @@ func _actor_drawing(center: Vector2, key: String, staff: bool, state: int, recei
&"riding": riding, &"flip": float(track.get(&"direction", 1.0)) < 0.0}
## Identity cores are tied to the unchanged PixelPeople poses, not sprite boxes.
func _life_core(row: Dictionary) -> Rect2:
var phase := int(row[&"frame"]) % 8
var core := Rect2(6, 1, 6, 12)
if row[&"mode"] in [&"sleep", &"collapsed", &"standard_room_sleep"]:
core = Rect2(2, 11, 13, 5)
if row[&"mode"] == &"standard_room_sleep" and phase in [2, 3, 4]:
core.position.y -= 1
core.size.y += 1
elif row[&"mode"] in [&"walk", &"carry_walk"] and phase in [2, 6]:
core.position.y -= 1
if row[&"flip"]:
core.position.x = 16-core.end.x
return Rect2(row[&"at"]+core.position*row[&"scale"], core.size*row[&"scale"])
func _life_track_region(key: String, cell: Vector3i, model: Dictionary) -> Dictionary:
var track: Dictionary = model[&"actor_tracks"].get(key, {})
var transit: bool = not model[&"paused"] and track.get(&"from", cell) != cell and _animation_time-float(track.get(&"at", 0.0)) < 0.42
var rect := get_cell_rect(cell)
if transit:
rect = rect.merge(get_cell_rect(track[&"from"]))
return {&"bounds": rect, &"transit": transit}
func _life_actor_region(row: Dictionary, person: Dictionary, model: Dictionary) -> Dictionary:
var key: String = row[&"key"]
var cell: Vector3i = person[&"cell"]
var result := _life_track_region(key, cell, model)
var p := _cell_dimensions().x/16.0
var world: Variant = _service(&"world_service")
var facility_id: StringName = &""
if model[&"queue_positions"].has(key):
result = {&"bounds": Rect2(model[&"queue_positions"][key]-_cell_dimensions()*0.5, _cell_dimensions()), &"transit": false}
elif not row[&"staff"]:
if not String(person[&"service"]).is_empty():
var room: Dictionary = model[&"rooms"].get(person[&"target"], {})
if (room.get(&"people", []) as Array).has(person[&"id"]):
facility_id = person[&"target"]
else:
var id: StringName = person[&"id"]
if model[&"carried_patients"].has(id):
for carrier: Dictionary in model[&"staff"]:
if carrier[&"id"] == model[&"carried_patients"][id]:
result = _life_track_region("staff/"+String(carrier[&"id"]), carrier[&"cell"], model)
var rect: Rect2 = result[&"bounds"]
rect.position.y -= 12*p
result[&"bounds"] = rect
elif model[&"resting_staff"].has(id):
facility_id = model[&"resting_staff"][id]
elif model[&"treated_patients"].has(id):
facility_id = model[&"treated_patients"][id]
elif person[&"state"] in ["WORKING", "AVAILABLE"] and world != null:
var cells: Array[Vector3i] = world.get_entity_cells(person[&"facility"])
var room: Dictionary = model[&"rooms"].get(person[&"facility"], {})
var slot := (room.get(&"staff", []) as Array).find(id)
var rect := _cell_bounds_rect(cells)
var columns := maxi(int(rect.size.x/(16*p)), 1)
if cells.has(cell) and slot >= 0 and slot < columns and rect.size.y >= 40*p:
result = {&"bounds": Rect2(rect.position+Vector2(slot*rect.size.x/columns, 0), Vector2(rect.size.x/columns, rect.size.y)), &"transit": false}
if not facility_id.is_empty() and world != null:
var cells: Array[Vector3i] = world.get_entity_cells(facility_id)
if not cells.is_empty():
result = {&"bounds": _cell_bounds_rect(cells), &"transit": false}
result[&"bounds"] = (result[&"bounds"] as Rect2).grow(-p)
return result
func _life_scaled(row: Dictionary, scale: float) -> Dictionary:
var result := row.duplicate(true)
result[&"scale"] = scale
result[&"at"] = (row[&"center"]+Vector2(-8*scale, _cell_dimensions().y*0.5-3*_cell_dimensions().x/16.0-17*scale)).round()
return result
## Forward/backward packing keeps each member's own interval. No relocation.
func _life_pack(group: Array, scale: float) -> Array:
var trial: Array = []
var right := -INF
for member: Dictionary in group:
var row := _life_scaled(member[&"row"], minf(scale, member[&"row"][&"scale"]))
var core := _life_core(row)
var bounds: Rect2 = member[&"bounds"]
var x := maxf(maxf(core.position.x, bounds.position.x), right)
row[&"at"].x += x-core.position.x
right = x+core.size.x
trial.append(row)
var left := INF
for i: int in range(trial.size()-1, -1, -1):
var row: Dictionary = trial[i]
var core := _life_core(row)
var bounds: Rect2 = group[i][&"bounds"]
var x := minf(core.position.x, minf(bounds.end.x, left)-core.size.x)
row[&"at"].x += x-core.position.x
left = x
if x < bounds.position.x or core.position.y < bounds.position.y or core.end.y > bounds.end.y:
return []
return trial
## Rebuild vertical components at every fitted scale. Ceilings only decrease;
## once none decreases, the feet/cores and therefore the partition are fixed.
func _life_fitted_groups(members: Array) -> Array:
var groups: Array = []
while true:
for member: Dictionary in members:
member[&"row"].merge(_life_scaled(member[&"row"], member[&"row"][&"scale"]), true)
members.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
var ay := _life_core(a[&"row"]).position.y
var by := _life_core(b[&"row"]).position.y
return ay < by or (ay == by and a[&"row"][&"key"] < b[&"row"][&"key"]))
groups = []
var bottom := -INF
for member: Dictionary in members:
var core := _life_core(member[&"row"])
if groups.is_empty() or core.position.y >= bottom:
groups.append([])
bottom = core.end.y
else:
bottom = maxf(bottom, core.end.y)
groups.back().append(member)
var reduced := false
for group: Array in groups:
group.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a[&"row"][&"center"].x < b[&"row"][&"center"].x or (a[&"row"][&"center"].x == b[&"row"][&"center"].x and a[&"row"][&"key"] < b[&"row"][&"key"]))
var ceiling := 1.0
for member: Dictionary in group:
ceiling = maxf(ceiling, member[&"row"][&"scale"])
if _life_pack(group, ceiling).is_empty() and ceiling > 1:
for member: Dictionary in group:
var scale := minf(member[&"row"][&"scale"], maxf(1, int(ceiling)-1))
member[&"row"].merge(_life_scaled(member[&"row"], scale), true)
reduced = true
if not reduced:
return groups
return groups
func _fit_life_actors(model: Dictionary) -> void:
var people: Dictionary = {}
for person: Dictionary in model[&"guests"]:
people["guest/"+String(person[&"id"])] = person
for person: Dictionary in model[&"staff"]:
people["staff/"+String(person[&"id"])] = person
var members: Array = []
var all_rows: Array = model[&"actor_drawings"].duplicate()
var passenger_bounds: Dictionary = {}
for cabin: Dictionary in model[&"cabins"]:
for row: Dictionary in cabin[&"passengers"]:
all_rows.append(row)
passenger_bounds[row[&"key"]] = (cabin[&"rect"] as Rect2).grow(-_cell_dimensions().x/16.0)
for row: Dictionary in all_rows:
var person: Dictionary = people[row[&"key"]]
row[&"cell"] = person[&"cell"]
var region := _life_actor_region(row, person, model) if not row[&"riding"] else {&"bounds": passenger_bounds[row[&"key"]], &"transit": false}
region[&"row"] = row
if not region[&"transit"]:
members.append(region)
var groups := _life_fitted_groups(members)
model[&"actor_aggregations"] = []
var hidden: Dictionary = {}
for group: Array in groups:
group.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a[&"row"][&"center"].x < b[&"row"][&"center"].x or (a[&"row"][&"center"].x == b[&"row"][&"center"].x and a[&"row"][&"key"] < b[&"row"][&"key"]))
var base := 1.0
for member: Dictionary in group:
base = maxf(base, member[&"row"][&"scale"])
var fitted := _life_pack(group, base)
var retained: Array = group
if fitted.is_empty():
retained = []
var identities: Array = []
var drawn: Array = []
for member: Dictionary in group:
var row: Dictionary = member[&"row"]
identities.append({&"key": row[&"key"], &"id": people[row[&"key"]][&"id"], &"state": row[&"state"], &"cell": row[&"cell"], &"mode": row[&"mode"]})
var attempt := retained.duplicate()
attempt.append(member)
var packed := _life_pack(attempt, 1)
if not packed.is_empty():
retained = attempt
fitted = packed
drawn.append(row[&"key"])
else:
hidden[row[&"key"]] = true
model[&"actor_aggregations"].append({&"members": identities, &"drawn_keys": drawn, &"at": (group[0][&"bounds"] as Rect2).position, &"text": "+%d" % (group.size()-retained.size())})
for i: int in fitted.size():
retained[i][&"row"].merge(fitted[i], true)
model[&"actor_drawings"] = model[&"actor_drawings"].filter(func(row: Dictionary) -> bool: return not hidden.has(row[&"key"]))
for cabin: Dictionary in model[&"cabins"]:
cabin[&"passengers"] = cabin[&"passengers"].filter(func(row: Dictionary) -> bool: return not hidden.has(row[&"key"]))
func _draw_excavation_jobs() -> void:
var excavation: Variant = _service(&"excavation_service")
if excavation == null or not excavation.has_method(&"get_jobs"):