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:
Takeshi
2026-09-30 23:52:24 +09:00
parent 6077e02ebe
commit 5dde0c5ec2
490 changed files with 54051 additions and 809 deletions
+12
View File
@@ -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
+6
View File
@@ -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()
+87 -17
View File
@@ -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