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:
@@ -46,6 +46,16 @@ const FACILITY_PATHS: Array[String] = [
|
||||
|
||||
## Required movement infrastructure added after the original sixteen-facility
|
||||
## catalog. Separate paths preserve the baseline design table and legacy scans.
|
||||
## Original-spec facilities integrated into normal construction beyond baseline.
|
||||
const ORIGINAL_FACILITY_PATHS: Array[String] = [
|
||||
"res://data/facilities/expansion/cleaning_room.tres",
|
||||
"res://data/facilities/expansion/chapel.tres",
|
||||
"res://data/facilities/expansion/premium_room.tres",
|
||||
"res://data/facilities/expansion/staff_dormitory.tres",
|
||||
]
|
||||
|
||||
const RESCUE_FACILITY_PATHS: Array[String] = ["res://data/facilities/rescue/infirmary.tres"]
|
||||
|
||||
const INFRASTRUCTURE_FACILITY_IDS: Array[StringName] = [
|
||||
&"guest_stairs",
|
||||
&"service_stairs",
|
||||
@@ -130,6 +140,8 @@ static func load_available(include_expansion := false) -> Array[FacilityDefiniti
|
||||
static func by_id(facility_id: StringName) -> FacilityDefinition:
|
||||
var definitions := load_available(true)
|
||||
definitions.append_array(load_infrastructure())
|
||||
for path: String in RESCUE_FACILITY_PATHS + ORIGINAL_FACILITY_PATHS:
|
||||
definitions.append(load(path) as FacilityDefinition)
|
||||
for definition in definitions:
|
||||
if definition.id == facility_id:
|
||||
return definition
|
||||
|
||||
@@ -58,6 +58,8 @@ enum ViewLayer {
|
||||
@export var primary_skill: StringName = &"service"
|
||||
|
||||
@export_group("Service")
|
||||
@export var customer_capacity := -1
|
||||
@export var service_attribute: StringName = &""
|
||||
@export var capacity := 0
|
||||
@export var queue_capacity := 0
|
||||
@export var base_service_minutes := 1
|
||||
@@ -84,6 +86,7 @@ enum ViewLayer {
|
||||
@export var content_replacement_id: StringName = &""
|
||||
|
||||
@export_group("Progression")
|
||||
@export var faith_per_service := 0
|
||||
@export_range(1, 7, 1) var unlock_rank := 1
|
||||
@export var research_requirements: Array[StringName] = []
|
||||
|
||||
@@ -105,10 +108,13 @@ func is_valid() -> bool:
|
||||
and maintenance_mana >= 0 \
|
||||
and staff_min >= 0 \
|
||||
and staff_max >= staff_min \
|
||||
and customer_capacity >= -1 \
|
||||
and service_attribute in [&"", &"appetite", &"relaxation", &"sexual", &"gambling"] \
|
||||
and capacity >= 0 \
|
||||
and queue_capacity >= 0 \
|
||||
and base_service_minutes > 0 \
|
||||
and base_price >= 0 \
|
||||
and faith_per_service >= 0 \
|
||||
and detailed_profile_is_valid()
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const DEFAULT_PATHS := {
|
||||
const REQUIRED_COUNTS := {
|
||||
&"guest_classes": 8,
|
||||
&"species": 4,
|
||||
&"staff_candidates": 20,
|
||||
&"staff_candidates": 21,
|
||||
&"unique_characters": 2,
|
||||
}
|
||||
const REQUIRED_IDS := {
|
||||
@@ -45,7 +45,7 @@ const REQUIRED_IDS := {
|
||||
&"staff_akane", &"staff_bardo", &"staff_chika", &"staff_doran", &"staff_eri",
|
||||
&"staff_fugo", &"staff_gina", &"staff_haru", &"staff_iris", &"staff_juno",
|
||||
&"staff_klaus", &"staff_lina", &"staff_marc", &"staff_nadia", &"staff_otto",
|
||||
&"staff_petra", &"staff_quinn", &"staff_rika", &"staff_sven", &"staff_tessa",
|
||||
&"staff_petra", &"staff_quinn", &"staff_rika", &"staff_sven", &"staff_tessa", &"staff_uma",
|
||||
],
|
||||
&"unique_characters": [&"heroine_anri", &"rival_rai"],
|
||||
}
|
||||
@@ -58,18 +58,35 @@ const SHIFT_IDS: Array[StringName] = [&"any", &"day", &"night"]
|
||||
const SCHEDULE_PROFILE_IDS: Array[StringName] = [&"day", &"evening", &"night"]
|
||||
const UNIQUE_KINDS: Array[StringName] = [&"heroine", &"rival"]
|
||||
const MECHANICAL_EFFECT_IDS: Array[StringName] = [
|
||||
&"assigned_floor_patience_loss_multiplier_bp",
|
||||
&"assigned_floor_revenue_multiplier_bp",
|
||||
&"base_shield_attack",
|
||||
&"night_shield_attack_bonus",
|
||||
&"cleaning_work_rate_multiplier",
|
||||
]
|
||||
const HEROINE_EFFECT_IDS: Array[StringName] = [
|
||||
&"assigned_floor_patience_loss_multiplier_bp",
|
||||
&"assigned_floor_revenue_multiplier_bp",
|
||||
]
|
||||
const RIVAL_EFFECT_IDS: Array[StringName] = [
|
||||
&"base_shield_attack",
|
||||
&"night_shield_attack_bonus",
|
||||
]
|
||||
|
||||
# Trusted per-person declarations. JSON cannot supply or broaden these policies.
|
||||
# The pair below preserves the current MVP JSON contract; it is not a new
|
||||
# relationship assignment inferred for the other characters in original 6.2.
|
||||
const UNIQUE_VALIDATION_POLICIES := {
|
||||
&"heroine_anri": {
|
||||
&"character_kind": &"heroine",
|
||||
&"mechanical_effect_ids": HEROINE_EFFECT_IDS,
|
||||
&"counterpart_id": &"rival_rai",
|
||||
},
|
||||
&"rival_rai": {
|
||||
&"character_kind": &"rival",
|
||||
&"mechanical_effect_ids": RIVAL_EFFECT_IDS,
|
||||
&"counterpart_id": &"heroine_anri",
|
||||
},
|
||||
}
|
||||
|
||||
const GUEST_TAGS: Array[StringName] = [
|
||||
&"combatant", &"diplomatic", &"faith_aligned", &"mana_sensitive",
|
||||
&"nocturnal", &"noise_sensitive", &"stealthy", &"trade_focused",
|
||||
@@ -85,6 +102,7 @@ const PREFERENCE_IDS: Array[StringName] = [
|
||||
&"underground", &"vip",
|
||||
]
|
||||
const ROLE_IDS: Array[StringName] = [
|
||||
&"cleric",
|
||||
&"attendant", &"bartender", &"clerk", &"cleaner", &"cook", &"guard",
|
||||
&"influencer", &"innkeeper", &"maintenance", &"masseur", &"medic",
|
||||
&"porter", &"receptionist",
|
||||
@@ -124,6 +142,8 @@ const UNIQUE_FIELDS: Array[StringName] = [
|
||||
&"role_id", &"skills", &"traits",
|
||||
]
|
||||
|
||||
# Transient revision of validated catalog content; never serialized.
|
||||
var _manifest_generation := 0
|
||||
var _entries_by_type: Dictionary = {}
|
||||
var _ordered_ids_by_type: Dictionary = {}
|
||||
var _errors: Array[Dictionary] = []
|
||||
@@ -172,6 +192,10 @@ func rebuild_from_documents(documents: Dictionary) -> Dictionary:
|
||||
return get_report()
|
||||
|
||||
|
||||
func get_manifest_generation() -> int:
|
||||
return _manifest_generation
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return _errors.is_empty() and _entries_by_type.size() == DEFAULT_PATHS.size()
|
||||
|
||||
@@ -274,22 +298,36 @@ func build_all_guest_definitions() -> Array[GuestDefinition]:
|
||||
return result
|
||||
|
||||
|
||||
## General candidates remain a separate catalog category. Only approved unique
|
||||
## recruitment is exposed here; rival joining conditions are not inferred.
|
||||
func get_recruitable_staff_ids() -> Array[StringName]:
|
||||
var ids := get_staff_candidate_ids()
|
||||
if not get_entry(&"unique_characters", &"heroine_anri").is_empty():
|
||||
ids.append(&"heroine_anri")
|
||||
ids.sort_custom(_name_less)
|
||||
return ids
|
||||
|
||||
|
||||
func get_recruitment_entry(candidate_id: StringName) -> Dictionary:
|
||||
return get_entry(&"unique_characters", candidate_id) if candidate_id == &"heroine_anri" else get_entry(&"staff_candidates", candidate_id)
|
||||
|
||||
|
||||
func staff_hire_payload(candidate_id: StringName) -> Dictionary:
|
||||
var candidate := get_entry(&"staff_candidates", candidate_id)
|
||||
var candidate := get_recruitment_entry(candidate_id)
|
||||
if candidate.is_empty():
|
||||
return {}
|
||||
return {
|
||||
&"staff_id": candidate_id,
|
||||
&"role_id": StringName(candidate[&"role_id"]),
|
||||
&"daily_wage": int(candidate[&"daily_wage"]),
|
||||
&"signing_cost": int(candidate[&"signing_cost"]),
|
||||
&"daily_wage": int(candidate.get(&"daily_wage", 0)),
|
||||
&"signing_cost": int(candidate.get(&"signing_cost", 0)),
|
||||
&"preferred_shift": StringName(candidate[&"preferred_shift"]),
|
||||
&"adult_verified": bool(candidate[&"adult_verified"]),
|
||||
&"age": int(candidate[&"age"]),
|
||||
&"options": {
|
||||
&"daily_wage": int(candidate[&"daily_wage"]),
|
||||
&"employment_kind": &"FREE_HIRE",
|
||||
&"permitted_task_tags": _to_name_array(candidate[&"permitted_task_tags"]),
|
||||
&"daily_wage": int(candidate.get(&"daily_wage", 0)),
|
||||
&"employment_kind": &"UNIQUE" if candidate_id == &"heroine_anri" else &"FREE_HIRE",
|
||||
&"permitted_task_tags": _to_name_array(candidate.get(&"permitted_task_tags", [])),
|
||||
&"role_tags": [StringName(candidate[&"role_id"])],
|
||||
&"skills": _name_keyed_integer_map(candidate[&"skills"]),
|
||||
},
|
||||
@@ -297,6 +335,7 @@ func staff_hire_payload(candidate_id: StringName) -> Dictionary:
|
||||
|
||||
|
||||
func _reset() -> void:
|
||||
_manifest_generation += 1
|
||||
_entries_by_type.clear()
|
||||
_ordered_ids_by_type.clear()
|
||||
_errors.clear()
|
||||
@@ -645,10 +684,20 @@ func _validate_unique(entry_id: StringName, entry: Dictionary) -> void:
|
||||
_add_error(content_type, entry_id, &"character_kind", &"unknown_character_kind", "")
|
||||
elif not String(entry_id).begins_with("%s_" % String(kind)):
|
||||
_add_error(content_type, entry_id, &"id", &"invalid_namespace", "%s_" % String(kind))
|
||||
var raw_counterpart_id: Variant = _value(entry, &"counterpart_id", null)
|
||||
var counterpart_id := StringName(raw_counterpart_id) if _is_string_variant(raw_counterpart_id) else &""
|
||||
if counterpart_id.is_empty() or counterpart_id == entry_id:
|
||||
_add_error(content_type, entry_id, &"counterpart_id", &"invalid_counterpart", "")
|
||||
var policy: Dictionary = _unique_validation_policy(entry_id)
|
||||
if policy.is_empty():
|
||||
_add_error(content_type, entry_id, &"id", &"missing_unique_policy", "")
|
||||
elif kind != StringName(policy[&"character_kind"]):
|
||||
_add_error(content_type, entry_id, &"character_kind", &"character_policy_mismatch", "")
|
||||
if policy.has(&"counterpart_id"):
|
||||
var raw_counterpart_id: Variant = _value(entry, &"counterpart_id", null)
|
||||
var counterpart_id := StringName(raw_counterpart_id) if _is_string_variant(raw_counterpart_id) else &""
|
||||
if counterpart_id.is_empty() or counterpart_id == entry_id:
|
||||
_add_error(content_type, entry_id, &"counterpart_id", &"invalid_counterpart", "")
|
||||
elif not policy.is_empty() and (entry.has(&"counterpart_id") or entry.has("counterpart_id")):
|
||||
# An unspecified relationship is represented by omission, not null,
|
||||
# an empty ID, a fabricated actor or an automatic reciprocal pair.
|
||||
_add_error(content_type, entry_id, &"counterpart_id", &"unexpected_counterpart", "")
|
||||
var traits := _to_name_array(_value(entry, &"traits", []))
|
||||
if not traits.has(&"unique") or not traits.has(&"unsellable"):
|
||||
_add_error(content_type, entry_id, &"traits", &"unique_safety_traits_required", "")
|
||||
@@ -669,7 +718,9 @@ func _validate_unique(entry_id: StringName, entry: Dictionary) -> void:
|
||||
if not MECHANICAL_EFFECT_IDS.has(effect_id) \
|
||||
or not _mechanical_effect_value_is_valid(effect_id, effect_value):
|
||||
_add_error(content_type, entry_id, &"mechanical_effects", &"invalid_mechanical_effect", String(raw_key))
|
||||
var expected_effects: Array[StringName] = HEROINE_EFFECT_IDS if kind == &"heroine" else RIVAL_EFFECT_IDS
|
||||
var expected_effects: Array[StringName] = []
|
||||
if not policy.is_empty():
|
||||
expected_effects.assign(policy[&"mechanical_effect_ids"])
|
||||
for expected_effect_id: StringName in expected_effects:
|
||||
if not seen_effects.has(expected_effect_id):
|
||||
_add_error(content_type, entry_id, &"mechanical_effects", &"missing_mechanical_effect", String(expected_effect_id))
|
||||
@@ -678,6 +729,11 @@ func _validate_unique(entry_id: StringName, entry: Dictionary) -> void:
|
||||
_add_error(content_type, entry_id, &"mechanical_effects", &"effect_kind_mismatch", String(effect_id))
|
||||
|
||||
|
||||
func _unique_validation_policy(entry_id: StringName) -> Dictionary:
|
||||
var policy: Dictionary = UNIQUE_VALIDATION_POLICIES.get(entry_id, {})
|
||||
return policy.duplicate(true)
|
||||
|
||||
|
||||
func _validate_adult(content_type: StringName, entry_id: StringName, entry: Dictionary) -> void:
|
||||
var verified: Variant = _value(entry, &"adult_verified", null)
|
||||
if not verified is bool or not bool(verified):
|
||||
@@ -714,12 +770,22 @@ func _validate_cross_references() -> void:
|
||||
var unique_index: Dictionary = _entries_by_type.get(&"unique_characters", {})
|
||||
for entry_id: StringName in get_unique_character_ids():
|
||||
var entry: Dictionary = unique_index[entry_id]
|
||||
var policy: Dictionary = _unique_validation_policy(entry_id)
|
||||
if policy.is_empty():
|
||||
_add_error(&"unique_characters", entry_id, &"id", &"missing_unique_policy", "")
|
||||
continue
|
||||
if not policy.has(&"counterpart_id"):
|
||||
continue
|
||||
var counterpart_id := StringName(entry[&"counterpart_id"])
|
||||
if not unique_index.has(counterpart_id):
|
||||
_add_error(&"unique_characters", entry_id, &"counterpart_id", &"missing_counterpart", String(counterpart_id))
|
||||
continue
|
||||
if counterpart_id != StringName(policy[&"counterpart_id"]):
|
||||
_add_error(&"unique_characters", entry_id, &"counterpart_id", &"counterpart_policy_mismatch", String(counterpart_id))
|
||||
var counterpart: Dictionary = unique_index[counterpart_id]
|
||||
if StringName(counterpart[&"counterpart_id"]) != entry_id:
|
||||
var counterpart_policy: Dictionary = _unique_validation_policy(counterpart_id)
|
||||
if StringName(_value(counterpart, &"counterpart_id", &"")) != entry_id \
|
||||
or StringName(counterpart_policy.get(&"counterpart_id", &"")) != entry_id:
|
||||
_add_error(&"unique_characters", entry_id, &"counterpart_id", &"counterpart_not_reciprocal", String(counterpart_id))
|
||||
if StringName(counterpart[&"character_kind"]) == StringName(entry[&"character_kind"]):
|
||||
_add_error(&"unique_characters", entry_id, &"character_kind", &"counterpart_kind_conflict", String(counterpart_id))
|
||||
@@ -983,10 +1049,14 @@ static func _mechanical_effect_value_is_valid(effect_id: StringName, raw_value:
|
||||
var value := int(raw_value)
|
||||
if value < 0 or value > MAX_MECHANICAL_EFFECT_VALUE:
|
||||
return false
|
||||
if effect_id == &"assigned_floor_patience_loss_multiplier_bp":
|
||||
if effect_id == &"assigned_floor_revenue_multiplier_bp":
|
||||
return value >= 2500 and value <= 30_000
|
||||
if effect_id in [&"base_shield_attack", &"night_shield_attack_bonus"]:
|
||||
return value <= 100
|
||||
if effect_id == &"cleaning_work_rate_multiplier":
|
||||
# Original 6.2 specifies exactly three. This validator does not register
|
||||
# Fine, assign an employment profile or activate any Runtime effect.
|
||||
return value == 3
|
||||
return false
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user