Add business pricing and closure controls with compatible digest optimization
This commit is contained in:
@@ -6590,9 +6590,13 @@ func _execute_create_service_request(command: SimulationCommand) -> CommandResul
|
||||
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Service request options must be a Dictionary.")
|
||||
var options: Dictionary = raw_options.duplicate(true)
|
||||
var quoted_price := maxi(
|
||||
int(options.get(&"quoted_price", options.get("quoted_price", instance.base_price))),
|
||||
int(options.get(&"quoted_price", options.get("quoted_price", FacilityPricing.quote(_business_base_price(instance), instance.policy_current)))),
|
||||
0
|
||||
)
|
||||
# Freeze the same price used by candidate selection and the business panel.
|
||||
options[&"quoted_price"] = quoted_price
|
||||
if not instance.operator_open or not instance.is_within_opening_hours(_runtime_event_minute()):
|
||||
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "This facility is closed to new service requests.", {&"reason": &"FACILITY_CLOSED"})
|
||||
var payer_account := _guest_wallet_account(guest_ids[0])
|
||||
var committed_price := _outstanding_service_commitment(guest_ids[0])
|
||||
if not economy_service.account_allows_negative(payer_account) \
|
||||
@@ -7597,6 +7601,7 @@ func _advance_guest_vitals(absolute_minute: int) -> void:
|
||||
func _advance_guest_queues(absolute_minute: int) -> void:
|
||||
if _operations_tick_blocked:
|
||||
return
|
||||
_release_closed_business_queues(absolute_minute)
|
||||
var guests_by_request: Dictionary = {}
|
||||
for guest: GuestRuntime in guest_service.get_guests():
|
||||
if guest.state != GuestRuntime.State.QUEUING:
|
||||
@@ -7606,6 +7611,9 @@ func _advance_guest_queues(absolute_minute: int) -> void:
|
||||
(guests_by_request[guest.queue_request_id] as Array).append(guest)
|
||||
for request_id: StringName in _sorted_name_keys(guests_by_request):
|
||||
var grouped_guests: Array = guests_by_request[request_id]
|
||||
var queued_facility := facility_service.get_instance(grouped_guests[0].target_facility_id)
|
||||
if queued_facility != null and (not queued_facility.operator_open or not queued_facility.is_within_opening_hours(absolute_minute)):
|
||||
continue # A deferred closure must not spend patience while retrying.
|
||||
var abandon_group := false
|
||||
for guest: GuestRuntime in grouped_guests:
|
||||
if guest.patience_remaining <= 1:
|
||||
@@ -8239,6 +8247,8 @@ func _project_guest_candidates(guest_id: StringName) -> Array:
|
||||
var definition := definition_registry.get_facility(facility.definition_id)
|
||||
if definition == null:
|
||||
continue
|
||||
if &"entrance" in definition.tags and _guest_has_checked_in(guest):
|
||||
continue
|
||||
var has_inputs := true
|
||||
for item_id: StringName in _sorted_name_keys(definition.inventory_inputs):
|
||||
if inventory_service.available_quantity(facility.local_inventory_id, item_id) < int(definition.inventory_inputs[item_id]):
|
||||
@@ -8256,7 +8266,7 @@ func _project_guest_candidates(guest_id: StringName) -> Array:
|
||||
&"reachable": bool(access.get(&"reachable", false)),
|
||||
&"travel_minutes": maxi(path.size() - 1, 0),
|
||||
&"expected_wait": facility.queue_entries.size() * facility.base_service_minutes / maxi(facility.capacity, 1),
|
||||
&"estimated_total_gold": facility.base_price,
|
||||
&"estimated_total_gold": FacilityPricing.quote(_business_base_price(facility), facility.policy_current),
|
||||
&"expected_quality": clampi(facility.base_quality, 0, 100),
|
||||
&"need_relief": need_relief,
|
||||
&"risk_level": 0,
|
||||
@@ -10638,7 +10648,7 @@ func _on_service_completed(service: ServiceInstance) -> void:
|
||||
guest_id,
|
||||
service.service_id,
|
||||
effective_minute,
|
||||
clampi(service.quality_snapshot, 0, 100),
|
||||
_service_satisfaction_sample(service, guest),
|
||||
_service_need_effects(service.facility_id)
|
||||
)
|
||||
if not bool(synchronized.get(&"ok", false)):
|
||||
@@ -11177,3 +11187,128 @@ func get_supply_management_report() -> Dictionary:
|
||||
|
||||
func preview_supply_purchase(warehouse_id: StringName, item_id: StringName, quantity: int) -> Dictionary:
|
||||
return supply_management.preview_purchase(self, warehouse_id, item_id, quantity)
|
||||
|
||||
|
||||
# Existing saves retain each facility's stored base rate; reception never quotes
|
||||
# a new direct sale. Already queued/in-service requests retain their old quote.
|
||||
func _business_base_price(facility: FacilityInstance) -> int:
|
||||
return 0 if facility.definition_id == &"reception" else facility.base_price
|
||||
|
||||
|
||||
func get_business_management_report() -> Dictionary:
|
||||
var minute: int = _clock.absolute_minute
|
||||
var start := maxi(0, int(floor(float(minute - 360) / 1440.0)) * 1440 + 360)
|
||||
var totals: Dictionary = {}
|
||||
for entry: LedgerEntry in economy_service.get_entries():
|
||||
if entry.absolute_minute < start or entry.absolute_minute > minute:
|
||||
continue
|
||||
if not totals.has(entry.source_entity_id):
|
||||
totals[entry.source_entity_id] = {&"revenue": 0, &"expenses": 0}
|
||||
if entry.category == &"SERVICE_SALE":
|
||||
totals[entry.source_entity_id][&"revenue"] += entry.amount
|
||||
elif entry.category in [&"OPERATING_EXPENSE", &"STAFF_SIGNING_COST", &"EVENT_ACTION", &"EVENT_COST_COMMITTED", &"DEFENSE_SKILL_COST", &"AUTHORITY_FINE", &"REFUND"]:
|
||||
totals[entry.source_entity_id][&"expenses"] += entry.amount
|
||||
var rows: Array[Dictionary] = []
|
||||
for facility: FacilityInstance in facility_service.get_instances():
|
||||
var definition := definition_registry.get_facility(facility.definition_id)
|
||||
if definition == null:
|
||||
continue
|
||||
var amounts: Dictionary = totals.get(facility.instance_id, {})
|
||||
var base := _business_base_price(facility)
|
||||
rows.append({
|
||||
&"facility_id": facility.instance_id,
|
||||
&"definition_id": facility.definition_id,
|
||||
&"display_name": definition.display_name,
|
||||
&"origin": Vector2i(facility.origin.x, facility.origin.y),
|
||||
&"layer": facility.origin.z,
|
||||
&"state": StringName(FacilityInstance.State.keys()[facility.state]),
|
||||
&"business_open": facility.operator_open,
|
||||
&"price_mode": StringName(facility.policy_current.get(&"price_mode", &"standard")),
|
||||
&"base_price": base,
|
||||
&"price": FacilityPricing.quote(base, facility.policy_current),
|
||||
&"can_price": base > 0,
|
||||
&"queued_count": facility.queue_entries.size(),
|
||||
&"active_count": facility.active_services.size(),
|
||||
&"today_revenue": int(amounts.get(&"revenue", 0)),
|
||||
&"today_expenses": int(amounts.get(&"expenses", 0)),
|
||||
})
|
||||
return {&"minute": minute, &"modes": FacilityPricing.modes(), &"facilities": rows}
|
||||
|
||||
|
||||
func _guest_has_checked_in(guest: GuestRuntime) -> bool:
|
||||
for visited_id: StringName in guest.visited_facility_ids:
|
||||
var visited := facility_service.get_instance(visited_id)
|
||||
if visited != null and visited.definition_id == &"reception":
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _service_satisfaction_sample(service: ServiceInstance, _guest: GuestRuntime) -> int:
|
||||
# P1 uses a common expectation of 50 until archetypes have an explicit
|
||||
# expected-quality field. Quality sensitivity is a separate concept.
|
||||
var expectation := 50
|
||||
return clampi(service.quality_snapshot + FacilityPricing.satisfaction_modifier(
|
||||
service.policy_snapshot, service.quality_snapshot, expectation
|
||||
), 0, 100)
|
||||
|
||||
|
||||
func _release_closed_business_queues(absolute_minute: int) -> void:
|
||||
for facility: FacilityInstance in facility_service.get_instances():
|
||||
if facility.operator_open and facility.is_within_opening_hours(absolute_minute):
|
||||
continue
|
||||
for request: ServiceRequest in facility_service.get_queue(facility.instance_id):
|
||||
_release_closed_business_request(facility, request, absolute_minute)
|
||||
for guest: GuestRuntime in guest_service.get_guests():
|
||||
if guest.state != GuestRuntime.State.MOVING:
|
||||
continue
|
||||
var facility := facility_service.get_instance(guest.target_facility_id)
|
||||
if facility == null or (facility.operator_open and facility.is_within_opening_hours(absolute_minute)):
|
||||
continue
|
||||
var transition := guest_service.transition_guest(guest.guest_id, GuestRuntime.State.REPLANNING, absolute_minute, &"FACILITY_CLOSED")
|
||||
if bool(transition.get(&"ok", false)):
|
||||
guest.target_facility_id = &""
|
||||
guest.path_handle = null
|
||||
|
||||
|
||||
func _release_closed_business_request(facility: FacilityInstance, request: ServiceRequest, minute: int) -> void:
|
||||
# Validate the entire group before any queue/state signals become observable.
|
||||
for guest_id: StringName in request.guest_ids:
|
||||
var guest := guest_service.get_guest(guest_id)
|
||||
if guest == null:
|
||||
continue
|
||||
if guest.state != GuestRuntime.State.QUEUING or guest.queue_request_id != request.request_id or guest.target_facility_id != facility.instance_id:
|
||||
return
|
||||
if guest.transition_tick == minute and guest.transitions_this_tick >= GuestService.MAX_TRANSITIONS_PER_TICK:
|
||||
return
|
||||
# A group releases its one reservation and queue atomically. A transition-budget
|
||||
# failure retries next minute, without taking patience or dropping any stock.
|
||||
var before_inventory := inventory_service.create_snapshot()
|
||||
var before_facility := facility_service.create_snapshot()
|
||||
var before_guest := guest_service.create_snapshot()
|
||||
var before_events := domain_events.create_snapshot()
|
||||
var before_ids := id_generator.create_snapshot()
|
||||
var released := false
|
||||
var release := func(_guest_id: StringName = &"", _facility_id: StringName = &"", _request_id: StringName = &"") -> Dictionary:
|
||||
for reservation_id: StringName in request.reserved_inventory_ids:
|
||||
var result := inventory_service.release_reservation(reservation_id, StringName("business.close.%s.%s" % [request.request_id, reservation_id]), minute)
|
||||
if not bool(result.get(&"ok", false)):
|
||||
return result
|
||||
var result := facility_service.leave_queue(facility.instance_id, request.request_id, &"FACILITY_CLOSED")
|
||||
return {&"ok": result.is_success()}
|
||||
var ok := true
|
||||
for guest_id: StringName in request.guest_ids:
|
||||
if not guest_service.has_guest(guest_id):
|
||||
continue
|
||||
var result: Dictionary = guest_service.release_queue_for_replanning(guest_id, minute, release if not released else Callable())
|
||||
if not bool(result.get(&"ok", false)):
|
||||
ok = false
|
||||
break
|
||||
released = true
|
||||
if ok and not released:
|
||||
ok = bool(release.call().get(&"ok", false))
|
||||
if not ok:
|
||||
inventory_service.restore_snapshot(before_inventory)
|
||||
facility_service.restore_snapshot(before_facility)
|
||||
guest_service.restore_snapshot(before_guest)
|
||||
domain_events.restore_snapshot(before_events)
|
||||
id_generator.restore_snapshot(before_ids)
|
||||
|
||||
Reference in New Issue
Block a user