| name | wcs-asset-patterns |
| description | Defines the 'Composite Asset' problem and the correct data mapping patterns for Wing Commander Saga (WCS) assets. This skill should be used when understanding the relationship between POF 3D models and TBL data files, or when designing asset migration strategies that preserve gameplay functionality. |
| version | 1.0.0 |
WCS Asset Patterns - The Composite Asset Problem
Section 1: The "Composite Asset" Problem
A Wing Commander Saga asset is a runtime "hydration" of two distinct components, not a single file. Naive separation of these components results in functionally inert assets.
The "Skeleton" (.pof) - Spatial and Visual Foundation
The binary 3D model file containing:
- 3D Geometry: Mesh data, texture coordinates, materials
- Critical Game Logic Metadata: Hardpoints, subsystems, spatial attachment points
- Hierarchical Structure: Submodel relationships and transforms
- Runtime Hooks: Spatial anchors for weapons, engines, cameras, damage zones
The "Brain" (.tbl) - Behavioral and Statistical Data
The plain-text key-value database containing:
- Ship Characteristics: Mass, velocity, shield/armor values
- Weapon Configurations: Firing rates, damage values, energy requirements
- Game Balance Parameters: AI behavior, mission suitability, tech level
- Classification Data: Ship class, faction, role, prerequisites
The Non-Negotiable Mandate
A naive 3D model conversion will fail, producing "visually correct but functionally inert assets". Both components must be preserved and reunited in Godot through an intelligent import pipeline.
Section 2: The Solution Pipeline
The solution requires a coordinated chain of skills that maintain the relationship between skeleton and brain components.
Brain Pipeline: Data Extraction and Resource Generation
- parse-tbl - Extract structured data from .tbl files into JSON schema
- generate-resource - Create Godot Custom Resource scripts from the schema
Skeleton Pipeline: Metadata Extraction and Import Reconstruction
- parse-pof - Extract critical metadata from .pof files (geometry handled separately)
- generate-importer - Create EditorImportPlugin to reconstruct composite assets on import
Geometry Pipeline: 3D Conversion and Material Translation
- convert-pof-to-glb - Convert 3D geometry and materials to modern format
- EditorImportPlugin - Automatically reassemble composite asset with metadata
Integration Pipeline: Runtime Rehydration
The EditorImportPlugin serves as the bridge, automatically combining:
- GLB Geometry: Visual representation with materials
- JSON Metadata: Spatial information from POF parsing
- Resource Data: Statistical information from TBL parsing
Section 3: Asset Metadata Reference
POF Chunk to Godot Node Mapping
This mapping defines how POF binary chunks are translated into Godot scene nodes during import.
| .pof Chunk | Godot Node Created | Node.name Pattern | Node.extras (JSON Data) | Purpose |
|---|---|---|---|---|
| GPNT (Gun Point) | Node3D | GPNT_Primary_{bank}_{index} |
{"type": "gun_mount", "bank": int, "weapon_type": string} |
Spatial hook to instance weapon scenes with correct orientation |
| MPNT (Missile Point) | Node3D | MPNT_Secondary_{bank}_{index} |
{"type": "missile_mount", "bank": int, "missile_type": string} |
Spatial hook for missile launcher instantiation |
| SUBS (Subsystem) | Area3D + CollisionShape3D | SUBS_{subsystem_name} |
{"type": "subsystem", "name": string, "hitpoints": float, "critical": bool} |
Targetable area for component damage and system failures |
| GLOW (Thruster) | Node3D | THRUSTER_{thruster_name}_{index} |
{"type": "thruster", "name": string, "radius": float, "thrust_vector": array} |
Attachment point for engine particle effects |
| EYE (Eyepoint) | Node3D | EYE_{camera_name} |
{"type": "camera_pos", "name": string, "default_fov": float} |
Camera attachment positions for different view modes |
| DOCK (Docking Point) | Node3D | DOCK_{port_name} |
{"type": "docking_port", "name": string, "size": string, "aligns": array} |
Spatial anchors for docking mechanics and ship-to-ship interactions |
TBL to Resource Property Mapping
This mapping defines how TBL key-value pairs are translated into Godot resource properties.
| .tbl Key | Data Type | Godot Property | Example Value | Validation Rules |
|---|---|---|---|---|
$Name: |
String | @export var ship_name: String |
"GTF Apollo" | Required, non-empty |
$Short Name: |
String | @export var short_name: String |
"Apollo" | Required, 3-8 chars |
$Ship Class: |
String | @export var ship_class: String |
"fighter" | Must be valid class |
$Mass: |
float | @export var mass: float |
20000.0 | > 0, realistic range |
$Max Velocity: |
float | @export var max_velocity: float |
70.0 | > 0, meters per second |
$Max Afterburner Velocity: |
float | @export var max_afterburner_velocity: float |
85.0 | ≥ max_velocity |
$Shields: |
float | @export var shield_strength: float |
150.0 | ≥ 0 |
$Armor: |
float | @export var armor_strength: float |
100.0 | ≥ 0 |
$Hitpoints: |
int | @export var hitpoints: int |
250 | > 0 |
$Gun Banks: |
int | @export var primary_banks: int |
2 | ≥ 1, ≤ max_supported |
$Missile Banks: |
int | @export var secondary_banks: int |
1 | ≥ 0 |
Advanced Mapping: Weapon Configuration
| .tbl Weapon Key | Godot Property | Data Structure | Purpose |
|---|---|---|---|
$Primary Bank {N} Weapons: |
primary_weapons: Array[WeaponData] |
Custom Resource Array | Weapon loadout per bank |
+Weapon: |
weapon_data.weapon_name: String |
String | Specific weapon type |
$Damage: |
weapon_data.damage: float |
Float | Base damage value |
$Fire Rate: |
weapon_data.fire_rate: float |
Float | Shots per second |
$Energy Use: |
weapon_data.energy_cost: float |
Float | Energy per shot |
Section 4: Implementation Patterns
Data-First Migration Strategy
The migration follows a strict data-first approach to ensure no information loss.
Step 1: Extract and Structure Data
# parse-tbl skill output structure
{
"ships": {
"GTF Apollo": {
"ship_name": "GTF Apollo",
"short_name": "Apollo",
"ship_class": "fighter",
"mass": 20000.0,
"max_velocity": 70.0,
"shield_strength": 150.0,
"armor_strength": 100.0,
"hitpoints": 250,
"primary_weapons": [
{
"weapon_name": "Subach HL-7",
"damage": 15.0,
"fire_rate": 8.5,
"energy_cost": 2.0
}
]
}
},
"validation": {
"total_ships": 1,
"required_fields_present": true,
"data_types_valid": true
}
}
# parse-pof skill output structure
{
"hardpoints": {
"primary": [
{
"position": [1.2, 0.0, 3.1],
"normal": [0.0, 0.0, 1.0],
"bank": 0,
"index": 0
},
{
"position": [-1.2, 0.0, 3.1],
"normal": [0.0, 0.0, 1.0],
"bank": 0,
"index": 1
}
],
"secondary": [
{
"position": [0.0, 0.5, 2.8],
"normal": [0.0, 0.2, 1.0],
"bank": 0,
"index": 0
}
]
},
"subsystems": [
{
"name": "engines",
"bounds": {
"min": [-2.1, -1.0, -4.2],
"max": [2.1, 1.0, 0.0]
},
"hitpoints": 75,
"critical": true
},
{
"name": "weapons",
"bounds": {
"min": [-1.5, -0.8, 2.0],
"max": [1.5, 0.8, 4.0]
},
"hitpoints": 50,
"critical": false
}
],
"eyepoints": [
{
"name": "cockpit",
"position": [0.0, 0.8, 1.2],
"default_fov": 75.0
}
]
}
Step 2: Generate Godot-Native Assets
# generate-resource skill output (ShipStats.gd)
@tool
extends Resource
class_name ShipStats
@export var ship_name: String
@export var short_name: String
@export var ship_class: String
@export var mass: float
@export var max_velocity: float
@export var max_afterburner_velocity: float
@export var shield_strength: float
@export var armor_strength: float
@export var hitpoints: int
@export var primary_banks: int
@export var secondary_banks: int
@export var primary_weapons: Array[WeaponData] = []
@export var secondary_weapons: Array[WeaponData] = []
func _validate_property(property_name: String) -> bool:
match property_name:
"mass":
return mass > 0.0 and mass < 100000.0
"max_velocity":
return max_velocity > 0.0
"shield_strength":
return shield_strength >= 0.0
_:
return true
Step 3: Create Import Pipeline
# generate-importer skill output (WCSAssetImporter.gd)
@tool
extends EditorImportPlugin
class_name WCSAssetImporter
func _get_importer_name() -> String:
return "WCS Composite Asset"
func _import(source_file: String, save_path: String, options: Dictionary, r_platform_variants: Dictionary, r_gen_files: Array) -> Error:
# Load GLB geometry
var glb_path = source_file.get_basename() + ".glb"
var gltf_document := GLTFDocument.new()
var gltf_state := GLTFState.new()
var result := gltf_document.append_from_file(glb_path, gltf_state)
if result != OK:
push_error("Failed to load GLB: " + glb_path)
return result
# Create base scene
var scene := gltf_document.generate_scene(gltf_state)
# Load companion metadata
var metadata_path = source_file.get_basename() + "_metadata.json"
if not FileAccess.file_exists(metadata_path):
push_warning("No metadata file found for: " + source_file)
return FAILED
var metadata_file = FileAccess.open(metadata_path, FileAccess.READ)
var metadata_json = metadata_file.get_as_text()
metadata_file.close()
var json := JSON.new()
var parse_result = json.parse(metadata_json)
if parse_result != OK:
push_error("Failed to parse metadata: " + metadata_path)
return FAILED
var metadata = json.data
# Apply WCS-specific reconstructions
_wcs_reconstruct_hardpoints(scene, metadata.get("hardpoints", {}))
_wcs_reconstruct_subsystems(scene, metadata.get("subsystems", []))
_wcs_reconstruct_eyepoints(scene, metadata.get("eyepoints", []))
_wcs_reconstruct_thrusters(scene, metadata.get("thrusters", []))
# Save as PackedScene
var packed_scene := PackedScene.new()
packed_scene.pack(scene)
return ResourceSaver.save(packed_scene, save_path + ".scn")
Section 5: Runtime Rehydration Pattern
Composite Asset Assembly
At runtime, the asset manager must perform intelligent rehydration:
# Runtime asset loading and rehydration
func load_wcs_ship(ship_name: String) -> Node3D:
# Load composite asset components
var scene_path := "res://assets/ships/" + ship_name + ".scn"
var stats_path := "res://resources/ships/" + ship_name + ".tres"
# Load scene (includes reconstructed metadata nodes)
var scene := load(scene_path).instantiate()
# Load ship statistics resource
var stats := load(stats_path)
if not stats:
push_error("Failed to load ship stats: " + stats_path)
return null
# Attach game data to scene
scene.ship_stats = stats
# Configure gameplay systems based on spatial metadata
_configure_weapon_systems(scene)
_setup_damage_systems(scene)
_initialize_thruster_effects(scene)
_configure_camera_positions(scene)
return scene
func _configure_weapon_systems(ship_scene: Node3D):
var stats := ship_scene.ship_stats as ShipStats
# Find hardpoint nodes
for i in range(stats.primary_banks):
var hardpoint_node := ship_scene.find_node("GPNT_Primary_0_" + str(i))
if hardpoint_node:
var weapon_data := stats.primary_weapons[i] if i < stats.primary_weapons.size() else null
if weapon_data:
# Instance weapon scene at hardpoint location
var weapon_scene := load(weapon_data.scene_path).instantiate()
hardpoint_node.add_child(weapon_scene)
weapon_scene.global_transform = hardpoint_node.global_transform
Damage System Integration
func _setup_damage_systems(ship_scene: Node3D):
var stats := ship_scene.ship_stats as ShipStats
# Find subsystem nodes and connect to damage system
for subsystem_node in ship_scene.find_children("*", "Area3D"):
if subsystem_node.name.begins_with("SUBS_"):
var subsystem_name := subsystem_node.name.substr(5) # Remove "SUBS_" prefix
# Connect to ship's damage system
var damageable_component := DamageableComponent.new()
damageable_component.subsystem_name = subsystem_name
damageable_component.max_hitpoints = _get_subsystem_hitpoints(stats, subsystem_name)
subsystem_node.add_child(damageable_component)
# Connect damage signals
damageable_component.damage_taken.connect(_on_subsystem_damage_taken)
damageable_component.destroyed.connect(_on_subsystem_destroyed)
Section 6: Validation Requirements
Critical Success Criteria
- Metadata Completeness: Every .glb has companion .json metadata file with all required fields
- Spatial Accuracy: All hardpoints maintain exact spatial positions from original POF
- Subsystem Integrity: Subsystem volumes match original collision bounds within tolerance
- Data Preservation: Ship stats preserve original balance values without drift
- Node Consistency: Node naming conventions are consistent for runtime access patterns
- Functional Testing: All weapon mounting points work correctly at runtime
- Performance Requirements: Import process completes within acceptable time limits
Failure Modes to Detect
- Missing Metadata: Incomplete composites without .json files
- Node Hierarchy Errors: Broken spatial relationships or incorrect parent-child relationships
- Data Validation Failures: NaN or invalid values in stats resources
- Count Mismatches: Different hardpoint counts between POF metadata and generated nodes
- Spatial Corruption: Hardpoints outside model bounds or with invalid orientations
- Resource Link Failures: Broken references between scenes and resource files
Automated Validation Pipeline
# Validation script for composite assets
func validate_composite_asset(ship_name: String) -> Dictionary:
var result := {
"valid": true,
"errors": [],
"warnings": []
}
# Check file existence
var scene_path := "res://assets/ships/" + ship_name + ".scn"
var stats_path := "res://resources/ships/" + ship_name + ".tres"
var metadata_path := "res://assets/ships/" + ship_name + "_metadata.json"
if not FileAccess.file_exists(scene_path):
result.errors.append("Missing scene file")
result.valid = false
if not FileAccess.file_exists(stats_path):
result.errors.append("Missing stats resource")
result.valid = false
if not FileAccess.file_exists(metadata_path):
result.errors.append("Missing metadata file")
result.valid = false
# Validate node structure
var scene := load(scene_path).instantiate()
var hardpoints := scene.find_children("*GPNT*", "Node3D", true, false)
var subsystems := scene.find_children("*SUBS*", "Area3D", true, false)
if hardpoints.is_empty():
result.warnings.append("No weapon hardpoints found")
if subsystems.is_empty():
result.warnings.append("No subsystems found")
return result
This comprehensive pattern ensures that Wing Commander Saga assets maintain their complete gameplay functionality while gaining modern Godot capabilities through the composite asset approach. The strict separation and intelligent recombination of skeleton and brain components preserves the original game design intent while enabling new possibilities.