class_name GuestObservationPanel extends VBoxContainer const NEED_NAMES := {&"hunger": "食欲", &"sleep": "睡眠欲", &"leisure": "娯楽欲", &"intimacy": "性欲", &"hygiene": "衛生", &"medical": "救護"} const STATES := ["到着", "入館", "行先を検討", "移動中", "待機中", "利用中", "退館中", "退館", "再検討", "待機終了", "中断", "避難中", "気絶"] const REJECTION_NAMES := { "MISSING_INPUTS": "食材・資材不足", "FACILITY_CLOSED": "閉店・稼働停止", "UNREACHABLE": "経路なし", "CANNOT_PAY": "利用可能な資金が不足", "ETA_AFTER_EXIT": "出発予定に間に合わない", "WAIT_HARD_LIMIT": "待ち時間が長すぎる", "QUALITY_TOO_LOW": "希望する品質に届かない", "RISK_REFUSED": "危険度が高い", "ILLEGALITY_REFUSED": "違法性を敬遠", "MISSING_PERMISSION": "利用資格なし", "FORBIDDEN_GUEST_TAG": "利用対象外", "GUEST_TYPE_NOT_ALLOWED": "利用対象外", "ADULT_NOT_VERIFIED": "年齢条件を満たさない", "CONTENT_FILTERED": "利用設定の対象外", "INVALID_NEED_MAP": "施設設定を確認できない", } var guests: OptionButton var details: Label var decision_label: Label var priority_label: Label var bars: Dictionary = {} var selected_guest_id: StringName = &"" var _runtime: Variant var _ids: Array[StringName] = [] var _elapsed := 0.0 func configure(runtime: Variant) -> void: _runtime = runtime func _ready() -> void: name = "GuestObservationPanel" custom_minimum_size = Vector2(420, 340) add_theme_constant_override("separation", 8) guests = OptionButton.new() guests.item_selected.connect(func(index: int) -> void: selected_guest_id = StringName(guests.get_item_metadata(index)) refresh()) add_child(guests) details = _label() for need: StringName in GuestService.PRIMARY_NEEDS: var row := HBoxContainer.new() var title := Label.new() title.text = NEED_NAMES[need] title.custom_minimum_size.x = 85 row.add_child(title) var bar := ProgressBar.new() bar.max_value = 100 bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(bar) bars[need] = bar add_child(row) priority_label = _label() decision_label = _label() _label().text = "欲求が高いほど、そのサービスを優先します。\n利用できる施設の中で欲求を優先し、同じ優先度なら距離・行列・料金などで比較します。\n表示は現在の欲求です。移動・利用の途中で目的地を勝手に変更しません。" refresh() func _label() -> Label: var label := Label.new() label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART add_child(label) return label func _process(delta: float) -> void: if not is_visible_in_tree() or not get_window().visible: return _elapsed += delta if _elapsed >= 0.5: _elapsed = 0 refresh() ## Clear optional saved presentation while rebuilding the actual loaded roster. ## Opening or polling the visible observer retains the normal first-row choice. func clear_selection() -> void: selected_guest_id = &"" _elapsed = 0.0 refresh(false) func refresh(select_first_if_empty: bool = true) -> void: if guests == null or _runtime == null: return var service: Variant = _runtime.get(&"guest_service") if service == null: return var next: Array[StringName] = [] for guest: GuestRuntime in service.get_guests(): next.append(guest.guest_id) if next != _ids: _ids = next guests.clear() for id: StringName in _ids: guests.add_item(String(id)) guests.set_item_metadata(guests.item_count-1, id) if not _ids.has(selected_guest_id) and (not selected_guest_id.is_empty() or select_first_if_empty): selected_guest_id = _ids[0] if not _ids.is_empty() else &"" guests.disabled = _ids.is_empty() if _ids.has(selected_guest_id): guests.select(_ids.find(selected_guest_id)) else: guests.select(-1) var report: Dictionary = service.get_observation(selected_guest_id) for need: StringName in bars: bars[need].value = report.get(&"needs", {}).get(need, 0) if report.is_empty(): details.text = "現在、来客はいません。" if _ids.is_empty() else "来客を選択してください。" priority_label.text = "" decision_label.text = "" return var target := String(report[&"target_facility_id"]) var facility: Variant = _runtime.facility_service.get_instance(StringName(target)) if facility != null: var definition: Variant = _runtime.definition_registry.get_facility(facility.definition_id) if definition != null: target = definition.display_name if target.is_empty(): target = "未選択" details.text = "%s · 所持金 %dG · 忍耐 %d\n目的地: %s" % [STATES[int(report[&"state"])], report[&"wallet_gold"], report[&"patience"], target] var names: PackedStringArray = [] for need: StringName in report[&"primary_needs"]: names.append(NEED_NAMES[need]) priority_label.text = "現在最も高い欲求: " + ("なし" if names.is_empty() else "・".join(names)) decision_label.text = _decision_text(_runtime.get_guest_decision_record(selected_guest_id)) func _decision_text(record: Dictionary) -> String: if record.is_empty(): return "直近の行先判断:記録なし(判断前、旧セーブ、または履歴の保存範囲外)" var minute := int(record.get(&"absolute_minute", 0)) var title := "直近の行先判断 · %d日目 %02d:%02d" % [minute / 1440 + 1, (minute % 1440) / 60, minute % 60] if StringName(record.get(&"action", &"")) == &"LEAVE": return title + "\n条件を満たして選べる施設がなく、退館を選びました。" + _rejection_text(record) var id := StringName(record.get(&"facility_id", &"")) var destination := String(id) var facility: Variant = _runtime.facility_service.get_instance(id) if facility != null: var definition: Variant = _runtime.definition_registry.get_facility(facility.definition_id) if definition != null: destination = definition.display_name var needs: PackedStringArray = [] for need: StringName in record.get(&"primary_needs", []): needs.append("%s %d" % [NEED_NAMES.get(need, String(need)), int(record.get(&"needs", {}).get(need, 0))]) return title + "\n選択:%s / 判断時の最大欲求:%s\n移動 %d分・待ち %d分・見込料金 %dG\n選択候補の欲求優先度 %d・比較評価 %d(判断時点)" % [destination, "・".join(needs) if not needs.is_empty() else "なし", int(record.get(&"travel_minutes", 0)), int(record.get(&"wait_minutes", 0)), int(record.get(&"estimated_total_gold", 0)), int(record.get(&"need_priority", 0)), int(record.get(&"utility", 0))] + _rejection_text(record) func _rejection_text(record: Dictionary) -> String: if not record.has(&"rejection_counts"): return "\n候補の除外理由:この記録にはありません。" var counts: Dictionary = record[&"rejection_counts"] if counts.is_empty(): return "\n今回検討した候補には、利用条件による除外はありません。" var reasons: PackedStringArray = [] var keys := counts.keys() keys.sort() for reason: String in keys: reasons.append("%s %d施設" % [REJECTION_NAMES.get(reason, "条件を満たさない"), int(counts[reason])]) var text := "\n判断時の除外理由:" + "・".join(reasons) for row: Dictionary in record.get(&"rejection_examples", []): var id := StringName(row.get(&"facility_id", &"")) var facility: Variant = _runtime.facility_service.get_instance(id) var label := String(id) if facility != null: var definition: Variant = _runtime.definition_registry.get_facility(facility.definition_id) if definition != null: label = "%s(%s)" % [definition.display_name, id] text += "\n・%s:%s" % [label, REJECTION_NAMES.get(String(row.get(&"reason", "")), "条件を満たさない")] var omitted: int = int(record.get(&"rejection_count", 0)) - record.get(&"rejection_examples", []).size() if omitted > 0: text += "\nほか%d施設(件数は上記に含みます)" % omitted return text