Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
150 lines
7.3 KiB
GDScript
150 lines
7.3 KiB
GDScript
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()
|
||
|
||
func refresh() -> 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):
|
||
selected_guest_id = _ids[0] if not _ids.is_empty() else &""
|
||
guests.disabled = _ids.is_empty()
|
||
if not _ids.is_empty(): guests.select(_ids.find(selected_guest_id))
|
||
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 = "現在、来客はいません。"
|
||
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
|