@@ -0,0 +1,383 @@
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 = " 設定後 %d G / %s \n 現在 %d G · 標準 %d G " % [ 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 売上 %d G · 直接支出 %d G \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