Show normal usage fees separately from the unspecified desire-match surcharge. Preserve free reception and omit guest pricing from warehouse and cleaning rooms. Validation: 155 suites / 12,310 checks; focused 700 checks; strict state and event equivalence; 16 normal and compact FRONT/BACK captures.
119 lines
4.9 KiB
GDScript
119 lines
4.9 KiB
GDScript
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 apply_multiplier(price: int, multiplier_bp: int) -> int:
|
|
# Same nearest-G, half-up convention as policy pricing.
|
|
@warning_ignore("integer_division")
|
|
return (maxi(price, 0) * maxi(multiplier_bp, 0) + 5000) / 10000
|
|
|
|
|
|
## Caller supplies already-determined deficient attributes. Threshold and surcharge
|
|
## are deliberately not invented. An unresolved quote must not be charged.
|
|
static func quote_service(base_price: int, policy: Dictionary, service_attribute: StringName, deficient_attributes: Array[StringName], surcharge_amount: int = -1) -> Dictionary:
|
|
var base := quote(base_price, policy)
|
|
var matches := not service_attribute.is_empty() and deficient_attributes.has(service_attribute)
|
|
var resolved := not matches or surcharge_amount > 0
|
|
var surcharge := surcharge_amount if matches and resolved else 0
|
|
return {&"base_price": base, &"matches": matches, &"resolved": resolved,
|
|
&"surcharge": surcharge, &"total": base + surcharge, &"eligible": true}
|
|
|
|
|
|
## The normal fee follows its existing charging contract. The added need-match
|
|
## rule has no approved assessment or amount. Null retains that uncertainty.
|
|
static func pending_service_readmodel(normal_price: int, service_attribute: StringName) -> Dictionary:
|
|
return {
|
|
&"normal_price": normal_price,
|
|
&"service_attribute": null if service_attribute.is_empty() else service_attribute,
|
|
&"deficient_attributes": null,
|
|
&"matches": null,
|
|
&"surcharge": null,
|
|
&"total": null,
|
|
&"resolved": false,
|
|
}
|
|
|
|
|
|
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))
|