class_name InnOpeningGuide extends PanelContainer ## 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) -> 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) 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(func() -> void: _body.visible = not _body.visible) 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")) _body.add_child(_detail) _action = Button.new() _action.add_theme_font_size_override("font_size", 13) _action.pressed.connect(func() -> void: action_requested.emit(StringName(_progress.get(&"action", &"")))) _body.add_child(_action) refresh() 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 facility.definition_id == &"standard_room": 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 翌朝の決算へ", "最初の宿泊売上が入りました。06: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(&"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 assigned := not reception.active_staff_ids.is_empty() # A repeating plan remains valid off-duty. Historical or cancelled shifts # cannot conceal a deliberate unassignment made before the first sale. for staff: StaffRuntime in _runtime.staff_service.get_staff_members(): var plan: Dictionary = _runtime.staff_service.get_work_assignment(staff.staff_id) if not plan.is_empty(): assigned = assigned or StringName(plan.get(&"facility_id", &"")) == reception.instance_id continue for shift: ShiftInstance in _runtime.staff_service.get_shifts(): if shift.staff_id == staff.staff_id and not shift.cancelled and shift.end_at > _clock.absolute_minute and shift.assigned_facility_id == reception.instance_id: assigned = true return assigned 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 job.definition_id == &"standard_room" 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 facility.definition_id == &"standard_room": 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 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}