Files
TheTree_mock/scripts/ui/business_management_panel.gd
T

384 lines
17 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name BusinessManagementPanel
extends VBoxContainer
## 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 STATE_NAMES := {&"BROKEN": "故障中", &"NO_ACCESS": "経路なし", &"NO_POWER": "魔力不足", &"NO_STAFF": "職員不足", &"CLOSED": "休業中", &"DEGRADED": "状態悪化", &"OPEN": "稼働中"}
const REASONS := {
&"FACILITY_NOT_FOUND": "施設が見つかりません。選び直してください。",
&"INVALID_POLICY": "営業設定を確認してください。",
&"INVALID_PRICE_MODE": "料金を選び直してください。",
&"INVALID_PAYLOAD": "営業設定を確認してください。",
&"FACILITY_CLOSED": "この施設は利用できません。",
&"BANKRUPT": "運営終了後は変更できません。",
}
var facility_option: OptionButton
var price_option: OptionButton
var business_open_button: CheckButton
var quote_label: Label
var effect_label: Label
var facility_status_label: Label
var today_label: Label
var explanation_label: Label
var apply_button: Button
var result_label: Label
var selected_facility_id: StringName = &""
var _runtime: Variant
var _style_button: Callable
var _report: Dictionary = {}
var _drafts: Dictionary = {}
var _pending_commands: Dictionary = {}
var _queue_instance_id := 0
var _sequence := 1
var _refreshing := false
var _built := false
func configure(runtime: Variant, style_button := Callable()) -> void:
_runtime = runtime
_style_button = style_button
func _ready() -> void:
name = "BusinessManagementPanel"
size_flags_horizontal = Control.SIZE_EXPAND_FILL
add_theme_constant_override("separation", 6)
_build_interface()
_built = true
if _runtime != null:
if _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:
add_child(_label("営業 · 料金と受付", "BusinessSummary"))
add_child(_label("設定する施設"))
facility_option = _option("BusinessFacilityOption")
facility_option.item_selected.connect(func(index: int) -> void:
selected_facility_id = StringName(facility_option.get_item_metadata(index))
refresh())
add_child(facility_option)
facility_status_label = _label("", "BusinessFacilityStatus")
add_child(facility_status_label)
add_child(_label("新規の料金"))
price_option = _option("BusinessPriceOption")
price_option.item_selected.connect(func(_index: int) -> void: _store_draft())
add_child(price_option)
quote_label = _label("", "BusinessPriceQuote")
quote_label.add_theme_font_size_override("font_size", 16)
add_child(quote_label)
effect_label = _label("", "BusinessPriceEffect")
add_child(effect_label)
business_open_button = CheckButton.new()
business_open_button.name = "BusinessOpenButton"
business_open_button.text = "新規の受付を開く"
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"))
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)
apply_button = _button("営業設定を適用", "ApplyBusinessPolicyButton", submit_policy)
add_child(apply_button)
explanation_label = _label(GUEST_POLICY_EXPLANATION, "BusinessPolicyExplanation")
add_child(explanation_label)
add_child(HSeparator.new())
today_label = _label("", "BusinessTodayResults")
add_child(today_label)
var actions := HBoxContainer.new()
actions.add_theme_constant_override("separation", 4)
for entry: Array in [[&"staff", "従業員"], [&"product", "商品"], [&"ledger", "帳簿"]]:
var button := _button(entry[1], "BusinessAction_%s" % entry[0], func() -> void: action_requested.emit(entry[0]))
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
actions.add_child(button)
add_child(actions)
result_label = _label("", "BusinessResult")
add_child(result_label)
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()
_drafts.clear()
facility_option.get_popup().hide()
price_option.get_popup().hide()
_synchronize_commands(queue)
_report = {}
if _runtime != null and _runtime.has_method(&"get_business_management_report"):
var value: Variant = _runtime.call(&"get_business_management_report")
if value is Dictionary:
_report = value
_refreshing = true
_fill_facilities()
var row := _facility_row(selected_facility_id)
var draft := _selected_draft(row)
_fill_modes(StringName(draft.get(&"price_mode", &"standard")))
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_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
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
apply_button.disabled = row.is_empty() or pending or not _is_changed(row, draft)
apply_button.text = "設定を受理済み…" if pending else "営業設定を適用"
if row.is_empty():
facility_status_label.text = "施設が完成すると営業を設定できます。"
quote_label.text = "施設を選んでください。"
effect_label.text = ""
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))]
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 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", &"")), "回"))
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)
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))]
_refreshing = false
func select_facility(id: StringName) -> void:
selected_facility_id = id
refresh()
func get_report() -> Dictionary:
return _report.duplicate(true)
func submit_policy() -> void:
var row := _facility_row(selected_facility_id)
if row.is_empty() or _has_pending(selected_facility_id):
return
var draft := _selected_draft(row)
if not _is_changed(row, draft):
return
if _runtime == null or not _runtime.has_method(&"submit_command"):
_set_result("営業サービスへ接続できません。", false)
return
var patch: Dictionary = {&"business_open": bool(draft.get(&"business_open", true))}
if bool(row.get(&"can_price", false)):
patch[&"price_mode"] = StringName(draft.get(&"price_mode", &"standard"))
var payload := {&"facility_id": selected_facility_id, &"patch": patch}
var key := StringName("%s%08d" % [KEY_PREFIX, _sequence])
_sequence += 1
var result: Variant = _runtime.call(&"submit_command", COMMAND, payload, &"player", selected_facility_id, 75, -1, key)
if result == null or not result.has_method(&"is_success"):
_set_result("操作に応答がありません。", false)
return
var queued := false
var queue: Variant = _runtime.get(&"command_queue")
if queue != null:
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_commands[int(result.command_id)] = payload.duplicate(true)
_set_result("営業設定を受理しました。次の時間更新で反映します。", true)
else:
_show_result(result, payload)
refresh()
func _show_result(result: Variant, payload: Dictionary) -> void:
if not result.is_success():
var details: Dictionary = result.details if result.details is Dictionary else {}
var reason := StringName(details.get(&"reason", &"UNAVAILABLE"))
_set_result(String(REASONS.get(reason, "営業設定を反映できませんでした(%s)。" % reason)), false)
return
var id := StringName(payload.get(&"facility_id", &""))
_drafts.erase(id)
var row := _facility_row(id)
_set_result("%sの営業設定を反映しました。" % String(row.get(&"display_name", "施設")), true)
func _selected_draft(row: Dictionary) -> Dictionary:
for payload: Dictionary in _pending_commands.values():
if StringName(payload.get(&"facility_id", &"")) == selected_facility_id:
var draft := row.duplicate(true)
draft.merge(payload.get(&"patch", {}), true)
return draft
return _drafts.get(selected_facility_id, row)
func _is_changed(row: Dictionary, draft: Dictionary) -> bool:
return bool(row.get(&"business_open", true)) != bool(draft.get(&"business_open", true)) or (bool(row.get(&"can_price", false)) and StringName(row.get(&"price_mode", &"standard")) != StringName(draft.get(&"price_mode", &"standard")))
func _store_draft() -> void:
if _refreshing or selected_facility_id.is_empty() or _has_pending(selected_facility_id):
return
var mode := &"standard"
if price_option.selected >= 0:
mode = StringName(price_option.get_item_metadata(price_option.selected))
_drafts[selected_facility_id] = {&"business_open": business_open_button.button_pressed, &"price_mode": mode}
refresh()
func _synchronize_commands(queue: Variant) -> void:
_pending_commands.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", &"")) == COMMAND:
_pending_commands[int(command.get("command_id", 0))] = command.get("payload", {}).duplicate(true)
func _has_pending(id: StringName) -> bool:
for payload: Dictionary in _pending_commands.values():
if StringName(payload.get(&"facility_id", &"")) == id:
return true
return false
func _on_command_completed(result: Variant) -> void:
if not _pending_commands.has(int(result.command_id)):
return
var payload: Dictionary = _pending_commands[int(result.command_id)]
_pending_commands.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 _fill_facilities() -> void:
if facility_option.get_popup().visible:
return
facility_option.clear()
var rows := _rows(&"facilities")
if rows.is_empty():
facility_option.add_item("完成した施設がありません")
facility_option.set_item_metadata(0, &"")
selected_facility_id = &""
return
var selected_index := 0
for row: Dictionary in rows:
facility_option.add_item(_facility_name(row))
var index := facility_option.item_count - 1
var id := StringName(row.get(&"facility_id", &""))
facility_option.set_item_metadata(index, id)
if id == selected_facility_id:
selected_index = index
facility_option.select(selected_index)
selected_facility_id = StringName(facility_option.get_item_metadata(selected_index))
func _fill_modes(selected: StringName) -> void:
if price_option.get_popup().visible:
return
price_option.clear()
var selected_index := 0
for mode: Dictionary in _rows(&"modes"):
price_option.add_item("%s(%d%%)" % [String(mode.get(&"label", "料金")), int(mode.get(&"percent", 100))])
var index := price_option.item_count - 1
price_option.set_item_metadata(index, StringName(mode.get(&"id", &"standard")))
if StringName(mode.get(&"id", &"standard")) == selected:
selected_index = index
if price_option.item_count > 0:
price_option.select(selected_index)
func _facility_name(row: Dictionary) -> String:
var label := String(row.get(&"display_name", row.get(&"facility_id", "施設")))
var origin: Variant = row.get(&"origin", null)
var layer := "裏" if int(row.get(&"layer", 0)) == 1 else "表"
if origin is Vector2i or origin is Vector3i:
label += " (%d,%d・%s)" % [origin.x, origin.y, layer]
elif origin is Dictionary:
label += " (%d,%d・%s)" % [int(origin.get(&"x", 0)), int(origin.get(&"y", 0)), layer]
return label
func _price_effect(mode: StringName) -> String:
match mode:
&"charitable": return "お財布にやさしい料金。満足度が大きく上がる一方、1回の売上は少なくなります。"
&"affordable": return "手頃な料金。満足度が上がり、1回の売上は少し少なくなります。"
&"expensive": return "1回の売上が増えますが、満足度は下がります。お客様の懐具合にもご注意を。"
&"exorbitant": return "とても高い料金。満足度が大きく下がり、お客様が支払えない場合もあります。"
_: return "基準となる料金。価格による満足度の増減はありません。"
func _rows(key: StringName) -> Array[Dictionary]:
var rows: Array[Dictionary] = []
var value: Variant = _report.get(key, [])
if value is Array:
for row: Variant in value:
if row is Dictionary:
rows.append(row)
return rows
func _facility_row(id: StringName) -> Dictionary:
for row: Dictionary in _rows(&"facilities"):
if StringName(row.get(&"facility_id", &"")) == id:
return row
return {}
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"))
status_requested.emit(message, success)
func _label(value: String, node_name := "") -> Label:
var label := Label.new()
if not node_name.is_empty():
label.name = node_name
label.text = value
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
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"))
return label
func _button(value: String, node_name: String, callback: Callable) -> Button:
var button := Button.new()
button.name = node_name
button.text = value
button.custom_minimum_size.y = 30
button.add_theme_font_size_override("font_size", 13)
button.pressed.connect(callback)
if _style_button.is_valid():
_style_button.call(button)
return button
func _option(node_name: String) -> OptionButton:
var picker := OptionButton.new()
picker.name = node_name
picker.fit_to_longest_item = false
picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
picker.custom_minimum_size.y = 30
if _style_button.is_valid():
_style_button.call(picker)
return picker