Files
TheTree_mock/scripts/ui/inn_opening_guide.gd
T
Takeshi 5dde0c5ec2 Record original-alignment implementation and traceability checkpoint
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.
2026-09-30 23:52:24 +09:00

211 lines
10 KiB
GDScript

class_name InnOpeningGuide
extends PanelContainer
const LayerSkin := preload("res://scripts/ui/layer_interface_skin.gd")
## Read-only guidance. Every action goes back through the normal dashboard.
signal action_requested(action: StringName)
var _runtime: Variant
var _clock: Variant
var _heading: Button
var _body: VBoxContainer
var _detail: Label
var _action: Button
var _progress: Dictionary = {}
var _sale_economy_id := 0
var _first_sale_minute := -1
func setup(runtime: Variant, clock: Variant, layer := 0) -> void:
_runtime = runtime
_clock = clock
name = "InnOpeningGuide"
custom_minimum_size.x = 290
var skin := StyleBoxFlat.new()
skin.bg_color = Color("30261be8")
skin.border_color = Color("b58a45")
skin.set_border_width_all(1)
skin.set_content_margin_all(8)
LayerSkin.register_style(skin, &"panel_leather", layer)
add_theme_stylebox_override("panel", skin)
var column := VBoxContainer.new()
column.add_theme_constant_override("separation", 5)
add_child(column)
_heading = Button.new()
_heading.alignment = HORIZONTAL_ALIGNMENT_LEFT
_heading.add_theme_font_size_override("font_size", 14)
_heading.pressed.connect(_toggle_body)
_style_button(_heading, layer)
column.add_child(_heading)
_body = VBoxContainer.new()
column.add_child(_body)
_detail = Label.new()
_detail.custom_minimum_size.x = 270
_detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_detail.add_theme_font_size_override("font_size", 13)
_detail.add_theme_color_override("font_color", Color("fff0cf"))
LayerSkin.register_text(_detail, layer)
_body.add_child(_detail)
_action = Button.new()
_action.add_theme_font_size_override("font_size", 13)
_style_button(_action, layer)
_action.pressed.connect(func() -> void: action_requested.emit(StringName(_progress.get(&"action", &""))))
_body.add_child(_action)
refresh()
func _toggle_body() -> void:
_body.visible = not _body.visible
reset_size.call_deferred()
func _style_button(button: Button, layer: int) -> void:
button.add_theme_color_override("font_color", Color("fff0cf"))
for state_name: StringName in [&"normal", &"hover", &"pressed", &"focus", &"disabled"]:
var style := StyleBoxFlat.new()
style.bg_color = Color("30261b") if state_name == &"normal" else Color("51402a")
style.border_color = Color("b58a45")
style.set_border_width_all(1)
style.set_content_margin_all(4)
LayerSkin.register_style(style, StringName("button_"+String(state_name if state_name != &"focus" else &"pressed")), layer)
button.add_theme_stylebox_override(state_name, style)
func refresh() -> void:
if _runtime == null or _heading == null:
return
_progress = _read_progress()
_heading.text = "開業の手引き · %s" % _progress[&"title"]
_detail.text = _progress[&"detail"]
_action.text = _progress[&"button"]
_action.visible = not String(_progress[&"action"]).is_empty()
# A collapsed guide must shrink as soon as its body is hidden.
reset_size()
func get_progress() -> Dictionary:
return _read_progress().duplicate(true) if _runtime != null else {}
func _read_progress() -> Dictionary:
var first_sale := _first_lodging_sale()
if first_sale >= 0 and _runtime.operations_coordinator != null:
for summary: Dictionary in _runtime.operations_coordinator.get_day_summaries():
if int(summary.get(&"closed_at", -1)) > first_sale:
return _step(&"complete", "初日の営業を達成", "宿泊売上と日次決算を確認できました。帳簿を見ながら、客室と従業員を少しずつ増やしましょう。", &"ledger", "決算を見る")
var reception: FacilityInstance
var rooms: Array[FacilityInstance] = []
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"reception":
if reception == null or _reception_score(facility) > _reception_score(reception):
reception = facility
elif _is_guest_lodging(facility.definition_id):
rooms.append(facility)
if reception == null:
return _build_step(&"reception", &"reception", "1/6 受付をつくる", "表の地表階に受付を配置します。完成した受付の上に客室を増築できます。", &"build_reception")
if rooms.is_empty():
return _build_step(&"room", &"standard_room", "2/6 客室をつくる", "受付が支えになる位置に一般客室を配置します。上階へは客用階段で道をつなぎます。", &"build_room")
var accessible_room := false
for room: FacilityInstance in rooms:
accessible_room = accessible_room or _can_reach_facility(room)
if not _can_reach_facility(reception) or not accessible_room:
var upper_room := false
for room: FacilityInstance in rooms:
upper_room = upper_room or room.origin.y != 0
return _step(&"access", "3/6 道をつなぐ", "上下階は客用階段でつなぎます。上階のマスに置くと、直下の通路へ接続します。同じ階は客用通路を延ばし、客室の左右の出入口まで道をつなぎましょう。", &"build_stairs" if upper_room else &"build_corridor", "客用階段を選ぶ" if upper_room else "客用通路を選ぶ")
if not _has_reception_assignment(reception):
return _step(&"staff", "4/6 受付係を配属", "従業員から受付係を雇い、雇用済み一覧でこの受付と勤務帯を選び「配属・勤務帯を変更」を押します。", &"staff", "従業員を開く")
if first_sale < 0:
return _step(&"sale", "5/6 最初の宿泊売上", "時間を進めて、お客さまの到着と宿泊を見守りましょう。売上は現在のサービスが完了すると記帳されます。", &"resume" if _clock.paused else &"ledger", "時間を進める" if _clock.paused else "帳簿を見る")
return _step(&"close", "6/6 深夜の決算へ", "最初の宿泊売上が入りました。00:00に営業日が締まり、給与・利益・未払いを帳簿で確認できます。", &"resume" if _clock.paused else &"ledger", "時間を進める" if _clock.paused else "帳簿を見る")
func _can_reach_facility(facility: FacilityInstance) -> bool:
if not facility.access_available:
return false
# Runtime uses the same public entrance and access cells as real guests.
# Component-only guide fixtures retain their explicit availability flags.
if _runtime.has_method(&"is_facility_reachable"):
return bool(_runtime.call(&"is_facility_reachable", facility.instance_id))
if _runtime.has_method(&"get_facility_access_report"):
var report: Dictionary = _runtime.call(&"get_facility_access_report", facility.instance_id)
return bool(report.get(&"reachable", false))
return true
func _reception_score(facility: FacilityInstance) -> int:
return (2 if _can_reach_facility(facility) else 0) + (1 if _has_reception_assignment(facility) else 0)
func _has_reception_assignment(reception: FacilityInstance) -> bool:
var definition: FacilityDefinition = _runtime.definition_registry.get_facility(reception.definition_id)
if definition == null:
return false
var unplanned_staff: Dictionary = {}
# Repeating plans remain valid off-duty; explicit plans take precedence
# over historical shifts. Management placement is not receptionist work.
for staff: StaffRuntime in _runtime.staff_service.get_staff_members():
if not definition.required_roles.is_empty() and staff.role_id not in definition.required_roles:
continue
if staff.staff_id in reception.active_staff_ids:
return true
var plan: Dictionary = _runtime.staff_service.get_work_assignment(staff.staff_id)
if not plan.is_empty():
if StringName(plan.get(&"facility_id", &"")) == reception.instance_id:
return true
else:
unplanned_staff[staff.staff_id] = true
if unplanned_staff.is_empty():
return false
# Enumerate/sort the historical shifts once, not once per employee.
for shift: ShiftInstance in _runtime.staff_service.get_shifts():
if unplanned_staff.has(shift.staff_id) and not shift.cancelled and shift.end_at > _clock.absolute_minute and shift.assigned_facility_id == reception.instance_id:
return true
return false
func _is_guest_lodging(definition_id: StringName) -> bool:
var definition: FacilityDefinition = _runtime.definition_registry.get_facility(definition_id)
return definition != null and definition.layer == FacilityDefinition.ViewLayer.FRONT and (
definition.service_types.has(&"lodging") or definition.tags.has(&"lodging")
)
func _first_lodging_sale() -> int:
# Once found, an append-only ledger cannot revoke historical first revenue.
# Restore/reset replaces EconomyService, which invalidates this visual cache.
var economy_id: int = _runtime.economy_service.get_instance_id()
if economy_id != _sale_economy_id:
_sale_economy_id = economy_id
_first_sale_minute = -1
if _first_sale_minute >= 0:
return _first_sale_minute
var room_ids: Dictionary = {}
for job: ConstructionJob in _runtime.construction_service.get_jobs():
if _is_guest_lodging(job.definition_id) and not job.facility_instance_id.is_empty():
room_ids[job.facility_instance_id] = true
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if _is_guest_lodging(facility.definition_id):
room_ids[facility.instance_id] = true
if room_ids.is_empty():
return -1
for entry: LedgerEntry in _runtime.economy_service.get_entries():
if entry.category == &"SERVICE_SALE" and room_ids.has(entry.source_entity_id):
_first_sale_minute = entry.absolute_minute if _first_sale_minute < 0 else mini(_first_sale_minute, entry.absolute_minute)
return _first_sale_minute
func _build_step(stage: StringName, definition_id: StringName, title: String, detail: String, action: StringName) -> Dictionary:
for job: ConstructionJob in _runtime.construction_service.get_jobs():
if (job.definition_id == definition_id or (stage == &"room" and _is_guest_lodging(job.definition_id))) and job.state not in [ConstructionJob.State.COMPLETED, ConstructionJob.State.CANCELLED]:
return _step(stage, title, "工事中 %d%%。時間を進めると完成します。" % (job.progress / 100), &"resume" if _clock.paused else &"", "時間を進める")
var definition: FacilityDefinition = _runtime.definition_registry.get_facility(definition_id)
if definition != null:
detail += "\n費用 %dG / 資材 %d" % [int(definition.construction_cost.get(&"gold", 0)), int(definition.construction_cost.get(&"materials", 0))]
return _step(stage, title, detail, action, "受付を選ぶ" if definition_id == &"reception" else "一般客室を選ぶ")
func _step(stage: StringName, title: String, detail: String, action: StringName, button: String) -> Dictionary:
return {&"stage": stage, &"title": title, &"detail": detail, &"action": action, &"button": button}