Add business pricing and closure controls with compatible digest optimization

This commit is contained in:
Takeshi
2026-09-09 12:04:12 +09:00
parent 071e338e82
commit b3b82ef03e
35 changed files with 2203 additions and 59 deletions
+16 -1
View File
@@ -2,6 +2,7 @@ class_name FacilityInstance
extends RefCounted
const ServiceInstanceType = preload("res://scripts/facility/service_instance.gd")
const FacilityPricingType = preload("res://scripts/facility/facility_pricing.gd")
## Values are deliberately ordered by operational priority.
enum State {
@@ -81,6 +82,8 @@ func configure(
access_available = bool(_value(options, &"access_available", true))
power_available = bool(_value(options, &"power_available", true))
operator_open = bool(_value(options, &"operator_open", true))
if _has_value(policy_current, &"business_open"):
operator_open = bool(_value(policy_current, &"business_open", operator_open))
var intervals := _parse_intervals(_value(options, &"opening_intervals", []))
if not intervals.is_empty():
opening_intervals = intervals
@@ -98,7 +101,8 @@ func is_valid() -> bool:
and base_service_minutes > 0 \
and staff_min >= 0 \
and staff_max >= staff_min \
and _intervals_are_valid(opening_intervals)
and _intervals_are_valid(opening_intervals) \
and bool(FacilityPricingType.validate_patch(policy_current)[&"valid"])
func set_opening_hours(open_minute_of_day: int, close_minute_of_day: int) -> bool:
@@ -144,6 +148,8 @@ func set_operational_conditions(conditions: Dictionary, absolute_minute: int) ->
power_available = bool(_value(conditions, &"power_available", power_available))
if _has_value(conditions, &"operator_open"):
operator_open = bool(_value(conditions, &"operator_open", operator_open))
if _has_value(policy_current, &"business_open"):
apply_policy_patch({&"business_open": operator_open})
if _has_value(conditions, &"active_staff_ids"):
active_staff_ids = _sorted_unique_names(_value(conditions, &"active_staff_ids", []))
if _has_value(conditions, &"degraded_reasons"):
@@ -218,6 +224,8 @@ func apply_policy_patch(patch: Dictionary) -> bool:
policy_current.erase(String(key))
policy_current[key] = _copy(value)
changed = true
if _has_value(patch, &"business_open"):
operator_open = bool(_value(patch, &"business_open", operator_open))
if changed:
policy_revision += 1
return changed
@@ -360,6 +368,13 @@ static func _snapshot_shapes_are_valid(snapshot: Dictionary) -> bool:
for field: StringName in dictionary_fields:
if not snapshot.get(field, null) is Dictionary:
return false
for field: StringName in [&"policy_current", &"policy_pending"]:
if not bool(FacilityPricingType.validate_patch(snapshot[field])[&"valid"]):
return false
var saved_policy: Dictionary = snapshot[&"policy_current"]
if _has_value(saved_policy, &"business_open") \
and _value(saved_policy, &"business_open", true) != snapshot.get(&"operator_open", true):
return false
return true
+87
View File
@@ -0,0 +1,87 @@
class_name FacilityPricing
extends RefCounted
## Shared integer pricing rules for quotations, guest outcomes and management UI.
## Unrelated policy keys remain the responsibility of their owning systems.
const DEFAULT_MODE := &"standard"
const MODES: Array[Dictionary] = [
{&"id": &"charitable", &"label": "奉仕価格", &"percent": 60, &"satisfaction_delta": 10},
{&"id": &"affordable", &"label": "安価", &"percent": 80, &"satisfaction_delta": 5},
{&"id": &"standard", &"label": "標準", &"percent": 100, &"satisfaction_delta": 0},
{&"id": &"expensive", &"label": "高価", &"percent": 150, &"satisfaction_delta": -5},
{&"id": &"exorbitant", &"label": "ぼったくり", &"percent": 250, &"satisfaction_delta": -20},
]
static func modes() -> Array[Dictionary]:
return MODES.duplicate(true)
static func quote(base_price: int, policy: Dictionary) -> int:
var percent := int(_mode(policy)[&"percent"])
# Currency is integer gold. An exact half gold rounds up, without depending
# on floating-point rounding (35 G at 150 percent is 53 G).
@warning_ignore("integer_division")
var result := (maxi(base_price, 0) * percent + 50) / 100
return result
static func satisfaction_modifier(
policy: Dictionary,
quality: int,
expected_quality: int
) -> int:
var modifier := int(_mode(policy)[&"satisfaction_delta"])
# GAME_SPECIFICATION 7.4 includes a difference of exactly twenty points.
if modifier < 0 and quality - expected_quality >= 20:
# Keep the same nearest-integer rule for a half penalty: -2.5 becomes -3.
@warning_ignore("integer_division")
return -((-modifier + 1) / 2)
return modifier
static func validate_patch(patch: Dictionary) -> Dictionary:
if _has(patch, &"price_mode"):
var mode: Variant = _value(patch, &"price_mode", DEFAULT_MODE)
if not (mode is String or mode is StringName) or not _known_mode(StringName(mode)):
return _invalid(&"INVALID_PRICE_MODE", "価格設定は5段階のいずれかを選んでください。")
if _has(patch, &"business_open") and not _value(patch, &"business_open", true) is bool:
return _invalid(&"INVALID_BUSINESS_OPEN", "営業状態は営業中または休業を指定してください。")
var normalized: Dictionary = {}
for raw_key: Variant in patch:
if not (raw_key is String or raw_key is StringName):
return _invalid(&"INVALID_POLICY_KEY", "施設方針の項目名が正しくありません。")
var value: Variant = patch[raw_key]
normalized[StringName(raw_key)] = value.duplicate(true) if value is Array or value is Dictionary else value
if normalized.has(&"price_mode"):
normalized[&"price_mode"] = StringName(normalized[&"price_mode"])
return {&"valid": true, &"reason": &"", &"message": "", &"patch": normalized}
static func _mode(policy: Dictionary) -> Dictionary:
var value: Variant = _value(policy, &"price_mode", DEFAULT_MODE)
if value is String or value is StringName:
for mode: Dictionary in MODES:
if mode[&"id"] == StringName(value):
return mode
return MODES[2]
static func _known_mode(mode_id: StringName) -> bool:
for mode: Dictionary in MODES:
if mode[&"id"] == mode_id:
return true
return false
static func _invalid(reason: StringName, message: String) -> Dictionary:
return {&"valid": false, &"reason": reason, &"message": message, &"patch": {}}
static func _has(source: Dictionary, key: StringName) -> bool:
return source.has(key) or source.has(String(key))
static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
return source.get(key, source.get(String(key), fallback))
+1
View File
@@ -0,0 +1 @@
uid://d4hh37purloll
+28 -3
View File
@@ -15,6 +15,7 @@ const FacilityInstanceType = preload("res://scripts/facility/facility_instance.g
const ServiceRequestType = preload("res://scripts/facility/service_request.gd")
const ServiceInstanceType = preload("res://scripts/facility/service_instance.gd")
const CommandResultType = preload("res://scripts/core/command_result.gd")
const FacilityPricingType = preload("res://scripts/facility/facility_pricing.gd")
const SNAPSHOT_VERSION := 1
const STAFF_REPLACEMENT_GRACE_MINUTES := 5
@@ -175,15 +176,22 @@ func set_policy(
return _not_found("Facility instance", instance_id)
if patch.is_empty():
return _invalid("Policy patch cannot be empty.")
var validation := FacilityPricingType.validate_patch(patch)
if not bool(validation[&"valid"]):
return _invalid(String(validation[&"message"]), {&"reason": validation[&"reason"]})
var normalized_patch: Dictionary = validation[&"patch"]
if effective_at >= 0 and effective_at > current_minute:
instance.schedule_policy_patch(patch, effective_at)
instance.schedule_policy_patch(normalized_patch, effective_at)
last_error = ""
return _success({
&"instance_id": instance_id,
&"pending_revision": instance.policy_pending_revision,
&"effective_at": effective_at,
})
var changed := instance.apply_policy_patch(patch)
var changed := instance.apply_policy_patch(normalized_patch)
var old_state := instance.state
instance.evaluate_state(current_minute)
_emit_state_change(instance, old_state)
last_error = ""
return _success({
&"instance_id": instance_id,
@@ -205,7 +213,7 @@ func create_request(
return null
var quotation := options.duplicate(true)
if not _has_value(quotation, &"quoted_price"):
quotation[&"quoted_price"] = instance.base_price
quotation[&"quoted_price"] = FacilityPricingType.quote(instance.base_price, instance.policy_current)
if not _has_value(quotation, &"expected_quality"):
quotation[&"expected_quality"] = instance.base_quality
if not _has_value(quotation, &"service_minutes"):
@@ -560,6 +568,11 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
if instance == null or restored_instances.has(instance.instance_id):
last_error = "Facility snapshot contains an invalid or duplicate instance."
return false
if not _saved_policy_is_valid(instance.policy_current) or not _saved_policy_is_valid(instance.policy_pending):
return false
for active: ServiceInstance in instance.active_services.values():
if not _saved_policy_is_valid(active.policy_snapshot):
return false
restored_instances[instance.instance_id] = instance
var restored_requests: Dictionary = {}
@@ -571,6 +584,8 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
if request == null or restored_requests.has(request.request_id):
last_error = "Facility snapshot contains an invalid or duplicate request."
return false
if not _saved_policy_is_valid(request.policy_snapshot):
return false
restored_requests[request.request_id] = request
var restored_history: Dictionary = {}
@@ -582,6 +597,8 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
if service == null or not service.is_terminal() or restored_history.has(service.service_id):
last_error = "Facility snapshot contains invalid service history."
return false
if not _saved_policy_is_valid(service.policy_snapshot):
return false
restored_history[service.service_id] = service
var restored_known_instances: Variant = _id_set(_value(snapshot, &"known_instance_ids", []))
@@ -619,6 +636,14 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
return true
func _saved_policy_is_valid(policy: Dictionary) -> bool:
var validation := FacilityPricingType.validate_patch(policy)
if not bool(validation[&"valid"]):
last_error = "Facility snapshot contains an invalid policy: %s" % validation[&"reason"]
return false
return true
func _validate_restored_graph(
instances: Dictionary,
requests: Dictionary,