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,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()
|
||||
|
||||
Reference in New Issue
Block a user