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.
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
class_name BusinessManagementPanel
|
||||
extends VBoxContainer
|
||||
|
||||
const LayerSkin := preload("res://scripts/ui/layer_interface_skin.gd")
|
||||
|
||||
## Policy drafts are local; accepted commands and read models belong to Runtime.
|
||||
signal status_requested(message: String, success: bool)
|
||||
signal action_requested(action: StringName)
|
||||
|
||||
const KEY_PREFIX := "ui.business."
|
||||
const COMMAND: StringName = &"set_facility_policy"
|
||||
const GUEST_POLICY_EXPLANATION := "料金変更は新規受付から。受付済みの料金は変わりません。\n休業すると待機中のお客様は別の施設を探します。開始済みのサービスは完了まで続きます。休業中も給与は発生します。"
|
||||
const SUPPLY_POLICY_EXPLANATION := "休業中は調理と新しい補給を止めます。在庫と運搬中の荷物は保持し、積込済みの荷物は納品先の営業状況に従って配送します。消費期限は経過し、給与も発生します。"
|
||||
const GUEST_POLICY_EXPLANATION := "料金変更は新規受付から。受付済みの料金は変わりません。\n休業すると待機中のお客様は別の施設を探します。開始済みのサービスは完了まで続きます。休業中も給与と施設維持費は発生します。"
|
||||
const SUPPLY_POLICY_EXPLANATION := "休業中は調理と新しい補給を止めます。在庫と運搬中の荷物は保持し、積込済みの荷物は納品先の営業状況に従って配送します。消費期限は経過し、給与と施設維持費も発生します。"
|
||||
const STATE_NAMES := {&"BROKEN": "故障中", &"NO_ACCESS": "経路なし", &"NO_POWER": "魔力不足", &"NO_STAFF": "職員不足", &"CLOSED": "休業中", &"DEGRADED": "状態悪化", &"OPEN": "稼働中"}
|
||||
const REASONS := {
|
||||
&"FACILITY_NOT_FOUND": "施設が見つかりません。選び直してください。",
|
||||
@@ -53,7 +55,9 @@ func _ready() -> void:
|
||||
_build_interface()
|
||||
_built = true
|
||||
if _runtime != null:
|
||||
if _runtime.has_signal(&"snapshot_published"):
|
||||
if _runtime.has_signal(&"state_updated"):
|
||||
_runtime.connect(&"state_updated", _on_snapshot_published)
|
||||
elif _runtime.has_signal(&"snapshot_published"):
|
||||
_runtime.connect(&"snapshot_published", _on_snapshot_published)
|
||||
if _runtime.has_signal(&"command_completed"):
|
||||
_runtime.connect(&"command_completed", _on_command_completed)
|
||||
@@ -85,6 +89,7 @@ func _build_interface() -> void:
|
||||
for color_name: StringName in [&"font_color", &"font_pressed_color", &"font_hover_color", &"font_hover_pressed_color", &"font_focus_color"]:
|
||||
business_open_button.add_theme_color_override(color_name, Color("55371e"))
|
||||
business_open_button.add_theme_color_override("font_disabled_color", Color("766650"))
|
||||
LayerSkin.register_toggle_text(business_open_button, LayerSkin.active_layer(self))
|
||||
business_open_button.add_theme_font_size_override("font_size", 13)
|
||||
business_open_button.toggled.connect(func(_enabled: bool) -> void: _store_draft())
|
||||
add_child(business_open_button)
|
||||
@@ -129,9 +134,13 @@ func refresh() -> void:
|
||||
business_open_button.set_pressed_no_signal(bool(draft.get(&"business_open", true)))
|
||||
var pending := _has_pending(selected_facility_id)
|
||||
var can_price := bool(row.get(&"can_price", false))
|
||||
var is_cleaning := StringName(row.get(&"definition_id", &"")) == &"cleaning_room"
|
||||
var is_supply := StringName(row.get(&"definition_id", &"")) in [&"kitchen", &"warehouse"]
|
||||
business_open_button.text = "営業する" if is_supply else "新規の受付を開く"
|
||||
explanation_label.text = SUPPLY_POLICY_EXPLANATION if is_supply else GUEST_POLICY_EXPLANATION
|
||||
if is_cleaning:
|
||||
business_open_button.text = "清掃業務を有効にする"
|
||||
explanation_label.text = "配置した清掃員が汚れた施設へ向かいます。清掃業務を無効にすると、途中の清掃も中断します。維持費と給与は継続します。"
|
||||
facility_option.disabled = _rows(&"facilities").is_empty()
|
||||
price_option.disabled = row.is_empty() or not can_price or pending or price_option.item_count == 0
|
||||
business_open_button.disabled = row.is_empty() or pending
|
||||
@@ -141,21 +150,36 @@ func refresh() -> void:
|
||||
facility_status_label.text = "施設が完成すると営業を設定できます。"
|
||||
quote_label.text = "施設を選んでください。"
|
||||
effect_label.text = ""
|
||||
if int(row.get(&"faith_per_service", 0)) > 0:
|
||||
effect_label.text += "\n礼拝完了ごとに信仰+%d" % int(row[&"faith_per_service"])
|
||||
today_label.text = "本日の実績はまだありません。"
|
||||
else:
|
||||
facility_status_label.text = "%s · %s\n待ち %d人 / 利用中 %d人" % [("営業する設定" if is_supply else "受付を開く設定") if bool(row.get(&"business_open", true)) else ("休業する設定" if is_supply else "新規受付を停止中"), String(STATE_NAMES.get(StringName(row.get(&"state", &"")), "状態確認中")), int(row.get(&"queued_count", 0)), int(row.get(&"active_count", 0))]
|
||||
facility_status_label.text = "%s · %s\n待ち %d人 / 利用中 %d人\n同時利用上限 %s" % [("営業する設定" if is_supply else "受付を開く設定") if bool(row.get(&"business_open", true)) else ("休業する設定" if is_supply else "新規受付を停止中"), String(STATE_NAMES.get(StringName(row.get(&"state", &"")), "状態確認中")), int(row.get(&"queued_customer_count", 0)), int(row.get(&"active_customer_count", 0)), ("%d人" % int(row[&"customer_capacity"])) if int(row.get(&"customer_capacity", -1)) >= 0 else "未設定"]
|
||||
if is_supply:
|
||||
facility_status_label.text = "%s · %s\n保管・補給の状況は「商品」で確認。" % ["営業する設定" if bool(row.get(&"business_open", true)) else "休業する設定", String(STATE_NAMES.get(StringName(row.get(&"state", &"")), "状態確認中"))]
|
||||
if is_cleaning:
|
||||
facility_status_label.text = "%s · %s\n顧客利用なし・清掃員の業務拠点" % ["清掃業務を有効にする設定" if bool(row.get(&"business_open", true)) else "清掃業務を停止中", String(STATE_NAMES.get(StringName(row.get(&"state", &"")), "状態確認中"))]
|
||||
if int(row.get(&"cleanliness", -1)) >= 0:
|
||||
facility_status_label.text += "\n清潔度 %d / 100" % int(row[&"cleanliness"])
|
||||
if not String(row.get(&"cleaning_status", "")).is_empty():
|
||||
facility_status_label.text += "\n" + String(row[&"cleaning_status"])
|
||||
if can_price:
|
||||
var mode := StringName(draft.get(&"price_mode", &"standard"))
|
||||
var quoted := FacilityPricing.quote(int(row.get(&"base_price", 0)), {&"price_mode": mode})
|
||||
var unit := String({&"standard_room": "泊", &"cafeteria": "食"}.get(StringName(row.get(&"definition_id", &"")), "回"))
|
||||
var quoted := FacilityPricing.apply_multiplier(FacilityPricing.quote(int(row.get(&"base_price", 0)), {&"price_mode": mode}) * int(row.get(&"donation_multiplier", 1)), int(row.get(&"floor_price_multiplier_bp", 10000)))
|
||||
var unit := String({&"standard_room": "泊", &"premium_room": "泊", &"cafeteria": "食"}.get(StringName(row.get(&"definition_id", &"")), "回"))
|
||||
quote_label.text = "設定後 %dG / %s\n現在 %dG · 標準 %dG" % [quoted, unit, int(row.get(&"price", 0)), int(row.get(&"base_price", 0))]
|
||||
effect_label.text = _price_effect(mode)
|
||||
if int(row.get(&"donation_multiplier", 1)) == 3:
|
||||
effect_label.text += "\nアメリア配置:寄付金3倍"
|
||||
if int(row.get(&"floor_price_multiplier_bp", 10000)) == 12000:
|
||||
effect_label.text += "\nアンリ配置フロア:利用料金+20%"
|
||||
else:
|
||||
quote_label.text = "この施設に利用料金はありません。"
|
||||
effect_label.text = "営業・休業を設定できます。" if is_supply else "受付の開閉だけを設定できます。"
|
||||
today_label.text = "本日の記帳 · 06:00〜\n売上 %dG · 直接支出 %dG\n給与の配賦と利益は翌朝の帳簿で確認。" % [int(row.get(&"today_revenue", 0)), int(row.get(&"today_expenses", 0))]
|
||||
if is_cleaning: effect_label.text = "清掃業務の有効・無効を設定できます。"
|
||||
if int(row.get(&"faith_per_service", 0)) > 0:
|
||||
effect_label.text += "\n礼拝完了ごとに信仰+%d" % int(row[&"faith_per_service"])
|
||||
today_label.text = "本日の記帳 · 00:00〜\n売上 %dG · 直接支出 %dG\n給与の配賦と利益は翌日の帳簿で確認。" % [int(row.get(&"today_revenue", 0)), int(row.get(&"today_expenses", 0))]
|
||||
_refreshing = false
|
||||
|
||||
|
||||
@@ -263,7 +287,7 @@ func _on_command_completed(result: Variant) -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
func _on_snapshot_published(_snapshot: Dictionary, _digest: String) -> void:
|
||||
func _on_snapshot_published(_snapshot: Dictionary = {}, _digest: String = "") -> void:
|
||||
if is_visible_in_tree():
|
||||
refresh()
|
||||
|
||||
@@ -344,7 +368,7 @@ func _facility_row(id: StringName) -> Dictionary:
|
||||
|
||||
func _set_result(message: String, success: bool) -> void:
|
||||
result_label.text = message
|
||||
result_label.add_theme_color_override("font_color", Color("397346") if success else Color("a7402e"))
|
||||
LayerSkin.register_status_text(result_label, success, LayerSkin.active_layer(self))
|
||||
status_requested.emit(message, success)
|
||||
|
||||
|
||||
@@ -357,6 +381,7 @@ func _label(value: String, node_name := "") -> Label:
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
label.add_theme_font_size_override("font_size", 13)
|
||||
label.add_theme_color_override("font_color", Color("55371e"))
|
||||
LayerSkin.register_text(label, LayerSkin.active_layer(self))
|
||||
return label
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
class_name DefenseManagementPanel
|
||||
extends VBoxContainer
|
||||
|
||||
const LayerSkin := preload("res://scripts/ui/layer_interface_skin.gd")
|
||||
|
||||
## Player controls for an incident that already exists. Threat creation,
|
||||
## combat balancing, skill authoring and automatic combat belong to Runtime.
|
||||
signal status_requested(message: String, success: bool)
|
||||
|
||||
const KEY_PREFIX := "ui.defense."
|
||||
const COMMANDS := [&"register_defense_combatant", &"register_defense_asset", &"start_defense_raid", &"set_defense_retreat_policy", &"begin_next_defense_wave", &"retreat_defenders"]
|
||||
const STATE_NAMES := ["準備中", "戦闘中", "次の襲撃待ち", "撃退", "突破された", "撤退", "準備終了"]
|
||||
const SOURCE_NAMES := {"THIEF": "盗賊の襲撃", "BANKRUPTCY_RIOT": "客の暴動", "BANK_DELINQUENCY": "銀行による取り立て", "AUTHORITY_ESCALATION": "監査による摘発"}
|
||||
const POLICIES := [&"HOLD", &"MANUAL", &"THRESHOLD", &"PROTECT_AND_RETREAT"]
|
||||
const POLICY_LABELS := ["持ち場を守る", "自分で撤退を指示", "体力が減ったら撤退", "体力・施設の損傷で撤退"]
|
||||
|
||||
var selected_raid_id: StringName = &""
|
||||
var selected_staff_id: StringName = &""
|
||||
var selected_objective_id: StringName = &""
|
||||
var raid_option: OptionButton
|
||||
var staff_option: OptionButton
|
||||
var objective_option: OptionButton
|
||||
var policy_option: OptionButton
|
||||
var threshold_spin: SpinBox
|
||||
var summary_label: Label
|
||||
var defenders_label: Label
|
||||
var assets_label: Label
|
||||
var staff_label: Label
|
||||
var objective_label: Label
|
||||
var result_label: Label
|
||||
var register_staff_button: Button
|
||||
var register_objective_button: Button
|
||||
var start_button: Button
|
||||
var policy_button: Button
|
||||
var next_wave_button: Button
|
||||
var retreat_button: Button
|
||||
var confirmation: ConfirmationDialog
|
||||
|
||||
var _runtime: Variant
|
||||
var _style_button: Callable
|
||||
var _report: Dictionary = {}
|
||||
var _raid_labels: Dictionary = {}
|
||||
var _pending: Dictionary = {}
|
||||
var _queue_instance_id := 0
|
||||
var _sequence := 1
|
||||
var _built := false
|
||||
var _confirmation_context: Dictionary = {}
|
||||
var _policy_drafts: Dictionary = {}
|
||||
var _refreshing := false
|
||||
|
||||
|
||||
func configure(runtime: Variant, style_button := Callable()) -> void:
|
||||
_runtime = runtime
|
||||
_style_button = style_button
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
name = "DefenseManagementPanel"
|
||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
add_theme_constant_override("separation", 6)
|
||||
_build_interface()
|
||||
_built = true
|
||||
if _runtime != null:
|
||||
if _runtime.has_signal(&"state_updated"):
|
||||
_runtime.connect(&"state_updated", _on_snapshot_published)
|
||||
elif _runtime.has_signal(&"snapshot_published"):
|
||||
_runtime.connect(&"snapshot_published", _on_snapshot_published)
|
||||
if _runtime.has_signal(&"command_completed"):
|
||||
_runtime.connect(&"command_completed", _on_command_completed)
|
||||
refresh()
|
||||
|
||||
|
||||
func _build_interface() -> void:
|
||||
raid_option = _option("DefenseRaidOption")
|
||||
raid_option.fit_to_longest_item = false
|
||||
raid_option.clip_text = true
|
||||
raid_option.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
raid_option.item_selected.connect(func(index: int) -> void: select_raid(StringName(raid_option.get_item_metadata(index))))
|
||||
add_child(raid_option)
|
||||
summary_label = _label("", "DefenseSummary")
|
||||
add_child(summary_label)
|
||||
defenders_label = _label("", "DefenseParticipants")
|
||||
add_child(defenders_label)
|
||||
assets_label = _label("", "DefenseAssets")
|
||||
add_child(assets_label)
|
||||
add_child(HSeparator.new())
|
||||
add_child(_label("参加する従業員"))
|
||||
staff_option = _option("DefenseStaffOption")
|
||||
staff_option.item_selected.connect(func(index: int) -> void:
|
||||
selected_staff_id = StringName(staff_option.get_item_metadata(index))
|
||||
_refresh_controls())
|
||||
add_child(staff_option)
|
||||
staff_label = _label("", "DefenseStaffStatus")
|
||||
add_child(staff_label)
|
||||
register_staff_button = _button("防衛に参加", "DefenseRegisterStaff", _register_staff)
|
||||
add_child(register_staff_button)
|
||||
add_child(_label("防衛する施設"))
|
||||
objective_option = _option("DefenseObjectiveOption")
|
||||
objective_option.item_selected.connect(func(index: int) -> void:
|
||||
selected_objective_id = StringName(objective_option.get_item_metadata(index))
|
||||
_refresh_controls())
|
||||
add_child(objective_option)
|
||||
objective_label = _label("", "DefenseObjectiveStatus")
|
||||
add_child(objective_label)
|
||||
register_objective_button = _button("防衛対象に登録", "DefenseRegisterObjective", _register_objective)
|
||||
add_child(register_objective_button)
|
||||
add_child(HSeparator.new())
|
||||
add_child(_label("撤退の方針"))
|
||||
policy_option = _option("DefenseRetreatPolicy")
|
||||
for i: int in POLICIES.size():
|
||||
policy_option.add_item(POLICY_LABELS[i])
|
||||
policy_option.set_item_metadata(i, POLICIES[i])
|
||||
policy_option.item_selected.connect(func(_index: int) -> void: _store_policy_draft())
|
||||
add_child(policy_option)
|
||||
var threshold_row := HBoxContainer.new()
|
||||
threshold_row.add_child(_label("撤退する残量(%)"))
|
||||
threshold_spin = SpinBox.new()
|
||||
threshold_spin.name = "DefenseRetreatThreshold"
|
||||
threshold_spin.min_value = 0
|
||||
threshold_spin.max_value = 100
|
||||
threshold_spin.step = 1
|
||||
threshold_spin.value_changed.connect(func(_value: float) -> void: _store_policy_draft())
|
||||
threshold_row.add_child(threshold_spin)
|
||||
add_child(threshold_row)
|
||||
policy_button = _button("撤退方針を適用", "DefenseApplyPolicy", _apply_policy)
|
||||
add_child(policy_button)
|
||||
var actions := HBoxContainer.new()
|
||||
start_button = _button("防衛を開始", "DefenseStart", func() -> void: _submit(&"start_defense_raid"))
|
||||
next_wave_button = _button("次の襲撃に対応", "DefenseNextWave", func() -> void: _submit(&"begin_next_defense_wave"))
|
||||
retreat_button = _button("撤退する", "DefenseRetreat", _request_retreat_confirmation)
|
||||
for button: Button in [start_button, next_wave_button, retreat_button]:
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
actions.add_child(button)
|
||||
add_child(actions)
|
||||
result_label = _label("", "DefenseResult")
|
||||
add_child(result_label)
|
||||
confirmation = ConfirmationDialog.new()
|
||||
confirmation.name = "DefenseConfirmation"
|
||||
confirmation.ok_button_text = "実行する"
|
||||
confirmation.cancel_button_text = "戻る"
|
||||
confirmation.confirmed.connect(_confirm_operation)
|
||||
confirmation.canceled.connect(func() -> void: _confirmation_context.clear())
|
||||
add_child(confirmation)
|
||||
confirmation.get_label().add_theme_color_override("font_color", Color("55371e"))
|
||||
LayerSkin.register_text(confirmation.get_label(), LayerSkin.active_layer(self))
|
||||
if _style_button.is_valid():
|
||||
for button: Button in [confirmation.get_ok_button(), confirmation.get_cancel_button()]:
|
||||
_style_button.call(button)
|
||||
|
||||
|
||||
func select_raid(raid_id: StringName) -> void:
|
||||
selected_raid_id = raid_id
|
||||
selected_staff_id = &""
|
||||
selected_objective_id = &""
|
||||
_confirmation_context.clear()
|
||||
if confirmation != null: confirmation.hide()
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
if not _built:
|
||||
return
|
||||
var queue: Variant = _runtime.get(&"command_queue") if _runtime != null else null
|
||||
if queue != null and queue.get_instance_id() != _queue_instance_id:
|
||||
_queue_instance_id = queue.get_instance_id()
|
||||
_policy_drafts.clear()
|
||||
_confirmation_context.clear()
|
||||
confirmation.hide()
|
||||
for option: OptionButton in [raid_option, staff_option, objective_option, policy_option]:
|
||||
option.get_popup().hide()
|
||||
_synchronize_commands(queue)
|
||||
_report = _runtime.call(&"get_defense_management_report", selected_raid_id) if _runtime != null and _runtime.has_method(&"get_defense_management_report") else {}
|
||||
_raid_labels = _build_raid_labels(_report.get(&"raids", []))
|
||||
selected_raid_id = StringName((_report.get(&"selected", {}) as Dictionary).get(&"raid_id", &""))
|
||||
selected_raid_id = _fill_option(raid_option, _report.get(&"raids", []), &"raid_id", selected_raid_id)
|
||||
selected_staff_id = _fill_option(staff_option, _report.get(&"staff", []), &"staff_id", selected_staff_id)
|
||||
selected_objective_id = _fill_option(objective_option, _report.get(&"objectives", []), &"facility_id", selected_objective_id)
|
||||
var row: Dictionary = _report.get(&"selected", {})
|
||||
var draft: Dictionary = _policy_drafts.get(selected_raid_id, row)
|
||||
var policy := StringName(draft.get(&"retreat_policy", &"HOLD"))
|
||||
_refreshing = true
|
||||
policy_option.select(maxi(POLICIES.find(policy), 0))
|
||||
threshold_spin.set_value_no_signal(int(draft.get(&"defender_retreat_threshold", 0)))
|
||||
_refreshing = false
|
||||
summary_label.text = "進行中・保存済みの防衛戦はありません。" if row.is_empty() else "%s · %s\n襲撃 %d / %d · 攻防 %d回\n%s" % [String(_raid_labels.get(selected_raid_id, _raid_base_label(row))), STATE_NAMES[int(row[&"state"])], int(row[&"current_wave"]), int(row[&"wave_count"]), int(row[&"current_round"]), "防衛対象が未登録です。" if not (row[&"missing_objectives"] as Array).is_empty() else "防衛対象は登録済みです。"]
|
||||
var names: Array[String] = []
|
||||
for defender: Dictionary in _report.get(&"defenders", []):
|
||||
var employee := _row(&"staff", &"staff_id", defender[&"actor_id"])
|
||||
names.append("%s 体力%d/%d・戦闘%d" % [employee.get(&"display_name", defender[&"actor_id"]), defender[&"health"], defender[&"max_health"], defender[&"combat"]])
|
||||
defenders_label.text = "参加者:" + ("、".join(names) if not names.is_empty() else "未登録")
|
||||
names.clear()
|
||||
for asset: Dictionary in _report.get(&"assets", []):
|
||||
var facility := _row(&"objectives", &"facility_id", asset[&"asset_id"])
|
||||
names.append("%s 耐久%d/%d" % [facility.get(&"display_name", asset[&"asset_id"]), asset[&"integrity"], asset[&"max_integrity"]])
|
||||
assets_label.text = "対象:" + ("、".join(names) if not names.is_empty() else "未登録")
|
||||
_refresh_controls()
|
||||
|
||||
|
||||
func _refresh_controls() -> void:
|
||||
var row: Dictionary = _report.get(&"selected", {})
|
||||
var state := int(row.get(&"state", -1))
|
||||
var pending := _has_pending(selected_raid_id)
|
||||
var blocked := row.is_empty() or pending or not bool(_report.get(&"can_control", false))
|
||||
var staff := _row(&"staff", &"staff_id", selected_staff_id)
|
||||
var objective := _row(&"objectives", &"facility_id", selected_objective_id)
|
||||
staff_label.text = "従業員がいません。" if staff.is_empty() else "体力%d/100・戦闘%d\n%s" % [staff[&"health"], staff[&"combat"], "参加できます。参加中は配属を変更できません。" if staff[&"can_register"] else staff[&"reason"]]
|
||||
objective_label.text = "防衛対象がありません。" if objective.is_empty() else "耐久%d/10000\n%s" % [objective[&"durability"], "登録できます。" if objective[&"can_register"] else objective[&"reason"]]
|
||||
register_staff_button.disabled = blocked or staff.is_empty() or not bool(staff.get(&"can_register", false))
|
||||
register_objective_button.disabled = blocked or objective.is_empty() or not bool(objective.get(&"can_register", false))
|
||||
start_button.visible = state == DefenseService.RaidState.PREPARING
|
||||
start_button.disabled = blocked or not (row.get(&"missing_objectives", []) as Array).is_empty()
|
||||
next_wave_button.visible = state == DefenseService.RaidState.BETWEEN_WAVES
|
||||
next_wave_button.disabled = blocked
|
||||
retreat_button.visible = state in [DefenseService.RaidState.ACTIVE, DefenseService.RaidState.BETWEEN_WAVES]
|
||||
retreat_button.disabled = blocked
|
||||
var terminal := state in DefenseService.TERMINAL_STATES or state < 0
|
||||
policy_option.disabled = blocked or terminal
|
||||
var chosen_policy := StringName(policy_option.get_item_metadata(policy_option.selected))
|
||||
threshold_spin.editable = not blocked and not terminal and chosen_policy in [&"THRESHOLD", &"PROTECT_AND_RETREAT"]
|
||||
policy_button.disabled = blocked or terminal
|
||||
staff_option.disabled = blocked or state != DefenseService.RaidState.PREPARING or staff.is_empty()
|
||||
objective_option.disabled = blocked or state != DefenseService.RaidState.PREPARING or objective.is_empty()
|
||||
if pending: result_label.text = "指示を受理しました。次の時間更新で反映します。"
|
||||
|
||||
|
||||
func _register_staff() -> void:
|
||||
var staff := _row(&"staff", &"staff_id", selected_staff_id)
|
||||
if staff.is_empty() or not bool(staff.get(&"can_register", false)): return
|
||||
_submit(&"register_defense_combatant", {&"actor_id": selected_staff_id, &"side": &"DEFENDER", &"combat": int(staff[&"combat"])})
|
||||
|
||||
|
||||
func _register_objective() -> void:
|
||||
var facility := _row(&"objectives", &"facility_id", selected_objective_id)
|
||||
if facility.is_empty() or not bool(facility.get(&"can_register", false)): return
|
||||
# Plain objectives have no defense-device bonus. Omit numeric overrides and
|
||||
# use the existing Runtime command defaults and current live durability.
|
||||
_submit(&"register_defense_asset", {&"asset_id": selected_objective_id, &"kind": &"OBJECTIVE"})
|
||||
|
||||
|
||||
func _apply_policy() -> void:
|
||||
_submit(&"set_defense_retreat_policy", {&"policy": policy_option.get_item_metadata(policy_option.selected), &"threshold": int(threshold_spin.value)})
|
||||
|
||||
|
||||
func _store_policy_draft() -> void:
|
||||
if _refreshing or selected_raid_id.is_empty() or _has_pending(selected_raid_id): return
|
||||
_policy_drafts[selected_raid_id] = {&"retreat_policy": policy_option.get_item_metadata(policy_option.selected), &"defender_retreat_threshold": int(threshold_spin.value)}
|
||||
_refresh_controls()
|
||||
|
||||
|
||||
func _request_retreat_confirmation() -> void:
|
||||
refresh()
|
||||
var row: Dictionary = _report.get(&"selected", {})
|
||||
if _has_pending(selected_raid_id) or selected_raid_id.is_empty() or not bool(_report.get(&"can_control", false)) or int(row.get(&"state", -1)) not in [DefenseService.RaidState.ACTIVE, DefenseService.RaidState.BETWEEN_WAVES]: return
|
||||
_confirmation_context = {&"raid_id": selected_raid_id, &"queue_instance_id": _queue_instance_id}
|
||||
confirmation.title = "撤退の確認"
|
||||
confirmation.dialog_text = "撤退すると防衛戦は終了します。参加者の状態と施設の損傷が反映されます。"
|
||||
confirmation.popup_centered(Vector2i(430, 170))
|
||||
|
||||
|
||||
func _confirm_operation() -> void:
|
||||
var context := _confirmation_context.duplicate()
|
||||
_confirmation_context.clear()
|
||||
var queue: Variant = _runtime.get(&"command_queue") if _runtime != null else null
|
||||
if queue == null or queue.get_instance_id() != int(context.get(&"queue_instance_id", 0)) or context.get(&"raid_id", &"") != selected_raid_id:
|
||||
_set_result("対象が変わりました。防衛戦を選び直してください。", false)
|
||||
refresh()
|
||||
return
|
||||
refresh()
|
||||
if context.get(&"raid_id", &"") != selected_raid_id:
|
||||
_set_result("対象が変わりました。防衛戦を選び直してください。", false)
|
||||
return
|
||||
var row: Dictionary = _report.get(&"selected", {})
|
||||
if _has_pending(selected_raid_id) or not bool(_report.get(&"can_control", false)) or int(row.get(&"state", -1)) not in [DefenseService.RaidState.ACTIVE, DefenseService.RaidState.BETWEEN_WAVES]:
|
||||
_set_result("現在の防衛戦では撤退を指示できません。", false)
|
||||
return
|
||||
_submit(&"retreat_defenders")
|
||||
|
||||
|
||||
func _submit(command_type: StringName, patch: Dictionary = {}) -> void:
|
||||
if selected_raid_id.is_empty() or _has_pending(selected_raid_id) or command_type not in COMMANDS: return
|
||||
var payload := patch.duplicate(true)
|
||||
payload[&"raid_id"] = selected_raid_id
|
||||
var key := StringName("%s%08d" % [KEY_PREFIX, _sequence])
|
||||
_sequence += 1
|
||||
var result: Variant = _runtime.call(&"submit_command", command_type, payload, &"player", selected_raid_id, 75, -1, key)
|
||||
if result == null or not result.has_method(&"is_success"):
|
||||
_set_result("防衛の指示に応答がありません。", false)
|
||||
return
|
||||
var queue: Variant = _runtime.get(&"command_queue")
|
||||
var queued := false
|
||||
for command: Dictionary in queue.create_snapshot().get("pending", []):
|
||||
queued = queued or int(command.get("command_id", 0)) == int(result.command_id)
|
||||
if result.is_success() and queued:
|
||||
_pending[int(result.command_id)] = payload
|
||||
else:
|
||||
_show_result(result, payload)
|
||||
refresh()
|
||||
|
||||
|
||||
func _show_result(result: Variant, payload: Dictionary) -> void:
|
||||
if result.is_success() and payload.has(&"policy"):
|
||||
_policy_drafts.erase(StringName(payload.get(&"raid_id", &"")))
|
||||
_set_result("防衛の指示を反映しました。" if result.is_success() else "指示を反映できませんでした。体調・業務中の状態・対象施設を確認してください。", result.is_success())
|
||||
|
||||
|
||||
func _synchronize_commands(queue: Variant) -> void:
|
||||
_pending.clear()
|
||||
for metadata: Dictionary in queue.get_accepted_command_metadata():
|
||||
var key := String(metadata.get("idempotency_key", ""))
|
||||
if key.begins_with(KEY_PREFIX) and key.trim_prefix(KEY_PREFIX).is_valid_int():
|
||||
_sequence = maxi(_sequence, int(key.trim_prefix(KEY_PREFIX)) + 1)
|
||||
for command: Dictionary in queue.create_snapshot().get("pending", []):
|
||||
if StringName(command.get("command_type", &"")) in COMMANDS:
|
||||
_pending[int(command.get("command_id", 0))] = command.get("payload", {}).duplicate(true)
|
||||
|
||||
|
||||
func _on_command_completed(result: Variant) -> void:
|
||||
if not _pending.has(int(result.command_id)): return
|
||||
var payload: Dictionary = _pending[int(result.command_id)]
|
||||
_pending.erase(int(result.command_id))
|
||||
_show_result(result, payload)
|
||||
refresh()
|
||||
|
||||
|
||||
func _on_snapshot_published(_snapshot: Dictionary = {}, _digest: String = "") -> void:
|
||||
if is_visible_in_tree(): refresh()
|
||||
|
||||
|
||||
func _has_pending(raid_id: StringName) -> bool:
|
||||
for payload: Dictionary in _pending.values():
|
||||
if payload.get(&"raid_id", &"") == raid_id: return true
|
||||
return false
|
||||
|
||||
|
||||
func _row(key: StringName, id_key: StringName, entity_id: StringName) -> Dictionary:
|
||||
for row: Dictionary in _report.get(key, []):
|
||||
if row.get(id_key, &"") == entity_id: return row
|
||||
return {}
|
||||
|
||||
|
||||
func _fill_option(option: OptionButton, rows: Array, id_key: StringName, selected: StringName) -> StringName:
|
||||
if option.get_popup().visible: return selected
|
||||
option.clear()
|
||||
if rows.is_empty():
|
||||
option.add_item("対象なし")
|
||||
option.set_item_metadata(0, &"")
|
||||
return &""
|
||||
var index := 0
|
||||
for row: Dictionary in rows:
|
||||
var id := StringName(row[id_key])
|
||||
var display_name := String(_raid_labels.get(id, _raid_base_label(row))) if id_key == &"raid_id" else String(row.get(&"display_name", id))
|
||||
option.add_item(display_name)
|
||||
if id_key == &"raid_id": option.set_item_tooltip(option.item_count - 1, display_name)
|
||||
option.set_item_metadata(option.item_count - 1, id)
|
||||
if id == selected: index = option.item_count - 1
|
||||
option.select(index)
|
||||
return StringName(option.get_item_metadata(index))
|
||||
|
||||
|
||||
## Cause labels are display-only. Never infer cause from an internal ID or
|
||||
## source_id, and never use the label as an option's metadata or command target.
|
||||
func _build_raid_labels(rows: Array) -> Dictionary:
|
||||
var totals: Dictionary = {}
|
||||
for row: Dictionary in rows:
|
||||
var base := _raid_base_label(row)
|
||||
totals[base] = int(totals.get(base, 0)) + 1
|
||||
var seen: Dictionary = {}
|
||||
var labels: Dictionary = {}
|
||||
for row: Dictionary in rows:
|
||||
var base := _raid_base_label(row)
|
||||
seen[base] = int(seen.get(base, 0)) + 1
|
||||
labels[StringName(row[&"raid_id"])] = base + ("(%d)" % int(seen[base]) if int(totals[base]) > 1 else "")
|
||||
return labels
|
||||
|
||||
|
||||
func _raid_base_label(row: Dictionary) -> String:
|
||||
var cause := String(SOURCE_NAMES.get(String(row.get(&"source_kind", "")), "防衛戦"))
|
||||
var raw_minute: Variant = row.get(&"created_at", null)
|
||||
if typeof(raw_minute) != TYPE_INT or int(raw_minute) < 0:
|
||||
return cause + " · 時刻不明"
|
||||
var minute := int(raw_minute)
|
||||
return "%s · %d日目 %02d:%02d" % [cause, minute / 1440 + 1, minute % 1440 / 60, minute % 60]
|
||||
|
||||
|
||||
func _set_result(message: String, success: bool) -> void:
|
||||
result_label.text = message
|
||||
LayerSkin.register_status_text(result_label, success, LayerSkin.active_layer(self))
|
||||
status_requested.emit(message, success)
|
||||
|
||||
|
||||
func _label(text_value: String, node_name := "") -> Label:
|
||||
var label := Label.new()
|
||||
if not node_name.is_empty(): label.name = node_name
|
||||
label.text = text_value
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.add_theme_font_size_override("font_size", 13)
|
||||
label.add_theme_color_override("font_color", Color("55371e"))
|
||||
LayerSkin.register_text(label, LayerSkin.active_layer(self))
|
||||
return label
|
||||
|
||||
|
||||
func _option(node_name: String) -> OptionButton:
|
||||
var option := OptionButton.new()
|
||||
option.name = node_name
|
||||
option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
if _style_button.is_valid(): _style_button.call(option)
|
||||
return option
|
||||
|
||||
|
||||
func _button(text_value: String, node_name: String, callback: Callable) -> Button:
|
||||
var button := Button.new()
|
||||
button.name = node_name
|
||||
button.text = text_value
|
||||
button.custom_minimum_size.y = 30
|
||||
button.pressed.connect(callback)
|
||||
if _style_button.is_valid(): _style_button.call(button)
|
||||
return button
|
||||
@@ -0,0 +1 @@
|
||||
uid://s1oh611b0xrj
|
||||
@@ -0,0 +1,149 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://diaqemp3k1wj
|
||||
@@ -0,0 +1,18 @@
|
||||
extends Node2D
|
||||
|
||||
# Reuse the same room art at low opacity; this layer has no input handlers.
|
||||
const PixelArt := preload("res://scripts/ui/pixel_tower_art.gd")
|
||||
var rooms: Array[Dictionary] = []
|
||||
var pixel_scale := 1.0
|
||||
var dungeon: Texture2D
|
||||
var town: Texture2D
|
||||
func _ready() -> void:
|
||||
modulate = Color(1,1,1,0.22)
|
||||
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
func _draw() -> void:
|
||||
for room: Dictionary in rooms:
|
||||
if bool(room.get(&"building", false)): continue
|
||||
if bool(room.get(&"legacy_stair", false)):
|
||||
PixelArt.stairs(self,room[&"rect"],room[&"rect"].size.x/14.0,true,true)
|
||||
continue
|
||||
PixelArt.room(self,room[&"rect"],room[&"definition_id"],pixel_scale,dungeon,town,true,room[&"origin_x"],{})
|
||||
@@ -0,0 +1 @@
|
||||
uid://cn241cup6ht38
|
||||
@@ -1,6 +1,8 @@
|
||||
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)
|
||||
|
||||
@@ -15,7 +17,7 @@ var _sale_economy_id := 0
|
||||
var _first_sale_minute := -1
|
||||
|
||||
|
||||
func setup(runtime: Variant, clock: Variant) -> void:
|
||||
func setup(runtime: Variant, clock: Variant, layer := 0) -> void:
|
||||
_runtime = runtime
|
||||
_clock = clock
|
||||
name = "InnOpeningGuide"
|
||||
@@ -25,6 +27,7 @@ func setup(runtime: Variant, clock: Variant) -> void:
|
||||
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)
|
||||
@@ -32,7 +35,8 @@ func setup(runtime: Variant, clock: Variant) -> void:
|
||||
_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)
|
||||
_heading.pressed.connect(_toggle_body)
|
||||
_style_button(_heading, layer)
|
||||
column.add_child(_heading)
|
||||
_body = VBoxContainer.new()
|
||||
column.add_child(_body)
|
||||
@@ -41,14 +45,33 @@ func setup(runtime: Variant, clock: Variant) -> void:
|
||||
_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
|
||||
@@ -70,14 +93,14 @@ func _read_progress() -> Dictionary:
|
||||
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", "決算を見る")
|
||||
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":
|
||||
elif _is_guest_lodging(facility.definition_id):
|
||||
rooms.append(facility)
|
||||
if reception == null:
|
||||
return _build_step(&"reception", &"reception", "1/6 受付をつくる", "表の地表階に受付を配置します。完成した受付の上に客室を増築できます。", &"build_reception")
|
||||
@@ -95,7 +118,7 @@ func _read_progress() -> Dictionary:
|
||||
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 "帳簿を見る")
|
||||
return _step(&"close", "6/6 深夜の決算へ", "最初の宿泊売上が入りました。00:00に営業日が締まり、給与・利益・未払いを帳簿で確認できます。", &"resume" if _clock.paused else &"ledger", "時間を進める" if _clock.paused else "帳簿を見る")
|
||||
|
||||
|
||||
func _can_reach_facility(facility: FacilityInstance) -> bool:
|
||||
@@ -103,6 +126,8 @@ func _can_reach_facility(facility: FacilityInstance) -> bool:
|
||||
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))
|
||||
@@ -114,18 +139,37 @@ func _reception_score(facility: FacilityInstance) -> int:
|
||||
|
||||
|
||||
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.
|
||||
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():
|
||||
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
|
||||
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:
|
||||
@@ -139,10 +183,10 @@ func _first_lodging_sale() -> int:
|
||||
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():
|
||||
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 facility.definition_id == &"standard_room":
|
||||
if _is_guest_lodging(facility.definition_id):
|
||||
room_ids[facility.instance_id] = true
|
||||
if room_ids.is_empty():
|
||||
return -1
|
||||
@@ -154,7 +198,7 @@ func _first_lodging_sale() -> int:
|
||||
|
||||
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]:
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
class_name LayerInterfaceSkin
|
||||
extends RefCounted
|
||||
## Visual-only palette for the persistent HUD. No simulation state is owned here.
|
||||
const STYLE_META := &"layer_interface_front_style"
|
||||
const TEXT_META := &"layer_interface_front_text"
|
||||
const BACK_TEXT_META := &"layer_interface_back_text"
|
||||
const BACK_PANELS := {
|
||||
&"panel_outer": Color("10191e"), &"panel_wood": Color("1a282e"),
|
||||
&"panel_leather": Color("172229"), &"panel_parchment": Color("202e34"),
|
||||
&"button_normal": Color("293c43"), &"button_hover": Color("385860"),
|
||||
&"button_pressed": Color("275954"), &"button_disabled": Color("293135"),
|
||||
}
|
||||
static func register_style(style: StyleBoxFlat, role: StringName, layer: int) -> void:
|
||||
style.set_meta(STYLE_META, {&"role":role, &"background":style.bg_color, &"border":style.border_color})
|
||||
apply_style(style, layer)
|
||||
static func apply_style(style: StyleBoxFlat, layer: int) -> void:
|
||||
if not style.has_meta(STYLE_META): return
|
||||
var front: Dictionary = style.get_meta(STYLE_META)
|
||||
var background: Color = BACK_PANELS.get(front[&"role"], Color("202e34")) if layer == 1 else front[&"background"]
|
||||
var border: Color = Color("73989a") if layer == 1 else front[&"border"]
|
||||
if style.bg_color != background: style.bg_color = background
|
||||
if style.border_color != border: style.border_color = border
|
||||
static func register_text(label: Label, layer: int) -> void:
|
||||
if not label.has_meta(TEXT_META): label.set_meta(TEXT_META, label.get_theme_color("font_color"))
|
||||
var color: Color = label.get_meta(BACK_TEXT_META, Color("d8eeea")) if layer == 1 else label.get_meta(TEXT_META)
|
||||
if label.get_theme_color("font_color") != color: label.add_theme_color_override("font_color", color)
|
||||
static func register_status_text(label: Label, success: bool, layer: int) -> void:
|
||||
label.set_meta(TEXT_META, Color("397346") if success else Color("a7402e"))
|
||||
label.set_meta(BACK_TEXT_META, Color("b5e7ba") if success else Color("ffb498"))
|
||||
register_text(label, layer)
|
||||
static func active_layer(node: Node) -> int:
|
||||
if not node.is_inside_tree(): return 0
|
||||
var state := node.get_node_or_null("/root/GameState")
|
||||
return int(state.get("active_layer")) if state != null else 0
|
||||
static func register_toggle_text(button: BaseButton, layer: int) -> void:
|
||||
const META := &"layer_interface_toggle_text"
|
||||
if not button.has_meta(META):
|
||||
var colors := {}
|
||||
for key: StringName in [&"font_color", &"font_pressed_color", &"font_hover_color", &"font_hover_pressed_color", &"font_focus_color", &"font_disabled_color"]:
|
||||
colors[key] = button.get_theme_color(key)
|
||||
button.set_meta(META, colors)
|
||||
var front: Dictionary = button.get_meta(META)
|
||||
for key: StringName in front:
|
||||
var color: Color = front[key] if layer == 0 else (Color("8da4a4") if key == &"font_disabled_color" else Color("d8eeea"))
|
||||
if button.get_theme_color(key) != color: button.add_theme_color_override(key, color)
|
||||
static func apply_tree(node: Node, layer: int) -> void:
|
||||
if node is Window and node.theme != null:
|
||||
for entry: Array in [[&"panel", &"AcceptDialog"], [&"embedded_border", &"Window"]]:
|
||||
if node.theme.has_stylebox(entry[0], entry[1]):
|
||||
var window_style: StyleBox = node.theme.get_stylebox(entry[0], entry[1])
|
||||
if window_style is StyleBoxFlat: apply_style(window_style, layer)
|
||||
if node is Control:
|
||||
for key: StringName in [&"panel", &"normal", &"hover", &"pressed", &"focus", &"disabled"]:
|
||||
if node.has_theme_stylebox_override(key):
|
||||
var style: StyleBox = node.get_theme_stylebox(key)
|
||||
if style is StyleBoxFlat: apply_style(style, layer)
|
||||
if node is Label and node.has_meta(TEXT_META): register_text(node, layer)
|
||||
if node is BaseButton and node.has_meta(&"layer_interface_toggle_text"): register_toggle_text(node, layer)
|
||||
for child: Node in node.get_children(): apply_tree(child, layer)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bl3cvg8q4ykt7
|
||||
@@ -1,7 +1,10 @@
|
||||
class_name OperatingLedgerPanel
|
||||
extends VBoxContainer
|
||||
|
||||
const LayerSkin := preload("res://scripts/ui/layer_interface_skin.gd")
|
||||
|
||||
## Projection only: authoritative close and ledger entries own all amounts.
|
||||
var _initial_layer := 0
|
||||
var _runtime: Variant
|
||||
var _clock: Variant
|
||||
var _cash_label: Label
|
||||
@@ -12,18 +15,21 @@ var _selected_day := -1
|
||||
var _picker_days: Array[int] = []
|
||||
|
||||
|
||||
func setup(runtime: Variant, clock: Variant) -> void:
|
||||
func setup(runtime: Variant, clock: Variant, style_button := Callable(), layer := 0) -> void:
|
||||
_initial_layer = layer
|
||||
_runtime = runtime
|
||||
_clock = clock
|
||||
name = "OperatingLedgerPanel"
|
||||
add_theme_constant_override("separation", 8)
|
||||
var heading := Label.new()
|
||||
heading.text = "帳簿 · 06:00締め"
|
||||
heading.text = "帳簿 · 00:00締め"
|
||||
heading.add_theme_color_override("font_color", Color("55371e"))
|
||||
LayerSkin.register_text(heading, _initial_layer)
|
||||
add_child(heading)
|
||||
_cash_label = _label()
|
||||
_day_picker = OptionButton.new()
|
||||
_day_picker.name = "LedgerDayPicker"
|
||||
if style_button.is_valid(): style_button.call(_day_picker)
|
||||
_day_picker.item_selected.connect(func(index: int) -> void:
|
||||
_selected_day = _day_picker.get_item_id(index)
|
||||
refresh())
|
||||
@@ -33,16 +39,26 @@ func setup(runtime: Variant, clock: Variant) -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
func select_latest_close() -> void:
|
||||
_selected_day = -1
|
||||
_day_picker.get_popup().hide()
|
||||
refresh()
|
||||
|
||||
|
||||
func get_report() -> Dictionary:
|
||||
if _runtime == null:
|
||||
return {}
|
||||
var minute: int = _clock.absolute_minute
|
||||
var business_day := maxi(1, int(floor(float(minute - 360) / 1440.0)) + 1)
|
||||
var start := (business_day - 1) * 1440 + 360
|
||||
var business_day := minute / 1440 + 1
|
||||
var start := (business_day - 1) * 1440
|
||||
if _runtime.operations_coordinator != null:
|
||||
business_day = _runtime.operations_coordinator.get_open_financial_day()
|
||||
start = _runtime.operations_coordinator.get_financial_period_start(business_day)
|
||||
var inflow := 0
|
||||
var outflow := 0
|
||||
var construction_outflow := 0
|
||||
var wage_outflow := 0
|
||||
var upkeep_outflow := 0
|
||||
for entry: LedgerEntry in _runtime.economy_service.get_entries():
|
||||
if entry.absolute_minute < start or entry.absolute_minute > minute or entry.category == &"INITIALIZATION":
|
||||
continue
|
||||
@@ -52,6 +68,8 @@ func get_report() -> Dictionary:
|
||||
outflow += entry.amount
|
||||
if entry.category == &"CONSTRUCTION":
|
||||
construction_outflow += entry.amount
|
||||
elif entry.category == &"MAINTENANCE_PAYMENT":
|
||||
upkeep_outflow += entry.amount
|
||||
elif entry.category == &"WAGE":
|
||||
wage_outflow += entry.amount
|
||||
var summaries: Array[Dictionary] = []
|
||||
@@ -68,6 +86,8 @@ func get_report() -> Dictionary:
|
||||
&"cash": _runtime.economy_service.get_balance(&"PLAYER_CASH"),
|
||||
&"cash_in": inflow, &"cash_out": outflow, &"cash_net": inflow - outflow,
|
||||
&"construction_cash_out": construction_outflow, &"wage_cash_out": wage_outflow,
|
||||
&"upkeep_cash_out": upkeep_outflow,
|
||||
&"upkeep_arrears": _runtime.upkeep_settlement.get_arrears_total(),
|
||||
&"closed_days": summaries, &"selected_close": selected.duplicate(true),
|
||||
}
|
||||
|
||||
@@ -78,13 +98,13 @@ func refresh() -> void:
|
||||
if _day_picker.get_popup().visible:
|
||||
return
|
||||
var report := get_report()
|
||||
_cash_label.text = "第%d営業日 · 手持ち %dG\n本日の入金 +%dG / 出金 −%dG\nうち建築 %dG / 給与支払 %dG\n資金増減 %+dG(営業利益は下の決算)" % [report[&"business_day"], report[&"cash"], report[&"cash_in"], report[&"cash_out"], report[&"construction_cash_out"], report[&"wage_cash_out"], report[&"cash_net"]]
|
||||
_cash_label.text = "第%d営業日 · 手持ち %dG\n本日の入金 +%dG / 出金 −%dG\nうち建築 %dG / 給与支払 %dG / 維持費支払 %dG\n未払い維持費 %dG\n資金増減 %+dG(営業利益は下の決算)" % [report[&"business_day"], report[&"cash"], report[&"cash_in"], report[&"cash_out"], report[&"construction_cash_out"], report[&"wage_cash_out"], report[&"upkeep_cash_out"], report[&"upkeep_arrears"], report[&"cash_net"]]
|
||||
var summary: Dictionary = report[&"selected_close"]
|
||||
if summary.is_empty():
|
||||
if not _day_picker.disabled or _day_picker.item_count == 0:
|
||||
_day_picker.clear()
|
||||
_picker_days.clear()
|
||||
_day_picker.add_item("決算は翌朝06:00に確定します")
|
||||
_day_picker.add_item("決算は翌日00:00に確定します")
|
||||
_day_picker.disabled = true
|
||||
_closed_label.text = "建築後の手持ち資金だけで営業成績は決まりません。宿泊などの売上と日々の費用をここで確認できます。"
|
||||
_facility_label.text = ""
|
||||
@@ -105,6 +125,10 @@ func refresh() -> void:
|
||||
for key: StringName in [&"maintenance", &"utilities", &"taxes_and_interest", &"fines"]:
|
||||
other += int(totals.get(key, 0))
|
||||
_closed_label.text = "売上 %dG / 返金 %dG\n運営費(仕入れ・契約金等) %dG\n給与 %dG / その他 %dG\n営業利益 %+dG\n前日比 %+dG / 未払給与 %dG\n帳簿照合:%s" % [totals.get(&"revenue", 0), totals.get(&"refunds", 0), totals.get(&"cost_of_goods", 0), totals.get(&"wages", 0), other, summary.get(&"net_profit", 0), summary.get(&"previous_day_delta", 0), summary.get(&"arrears_total", 0), "一致" if bool(summary.get(&"balanced", false)) else "差額あり"]
|
||||
var legacy: bool = _runtime.operations_coordinator.is_legacy_financial_history(int(summary[&"day"]))
|
||||
_closed_label.text += "\n%s %d回%s" % ["旧版の連続未払い" if legacy else "連続赤字", int(summary.get(&"consecutive_insolvency_count", 0)), "" if legacy else "(3回でゲーム終了)"]
|
||||
var close_minute := int(summary[&"closed_at"]) % 1440
|
||||
_closed_label.text += "\n確定時刻 %02d:%02d" % [close_minute / 60, close_minute % 60]
|
||||
var lines: PackedStringArray = ["施設別 · 売上 / 費用 / 利益"]
|
||||
var rows: Dictionary = summary.get(&"facility_profit_loss", {})
|
||||
var ids := rows.keys()
|
||||
@@ -115,6 +139,14 @@ func refresh() -> void:
|
||||
if facility != null:
|
||||
var definition: FacilityDefinition = _runtime.definition_registry.get_facility(facility.definition_id)
|
||||
label = "%s (%d,%d)" % [definition.display_name if definition != null else facility.definition_id, facility.origin.x, facility.origin.y]
|
||||
else:
|
||||
# Corridors and stairs are completed construction entities without
|
||||
# service instances, but their ownership upkeep still appears here.
|
||||
var completed: Dictionary = _runtime.construction_service.get_facility(StringName(id))
|
||||
if not completed.is_empty():
|
||||
var definition: FacilityDefinition = _runtime.definition_registry.get_facility(completed[&"definition_id"])
|
||||
var origin: Vector3i = completed[&"origin"]
|
||||
label = "%s (%d,%d)" % [definition.display_name if definition != null else completed[&"definition_id"], origin.x, origin.y]
|
||||
var row: Dictionary = rows[id]
|
||||
lines.append("%s\n%dG / %dG / %+dG" % [label, row.get(&"revenue", 0), row.get(&"expenses", 0), row.get(&"net_profit", 0)])
|
||||
_facility_label.text = "\n".join(lines) if not rows.is_empty() else "施設別の記帳はまだありません。"
|
||||
@@ -124,6 +156,7 @@ func _label() -> Label:
|
||||
var label := Label.new()
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.add_theme_color_override("font_color", Color("55371e"))
|
||||
LayerSkin.register_text(label, _initial_layer)
|
||||
label.add_theme_font_size_override("font_size", 13)
|
||||
add_child(label)
|
||||
return label
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
class_name PixelPeople
|
||||
extends RefCounted
|
||||
|
||||
## Authored side-view poses on a 16x18 grid. No neighbouring atlas characters
|
||||
## are used as animation frames. All motion is presentation-only.
|
||||
const COATS := [Color("688da0"), Color("b87659"), Color("a69a63"), Color("89789e"), Color("638d72")]
|
||||
const SKINS := [Color("edc49d"), Color("c99371"), Color("a56f50")]
|
||||
const INK := Color("303c40")
|
||||
|
||||
static func pose(mode: StringName, frame: int, variant: int, staff: bool) -> Array[Dictionary]:
|
||||
var marks: Array[Dictionary] = []
|
||||
var skin: Color = SKINS[posmod(variant, SKINS.size())]
|
||||
var coat: Color = Color("719d94") if staff else COATS[posmod(variant, COATS.size())]
|
||||
var phase := posmod(frame, 8)
|
||||
var walk := mode == &"walk" or mode == &"carry_walk"
|
||||
var work := mode in [&"work", &"clean", &"eat"]
|
||||
var stride := [0, 1, 2, 1, 0, -1, -2, -1][phase] as int
|
||||
var bob := -1 if walk and phase in [2, 6] else 0
|
||||
if mode in [&"sleep", &"collapsed"]:
|
||||
mark(marks, Rect2(2, 14, 12, 2), INK)
|
||||
mark(marks, Rect2(3, 12, 4, 3), skin)
|
||||
mark(marks, Rect2(2, 12, 2, 3), INK)
|
||||
mark(marks, Rect2(7, 12, 6, 3), coat)
|
||||
mark(marks, Rect2(13, 13, 2, 2), INK)
|
||||
if mode == &"sleep":
|
||||
mark(marks, Rect2(3 + phase / 3, 8 - phase / 3, 2, 1), Color("eee6c7"))
|
||||
return marks
|
||||
# Far arm and leg, then silhouette, face, near arm and leg.
|
||||
mark(marks, Rect2(6, 8 + bob, 2, 5), coat.darkened(0.25))
|
||||
var far := -stride if walk else 0
|
||||
var near := stride if walk else 0
|
||||
mark(marks, Rect2(7 + far, 12 + bob, 2, 4 - bob), INK.lightened(0.12))
|
||||
mark(marks, Rect2(7 + far, 16, 3, 1), INK)
|
||||
mark(marks, Rect2(6, 6 + bob, 5, 7), INK)
|
||||
mark(marks, Rect2(7, 7 + bob, 4, 5), coat)
|
||||
mark(marks, Rect2(7, 1 + bob, 4, 5), skin)
|
||||
mark(marks, Rect2(6, 1 + bob, 4, 2), INK)
|
||||
mark(marks, Rect2(6, 3 + bob, 1, 2), INK)
|
||||
mark(marks, Rect2(11, 3 + bob, 1, 2), skin)
|
||||
mark(marks, Rect2(10, 3 + bob, 1, 1), INK)
|
||||
mark(marks, Rect2(8, 6 + bob, 2, 1), skin)
|
||||
mark(marks, Rect2(8 + near, 12 + bob, 2, 4 - bob), INK)
|
||||
mark(marks, Rect2(8 + near, 16, 3, 1), INK)
|
||||
var arm := -stride if walk else 0
|
||||
if work:
|
||||
arm = 2 if phase < 4 else 1
|
||||
mark(marks, Rect2(9 + arm, 8 + bob, 2, 3), coat.lightened(0.12))
|
||||
mark(marks, Rect2(9 + arm, 11 + bob, 2, 1), skin)
|
||||
if staff:
|
||||
mark(marks, Rect2(7, 9 + bob, 2, 3), Color("e3dfc2"))
|
||||
if mode in [&"carry", &"carry_walk"]:
|
||||
mark(marks, Rect2(10, 8 + bob, 5, 5), Color("956940"))
|
||||
mark(marks, Rect2(10, 8 + bob, 5, 1), Color("d3ad6b"))
|
||||
mark(marks, Rect2(12, 9 + bob, 1, 4), Color("b48850"))
|
||||
elif mode == &"clean":
|
||||
mark(marks, Rect2(11 + arm, 9, 1, 7), Color("af9059"))
|
||||
mark(marks, Rect2(9 + arm, 16, 5, 1), Color("d8bc78"))
|
||||
elif mode == &"eat":
|
||||
mark(marks, Rect2(11, 6 if phase < 4 else 10, 3, 2), Color("f3ebcf"))
|
||||
elif mode == &"work":
|
||||
mark(marks, Rect2(11, 12, 4, 1), Color("efe7ce"))
|
||||
mark(marks, Rect2(12 + phase % 2, 10, 1, 2), Color("805e44"))
|
||||
elif mode == &"wait":
|
||||
mark(marks, Rect2(9, 9, 3, 1), skin)
|
||||
return marks
|
||||
|
||||
static func mark(marks: Array[Dictionary], rect: Rect2, color: Color) -> void:
|
||||
marks.append({&"rect": rect, &"color": color})
|
||||
|
||||
static func draw(canvas: CanvasItem, origin: Vector2, pixel: float, mode: StringName, frame: int, variant: int, staff: bool, facing_left: bool) -> void:
|
||||
for item: Dictionary in pose(mode, frame, variant, staff):
|
||||
var rect: Rect2 = item[&"rect"]
|
||||
if facing_left:
|
||||
rect.position.x = 16 - rect.end.x
|
||||
canvas.draw_rect(Rect2(origin + rect.position * pixel, rect.size * pixel), item[&"color"])
|
||||
@@ -0,0 +1 @@
|
||||
uid://bvhvqan76tawa
|
||||
@@ -3,6 +3,7 @@ extends RefCounted
|
||||
|
||||
## Small, code-native room furnishings. Coordinates use the same pixel size as
|
||||
## Kenney's 16 px sprites. This renderer never changes simulation state.
|
||||
const Layout := preload("res://scripts/ui/room_presentation.gd")
|
||||
const INK := Color("34413b")
|
||||
const WOOD := Color("8e6348")
|
||||
const WOOD_LIGHT := Color("c39665")
|
||||
@@ -15,7 +16,7 @@ static func tile(canvas: CanvasItem, texture: Texture2D, index: int, target: Rec
|
||||
canvas.draw_texture_rect_region(texture, target, Rect2((index % 12) * 16, (index / 12) * 16, 16, 16), tint)
|
||||
|
||||
|
||||
static func room(canvas: CanvasItem, rect: Rect2, id: StringName, pixel: float, dungeon: Texture2D, town: Texture2D, back: bool, decoration_seed := 0) -> void:
|
||||
static func room(canvas: CanvasItem, rect: Rect2, id: StringName, pixel: float, dungeon: Texture2D, town: Texture2D, back: bool, decoration_seed := 0, presentation: Dictionary = {}) -> void:
|
||||
var p := pixel
|
||||
if id in [&"guest_stairs", &"service_stairs"]:
|
||||
stairs(canvas, rect, p, id == &"service_stairs")
|
||||
@@ -69,17 +70,14 @@ static func room(canvas: CanvasItem, rect: Rect2, id: StringName, pixel: float,
|
||||
counter(canvas, Vector2(mid-9*p, floor_y-6*p), 18*p, p)
|
||||
canvas.draw_rect(Rect2(mid-6*p, floor_y-8*p, 4*p, 2*p), CREAM)
|
||||
canvas.draw_rect(Rect2(mid+4*p, floor_y-8*p, 2*p, 2*p), Color("d6ad52"))
|
||||
&"standard_room", &"adult_private_room":
|
||||
bed(canvas, Vector2(rect.position.x+2*p, floor_y-9*p), p, Color("709b94") if not dark else Color("a37b91"),13)
|
||||
if w >= 28*p:
|
||||
bed(canvas, Vector2(rect.end.x-15*p, floor_y-9*p), p, Color("709b94") if not dark else Color("a37b91"),13)
|
||||
&"standard_room", &"premium_room", &"staff_dormitory", &"adult_private_room", &"infirmary":
|
||||
for at: Vector2 in Layout.slots(rect, p, id):
|
||||
bed(canvas, at + Vector2(-7, -9)*p, p, Color("709b94") if not dark else Color("a37b91"), 13)
|
||||
&"cafeteria", &"tavern":
|
||||
for x in range(int(rect.position.x+5*p), int(rect.end.x-13*p), int(20*p)):
|
||||
table(canvas, Vector2(x, floor_y-7*p), p)
|
||||
if rect.size.y > 28*p:
|
||||
counter(canvas, Vector2(rect.position.x+7*p, top+13*p), w-14*p, p)
|
||||
for x in range(int(rect.position.x+10*p), int(rect.end.x-8*p), int(12*p)):
|
||||
canvas.draw_rect(Rect2(x, top+11*p, 3*p, 2*p), CREAM)
|
||||
for at: Vector2 in Layout.slots(rect, p, id):
|
||||
table(canvas, at + Vector2(-2, -7)*p, p)
|
||||
&"chapel":
|
||||
chapel_furnishings(canvas, rect, p)
|
||||
&"public_bath":
|
||||
canvas.draw_rect(Rect2(rect.position.x+4*p, floor_y-7*p, w-8*p, 7*p), CREAM)
|
||||
canvas.draw_rect(Rect2(rect.position.x+6*p, floor_y-6*p, w-12*p, 4*p), Color("72b6b8"))
|
||||
@@ -92,6 +90,15 @@ static func room(canvas: CanvasItem, rect: Rect2, id: StringName, pixel: float,
|
||||
&"warehouse":
|
||||
for x in range(int(rect.position.x+2*p), int(rect.end.x-12*p), int(13*p)):
|
||||
tile(canvas, dungeon, 63, Rect2(x, floor_y-12*p, 12*p, 12*p))
|
||||
&"cleaning_room":
|
||||
# Original code-drawn supply rack, mop and bucket; existing palette.
|
||||
counter(canvas, Vector2(rect.position.x+3*p, floor_y-10*p), 10*p, p)
|
||||
for shelf in 2:
|
||||
canvas.draw_rect(Rect2(rect.position.x+4*p,floor_y-(12+3*shelf)*p,8*p,2*p),CREAM)
|
||||
canvas.draw_rect(Rect2(mid+3*p,floor_y-16*p,p,14*p),WOOD_LIGHT)
|
||||
canvas.draw_rect(Rect2(mid+p,floor_y-3*p,6*p,2*p),CREAM)
|
||||
canvas.draw_rect(Rect2(rect.end.x-9*p,floor_y-6*p,5*p,5*p),TEAL)
|
||||
canvas.draw_rect(Rect2(rect.end.x-8*p,floor_y-7*p,3*p,p),INK)
|
||||
&"staff_waiting_room":
|
||||
table(canvas, Vector2(mid-6*p, floor_y-7*p), p)
|
||||
tile(canvas, dungeon, 89, Rect2(rect.position.x+2*p, floor_y-12*p, 12*p, 12*p))
|
||||
@@ -103,9 +110,65 @@ static func room(canvas: CanvasItem, rect: Rect2, id: StringName, pixel: float,
|
||||
_:
|
||||
table(canvas, Vector2(mid-6*p, floor_y-7*p), p)
|
||||
|
||||
if not presentation.is_empty():
|
||||
if rect.size.y >= 40*p and id != &"chapel":
|
||||
var workers: Array = presentation.get(&"staff", [])
|
||||
var columns := maxi(int(w / (16*p)), 1)
|
||||
if not workers.is_empty():
|
||||
canvas.draw_rect(Rect2(rect.position.x+p,top+24*p,w-2*p,p), WOOD_LIGHT)
|
||||
for slot: int in mini(workers.size(), columns):
|
||||
counter(canvas, Vector2(rect.position.x+(slot+0.5)*w/columns-6*p, top+20*p), 12*p, p)
|
||||
|
||||
var operation := int(presentation[&"operation"])
|
||||
if operation != FacilityInstance.OperationState.OPERATING:
|
||||
canvas.draw_rect(rect.grow(-p), Color(0.12, 0.16, 0.18, 0.42))
|
||||
# A closed shutter differs in shape as well as colour from an open room.
|
||||
for y: int in range(int(top+10*p), int(top+17*p), maxi(int(3*p), 1)):
|
||||
canvas.draw_rect(Rect2(rect.position.x+2*p, y, w-4*p, p), WOOD)
|
||||
if operation == FacilityInstance.OperationState.SUSPENDED:
|
||||
canvas.draw_line(rect.position+Vector2(3,10)*p, rect.end-Vector2(3,5)*p, Color("ca9c62"), 2*p)
|
||||
canvas.draw_line(Vector2(rect.end.x-3*p,top+10*p), Vector2(rect.position.x+3*p,rect.end.y-5*p), Color("ca9c62"), 2*p)
|
||||
elif int(presentation[&"count"]) > 0:
|
||||
canvas.draw_rect(Rect2(lamp_x-2*p, top+5*p, 4*p, 2*p), Color("ffe9a8"))
|
||||
|
||||
|
||||
|
||||
## The upper landing owns a short flight down to the floor below. Six square
|
||||
## treads and a stepped handrail keep the shape legible at the 16 px room scale.
|
||||
static func chapel_furnishings(canvas: CanvasItem, rect: Rect2, p: float) -> void:
|
||||
# Two rows share the live visitor anchors. Keep the central aisle clear;
|
||||
# the altar and world-tree glass occupy the otherwise unused upper wall.
|
||||
var mid := rect.get_center().x
|
||||
var top := rect.position.y
|
||||
canvas.draw_rect(Rect2(mid-6*p,top+3*p,12*p,19*p), INK)
|
||||
canvas.draw_rect(Rect2(mid-5*p,top+4*p,10*p,17*p), Color("6c8993"))
|
||||
canvas.draw_rect(Rect2(mid-4*p,top+6*p,8*p,13*p), Color("adc5b4"))
|
||||
canvas.draw_rect(Rect2(mid-p,top+9*p,2*p,10*p), WOOD)
|
||||
for row in 3:
|
||||
var width := float(row+1)*2*p
|
||||
canvas.draw_rect(Rect2(mid-width/2,top+(6+row*3)*p,width,3*p), Color("52836c"))
|
||||
canvas.draw_rect(Rect2(mid-p,top+5*p,2*p,p), Color("edc97c"))
|
||||
var altar_y := top+28*p
|
||||
counter(canvas,Vector2(mid-9*p,altar_y),18*p,p)
|
||||
canvas.draw_rect(Rect2(mid-9*p,altar_y,18*p,2*p), CREAM)
|
||||
canvas.draw_rect(Rect2(mid-2*p,altar_y+2*p,4*p,4*p), TEAL)
|
||||
for direction in [-1,1]:
|
||||
canvas.draw_rect(Rect2(mid+direction*6*p,altar_y-3*p,p,3*p), CREAM)
|
||||
canvas.draw_rect(Rect2(mid+direction*6*p,altar_y-4*p,p,p), Color("edc97c"))
|
||||
var rows: Dictionary = {}
|
||||
for at: Vector2 in Layout.slots(rect,p,&"chapel"):
|
||||
rows[at.y] = true
|
||||
for y: float in rows:
|
||||
for side in 2:
|
||||
var x := rect.position.x+3*p if side==0 else mid+3*p
|
||||
var width := rect.size.x/2-6*p
|
||||
canvas.draw_rect(Rect2(x,y-8*p,width,5*p), WOOD)
|
||||
canvas.draw_rect(Rect2(x,y-8*p,width,p), WOOD_LIGHT)
|
||||
canvas.draw_rect(Rect2(x-p,y-3*p,width+2*p,2*p), WOOD_LIGHT)
|
||||
canvas.draw_rect(Rect2(x,y-p,p,p), INK)
|
||||
canvas.draw_rect(Rect2(x+width-p,y-p,p,p), INK)
|
||||
|
||||
|
||||
static func stairs(canvas: CanvasItem, rect: Rect2, p: float, staff_only: bool, compact := false) -> void:
|
||||
if not compact:
|
||||
canvas.draw_rect(rect, INK)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
extends RefCounted
|
||||
|
||||
## Read-only presentation. Counts refer to people, never storage capacity.
|
||||
static func describe(instance: Variant) -> Dictionary:
|
||||
if instance == null:
|
||||
return {}
|
||||
var ids: Array[StringName] = []
|
||||
for service: Variant in instance.active_services.values():
|
||||
for id: StringName in service.guest_ids:
|
||||
if not ids.has(id):
|
||||
ids.append(id)
|
||||
ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
|
||||
return {&"operation": int(instance.operation_state), &"people": ids,
|
||||
&"count": ids.size(), &"limit": int(instance.customer_capacity),
|
||||
&"staff": instance.active_staff_ids.duplicate(), &"suspension": StringName(instance.suspension_reason)}
|
||||
|
||||
|
||||
static func slots(rect: Rect2, p: float, id: StringName) -> Array[Vector2]:
|
||||
# Each anchor owns a 16x18 person footprint. Overflow is reported as a count,
|
||||
# not drawn on top of another person. Furniture uses these same anchors.
|
||||
var result: Array[Vector2] = []
|
||||
var step := 20.0 if id in [&"cafeteria", &"tavern"] else 16.0
|
||||
var columns := maxi(int(rect.size.x / p / step), 1)
|
||||
var rows := maxi(int((rect.size.y / p - 10.0) / 20.0), 1)
|
||||
if id == &"chapel":
|
||||
columns = mini(columns, 4)
|
||||
rows = maxi(int((rect.size.y / p - 30.0) / 20.0), 1)
|
||||
for row: int in rows:
|
||||
for col: int in columns:
|
||||
if id == &"chapel" and result.size() == 8:
|
||||
return result
|
||||
result.append(Vector2(rect.position.x + (float(col) + 0.5) * rect.size.x / columns,
|
||||
rect.end.y - (4.0 + row * 20.0) * p))
|
||||
return result
|
||||
|
||||
|
||||
static func label(data: Dictionary, visible_count: int) -> String:
|
||||
if data.is_empty():
|
||||
return ""
|
||||
var operation := int(data[&"operation"])
|
||||
if operation == FacilityInstance.OperationState.SUSPENDED:
|
||||
return "休止・摘発" if data[&"suspension"] == &"RAIDED" else "休止・破壊"
|
||||
if operation == FacilityInstance.OperationState.INACTIVE:
|
||||
return "未稼働"
|
||||
var count := int(data[&"count"])
|
||||
var limit := int(data[&"limit"])
|
||||
var value := "利用%d/%d人" % [count, limit] if limit >= 0 else "利用%d人" % count
|
||||
if limit > 0 and count >= limit:
|
||||
value = "満員 " + value
|
||||
if count > visible_count:
|
||||
value += "(他%d人)" % (count - visible_count)
|
||||
return value
|
||||
@@ -0,0 +1 @@
|
||||
uid://dllkdogv348p
|
||||
@@ -1,6 +1,8 @@
|
||||
class_name StaffManagementPanel
|
||||
extends VBoxContainer
|
||||
|
||||
const LayerSkin := preload("res://scripts/ui/layer_interface_skin.gd")
|
||||
|
||||
## Roster and assignment editing only. Runtime owns Shift, staffing, movement,
|
||||
## and persistence; this panel never edits a Staff or Shift instance directly.
|
||||
signal status_requested(message: String, success: bool)
|
||||
@@ -11,7 +13,7 @@ const ROLE_NAMES := {
|
||||
&"attendant": "接客", &"bartender": "酒場", &"clerk": "会計",
|
||||
&"cleaner": "清掃", &"cook": "調理", &"guard": "警備",
|
||||
&"maintenance": "保守", &"masseur": "施術", &"medic": "医療",
|
||||
&"porter": "物流", &"receptionist": "受付",
|
||||
&"porter": "物流", &"receptionist": "受付", &"innkeeper": "女将", &"cleric": "聖職者",
|
||||
}
|
||||
const STATE_NAMES := ["勤務外", "出勤中", "勤務中", "移動中", "作業中", "休憩中", "食事中", "睡眠中", "治療へ移動", "避難中", "行動不能", "ストライキ", "拘束中", "捕虜", "勤務不可"]
|
||||
|
||||
@@ -51,7 +53,9 @@ func _ready() -> void:
|
||||
_build_interface()
|
||||
_built = true
|
||||
if _runtime != null:
|
||||
if _runtime.has_signal(&"snapshot_published"):
|
||||
if _runtime.has_signal(&"state_updated"):
|
||||
_runtime.connect(&"state_updated", _on_snapshot_published)
|
||||
elif _runtime.has_signal(&"snapshot_published"):
|
||||
_runtime.connect(&"snapshot_published", _on_snapshot_published)
|
||||
if _runtime.has_signal(&"command_completed"):
|
||||
_runtime.connect(&"command_completed", _on_command_completed)
|
||||
@@ -139,11 +143,12 @@ func refresh() -> void:
|
||||
button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
button.toggle_mode = true
|
||||
button.pressed.connect(select_staff.bind(staff_id))
|
||||
button.set_drag_forwarding(_staff_drag_data.bind(staff_id), Callable(), Callable())
|
||||
_style(button)
|
||||
roster_list.add_child(button)
|
||||
roster_buttons[staff_id] = button
|
||||
button.text = "%s %s%s" % [row.get(&"display_name", staff_id), _role_name(row), "・未配属" if StringName(row.get(&"assigned_facility_id", &"")).is_empty() else ""]
|
||||
button.tooltip_text = "%s / %s" % [_facility_name(StringName(row.get(&"assigned_facility_id", &""))), _state_name(row)]
|
||||
button.tooltip_text = "施設へドラッグして配属(選択中の勤務帯を適用)\n%s / %s" % [_facility_name(StringName(row.get(&"assigned_facility_id", &""))), _state_name(row)]
|
||||
for raw_id: Variant in roster_buttons.keys():
|
||||
if not live_ids.has(StringName(raw_id)):
|
||||
var stale := roster_buttons[raw_id] as Button
|
||||
@@ -182,11 +187,16 @@ func _refresh_editor() -> void:
|
||||
if not _draft_dirty:
|
||||
_draft_facility_id = current_facility
|
||||
_draft_template_id = &"NIGHT" if template == &"NIGHT" else &"DAY"
|
||||
staff_detail_label.text = "%s(%s) 日給 G%d\n配属:%s / %s\n%s 疲労 %d / 未払 G%d" % [
|
||||
staff_detail_label.text = "%s(%s) 日給 G%d\n配属:%s / %s\n%s 体力 %d / 疲労 %d / 未払 G%d" % [
|
||||
row.get(&"display_name", selected_staff_id), _role_name(row), int(row.get(&"daily_wage", 0)),
|
||||
_facility_name(current_facility), "夜勤" if template == &"NIGHT" else "日勤" if template == &"DAY" else "自由勤務",
|
||||
_state_name(row), int(row.get(&"fatigue", 0)), int(row.get(&"unpaid_wages", 0)),
|
||||
_state_name(row), int(row.get(&"health", 100)), int(row.get(&"fatigue", 0)), int(row.get(&"unpaid_wages", 0)),
|
||||
]
|
||||
staff_detail_label.text += _unique_contract_text(row)
|
||||
if int(row.get(&"state", 0)) == StaffRuntime.State.INCAPACITATED:
|
||||
staff_detail_label.text += "\n" + String(row.get(&"rescue_status", "救護待ち"))
|
||||
if not String(row.get(&"rest_status", "")).is_empty():
|
||||
staff_detail_label.text += "\n" + String(row[&"rest_status"])
|
||||
var effective_from := int(row.get(&"effective_from", -1))
|
||||
var now := int(get_node("/root/GameClock").get("absolute_minute"))
|
||||
if effective_from > now:
|
||||
@@ -216,12 +226,35 @@ func _refresh_editor() -> void:
|
||||
shift_option.disabled = pending
|
||||
var can_edit := bool(row.get(&"can_edit", true))
|
||||
apply_button.disabled = pending or not can_edit or facility_option.is_item_disabled(selected_index)
|
||||
apply_button.tooltip_text = "現在の作業や拘束が終わってから変更できます。" if not can_edit else "配属先と毎日の勤務帯をまとめて変更します。"
|
||||
apply_button.tooltip_text = _assignment_block_text(row) if not can_edit else "配属先と毎日の勤務帯をまとめて変更します。"
|
||||
if not can_edit:
|
||||
staff_detail_label.text += "\n現在の作業や拘束が終わってから変更できます。"
|
||||
staff_detail_label.text += "\n" + _assignment_block_text(row)
|
||||
apply_button.text = "変更を受理済み…" if pending else "配属・勤務帯を変更"
|
||||
|
||||
|
||||
## Runtime supplies canonical, owned presentation data after every normal
|
||||
## load. The panel neither infers a unique identity from wages nor edits a save.
|
||||
func _unique_contract_text(row: Dictionary) -> String:
|
||||
var contract: Dictionary = row.get(&"unique_contract", {})
|
||||
if contract.is_empty():
|
||||
return ""
|
||||
var text := ""
|
||||
if bool(contract.get(&"free_employment", false)) and bool(contract.get(&"free_upkeep", false)) and bool(contract.get(&"permanent_removal_forbidden", false)):
|
||||
text = "\nユニーク:雇用・維持費無料 / 解雇・売却不可"
|
||||
for effect: Dictionary in contract.get(&"passive_effects", []):
|
||||
if effect.get(&"effect_id") != &"assigned_floor_revenue_multiplier_bp" or effect.get(&"scope") != &"ASSIGNED_FLOOR_ALL_LAYERS":
|
||||
continue
|
||||
var bonus_percent := int((int(effect[&"multiplier_bp"]) - 10000) / 100)
|
||||
text += "\n[%s] 配置階の全施設売上+%d%%" % [effect[&"display_name"], bonus_percent]
|
||||
if bool(effect.get(&"applied", false)) and effect.has(&"floor_y"):
|
||||
var floor_y := int(effect[&"floor_y"])
|
||||
var floor_name := "%dF" % (floor_y + 1) if floor_y >= 0 else "B%d" % -floor_y
|
||||
text += "\n対象:%s(表層・裏層の全施設)" % floor_name
|
||||
else:
|
||||
text += "\n未適用(配属先なし・施設不在)"
|
||||
return text
|
||||
|
||||
|
||||
func _on_facility_changed(index: int) -> void:
|
||||
_draft_facility_id = StringName(facility_option.get_item_metadata(index))
|
||||
_draft_dirty = true
|
||||
@@ -288,6 +321,18 @@ func _show_command_result(result: Variant, staff_id: StringName = &"") -> void:
|
||||
func _failure_message(result: Variant) -> String:
|
||||
var details: Dictionary = result.details if result.details is Dictionary else {}
|
||||
var reason := StringName(details.get(&"reason", details.get("reason", &"")))
|
||||
var translated := _assignment_reason_text(reason)
|
||||
if not translated.is_empty():
|
||||
return translated
|
||||
return "変更できませんでした:%s" % String(result.message) if not String(result.message).is_empty() else "変更できませんでした。配属先と勤務帯を確認してください。"
|
||||
|
||||
|
||||
func _assignment_block_text(row: Dictionary) -> String:
|
||||
var translated := _assignment_reason_text(StringName(row.get(&"assignment_block_reason", &"")))
|
||||
return translated if not translated.is_empty() else "現在の作業や拘束が終わってから変更できます。"
|
||||
|
||||
|
||||
func _assignment_reason_text(reason: StringName) -> String:
|
||||
var reasons := {
|
||||
&"STAFF_NOT_FOUND": "従業員が見つかりません。",
|
||||
&"FACILITY_NOT_FOUND": "配属先が見つかりません。施設が完成しているか確認してください。",
|
||||
@@ -306,9 +351,7 @@ func _failure_message(result: Variant) -> String:
|
||||
&"SHIFT_CONFLICT": "既存の勤務と時間が重なります。勤務予定を確認してください。",
|
||||
&"BANKRUPT": "運営終了後は配属を変更できません。",
|
||||
}
|
||||
if reasons.has(reason):
|
||||
return reasons[reason]
|
||||
return "変更できませんでした:%s" % String(result.message) if not String(result.message).is_empty() else "変更できませんでした。配属先と勤務帯を確認してください。"
|
||||
return reasons.get(reason, "")
|
||||
|
||||
|
||||
func _synchronize_commands(queue: Variant) -> void:
|
||||
@@ -324,7 +367,7 @@ func _synchronize_commands(queue: Variant) -> void:
|
||||
_pending_commands[int(command.get("command_id", 0))] = StringName(payload.get(&"staff_id", payload.get("staff_id", &"")))
|
||||
|
||||
|
||||
func _on_snapshot_published(_snapshot: Dictionary, _digest: String) -> void:
|
||||
func _on_snapshot_published(_snapshot: Dictionary = {}, _digest: String = "") -> void:
|
||||
if is_visible_in_tree():
|
||||
refresh()
|
||||
|
||||
@@ -340,7 +383,7 @@ func _on_command_completed(result: Variant) -> void:
|
||||
|
||||
func _set_result(message: String, success: bool) -> void:
|
||||
result_label.text = message
|
||||
result_label.add_theme_color_override("font_color", Color("397346") if success else Color("a7402e"))
|
||||
LayerSkin.register_status_text(result_label, success, _active_layer())
|
||||
status_requested.emit(message, success)
|
||||
|
||||
|
||||
@@ -400,10 +443,49 @@ func _label(text_value: String) -> Label:
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.add_theme_font_size_override("font_size", 12)
|
||||
label.add_theme_color_override("font_color", Color("394937"))
|
||||
LayerSkin.register_text(label, _active_layer())
|
||||
return label
|
||||
|
||||
|
||||
func _active_layer() -> int:
|
||||
var state := get_node_or_null("/root/GameState")
|
||||
return int(state.get("active_layer")) if state != null else 0
|
||||
|
||||
|
||||
func _style(button: Button) -> void:
|
||||
button.add_theme_font_size_override("font_size", 12)
|
||||
if _style_button.is_valid():
|
||||
_style_button.call(button)
|
||||
|
||||
|
||||
func _staff_drag_data(_position: Vector2, staff_id: StringName) -> Variant:
|
||||
if selected_staff_id != staff_id:
|
||||
select_staff(staff_id)
|
||||
if _pending_commands.values().has(staff_id) or not bool(_selected_row().get(&"can_edit", false)):
|
||||
_set_result("変更を処理中です。" if _pending_commands.values().has(staff_id) else _assignment_block_text(_selected_row()), false)
|
||||
return null
|
||||
var preview := Label.new()
|
||||
preview.text = "%s:%s → 施設へ配置" % [_selected_row().get(&"display_name", staff_id), "夜勤" if _draft_template_id == &"NIGHT" else "日勤"]
|
||||
(roster_buttons[staff_id] as Control).set_drag_preview(preview)
|
||||
return {&"kind": &"staff_assignment", &"staff_id": staff_id, &"template_id": _draft_template_id, &"queue_instance_id": _queue_instance_id}
|
||||
|
||||
|
||||
func assign_from_drop(data: Dictionary, facility_id: StringName) -> void:
|
||||
# A successful load replaces the queue, even when staff/facility IDs match.
|
||||
# Never carry an unfinished pointer gesture into that restored world.
|
||||
refresh()
|
||||
if data.get(&"queue_instance_id", 0) != _queue_instance_id:
|
||||
_set_result("読み込み前のドラッグは取り消しました。従業員を選び直して配置してください。", false)
|
||||
return
|
||||
if facility_id.is_empty():
|
||||
_set_result("完成した配属先施設へドロップしてください。表裏の層も確認してください。", false)
|
||||
return
|
||||
var staff_id := StringName(data.get(&"staff_id", &""))
|
||||
if not roster_buttons.has(staff_id):
|
||||
_set_result("従業員が見つかりません。", false)
|
||||
return
|
||||
select_staff(staff_id)
|
||||
_draft_facility_id = facility_id
|
||||
_draft_template_id = StringName(data.get(&"template_id", &"DAY"))
|
||||
_draft_dirty = true
|
||||
submit_assignment()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
class_name SupplyManagementPanel
|
||||
extends VBoxContainer
|
||||
|
||||
const LayerSkin := preload("res://scripts/ui/layer_interface_skin.gd")
|
||||
|
||||
## Read models and commands only. Prices, stock, recipes and delivery progress
|
||||
## belong to Runtime; changing a control never mutates inventory directly.
|
||||
signal status_requested(message: String, success: bool)
|
||||
@@ -11,6 +13,8 @@ const PURCHASE: StringName = &"purchase_supplies"
|
||||
const PRODUCTION: StringName = &"set_kitchen_production"
|
||||
const ITEM_NAMES := {&"ingredient/raw_food": "食材", &"ingredient/basic_meal": "完成料理"}
|
||||
const STATE_NAMES := {
|
||||
&"SUSPENDED": "破壊・摘発による休止",
|
||||
&"RELATED_FACILITY_INACTIVE": "関連施設が未稼働",
|
||||
&"OPEN": "補給待ち", &"RESERVED": "運搬員待ち", &"ASSIGNED": "荷元へ移動",
|
||||
&"PICKING_UP": "積込中", &"DELIVERING": "配送中", &"COMPLETED": "納品済み",
|
||||
&"BLOCKED": "配送停止", &"CANCELLED": "取消済み", &"WORKING": "調理中",
|
||||
@@ -99,7 +103,9 @@ func _ready() -> void:
|
||||
_build_interface()
|
||||
_built = true
|
||||
if _runtime != null:
|
||||
if _runtime.has_signal(&"snapshot_published"):
|
||||
if _runtime.has_signal(&"state_updated"):
|
||||
_runtime.connect(&"state_updated", _on_snapshot_published)
|
||||
elif _runtime.has_signal(&"snapshot_published"):
|
||||
_runtime.connect(&"snapshot_published", _on_snapshot_published)
|
||||
if _runtime.has_signal(&"command_completed"):
|
||||
_runtime.connect(&"command_completed", _on_command_completed)
|
||||
@@ -159,6 +165,7 @@ func _build_interface() -> void:
|
||||
for color_name: StringName in [&"font_color", &"font_pressed_color", &"font_hover_color", &"font_hover_pressed_color", &"font_focus_color"]:
|
||||
production_enabled.add_theme_color_override(color_name, Color("55371e"))
|
||||
production_enabled.add_theme_color_override("font_disabled_color", Color("766650"))
|
||||
LayerSkin.register_toggle_text(production_enabled, LayerSkin.active_layer(self))
|
||||
production_enabled.toggled.connect(_on_production_enabled_changed)
|
||||
kitchen_page.add_child(production_enabled)
|
||||
kitchen_page.add_child(_label("目標在庫 · 完成料理"))
|
||||
@@ -395,7 +402,7 @@ func _on_command_completed(result: Variant) -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
func _on_snapshot_published(_snapshot: Dictionary, _digest: String) -> void:
|
||||
func _on_snapshot_published(_snapshot: Dictionary = {}, _digest: String = "") -> void:
|
||||
if is_visible_in_tree():
|
||||
refresh()
|
||||
|
||||
@@ -519,7 +526,7 @@ func _reasons_text(value: Variant) -> String:
|
||||
|
||||
func _set_result(message: String, success: bool) -> void:
|
||||
result_label.text = message
|
||||
result_label.add_theme_color_override("font_color", Color("397346") if success else Color("a7402e"))
|
||||
LayerSkin.register_status_text(result_label, success, LayerSkin.active_layer(self))
|
||||
status_requested.emit(message, success)
|
||||
|
||||
|
||||
@@ -532,6 +539,7 @@ func _label(value: String, node_name := "") -> Label:
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
label.add_theme_font_size_override("font_size", 13)
|
||||
label.add_theme_color_override("font_color", Color("55371e"))
|
||||
LayerSkin.register_text(label, LayerSkin.active_layer(self))
|
||||
return label
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
class_name WorldTreeRankDialog
|
||||
extends ConfirmationDialog
|
||||
|
||||
const LayerSkin := preload("res://scripts/ui/layer_interface_skin.gd")
|
||||
|
||||
signal status_requested(message: String, success: bool)
|
||||
const LABELS := {
|
||||
&"reputation":"評判", &"asset_gold":"所持金", &"faith_lifetime":"累計信仰", &"guest_lifetime":"累計来客数",
|
||||
&"underground_influence":"裏社会への影響", &"reputation_or_underground_influence":"評判または裏社会への影響",
|
||||
&"active_above_ground_facilities":"地上の稼働施設", &"active_underground_facilities":"地下の稼働施設",
|
||||
&"important_rank_5":"重要クエスト", &"main_chapter_complete":"本編の章目標",
|
||||
}
|
||||
var _runtime: Variant
|
||||
var _target := -1
|
||||
var _pending_id := 0
|
||||
var _queue_id := 0
|
||||
var _skin_styles: Array[StyleBoxFlat] = []
|
||||
func configure(runtime: Variant) -> void: _runtime=runtime
|
||||
func _ready() -> void:
|
||||
name="WorldTreeRankDialog"
|
||||
title="世界樹ランク"
|
||||
get_ok_button().text="昇格する"
|
||||
get_cancel_button().text="閉じる"
|
||||
confirmed.connect(_promote)
|
||||
_runtime.command_completed.connect(_on_command_completed)
|
||||
_build_skin()
|
||||
apply_layer_skin(LayerSkin.active_layer(self))
|
||||
func show_rank() -> void:
|
||||
refresh()
|
||||
popup_centered(Vector2i(480,260))
|
||||
func refresh() -> void:
|
||||
var queue_id: int=_runtime.command_queue.get_instance_id()
|
||||
if queue_id!=_queue_id:
|
||||
_pending_id=0
|
||||
_queue_id=queue_id
|
||||
for command: Dictionary in _runtime.command_queue.create_snapshot().get("pending",[]):
|
||||
if command.get("command_type","")=="set_world_tree_rank":
|
||||
_pending_id=int(command["command_id"])
|
||||
break
|
||||
var report: Dictionary=_runtime.get_live_progression_report()
|
||||
var rank: int=report[&"snapshot"][&"current_rank"]
|
||||
var evaluation: Dictionary=report[&"evaluation"]
|
||||
_target=rank+1
|
||||
var lines: Array[String]=["現在のランク:%d" % rank]
|
||||
if rank>=ProgressionService.MAX_RANK:
|
||||
lines.append("最高ランクに到達しています。")
|
||||
else:
|
||||
lines.append("ランク%dへの条件" % _target)
|
||||
for requirement: Dictionary in evaluation[&"requirements"]:
|
||||
var label: String=LABELS.get(StringName(requirement[&"id"]),"進行条件")
|
||||
var value: String="%d / %d" % [requirement[&"current"],requirement[&"required"]]
|
||||
if requirement[&"comparison"]==&"QUEST_COMPLETED": value="達成済み" if requirement[&"met"] else "未達成"
|
||||
lines.append("%s %s:%s" % ["✓" if requirement[&"met"] else "・",label,value])
|
||||
if evaluation.get(&"eligible",false):lines.append("条件を満たしています。昇格すると建設範囲が広がります。")
|
||||
elif evaluation.get(&"reason",&"")==&"QUARANTINED":lines.append("進行データの確認が必要なため、現在は昇格できません。")
|
||||
if evaluation.get(&"reason",&"")==&"OPERATIONS_BLOCKED":
|
||||
var reason: StringName=evaluation.get(&"block_reason",&"")
|
||||
lines.append("未払い経費を精算するまで昇格できません。" if reason in [&"ARREARS_REMAIN",&"UNPAID_EXPENSES"] else "ゲーム終了後は昇格できません。" if reason==&"BANKRUPT" else "営業状態の復旧が必要なため、現在は昇格できません。")
|
||||
if evaluation.get(&"reason",&"")==&"UNAVAILABLE":lines.append("条件を確認できません。もう一度開き直してください。")
|
||||
if _pending_id!=0:lines.append("昇格申請を受理しました。時間更新を待っています。")
|
||||
dialog_text="\n\n".join(lines)
|
||||
get_ok_button().disabled=rank>=ProgressionService.MAX_RANK or not evaluation.get(&"eligible",false) or _pending_id!=0
|
||||
func _promote() -> void:
|
||||
if _pending_id!=0 or _target<2:return
|
||||
var result: CommandResult=_runtime.submit_command(&"set_world_tree_rank",{&"rank":_target,&"request_id":StringName("ui_rank_%d" % _target)})
|
||||
for command: Dictionary in _runtime.command_queue.create_snapshot().get("pending",[]):
|
||||
if result.is_success() and int(command["command_id"])==result.command_id:
|
||||
_pending_id=result.command_id
|
||||
status_requested.emit("昇格申請を受理しました。次の時間更新で反映します。",true)
|
||||
return
|
||||
_finish(result)
|
||||
func _on_command_completed(result: CommandResult) -> void:
|
||||
if _pending_id!=0 and result.command_id==_pending_id:
|
||||
_pending_id=0
|
||||
_finish(result)
|
||||
func _finish(result: CommandResult) -> void:
|
||||
status_requested.emit("世界樹ランクが上がりました。" if result.is_success() else "昇格できませんでした。現在の条件を確認してください。",result.is_success())
|
||||
refresh()
|
||||
|
||||
|
||||
func _build_skin() -> void:
|
||||
# Window uses a local Theme rather than Control theme overrides.
|
||||
# Keep native dialog input, focus and confirmation behaviour intact.
|
||||
theme = Theme.new()
|
||||
var panel := _skin_box(Color("f4e3bd"), &"panel_parchment")
|
||||
theme.set_stylebox("panel", "AcceptDialog", panel)
|
||||
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"))
|
||||
get_label().add_theme_color_override("font_color", Color("55371e"))
|
||||
LayerSkin.register_text(get_label(), 0)
|
||||
for button: Button in [get_ok_button(), get_cancel_button()]:
|
||||
for key: StringName in [&"normal", &"hover", &"pressed", &"disabled", &"focus"]:
|
||||
var role: StringName = &"button_normal"
|
||||
var color := Color("65452d")
|
||||
if key in [&"hover", &"focus"]: role = &"button_hover"; color = Color("896139")
|
||||
elif key == &"pressed": role = &"button_pressed"; color = Color("49311f")
|
||||
elif key == &"disabled": role = &"button_disabled"; color = Color("786852")
|
||||
button.add_theme_stylebox_override(key, _skin_box(color, role))
|
||||
for key: StringName in [&"font_color", &"font_hover_color", &"font_pressed_color", &"font_hover_pressed_color", &"font_focus_color"]:
|
||||
button.add_theme_color_override(key, Color("fff0cf"))
|
||||
button.add_theme_color_override("font_disabled_color", Color("c7bda9"))
|
||||
LayerSkin.register_toggle_text(button, 0)
|
||||
|
||||
|
||||
func _skin_box(background: Color, role: StringName) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = background
|
||||
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)
|
||||
LayerSkin.register_text(get_label(), layer)
|
||||
for button: Button in [get_ok_button(), get_cancel_button()]:
|
||||
LayerSkin.register_toggle_text(button, layer)
|
||||
@@ -0,0 +1 @@
|
||||
uid://lr3xhtjonmwu
|
||||
Reference in New Issue
Block a user