Files
TheTree_mock/scripts/ui/event_log_dialog.gd
T

346 lines
15 KiB
GDScript

class_name EventLogDialog
extends AcceptDialog
## View the existing retained event stream. No catalog membership, unlock,
## replay, acquisition, or serialized state is created by this dialog.
const LayerSkin := preload("res://scripts/ui/layer_interface_skin.gd")
const EVENT_NAMES := {
&"AUTHORITY_ADVANCE_FAILED": "取締りの進行に失敗",
&"AUTHORITY_EVIDENCE_DISCOVERED": "証拠が発見された",
&"AUTHORITY_EVIDENCE_REGISTERED": "証拠を登録",
&"AUTHORITY_HEAT_CHANGED": "取締りの警戒度が変化",
&"AUTHORITY_INSPECTION_EVALUATED": "検査の状況を評価",
&"AUTHORITY_INSPECTION_EVALUATION_FAILED": "検査の評価に失敗",
&"AUTOSAVE_CHECKPOINT_FAILED": "自動保存に失敗",
&"AUTOSAVE_CHECKPOINT_SUCCEEDED": "自動保存が完了",
&"COMMAND_COMPLETED": "操作が完了",
&"COMMAND_FAILED": "操作に失敗",
&"DEFENSE_ADVANCE_FAILED": "防衛の進行に失敗",
&"DEFENSE_AUTOMATIC_ROUND_FAILED": "防衛の自動ラウンドに失敗",
&"DEFENSE_RAID_CREATED": "襲撃が発生",
&"DEFENSE_RAID_RESOLVED": "襲撃の決着",
&"DEFENSE_RAID_STATE_CHANGED": "襲撃の状況が変化",
&"DEFENSE_ROUND_RESOLVED": "防衛ラウンドの結果",
&"DEFENSE_SKILL_APPLIED": "防衛スキルを使用",
&"ELEVATOR_ARRIVED": "エレベーターが到着",
&"ELEVATOR_BOARDED": "エレベーターに乗車",
&"ELEVATOR_COMMUTE_CANCELLED": "エレベーター通勤を中止",
&"ELEVATOR_OPERATION_CHANGED": "エレベーターの営業設定を変更",
&"ELEVATOR_RESCUE_REQUESTED": "救護のエレベーター搬送を依頼",
&"ELEVATOR_TRIP_REQUESTED": "エレベーターの移動を依頼",
&"ENTITY_RECOVERY_TELEPORTED": "移動できない対象を復旧",
&"EVENT_CANCELLED": "イベントを中止",
&"EVENT_CHOICE_RESOLVED": "イベントの選択を処理",
&"EVENT_ENQUEUED": "イベントが待機",
&"EVENT_FINISHED": "イベントが終了",
&"EVENT_STARTED": "イベントが開始",
&"EXCAVATION_COMPLETED": "掘削が完了",
&"FACILITY_COMPLETED": "施設の建築が完了",
&"FACILITY_DEMOLISHED": "施設を撤去",
&"FACILITY_INFRASTRUCTURE_PROJECTED": "施設を交通設備に反映",
&"FACILITY_STATE_CHANGED": "施設の稼働状態が変化",
&"GUEST_AUTO_ARRIVED": "来客が到着",
&"GUEST_DECISION_RECORDED": "来客が行き先を検討",
&"GUEST_DESPAWNED": "来客が退館",
&"GUEST_MOVED": "来客が移動",
&"GUEST_QUEUE_ABANDONED": "来客が行列を離脱",
&"GUEST_RECOVERY_STARTED": "来客の移動復旧を開始",
&"GUEST_SERVICE_SYNC_FAILED": "来客のサービス状態更新に失敗",
&"GUEST_SPAWNED": "来客を登録",
&"GUEST_STATE_CHANGED": "来客の状態が変化",
&"INCIDENT_CREATED": "事件が発生",
&"INCIDENT_DETECTOR_ADDED": "事件の発見者を追加",
&"INSPECTION_PATH_BLOCKED": "検査員の移動経路が遮断",
&"INSPECTION_RESOLVED": "検査の決着",
&"INSPECTION_SCHEDULED": "検査が予定された",
&"INSPECTION_STARTED": "検査が開始",
&"INSPECTION_STATE_CHANGED": "検査の状況が変化",
&"INSPECTION_WAYPOINT_INSPECTED": "検査地点の調査が完了",
&"INSPECTION_WAYPOINT_REACHED": "検査員が検査地点に到着",
&"MANA_STATE_CHANGED": "魔力の供給状態が変化",
&"PATH_ACTOR_CROSSLINK_FAILED": "移動状態の整合確認に失敗",
&"PATH_ACTOR_STUCK": "移動が滞留",
&"RELATIONSHIP_REGISTERED": "人物の関係を登録",
&"RELATIONSHIP_ROUTE_LOCK_CHANGED": "人物のルート状態が変化",
&"RELATIONSHIP_SHIELD_CHANGED": "人物のシールド値が変化",
&"RELATIONSHIP_VALUE_CHANGED": "人物の関係値が変化",
&"SAVE_CHECKPOINT_FAILED": "保存に失敗",
&"SAVE_CHECKPOINT_SUCCEEDED": "保存が完了",
&"SAVE_LOADED": "保存データを読み込み",
&"SAVE_LOAD_FAILED": "保存データの読み込みに失敗",
&"SAVE_MIGRATION_FAILED": "保存データの移行に失敗",
&"SAVE_MIGRATION_SUCCEEDED": "保存データの移行が完了",
&"SAVE_RECOVERY_CANDIDATE_LOADED": "選択した復旧データを読み込み",
&"SAVE_RECOVERY_FAILED": "保存データの復旧に失敗",
&"SAVE_RECOVERY_LOADED": "保存データを復旧",
&"SERVICE_COMPLETED": "サービスが完了",
&"SERVICE_FAILED": "サービスに失敗",
&"SERVICE_INTERRUPTED": "サービスが中断",
&"SERVICE_STARTED": "サービスが開始",
&"STAFF_CANDIDATE_HIRED": "従業員を雇用",
&"STAFF_COLLAPSED": "従業員が倒れた",
&"STAFF_COLLAPSE_WITNESSED": "従業員の気絶が目撃された",
&"STAFF_MOVED": "従業員が移動",
&"STAFF_REGISTERED": "従業員を登録",
&"STAFF_REMOVED": "従業員が離脱",
&"STAFF_RESCUED": "従業員の救護が完了",
&"STAFF_RESCUE_PICKED_UP": "従業員の救護搬送を開始",
&"STAFF_SHIFT_REGISTERED": "勤務シフトを登録",
&"STAFF_SHIFT_STATE_CHANGED": "勤務シフトの状態が変化",
&"STAFF_TASK_ASSIGNED": "仕事を割り当て",
&"STAFF_TASK_COMPLETED": "仕事が完了",
&"STAFF_TASK_REGISTERED": "仕事を登録",
&"STAFF_TASK_REOPENED": "仕事を再募集",
&"STAFF_WORK_ASSIGNMENT_CHANGED": "従業員の配属を変更",
&"WORLD_TREE_RANK_ADVANCED": "世界樹のランクが上昇",
}
var _runtime: Variant
var _event_list: ItemList
var _details: Label
var _details_scroll: ScrollContainer
var _empty_label: Label
var _rows: Array[Dictionary] = []
var _selected_event_id := ""
var _skin_styles: Array[StyleBoxFlat] = []
func configure(runtime: Variant) -> void:
_runtime = runtime
func _ready() -> void:
name = "EventLogDialog"
title = "最近のイベント"
get_ok_button().text = "閉じる"
var column := VBoxContainer.new()
column.custom_minimum_size = Vector2(440, 300)
add_child(column)
_empty_label = Label.new()
_empty_label.name = "EventLogEmptyLabel"
_empty_label.text = "イベントの記録はまだありません。"
column.add_child(_empty_label)
_event_list = ItemList.new()
_event_list.name = "EventLogList"
_event_list.custom_minimum_size.y = 150
_event_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
_event_list.item_selected.connect(_select_row)
column.add_child(_event_list)
# The scroll viewport owns the fixed detail height; a wrapped Label which
# has not yet been laid out cannot inflate the first popup minimum height.
_details_scroll = ScrollContainer.new()
_details_scroll.name = "EventLogDetailsScroll"
_details_scroll.custom_minimum_size.y = 90
_details_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
column.add_child(_details_scroll)
_details = Label.new()
_details.name = "EventLogDetails"
_details.custom_minimum_size.x = 420
_details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_details.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_details_scroll.add_child(_details)
_build_skin()
apply_layer_skin(LayerSkin.active_layer(self))
func show_events() -> void:
refresh()
popup_centered(Vector2i(540, 400))
## Full defensive dictionaries are returned for ownership/restore checks.
## Publication order and every existing field/type are preserved.
func get_event_rows() -> Array[Dictionary]:
var rows: Array[Dictionary] = []
if not is_instance_valid(_runtime):
return rows
var stream: Variant = _runtime.get("domain_events")
if stream == null or not stream.has_method("recent_events"):
return rows
var seen_ids: Dictionary = {}
for event: SimulationDomainEvent in stream.recent_events():
var dict: Dictionary = event.to_dictionary()
var eid: String = String(dict.get("event_id", ""))
if not eid.is_empty():
if seen_ids.has(eid):
continue
seen_ids[eid] = true
rows.append(dict)
# Bound to recent 256 entries in strict sequence
if rows.size() > 256:
rows = rows.slice(rows.size() - 256)
return rows
func get_selected_event() -> Dictionary:
for row: Dictionary in _rows:
if String(row["event_id"]) == _selected_event_id:
return row.duplicate(true)
return {}
func refresh() -> void:
if _event_list == null:
return
_rows = get_event_rows()
_event_list.clear()
var selected_index := -1
for index: int in range(_rows.size()):
var row: Dictionary = _rows[index]
var presentation := get_event_presentation(row)
_event_list.add_item("%s %s — %s" % [presentation[&"time"], presentation[&"title"], presentation[&"targets"]])
_event_list.set_item_tooltip(index, presentation[&"supplement"])
_event_list.set_item_metadata(index, String(row["event_id"]))
if String(row["event_id"]) == _selected_event_id:
selected_index = index
_empty_label.visible = _rows.is_empty()
if selected_index < 0 and not _rows.is_empty():
selected_index = 0
if selected_index >= 0:
_event_list.select(selected_index)
_select_row(selected_index)
else:
_selected_event_id = ""
_details.text = ""
func _select_row(index: int) -> void:
if index < 0 or index >= _rows.size():
return
var row: Dictionary = _rows[index]
_selected_event_id = String(row["event_id"])
var presentation := get_event_presentation(row)
var detail_text := "%s\n%s\n対象(現在の名称):%s" % [
presentation[&"time"], presentation[&"title"], presentation[&"targets"],
]
if not String(presentation.get(&"extra", "")).is_empty():
detail_text += "\n" + String(presentation[&"extra"])
_details.text = detail_text
_details.tooltip_text = presentation[&"supplement"]
## This is presentation of existing notification facts, not a story catalog.
## Unknown kinds/targets remain readable placeholders; raw metadata is only a
## tooltip supplement. Names are resolved from current validated definitions.
func get_event_presentation(row: Dictionary) -> Dictionary:
var names: Array[String] = []
var raw_ids: Array[String] = []
var unknown_count := 0
for raw_id: Variant in row.get("entity_ids", []):
var entity_id := String(raw_id)
raw_ids.append(entity_id)
var resolved := _entity_name(StringName(entity_id))
if resolved.is_empty():
unknown_count += 1
else:
names.append(resolved)
if unknown_count > 0:
names.append("名称を確認できない対象(%d件)" % unknown_count)
var title_text := String(EVENT_NAMES.get(StringName(row.get("event_type", "")), "未識別の記録"))
var extras: Array[String] = []
# Check for amount / gold / cost
var payload: Dictionary = row.get("payload", {}) if row.get("payload") is Dictionary else {}
var amount_val: Variant = row.get("amount", payload.get("amount", row.get("gold", payload.get("gold", null))))
if amount_val != null and (amount_val is int or amount_val is float) and int(amount_val) != 0:
extras.append("金額:G%+d" % int(amount_val) if int(amount_val) < 0 else "金額:G%d" % int(amount_val))
# Check for reason
var reason_val: Variant = row.get("reason", payload.get("reason", row.get("error", payload.get("error", ""))))
if not String(reason_val).is_empty():
extras.append("理由:%s" % String(reason_val))
return {
&"title": title_text,
&"time": _format_minute(int(row.get("absolute_minute", 0))),
&"targets": "、".join(names) if not names.is_empty() else "対象の指定なし",
&"extra": " ".join(extras),
&"supplement": "補足情報\n記録:%s\n種別:%s\n対象:%s\n発行元:%s%s" % [
row.get("event_id", ""), row.get("event_type", ""),
"、".join(raw_ids), row.get("source_service", ""),
"\n" + "\n".join(extras) if not extras.is_empty() else "",
],
}
func _entity_name(entity_id: StringName) -> String:
if not is_instance_valid(_runtime):
return ""
var catalog: Variant = _runtime.get("mvp_content_catalog")
if catalog != null:
for content_type: StringName in [&"staff_candidates", &"unique_characters", &"guest_classes", &"species"]:
var entry: Dictionary = catalog.get_entry(content_type, entity_id)
if not entry.is_empty():
return String(entry.get(&"display_name", ""))
var facilities: Variant = _runtime.get("facility_service")
var definitions: Variant = _runtime.get("definition_registry")
if facilities != null and definitions != null:
var facility: Variant = facilities.get_instance(entity_id)
if facility != null:
var instance_definition: Variant = definitions.get_facility(facility.definition_id)
if instance_definition != null:
var floor_name := "%dF" % (facility.origin.y + 1) if facility.origin.y >= 0 else "B%d" % -facility.origin.y
return "%s %s・%s" % [instance_definition.display_name, floor_name, "客用" if facility.origin.z == 0 else "裏方"]
var registered_definition: Variant = definitions.get_facility(entity_id)
if registered_definition != null:
return String(registered_definition.display_name)
var guests: Variant = _runtime.get("guest_service")
if guests != null and guests.get_guest(entity_id) != null:
return "来客"
return ""
func _format_minute(minute: int) -> String:
return "%d日目 %02d:%02d" % [minute / 1440 + 1, (minute % 1440) / 60, minute % 60]
func _build_skin() -> void:
theme = Theme.new()
theme.set_stylebox("panel", "AcceptDialog", _skin_box(Color("f4e3bd"), &"panel_parchment"))
var border := _skin_box(Color("49311f"), &"panel_outer")
border.expand_margin_top = 32
theme.set_stylebox("embedded_border", "Window", border)
theme.set_color("title_color", "Window", Color("fff0cf"))
for label: Label in [_empty_label, _details]:
label.add_theme_color_override("font_color", Color("55371e"))
LayerSkin.register_text(label, 0)
_event_list.add_theme_stylebox_override("panel", _skin_box(Color("f4e3bd"), &"panel_parchment"))
# Reuse the existing dark button palette with light selected text.
for key: StringName in [&"selected", &"selected_focus"]:
_event_list.add_theme_stylebox_override(key, _skin_box(Color("65452d"), &"button_pressed"))
for key: StringName in [&"normal", &"hover", &"pressed", &"disabled", &"focus"]:
var role: StringName = &"button_normal"
if key in [&"hover", &"focus"]:
role = &"button_hover"
elif key == &"pressed":
role = &"button_pressed"
elif key == &"disabled":
role = &"button_disabled"
get_ok_button().add_theme_stylebox_override(key, _skin_box(Color("65452d"), role))
LayerSkin.register_toggle_text(get_ok_button(), 0)
func _skin_box(color: Color, role: StringName) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = color
style.border_color = Color("a96f2f")
style.set_border_width_all(2)
style.set_content_margin_all(10)
LayerSkin.register_style(style, role, 0)
_skin_styles.append(style)
return style
func apply_layer_skin(layer: int) -> void:
for style: StyleBoxFlat in _skin_styles:
LayerSkin.apply_style(style, layer)
for label: Label in [_empty_label, _details]:
if label != null:
LayerSkin.register_text(label, layer)
if _event_list != null:
_event_list.add_theme_color_override("font_color", Color("d8eeea") if layer == 1 else Color("55371e"))
_event_list.add_theme_color_override("font_selected_color", Color("fff0cf"))
LayerSkin.register_toggle_text(get_ok_button(), layer)