Compare commits
128 Commits
0bd98ea3e2
...
a553621c7f
| Author | SHA1 | Date | |
|---|---|---|---|
| a553621c7f | |||
| 86310e96f2 | |||
| 1a0fb87b93 | |||
| b71b599d9c | |||
| eea50adfcb | |||
| c7ef9283cd | |||
| 3a3edf785c | |||
| fc486eea82 | |||
| b19033b557 | |||
| 9968cb8c75 | |||
| aae7620512 | |||
| 970d0f9034 | |||
| bea438bd50 | |||
| 3f40d84847 | |||
| 8630bfcf18 | |||
| 5bb20d416d | |||
| eb0d05a577 | |||
| ef49506515 | |||
| 089d13520e | |||
| 472af01e94 | |||
| f9e61dcb05 | |||
| 42f6a218fb | |||
| ce888bc5bb | |||
| 333a0b9938 | |||
| 11530dd7fc | |||
| 3fd167ebff | |||
| 5952a96ee6 | |||
| 76c3ead4a8 | |||
| 39a053d4ee | |||
| c5da977857 | |||
| 3e7b0169d5 | |||
| f918c5cefb | |||
| 976ced3731 | |||
| 0fd8deaf53 | |||
| 4d843c18c7 | |||
| 0ed83966da | |||
| 998984f75a | |||
| 02fa78764a | |||
| abe6eef6b3 | |||
| cca732b41b | |||
| 8507a3a501 | |||
| b9cce0248a | |||
| fa49bb5005 | |||
| 37441aa8fd | |||
| a1b74aa2d5 | |||
| c80d9c96e6 | |||
| a75db85027 | |||
| 7563937ab8 | |||
| 425bb8411d | |||
| 9b29b68b33 | |||
| 7557c710fb | |||
| ce2f6c1306 | |||
| e0e8e316d4 | |||
| abd2dc22d3 | |||
| a5df60769f | |||
| 75ba39895f | |||
| 2cff982473 | |||
| 3bd2801d1d | |||
| 2e358275f0 | |||
| 5ed7552164 | |||
| 2b3482da88 | |||
| 1d2c330481 | |||
| a0d2561587 | |||
| e95b904f4e | |||
| 9d4fad1d10 | |||
| 4335a8cb05 | |||
| d55bf970e0 | |||
| 30814ea35a | |||
| 35f50f7f51 | |||
| ca5b5b3052 | |||
| 7e4e8f6638 | |||
| c6fb3bb463 | |||
| 1411990def | |||
| 1488d7d918 | |||
| ef708fa14a | |||
| 6d7fcb1157 | |||
| 4313d190f9 | |||
| a2173114b9 | |||
| fb6881998c | |||
| 529476d8cd | |||
| 43e9fb330f | |||
| a392eb0bf9 | |||
| e2960d67e4 | |||
| 79b6af1fff | |||
| 863c401230 | |||
| eec0d8f6f7 | |||
| c2a1db5a65 | |||
| 77f93659d5 | |||
| febeb8ff8d | |||
| 611dcd0d46 | |||
| e6494936d6 | |||
| e3b90e8bba | |||
| 7846082220 | |||
| a955f0b218 | |||
| da4a1a6722 | |||
| 21879c2784 | |||
| 5377d1a75a | |||
| 03f72bdd77 | |||
| 3c47a87768 | |||
| 4ba28fe512 | |||
| 9f2f0be4a3 | |||
| 7d64ba30cb | |||
| 82c0e8c6ce | |||
| 3798f227a7 | |||
| 19e4d80741 | |||
| d8122e3275 | |||
| 0ebba40867 | |||
| 64b03abb48 | |||
| cfd9dde5da | |||
| 07101fcc64 | |||
| b8c61da1f7 | |||
| b1413d6d00 | |||
| f785339852 | |||
| c2cbd0974d | |||
| 9e72a48457 | |||
| ebd875feac | |||
| 2a2fd53c4f | |||
| d4061386ec | |||
| ec2695bc6c | |||
| d68da8fc04 | |||
| 2371ba3b19 | |||
| bcf9291c03 | |||
| c31892ac05 | |||
| abd961ea0f | |||
| e5f4bbfb90 | |||
| 3ebb41647e | |||
| db15c6a48a | |||
| 9c2adbb698 |
@@ -78,6 +78,7 @@ add_subdirectory(assets/blender/buildings/parts)
|
||||
add_subdirectory(assets/blender/characters)
|
||||
add_subdirectory(resources)
|
||||
add_subdirectory(src/text_editor)
|
||||
add_subdirectory(src/features)
|
||||
|
||||
add_executable(Game Game.cpp ${WATER_SRC})
|
||||
target_include_directories(Game PRIVATE src/gamedata)
|
||||
|
||||
@@ -47,15 +47,12 @@ set(VRM_IMPORTED_BLENDS
|
||||
# COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${CHARACTER_GLBS}
|
||||
# DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models.py ${VRM_IMPORTED_BLENDS} ${EDITED_BLEND_TARGETS}
|
||||
# WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
set(FEMALE_OBJECTS "BodyTopRobe;BodyTop;BodyBottom;BodyFeet;Hair;Face;BackHair;Accessoty")
|
||||
set(MALE_OBJECTS "BodyTopRobe;BodyTop;BodyBottomPants;BodyBottom_Panties001;BodyBottom;BodyFeetPants;BodyFeetPantsShoes;BodyFeet;Hair;Face;BackHair;Accessory")
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend
|
||||
${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
|
||||
"${MALE_OBJECTS}"
|
||||
"male"
|
||||
tmp-edited-male.blend
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
|
||||
@@ -76,7 +73,6 @@ add_custom_command(
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend
|
||||
${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
|
||||
"${FEMALE_OBJECTS}"
|
||||
"female"
|
||||
tmp-edited-female.blend
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
|
||||
@@ -164,9 +160,9 @@ function(blender_import_vrm BLEND VRM EDITABLE RIG)
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py -- ${VRM_NAME}.vrm ${BLEND} ${EDITABLE} ${RIG}
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${BLEND} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${BLEND}
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
${VRM}
|
||||
${VRM} ${CMAKE_BINARY_DIR}/assets/blender/mixamo
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
)
|
||||
endfunction()
|
||||
@@ -328,8 +324,8 @@ function(add_blend_consolidation INPUT_BLEND COMBINED_BLEND OUTPUT_BLEND)
|
||||
|
||||
# Get the base name from the combined blend file for stamp derivation
|
||||
get_filename_component(COMBINED_NAME "${COMBINED_BLEND}" NAME_WE)
|
||||
# Remove "_combined" suffix if present
|
||||
string(REGEX REPLACE "_combined$" "" TARGET_BASE "${COMBINED_NAME}")
|
||||
# Remove "_combined" or "_shaped" suffix if present
|
||||
string(REGEX REPLACE "_(combined|shaped)$" "" TARGET_BASE "${COMBINED_NAME}")
|
||||
|
||||
# Derive stamp dependency
|
||||
get_filename_component(COMBINED_DIR "${COMBINED_BLEND}" DIRECTORY)
|
||||
@@ -408,10 +404,10 @@ function(add_clothes_pipeline INPUT_BLEND WEIGHTED_BLEND FINAL_OUTPUT_BLEND)
|
||||
${COMBINED_BLEND}
|
||||
${SHAPED_BLEND}
|
||||
)
|
||||
# Step 2: Consolidate
|
||||
# Step 2: Consolidate (use shaped blend which has shape keys transferred)
|
||||
add_blend_consolidation(
|
||||
"${INPUT_BLEND}"
|
||||
"${COMBINED_BLEND}"
|
||||
"${SHAPED_BLEND}"
|
||||
"${FINAL_OUTPUT_BLEND}"
|
||||
)
|
||||
|
||||
@@ -422,18 +418,74 @@ function(add_clothes_pipeline INPUT_BLEND WEIGHTED_BLEND FINAL_OUTPUT_BLEND)
|
||||
)
|
||||
endfunction()
|
||||
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-top.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-bottom.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-feet.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-top.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-bottom.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-feet.blend)
|
||||
|
||||
# Propagate missing shape keys (like "fat") from Body_shapes to base body parts.
|
||||
# This ensures all body part meshes have consistent shape keys, preventing seams.
|
||||
function(add_shape_key_propagation INPUT_BLEND OUTPUT_BLEND)
|
||||
add_custom_command(
|
||||
OUTPUT ${OUTPUT_BLEND}
|
||||
DEPENDS ${INPUT_BLEND}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/add_missing_shape_keys.py
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/transfer_shape_keys.py
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${INPUT_BLEND} ${OUTPUT_BLEND}
|
||||
COMMAND ${BLENDER} -b -Y ${OUTPUT_BLEND}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/add_missing_shape_keys.py --
|
||||
${OUTPUT_BLEND} ${OUTPUT_BLEND}
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
)
|
||||
endfunction()
|
||||
|
||||
add_shape_key_propagation(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-shapes.blend
|
||||
)
|
||||
add_shape_key_propagation(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-shapes.blend
|
||||
)
|
||||
|
||||
# male
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-shapes.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-bottom_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-bottom.blend" # BOTTOM_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-bottom_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-bottom.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-top_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-top.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-top.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-feet_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
# female
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-shapes.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-bottom_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-bottom.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-bottom.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-top_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-top.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-top.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-feet_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
Add missing shape keys from Body_shapes to body part objects.
|
||||
Preserves existing shape keys.
|
||||
Usage: blender -b --python add_missing_shape_keys.py -- <blend_file> <output_file>
|
||||
"""
|
||||
|
||||
import bpy
|
||||
import sys
|
||||
import os
|
||||
import mathutils
|
||||
from mathutils.bvhtree import BVHTree
|
||||
|
||||
# Import functions from transfer_shape_keys.py
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from transfer_shape_keys import (
|
||||
compute_robust_surface_mapping,
|
||||
compute_side_preserving_position,
|
||||
smooth_boundary_areas,
|
||||
smooth_penetration_areas,
|
||||
create_bvh_for_source,
|
||||
load_source_data,
|
||||
fix_seams_across_objects,
|
||||
fix_normals_across_objects
|
||||
)
|
||||
|
||||
def get_source_object_name(target_obj_info):
|
||||
if target_obj_info.get('ref_shapes'):
|
||||
return target_obj_info['ref_shapes']
|
||||
return None
|
||||
|
||||
def add_missing_shape_keys(source_data, target_obj):
|
||||
"""Add only missing shape keys to target object, preserving existing ones."""
|
||||
if not target_obj.data.shape_keys:
|
||||
target_obj.shape_key_add(name='Basis', from_mix=False)
|
||||
|
||||
existing_names = {kb.name for kb in target_obj.data.shape_keys.key_blocks}
|
||||
missing_names = [n for n in source_data['names'] if n != 'Basis' and n not in existing_names]
|
||||
|
||||
if not missing_names:
|
||||
print(f" {target_obj.name}: all shape keys present, skipping")
|
||||
return
|
||||
|
||||
print(f" {target_obj.name}: adding missing keys: {missing_names}")
|
||||
|
||||
# Create missing shape keys
|
||||
for sk_name in missing_names:
|
||||
target_obj.shape_key_add(name=sk_name, from_mix=False)
|
||||
|
||||
target_obj.data.shape_keys.use_relative = True
|
||||
|
||||
# Compute mapping
|
||||
mapping = compute_robust_surface_mapping(target_obj, source_data)
|
||||
|
||||
# Set values for missing shape keys
|
||||
for sk_name in missing_names:
|
||||
print(f" Setting {sk_name}...")
|
||||
sk = target_obj.data.shape_keys.key_blocks[sk_name]
|
||||
|
||||
target_obj.data.shape_keys.use_relative = False
|
||||
for i, v in enumerate(sk.data):
|
||||
if i < len(mapping['target_verts']):
|
||||
pos = compute_side_preserving_position(i, sk_name, mapping, source_data, 1.0)
|
||||
v.co = pos
|
||||
|
||||
sk.data.update()
|
||||
target_obj.data.update_tag()
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
target_obj.data.shape_keys.use_relative = True
|
||||
target_obj.data.update_tag()
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
smooth_boundary_areas(target_obj, sk_name, mapping, source_data)
|
||||
smooth_penetration_areas(target_obj, sk_name, mapping, source_data)
|
||||
|
||||
print(f" ✓ Added {sk_name}")
|
||||
|
||||
def process_blend(blend_path, output_path):
|
||||
print(f"Processing: {blend_path}")
|
||||
bpy.ops.wm.open_mainfile(filepath=blend_path)
|
||||
|
||||
# Find source object
|
||||
source_obj = None
|
||||
for name in ['Body_shapes', 'Body']:
|
||||
if name in bpy.data.objects and bpy.data.objects[name].type == 'MESH':
|
||||
obj = bpy.data.objects[name]
|
||||
if obj.data.shape_keys and len(obj.data.shape_keys.key_blocks) > 1:
|
||||
source_obj = obj
|
||||
print(f"Found source: {name}")
|
||||
break
|
||||
|
||||
if not source_obj:
|
||||
print("ERROR: No source object with shape keys found")
|
||||
return False
|
||||
|
||||
# Build source data
|
||||
source_data = {
|
||||
'names': [kb.name for kb in source_obj.data.shape_keys.key_blocks],
|
||||
'vertex_positions': {},
|
||||
'polygons': [],
|
||||
'is_relative': source_obj.data.shape_keys.use_relative,
|
||||
'source_obj_name': source_obj.name
|
||||
}
|
||||
for poly in source_obj.data.polygons:
|
||||
source_data['polygons'].append([v for v in poly.vertices])
|
||||
for sk in source_obj.data.shape_keys.key_blocks:
|
||||
source_data['vertex_positions'][sk.name] = [(v.co.x, v.co.y, v.co.z) for v in sk.data]
|
||||
|
||||
print(f"Source shape keys: {source_data['names']}")
|
||||
|
||||
# Process body part objects
|
||||
required_props = {'age', 'sex', 'slot'}
|
||||
modified = False
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == 'MESH' and all(p in obj for p in required_props):
|
||||
if obj == source_obj:
|
||||
continue
|
||||
add_missing_shape_keys(source_data, obj)
|
||||
modified = True
|
||||
|
||||
# Fix seams: synchronize shape key offsets for vertices sharing
|
||||
# the same basis position across body parts.
|
||||
fix_seams_across_objects()
|
||||
fix_normals_across_objects()
|
||||
|
||||
bpy.ops.wm.save_as_mainfile(filepath=output_path)
|
||||
print(f"Saved: {output_path}")
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
args = sys.argv[sys.argv.index("--") + 1:]
|
||||
if len(args) >= 2:
|
||||
process_blend(args[0], args[1])
|
||||
else:
|
||||
print("Usage: blender -b -P script.py -- <input.blend> <output.blend>")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,14 +2,21 @@ import bpy
|
||||
import bmesh
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import mathutils
|
||||
from mathutils.bvhtree import BVHTree
|
||||
|
||||
def load_blend_files(clothes_blend_path, body_blend_path):
|
||||
"""Load objects from blend files and return all loaded objects"""
|
||||
"""Load objects from blend files and return all loaded objects
|
||||
|
||||
IMPORTANT: Body file must be loaded FIRST so its objects keep their original names.
|
||||
The clothes file may contain reference meshes with the same names as body parts
|
||||
(e.g., 'BodyTop' used for weight painting). If clothes are loaded first, the body's
|
||||
real objects get renamed to 'BodyTop.001' etc., breaking the ref_part lookup.
|
||||
"""
|
||||
loaded_objects = []
|
||||
|
||||
for path in [clothes_blend_path, body_blend_path]:
|
||||
for path in [body_blend_path, clothes_blend_path]:
|
||||
with bpy.data.libraries.load(path) as (data_from, data_to):
|
||||
data_to.objects = data_from.objects
|
||||
for obj in data_to.objects:
|
||||
@@ -36,34 +43,27 @@ def get_transformation_matrices(obj):
|
||||
return m, m_inv, m_normal
|
||||
|
||||
def raycast_and_adjust_vertices(target_body, bvh_cloth, out_ray_length=0.15):
|
||||
"""Raycast from body to cloth and adjust vertices that intersect"""
|
||||
m_body, m_body_inv, m_body_normal = get_transformation_matrices(target_body)
|
||||
|
||||
"""Raycast from body to cloth and mark vertices that are covered.
|
||||
Returns a list where 1 = vertex is covered by clothing, 0 = visible.
|
||||
Does NOT modify vertex positions -- that used to cause visible steps
|
||||
at clothing boundaries because border vertices kept the inward offset
|
||||
while exposed neighbours did not."""
|
||||
m_body, _, m_body_normal = get_transformation_matrices(target_body)
|
||||
|
||||
num_verts = len(target_body.data.vertices)
|
||||
hit_values = [0] * num_verts
|
||||
has_shape_keys = target_body.data.shape_keys is not None
|
||||
|
||||
# Forward raycast (into cloth)
|
||||
|
||||
for i, v in enumerate(target_body.data.vertices):
|
||||
v_world = m_body @ v.co
|
||||
n_world = (m_body_normal @ v.normal).normalized()
|
||||
|
||||
# Raycast forward (into cloth) and backward (from inside cloth)
|
||||
|
||||
# Raycast forward (outward) and backward (inward)
|
||||
hit_f, _, _, _ = bvh_cloth.ray_cast(v_world, n_world, out_ray_length)
|
||||
hit_b, _, _, _ = bvh_cloth.ray_cast(v_world, -n_world, 0.005)
|
||||
|
||||
|
||||
if hit_f or hit_b:
|
||||
hit_values[i] = 1
|
||||
# Adjust vertex position to be slightly outside cloth
|
||||
offset = -n_world * (0.005 if hit_f else 0.01)
|
||||
new_co = m_body_inv @ (v_world + offset)
|
||||
v.co = new_co
|
||||
|
||||
# Update shape keys if they exist
|
||||
if has_shape_keys:
|
||||
for kb in target_body.data.shape_keys.key_blocks:
|
||||
kb.data[i].co = new_co
|
||||
|
||||
|
||||
return hit_values
|
||||
|
||||
def protect_and_remove_hidden_geometry(target_body, hit_values, threshold=4.0):
|
||||
@@ -158,6 +158,13 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
|
||||
clothing_props[prop_name] = clothing_obj[prop_name]
|
||||
print(f" Stored property '{prop_name}' = {clothing_obj[prop_name]} from clothing")
|
||||
|
||||
# Also store metadata tags for export pipeline
|
||||
clothing_meta = {}
|
||||
for prop_name in ["ref_layer", "ref_part", "garment_id", "tags", "age", "sex", "slot"]:
|
||||
if prop_name in clothing_obj:
|
||||
clothing_meta[prop_name] = clothing_obj[prop_name]
|
||||
print(f" Stored meta '{prop_name}' = {clothing_obj[prop_name]} from clothing")
|
||||
|
||||
# Step A: Raycast & adjust vertices
|
||||
out_ray_length = 0.015
|
||||
if "ref_ray_length" in clothing_obj:
|
||||
@@ -204,6 +211,26 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
|
||||
new_target[prop_name] = prop_value
|
||||
print(f" Copied property '{prop_name}' = {prop_value} to combined object")
|
||||
|
||||
# NEW CODE: Aggregate clothing metadata onto combined object
|
||||
# Collect from all clothing objects processed for this target
|
||||
# Use JSON string since Blender ID properties don't support Python lists/sets
|
||||
if "_combined_meta" in new_target:
|
||||
meta = json.loads(new_target["_combined_meta"])
|
||||
else:
|
||||
meta = {"garments": [], "tags": [], "layers": []}
|
||||
if "garment_id" in clothing_meta:
|
||||
meta["garments"].append(clothing_meta["garment_id"])
|
||||
else:
|
||||
meta["garments"].append(clothing_name_for_final)
|
||||
if "tags" in clothing_meta and clothing_meta["tags"]:
|
||||
for t in str(clothing_meta["tags"]).split(";"):
|
||||
t = t.strip()
|
||||
if t and t not in meta["tags"]:
|
||||
meta["tags"].append(t)
|
||||
if "ref_layer" in clothing_meta:
|
||||
meta["layers"].append(int(clothing_meta["ref_layer"]))
|
||||
new_target["_combined_meta"] = json.dumps(meta)
|
||||
|
||||
# Rename the combined object using the appropriate clothing name
|
||||
new_target.name = f"{target_name}_{clothing_name_for_final}"
|
||||
whitelist.add(new_target)
|
||||
@@ -237,11 +264,66 @@ def run_batch_combine():
|
||||
all_objs = bpy.data.objects
|
||||
|
||||
# Separate body objects (no ref_layer property)
|
||||
body_objects = [o for o in all_objs if o.type == 'MESH' and "ref_layer" not in o]
|
||||
# Filter out rig control shapes (cs_*), helper objects, and objects with no vertices
|
||||
body_objects = []
|
||||
skipped_objects = []
|
||||
for o in all_objs:
|
||||
if o.type != 'MESH' or "ref_layer" in o:
|
||||
continue
|
||||
# Skip rig control shapes and helper objects
|
||||
if o.name.startswith("cs_") or o.name.startswith("WGT-") or o.name.startswith("MCH-") or o.name.startswith("ORG-"):
|
||||
skipped_objects.append(o.name)
|
||||
continue
|
||||
# Skip objects with no vertices (empty meshes)
|
||||
if len(o.data.vertices) == 0:
|
||||
skipped_objects.append(o.name)
|
||||
continue
|
||||
body_objects.append(o)
|
||||
|
||||
# Separate clothing by layer
|
||||
clothing_layer1 = [o for o in all_objs if o.type == 'MESH' and "ref_layer" in o and o["ref_layer"] == 1]
|
||||
clothing_layer2 = [o for o in all_objs if o.type == 'MESH' and "ref_layer" in o and o["ref_layer"] == 2]
|
||||
if skipped_objects:
|
||||
print(f"Skipped {len(skipped_objects)} non-body mesh objects: {', '.join(skipped_objects[:10])}{'...' if len(skipped_objects) > 10 else ''}")
|
||||
|
||||
# Filter body objects: those missing age/sex/slot are treated as helpers/references
|
||||
required_body_props = {"age", "sex", "slot"}
|
||||
valid_body_objects = []
|
||||
skipped_body_objects = []
|
||||
for body_obj in body_objects:
|
||||
missing = [p for p in required_body_props if p not in body_obj]
|
||||
if missing:
|
||||
print(f"WARNING: Mesh object '{body_obj.name}' is missing required body properties {missing}.")
|
||||
print(f" Treating as helper/reference object (not a body part).")
|
||||
print(f" Available properties: {[k for k in body_obj.keys() if not k.startswith('_')]}")
|
||||
skipped_body_objects.append(body_obj.name)
|
||||
else:
|
||||
valid_body_objects.append(body_obj)
|
||||
|
||||
if skipped_body_objects:
|
||||
print(f"Skipped {len(skipped_body_objects)} mesh objects treated as helpers: {', '.join(skipped_body_objects)}")
|
||||
|
||||
body_objects = valid_body_objects
|
||||
|
||||
# Separate clothing by layer, filtering out objects missing required clothing properties
|
||||
required_clothing_props = {"ref_layer", "ref_part", "garment_id"}
|
||||
clothing_layer1 = []
|
||||
clothing_layer2 = []
|
||||
skipped_clothing_objects = []
|
||||
for o in all_objs:
|
||||
if o.type != 'MESH' or "ref_layer" not in o:
|
||||
continue
|
||||
# Check if this is a valid clothing object (has required clothing properties)
|
||||
missing = [p for p in required_clothing_props if p not in o]
|
||||
if missing:
|
||||
print(f"WARNING: Mesh object '{o.name}' has ref_layer but is missing required clothing properties {missing}.")
|
||||
print(f" Treating as helper/reference object (not clothing).")
|
||||
skipped_clothing_objects.append(o.name)
|
||||
continue
|
||||
if o["ref_layer"] == 1:
|
||||
clothing_layer1.append(o)
|
||||
elif o["ref_layer"] == 2:
|
||||
clothing_layer2.append(o)
|
||||
|
||||
if skipped_clothing_objects:
|
||||
print(f"Skipped {len(skipped_clothing_objects)} mesh objects treated as helpers: {', '.join(skipped_clothing_objects)}")
|
||||
|
||||
print(f"Found {len(body_objects)} body objects")
|
||||
print(f"Found {len(clothing_layer1)} layer 1 clothing objects")
|
||||
@@ -349,9 +431,34 @@ def run_batch_combine():
|
||||
print(f"Layer 2 direct to body results: {len(layer2_results_direct)}")
|
||||
print(f"Total combined objects: {len(all_results)}")
|
||||
|
||||
# Final cleanup - keep all combined objects and their armatures
|
||||
# Finalize aggregated metadata on all combined objects
|
||||
for obj in all_results:
|
||||
whitelist.add(obj)
|
||||
if "_combined_meta" in obj:
|
||||
meta = json.loads(obj["_combined_meta"])
|
||||
layers = meta.get("layers", [])
|
||||
garments = meta.get("garments", [])
|
||||
tags = meta.get("tags", [])
|
||||
|
||||
if layers:
|
||||
obj["layer"] = max(layers)
|
||||
else:
|
||||
obj["layer"] = 0
|
||||
|
||||
if garments:
|
||||
obj["garments"] = ";".join(garments)
|
||||
else:
|
||||
obj["garments"] = ""
|
||||
|
||||
if tags:
|
||||
obj["clothing_tags"] = ";".join(sorted(set(tags)))
|
||||
else:
|
||||
obj["clothing_tags"] = ""
|
||||
|
||||
print(f"Finalized metadata for {obj.name}: layer={obj['layer']}, garments={obj['garments']}, tags={obj['clothing_tags']}")
|
||||
|
||||
# Remove temporary meta property
|
||||
del obj["_combined_meta"]
|
||||
|
||||
cleanup_unused_objects(whitelist)
|
||||
|
||||
|
||||
@@ -2,11 +2,18 @@ import bpy
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Import seam/normal fix functions from transfer_shape_keys.py
|
||||
_script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if _script_dir not in sys.path:
|
||||
sys.path.insert(0, _script_dir)
|
||||
from transfer_shape_keys import fix_seams_across_objects, fix_normals_across_objects
|
||||
|
||||
def process_append(source_files, output_path):
|
||||
required_props = {"age", "sex", "slot"}
|
||||
|
||||
for file_path in source_files:
|
||||
if not os.path.exists(file_path):
|
||||
print(f"Warning: Source file not found: {file_path}")
|
||||
continue
|
||||
|
||||
with bpy.data.libraries.load(file_path) as (data_from, data_to):
|
||||
@@ -50,6 +57,13 @@ def process_append(source_files, output_path):
|
||||
print(f"Processed {obj.name}: Parented and Modset to {arm_name}")
|
||||
else:
|
||||
print(f"Warning: Armature '{arm_name}' not found for {obj.name}")
|
||||
elif obj.type == 'MESH':
|
||||
# Object is a mesh but missing required properties - treat as helper/reference
|
||||
missing = [p for p in required_props if p not in obj.keys()]
|
||||
print(f"WARNING: Mesh object '{obj.name}' is missing required properties {missing}.")
|
||||
print(f" Treating as helper/reference object (not a body part).")
|
||||
print(f" Available properties: {[k for k in obj.keys() if not k.startswith('_')]}")
|
||||
# Don't remove - keep helper/reference objects in the scene
|
||||
else:
|
||||
# Clean up data not meeting criteria
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
@@ -57,6 +71,14 @@ def process_append(source_files, output_path):
|
||||
# 4. Recursive Purge of all unlinked data (Materials, Textures, Meshes)
|
||||
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||
|
||||
# 5. Fix seams across ALL body parts in the consolidated scene.
|
||||
# This synchronizes shape key offsets and normals for matching vertices
|
||||
# across base body parts and combined clothing variants.
|
||||
print("\nFixing seams across all consolidated body parts...")
|
||||
fix_seams_across_objects()
|
||||
fix_normals_across_objects()
|
||||
print("Consolidated seam/normal fix complete.\n")
|
||||
|
||||
# Save
|
||||
bpy.ops.wm.save_as_mainfile(filepath=output_path)
|
||||
|
||||
|
||||
@@ -0,0 +1,501 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Garment System UI Panel for Blender
|
||||
|
||||
Run this script in Blender's Scripting editor (or via blender -b -P garment_system_ui.py)
|
||||
to add a side-panel for editing garment-system custom properties on mesh objects.
|
||||
|
||||
Properties managed:
|
||||
Body Part -> age, sex, slot, layer, garments, clothing_tags
|
||||
Clothing -> ref_layer, ref_part, garment_id, ref_sex, ref_age, ref_clothing, tags
|
||||
"""
|
||||
|
||||
import bpy
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SLOT_ITEMS = [
|
||||
("face", "Face", ""),
|
||||
("bottom", "Bottom", ""),
|
||||
("top", "Top", ""),
|
||||
("feet", "Feet", ""),
|
||||
("hair", "Hair", ""),
|
||||
]
|
||||
|
||||
SEX_ITEMS = [
|
||||
("male", "Male", ""),
|
||||
("female", "Female", ""),
|
||||
]
|
||||
|
||||
AGE_ITEMS = [
|
||||
("adult", "Adult", ""),
|
||||
("child", "Child", ""),
|
||||
("teen", "Teen", ""),
|
||||
("baby", "Baby", ""),
|
||||
]
|
||||
|
||||
|
||||
def _ensure_prop(obj, key, default):
|
||||
if key not in obj:
|
||||
obj[key] = default
|
||||
return obj[key]
|
||||
|
||||
|
||||
def _get_prop_str(obj, key, default=""):
|
||||
if key not in obj:
|
||||
return default
|
||||
return str(obj[key])
|
||||
|
||||
|
||||
def _get_prop_int(obj, key, default=0):
|
||||
if key not in obj:
|
||||
return default
|
||||
try:
|
||||
return int(obj[key])
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def _set_prop_str(obj, key, value):
|
||||
if value:
|
||||
obj[key] = value
|
||||
elif key in obj:
|
||||
del obj[key]
|
||||
|
||||
|
||||
def _set_prop_int(obj, key, value):
|
||||
obj[key] = int(value)
|
||||
|
||||
|
||||
def _del_prop(obj, key):
|
||||
if key in obj:
|
||||
del obj[key]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Operators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class GARMENT_OT_apply_body_part(bpy.types.Operator):
|
||||
bl_idname = "garment.apply_body_part"
|
||||
bl_label = "Apply Body Part"
|
||||
bl_description = "Write body-part custom properties to the active object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
age: bpy.props.EnumProperty(name="Age", items=AGE_ITEMS)
|
||||
sex: bpy.props.EnumProperty(name="Sex", items=SEX_ITEMS)
|
||||
slot: bpy.props.EnumProperty(name="Slot", items=SLOT_ITEMS)
|
||||
layer: bpy.props.IntProperty(name="Layer", min=0, max=2, default=0)
|
||||
garments: bpy.props.StringProperty(name="Garments", description="Semicolon-separated garment names")
|
||||
clothing_tags: bpy.props.StringProperty(name="Clothing Tags", description="Semicolon-separated tags")
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj or obj.type != "MESH":
|
||||
self.report({"WARNING"}, "Active object is not a mesh")
|
||||
return {"CANCELLED"}
|
||||
|
||||
_set_prop_str(obj, "age", self.age)
|
||||
_set_prop_str(obj, "sex", self.sex)
|
||||
_set_prop_str(obj, "slot", self.slot)
|
||||
_set_prop_int(obj, "layer", self.layer)
|
||||
_set_prop_str(obj, "garments", self.garments)
|
||||
_set_prop_str(obj, "clothing_tags", self.clothing_tags)
|
||||
|
||||
# Remove clothing-only props so they don't confuse the pipeline
|
||||
for k in ("ref_layer", "ref_part", "garment_id", "ref_sex",
|
||||
"ref_age", "ref_clothing", "tags"):
|
||||
_del_prop(obj, k)
|
||||
|
||||
self.report({"INFO"}, f"Body-part props applied to {obj.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
obj = context.active_object
|
||||
if obj:
|
||||
self.age = _get_prop_str(obj, "age", "adult")
|
||||
self.sex = _get_prop_str(obj, "sex", "male")
|
||||
self.slot = _get_prop_str(obj, "slot", "bottom")
|
||||
self.layer = _get_prop_int(obj, "layer", 0)
|
||||
self.garments = _get_prop_str(obj, "garments", "")
|
||||
self.clothing_tags = _get_prop_str(obj, "clothing_tags", "")
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
|
||||
class GARMENT_OT_apply_clothing(bpy.types.Operator):
|
||||
bl_idname = "garment.apply_clothing"
|
||||
bl_label = "Apply Clothing"
|
||||
bl_description = "Write clothing custom properties to the active object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
ref_layer: bpy.props.IntProperty(name="Ref Layer", min=1, max=2, default=1,
|
||||
description="1 = lingerie, 2 = clothing")
|
||||
ref_part: bpy.props.StringProperty(name="Ref Part",
|
||||
description="Target body-part object name, e.g. BodyBottom")
|
||||
garment_id: bpy.props.StringProperty(name="Garment ID",
|
||||
description="Garment name used for labels, e.g. panties7")
|
||||
ref_sex: bpy.props.EnumProperty(name="Ref Sex", items=SEX_ITEMS)
|
||||
ref_age: bpy.props.EnumProperty(name="Ref Age", items=AGE_ITEMS)
|
||||
ref_clothing: bpy.props.StringProperty(name="Ref Clothing",
|
||||
description="Mesh name used for weight transfer, e.g. BodyBottom")
|
||||
tags: bpy.props.StringProperty(name="Tags", description="Semicolon-separated tags")
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj or obj.type != "MESH":
|
||||
self.report({"WARNING"}, "Active object is not a mesh")
|
||||
return {"CANCELLED"}
|
||||
|
||||
_set_prop_int(obj, "ref_layer", self.ref_layer)
|
||||
_set_prop_str(obj, "ref_part", self.ref_part)
|
||||
_set_prop_str(obj, "garment_id", self.garment_id)
|
||||
_set_prop_str(obj, "ref_sex", self.ref_sex)
|
||||
_set_prop_str(obj, "ref_age", self.ref_age)
|
||||
_set_prop_str(obj, "ref_clothing", self.ref_clothing)
|
||||
_set_prop_str(obj, "tags", self.tags)
|
||||
|
||||
# Remove body-part-only props so they don't confuse the pipeline
|
||||
for k in ("age", "sex", "slot", "layer", "garments", "clothing_tags"):
|
||||
_del_prop(obj, k)
|
||||
|
||||
self.report({"INFO"}, f"Clothing props applied to {obj.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
obj = context.active_object
|
||||
if obj:
|
||||
self.ref_layer = _get_prop_int(obj, "ref_layer", 1)
|
||||
self.ref_part = _get_prop_str(obj, "ref_part", "")
|
||||
self.garment_id = _get_prop_str(obj, "garment_id", "")
|
||||
self.ref_sex = _get_prop_str(obj, "ref_sex", "male")
|
||||
self.ref_age = _get_prop_str(obj, "ref_age", "adult")
|
||||
self.ref_clothing = _get_prop_str(obj, "ref_clothing", "")
|
||||
self.tags = _get_prop_str(obj, "tags", "")
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
|
||||
class GARMENT_OT_auto_detect(bpy.types.Operator):
|
||||
bl_idname = "garment.auto_detect"
|
||||
bl_label = "Auto-detect from Name"
|
||||
bl_description = "Guess sex/slot from object name"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj or obj.type != "MESH":
|
||||
self.report({"WARNING"}, "Active object is not a mesh")
|
||||
return {"CANCELLED"}
|
||||
|
||||
name = obj.name.lower()
|
||||
changed = []
|
||||
|
||||
# Guess sex
|
||||
if "female" in name:
|
||||
obj["sex"] = "female"
|
||||
changed.append("sex=female")
|
||||
elif "male" in name:
|
||||
obj["sex"] = "male"
|
||||
changed.append("sex=male")
|
||||
|
||||
# Guess slot
|
||||
slot_map = {
|
||||
"face": "face",
|
||||
"bottom": "bottom",
|
||||
"top": "top",
|
||||
"feet": "feet",
|
||||
"hair": "hair",
|
||||
}
|
||||
for key, slot in slot_map.items():
|
||||
if key in name:
|
||||
obj["slot"] = slot
|
||||
changed.append(f"slot={slot}")
|
||||
break
|
||||
|
||||
# Guess layer from name hints
|
||||
if any(x in name for x in ("panties", "lingerie", "underwear")):
|
||||
obj["layer"] = 1
|
||||
changed.append("layer=1")
|
||||
elif any(x in name for x in ("pants", "skirt", "robe", "shirt", "dress", "cloth")):
|
||||
obj["layer"] = 2
|
||||
changed.append("layer=2")
|
||||
else:
|
||||
obj["layer"] = 0
|
||||
changed.append("layer=0")
|
||||
|
||||
# Set garment_id from object name for clothing
|
||||
if "ref_layer" in obj or "ref_part" in obj:
|
||||
if "garment_id" not in obj or not obj["garment_id"]:
|
||||
obj["garment_id"] = obj.name
|
||||
changed.append(f"garment_id={obj.name}")
|
||||
|
||||
self.report({"INFO"}, "Auto-detect: " + ", ".join(changed) if changed else "nothing changed")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GARMENT_OT_clear_props(bpy.types.Operator):
|
||||
bl_idname = "garment.clear_props"
|
||||
bl_label = "Clear All Garment Props"
|
||||
bl_description = "Remove all garment-system custom properties from the active object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
keys = list(obj.keys())
|
||||
removed = []
|
||||
for k in keys:
|
||||
if k in ("age", "sex", "slot", "layer", "garments", "clothing_tags",
|
||||
"ref_layer", "ref_part", "garment_id", "ref_sex", "ref_age",
|
||||
"ref_clothing", "tags"):
|
||||
del obj[k]
|
||||
removed.append(k)
|
||||
self.report({"INFO"}, f"Removed {len(removed)} props" if removed else "No garment props found")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GARMENT_OT_sync_tags(bpy.types.Operator):
|
||||
bl_idname = "garment.sync_tags"
|
||||
bl_label = "Sync Tags from Garments"
|
||||
bl_description = "Copy clothing_tags from garments field"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
garments = _get_prop_str(obj, "garments", "")
|
||||
obj["clothing_tags"] = garments
|
||||
self.report({"INFO"}, f"Tags set to: {garments}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GARMENT_OT_check_scene(bpy.types.Operator):
|
||||
bl_idname = "garment.check_scene"
|
||||
bl_label = "Check Scene"
|
||||
bl_description = "Validate all garment-system objects for pipeline readiness"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def execute(self, context):
|
||||
errors = []
|
||||
warnings = []
|
||||
body_count = 0
|
||||
cloth_count = 0
|
||||
ok_count = 0
|
||||
|
||||
required_body_props = {"age", "sex", "slot"}
|
||||
required_clothing_props = {"ref_layer", "ref_part", "garment_id"}
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
|
||||
has_body_props = all(p in obj for p in required_body_props)
|
||||
has_clothing_props = all(p in obj for p in required_clothing_props)
|
||||
|
||||
if has_body_props and has_clothing_props:
|
||||
errors.append(f"'{obj.name}': has BOTH body-part and clothing properties (mixed)")
|
||||
continue
|
||||
|
||||
if has_body_props:
|
||||
body_count += 1
|
||||
# Check body part values are valid
|
||||
age = str(obj.get("age", ""))
|
||||
sex = str(obj.get("sex", ""))
|
||||
slot = str(obj.get("slot", ""))
|
||||
valid_ages = {"adult", "child", "teen", "baby"}
|
||||
valid_sexes = {"male", "female"}
|
||||
valid_slots = {"face", "bottom", "top", "feet", "hair"}
|
||||
if age not in valid_ages:
|
||||
errors.append(f"'{obj.name}': invalid age='{age}' (valid: {', '.join(sorted(valid_ages))})")
|
||||
if sex not in valid_sexes:
|
||||
errors.append(f"'{obj.name}': invalid sex='{sex}' (valid: {', '.join(sorted(valid_sexes))})")
|
||||
if slot not in valid_slots:
|
||||
errors.append(f"'{obj.name}': invalid slot='{slot}' (valid: {', '.join(sorted(valid_slots))})")
|
||||
ok_count += 1
|
||||
|
||||
elif has_clothing_props:
|
||||
cloth_count += 1
|
||||
# Check clothing values are valid
|
||||
ref_sex = str(obj.get("ref_sex", ""))
|
||||
ref_age = str(obj.get("ref_age", ""))
|
||||
valid_ages = {"adult", "child", "teen", "baby"}
|
||||
valid_sexes = {"male", "female"}
|
||||
if ref_sex and ref_sex not in valid_sexes:
|
||||
errors.append(f"'{obj.name}': invalid ref_sex='{ref_sex}' (valid: {', '.join(sorted(valid_sexes))})")
|
||||
if ref_age and ref_age not in valid_ages:
|
||||
errors.append(f"'{obj.name}': invalid ref_age='{ref_age}' (valid: {', '.join(sorted(valid_ages))})")
|
||||
# Check ref_part points to an existing body object
|
||||
ref_part = str(obj.get("ref_part", ""))
|
||||
if ref_part and ref_part not in bpy.data.objects:
|
||||
warnings.append(f"'{obj.name}': ref_part='{ref_part}' not found in scene")
|
||||
ok_count += 1
|
||||
|
||||
else:
|
||||
# Mesh object with no garment props - check if it should have them
|
||||
if obj.name.startswith("Body") or any(x in obj.name.lower() for x in ("shoes", "pants", "shirt", "skirt", "dress", "hat", "hair", "top", "bottom")):
|
||||
warnings.append(f"'{obj.name}': mesh object with no garment properties (may need age/sex/slot or ref_layer/ref_part/garment_id)")
|
||||
|
||||
# Report results
|
||||
self.report({"INFO"}, f"Check complete: {body_count} body parts, {cloth_count} clothing, {len(errors)} errors, {len(warnings)} warnings")
|
||||
|
||||
# Print detailed results to console
|
||||
print("\n" + "=" * 60)
|
||||
print("GARMENT SYSTEM SCENE CHECK")
|
||||
print("=" * 60)
|
||||
print(f"Body parts: {body_count}")
|
||||
print(f"Clothing: {cloth_count}")
|
||||
print(f"OK objects: {ok_count}")
|
||||
print(f"Errors: {len(errors)}")
|
||||
print(f"Warnings: {len(warnings)}")
|
||||
print("-" * 60)
|
||||
|
||||
if errors:
|
||||
print("\nERRORS (must fix before pipeline run):")
|
||||
for e in errors:
|
||||
print(f" [ERROR] {e}")
|
||||
|
||||
if warnings:
|
||||
print("\nWARNINGS (review recommended):")
|
||||
for w in warnings:
|
||||
print(f" [WARN] {w}")
|
||||
|
||||
if not errors and not warnings:
|
||||
print("\n ✓ Scene looks good! Pipeline should run successfully.")
|
||||
print("=" * 60)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Panel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class GARMENT_PT_panel(bpy.types.Panel):
|
||||
bl_label = "Garment System"
|
||||
bl_idname = "GARMENT_PT_panel"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Garment"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
obj = context.active_object
|
||||
|
||||
if not obj:
|
||||
layout.label(text="No active object")
|
||||
return
|
||||
|
||||
layout.label(text=f"Object: {obj.name}", icon="OBJECT_DATA")
|
||||
|
||||
# Detect current type
|
||||
is_body = any(k in obj for k in ("age", "sex", "slot"))
|
||||
is_cloth = any(k in obj for k in ("ref_layer", "ref_part", "garment_id"))
|
||||
|
||||
if is_body and is_cloth:
|
||||
layout.alert = True
|
||||
layout.label(text="WARNING: mixed props", icon="ERROR")
|
||||
layout.alert = False
|
||||
elif is_body:
|
||||
layout.label(text="Type: Body Part", icon="MESH_DATA")
|
||||
elif is_cloth:
|
||||
layout.label(text="Type: Clothing", icon="MOD_CLOTH")
|
||||
else:
|
||||
layout.label(text="Type: (unset)", icon="QUESTION")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Quick action buttons
|
||||
row = layout.row(align=True)
|
||||
row.operator("garment.apply_body_part", icon="MESH_DATA")
|
||||
row = layout.row(align=True)
|
||||
row.operator("garment.apply_clothing", icon="MOD_CLOTH")
|
||||
|
||||
layout.separator()
|
||||
row = layout.row(align=True)
|
||||
row.operator("garment.auto_detect", icon="VIEWZOOM")
|
||||
row.operator("garment.clear_props", icon="X")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Show current body-part props
|
||||
if is_body or not is_cloth:
|
||||
box = layout.box()
|
||||
box.label(text="Body Part Props", icon="MESH_DATA")
|
||||
col = box.column(align=True)
|
||||
col.label(text=f"age: {_get_prop_str(obj, 'age', '—')}")
|
||||
col.label(text=f"sex: {_get_prop_str(obj, 'sex', '—')}")
|
||||
col.label(text=f"slot: {_get_prop_str(obj, 'slot', '—')}")
|
||||
col.label(text=f"layer: {_get_prop_int(obj, 'layer', 0)}")
|
||||
col.label(text=f"garments: {_get_prop_str(obj, 'garments', '—')}")
|
||||
col.label(text=f"clothing_tags: {_get_prop_str(obj, 'clothing_tags', '—')}")
|
||||
if _get_prop_str(obj, "garments", ""):
|
||||
col.operator("garment.sync_tags", icon="FILE_REFRESH")
|
||||
|
||||
# Show current clothing props
|
||||
if is_cloth:
|
||||
box = layout.box()
|
||||
box.label(text="Clothing Props", icon="MOD_CLOTH")
|
||||
col = box.column(align=True)
|
||||
col.label(text=f"ref_layer: {_get_prop_int(obj, 'ref_layer', 1)}")
|
||||
col.label(text=f"ref_part: {_get_prop_str(obj, 'ref_part', '—')}")
|
||||
col.label(text=f"garment_id: {_get_prop_str(obj, 'garment_id', '—')}")
|
||||
col.label(text=f"ref_sex: {_get_prop_str(obj, 'ref_sex', '—')}")
|
||||
col.label(text=f"ref_age: {_get_prop_str(obj, 'ref_age', '—')}")
|
||||
col.label(text=f"ref_clothing: {_get_prop_str(obj, 'ref_clothing', '—')}")
|
||||
col.label(text=f"tags: {_get_prop_str(obj, 'tags', '—')}")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Check scene button
|
||||
layout.separator()
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 1.5
|
||||
row.operator("garment.check_scene", icon="CHECKBOX_HLT")
|
||||
|
||||
# Scene overview
|
||||
box = layout.box()
|
||||
box.label(text="Scene Overview", icon="SCENE_DATA")
|
||||
body_count = 0
|
||||
cloth_count = 0
|
||||
for o in bpy.data.objects:
|
||||
if o.type != "MESH":
|
||||
continue
|
||||
if any(k in o for k in ("age", "sex", "slot")):
|
||||
body_count += 1
|
||||
elif any(k in o for k in ("ref_layer", "ref_part", "garment_id")):
|
||||
cloth_count += 1
|
||||
col = box.column(align=True)
|
||||
col.label(text=f"Body parts: {body_count}")
|
||||
col.label(text=f"Clothing: {cloth_count}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
classes = [
|
||||
GARMENT_OT_apply_body_part,
|
||||
GARMENT_OT_apply_clothing,
|
||||
GARMENT_OT_auto_detect,
|
||||
GARMENT_OT_clear_props,
|
||||
GARMENT_OT_sync_tags,
|
||||
GARMENT_OT_check_scene,
|
||||
GARMENT_PT_panel,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
@@ -122,6 +122,10 @@ def process_batch():
|
||||
|
||||
remove_empty_vertex_groups(clothing, threshold=0.001)
|
||||
|
||||
# Set garment_id from object name if not already set
|
||||
if "garment_id" not in clothing:
|
||||
clothing["garment_id"] = clothing.name
|
||||
|
||||
# 7. Final Parenting
|
||||
clothing.parent = rig
|
||||
arm_mod = clothing.modifiers.new(name="Armature", type='ARMATURE')
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import bpy
|
||||
import sys
|
||||
import os
|
||||
|
||||
def propagate_shape_keys(blend_path, output_path):
|
||||
"""Copy missing shape keys from Body_shapes to all body part objects."""
|
||||
print(f"Propagating shape keys from {blend_path} to {output_path}")
|
||||
|
||||
bpy.ops.wm.open_mainfile(filepath=blend_path)
|
||||
|
||||
# Find source object with all shape keys
|
||||
source_obj = None
|
||||
for name in ['Body_shapes', 'Body']:
|
||||
if name in bpy.data.objects and bpy.data.objects[name].type == 'MESH':
|
||||
obj = bpy.data.objects[name]
|
||||
if obj.data.shape_keys and len(obj.data.shape_keys.key_blocks) > 1:
|
||||
source_obj = obj
|
||||
print(f"Found source object: {name}")
|
||||
break
|
||||
|
||||
if not source_obj:
|
||||
print("ERROR: No source object with shape keys found")
|
||||
return False
|
||||
|
||||
source_keys = source_obj.data.shape_keys.key_blocks
|
||||
source_basis = source_keys['Basis'].data
|
||||
|
||||
modified = False
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
# Only process body part objects
|
||||
if not all(p in obj for p in ['age', 'sex', 'slot']):
|
||||
continue
|
||||
# Skip the source object itself
|
||||
if obj == source_obj:
|
||||
continue
|
||||
|
||||
if not obj.data.shape_keys:
|
||||
# Create basis if missing
|
||||
obj.shape_key_add(name='Basis', from_mix=False)
|
||||
|
||||
existing_names = {kb.name for kb in obj.data.shape_keys.key_blocks}
|
||||
|
||||
for sk in source_keys:
|
||||
if sk.name == 'Basis':
|
||||
continue
|
||||
if sk.name in existing_names:
|
||||
print(f" {obj.name}: already has '{sk.name}', skipping")
|
||||
continue
|
||||
|
||||
# Copy shape key
|
||||
print(f" {obj.name}: adding '{sk.name}' from {source_obj.name}")
|
||||
new_sk = obj.shape_key_add(name=sk.name, from_mix=False)
|
||||
|
||||
# Transfer vertex positions relative to basis
|
||||
# We need to map source vertices to target vertices
|
||||
# For now, assume same topology (body parts were separated from same mesh)
|
||||
num_verts = min(len(new_sk.data), len(sk.data), len(source_basis))
|
||||
for i in range(num_verts):
|
||||
# The shape key position in source
|
||||
src_pos = sk.data[i].co
|
||||
src_basis_pos = source_basis[i].co
|
||||
# Offset from basis
|
||||
offset = src_pos - src_basis_pos
|
||||
# Apply to target using target's basis position
|
||||
new_sk.data[i].co = obj.data.shape_keys.key_blocks['Basis'].data[i].co + offset
|
||||
|
||||
new_sk.data.update()
|
||||
modified = True
|
||||
|
||||
if modified:
|
||||
obj.data.update_tag()
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
bpy.ops.wm.save_as_mainfile(filepath=output_path)
|
||||
print(f"Saved: {output_path}")
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
args = sys.argv[sys.argv.index("--") + 1:]
|
||||
if len(args) >= 2:
|
||||
propagate_shape_keys(args[0], args[1])
|
||||
else:
|
||||
print("Usage: blender -b -P script.py -- <input.blend> <output.blend>")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -137,9 +137,10 @@ def load_target_file(target_path):
|
||||
|
||||
# Store information about target objects without keeping references
|
||||
target_objects_info = []
|
||||
required_props = {'age', 'sex', 'slot'}
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == 'MESH' and obj.data:
|
||||
if all(prop in obj for prop in ['age', 'sex', 'slot']):
|
||||
if all(prop in obj for prop in required_props):
|
||||
obj_info = {
|
||||
'name': obj.name,
|
||||
'age': obj['age'],
|
||||
@@ -157,6 +158,24 @@ def load_target_file(target_path):
|
||||
if obj_info['ref_shapes']:
|
||||
print(f" - ref_shapes: '{obj_info['ref_shapes']}'")
|
||||
|
||||
if not target_objects_info:
|
||||
print("\n" + "=" * 70)
|
||||
print("WARNING: No target objects found with required properties (age, sex, slot)")
|
||||
print("=" * 70)
|
||||
print("\nThis likely means the combine_clothes.py step did not properly")
|
||||
print("propagate age/sex/slot properties from body objects to combined objects,")
|
||||
print("or the source blend file only contains helper/reference objects.")
|
||||
print("\nAvailable mesh objects and their properties:")
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == 'MESH' and obj.data:
|
||||
props = [k for k in obj.keys() if not k.startswith('_')]
|
||||
has_req = all(p in obj for p in required_props)
|
||||
marker = " [OK]" if has_req else " [MISSING age/sex/slot]"
|
||||
print(f" - {obj.name}: {props}{marker}")
|
||||
print("\nReturning empty target list. The pipeline will skip shape key transfer.")
|
||||
print("=" * 70)
|
||||
return target_objects_info, temp_target
|
||||
|
||||
return target_objects_info, temp_target
|
||||
|
||||
def get_target_object_by_name(obj_name):
|
||||
@@ -166,22 +185,45 @@ def get_target_object_by_name(obj_name):
|
||||
return None
|
||||
|
||||
def delete_existing_shape_keys(target_obj):
|
||||
"""Delete all existing shape keys from target object"""
|
||||
"""Delete all existing shape keys from target object.
|
||||
CRITICAL: Blender's shape_key_remove() leaves mesh vertices in their
|
||||
*deformed* state rather than restoring basis positions. We must save
|
||||
the Basis positions before deletion and restore them afterwards."""
|
||||
if not target_obj.data.shape_keys:
|
||||
return False
|
||||
|
||||
|
||||
# Save Basis positions before deletion
|
||||
basis_positions = None
|
||||
for kb in target_obj.data.shape_keys.key_blocks:
|
||||
if kb.name == 'Basis':
|
||||
basis_positions = [v.co.copy() for v in kb.data]
|
||||
break
|
||||
|
||||
# Fallback: if no Basis exists, use current vertex positions
|
||||
if basis_positions is None:
|
||||
basis_positions = [v.co.copy() for v in target_obj.data.vertices]
|
||||
|
||||
num_keys = len(target_obj.data.shape_keys.key_blocks)
|
||||
print(f" Deleting {num_keys} existing shape keys...")
|
||||
|
||||
|
||||
bpy.context.view_layer.objects.active = target_obj
|
||||
target_obj.select_set(True)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
|
||||
while target_obj.data.shape_keys:
|
||||
target_obj.active_shape_key_index = 0
|
||||
bpy.ops.object.shape_key_remove()
|
||||
|
||||
|
||||
target_obj.select_set(False)
|
||||
|
||||
# RESTORE BASIS POSITIONS - Blender leaves mesh deformed after removal
|
||||
for i, v in enumerate(target_obj.data.vertices):
|
||||
if i < len(basis_positions):
|
||||
v.co = basis_positions[i]
|
||||
|
||||
target_obj.data.update_tag()
|
||||
bpy.context.view_layer.update()
|
||||
print(f" Restored basis positions for {len(basis_positions)} vertices")
|
||||
return True
|
||||
|
||||
def ensure_shape_keys_structure(shape_key_names, target_obj):
|
||||
@@ -224,12 +266,16 @@ def compute_signed_distance_and_direction(bvh, point, reference_normal=None):
|
||||
if location is None:
|
||||
return None, None, None, None
|
||||
|
||||
# Determine sign using reference normal
|
||||
# Determine sign using reference normal.
|
||||
# For points outside (in the direction of the normal), signed_distance
|
||||
# must be positive. For points inside, negative.
|
||||
if reference_normal is not None:
|
||||
to_surface = location - point
|
||||
if to_surface.length > 0:
|
||||
dot = to_surface.normalized().dot(reference_normal)
|
||||
signed_distance = distance if dot > 0 else -distance
|
||||
# Inverted relative to the no-reference-normal branch because
|
||||
# to_surface here points *toward* the surface, not away.
|
||||
signed_distance = -distance if dot > 0 else distance
|
||||
else:
|
||||
signed_distance = 0
|
||||
else:
|
||||
@@ -503,9 +549,9 @@ def enforce_side_constraint(pos, surface_point, reference_normal, target_side, c
|
||||
# Very small offset to stay near surface
|
||||
offset = abs((surface_point - pos).length) * 0.2
|
||||
if target_side > 0:
|
||||
return proj_point - reference_normal * offset
|
||||
else:
|
||||
return proj_point + reference_normal * offset
|
||||
else:
|
||||
return proj_point - reference_normal * offset
|
||||
return pos
|
||||
|
||||
# Regular vertices - stronger enforcement
|
||||
@@ -515,9 +561,9 @@ def enforce_side_constraint(pos, surface_point, reference_normal, target_side, c
|
||||
|
||||
offset = abs((surface_point - pos).length) * 0.95
|
||||
if target_side > 0:
|
||||
corrected_pos = proj_point - reference_normal * offset
|
||||
else:
|
||||
corrected_pos = proj_point + reference_normal * offset
|
||||
else:
|
||||
corrected_pos = proj_point - reference_normal * offset
|
||||
|
||||
return corrected_pos
|
||||
|
||||
@@ -724,15 +770,21 @@ def smooth_penetration_areas(target_obj, sk_name, mapping, source_data, threshol
|
||||
problem_vertices = set()
|
||||
bvh_deformed, _ = create_bvh_for_source(source_data, sk_name)
|
||||
|
||||
# Shape keys are in relative mode here, so current_positions are offsets.
|
||||
# The BVH is built from absolute source positions, so we must convert
|
||||
# relative offsets to absolute positions before querying it.
|
||||
basis_positions = mapping['target_verts']
|
||||
|
||||
for i, pos in enumerate(current_positions):
|
||||
if i < len(mapping['surface_points']) and i < len(mapping['side_flags']):
|
||||
surface_point = mapping['surface_points'][i]
|
||||
target_side = mapping['side_flags'][i]
|
||||
|
||||
if target_side != 0:
|
||||
location, _, _, _ = bvh_deformed.find_nearest(pos)
|
||||
if target_side != 0 and i < len(basis_positions):
|
||||
abs_pos = basis_positions[i] + pos
|
||||
location, _, _, _ = bvh_deformed.find_nearest(abs_pos)
|
||||
if location and i < len(mapping['direction_vectors']):
|
||||
to_surface = pos - location
|
||||
to_surface = abs_pos - location
|
||||
current_side = 1 if to_surface.dot(mapping['direction_vectors'][i]) > 0 else -1
|
||||
|
||||
if current_side != target_side:
|
||||
@@ -860,7 +912,7 @@ def set_shape_key_with_side_preservation(target_obj, sk_name, mapping, source_da
|
||||
smooth_penetration_areas(target_obj, sk_name, mapping, source_data)
|
||||
|
||||
def test_shape_key_quality(target_obj, sk_name, mapping, source_data):
|
||||
"""Test shape key quality with side tracking"""
|
||||
"""Test shape key quality with side tracking (READ-ONLY - does not modify shape key data)"""
|
||||
|
||||
sk = target_obj.data.shape_keys.key_blocks[sk_name]
|
||||
|
||||
@@ -870,6 +922,9 @@ def test_shape_key_quality(target_obj, sk_name, mapping, source_data):
|
||||
prev_positions = None
|
||||
boundary_vertices = mapping.get('boundary_vertices', set())
|
||||
|
||||
# Save original shape key data so we can restore it after testing
|
||||
original_positions = [v.co.copy() for v in sk.data]
|
||||
|
||||
for val in test_values:
|
||||
if val == 0.0:
|
||||
sk.value = 0.0
|
||||
@@ -913,6 +968,13 @@ def test_shape_key_quality(target_obj, sk_name, mapping, source_data):
|
||||
|
||||
prev_positions = [v.co.copy() for v in target_obj.data.vertices]
|
||||
|
||||
# Restore original shape key data (the test should not modify the shape key)
|
||||
target_obj.data.shape_keys.use_relative = False
|
||||
for i, v in enumerate(sk.data):
|
||||
if i < len(original_positions):
|
||||
v.co = original_positions[i]
|
||||
target_obj.data.shape_keys.use_relative = True
|
||||
|
||||
sk.value = 0.0
|
||||
target_obj.data.update_tag()
|
||||
bpy.context.view_layer.update()
|
||||
@@ -980,16 +1042,197 @@ def process_target_object(target_obj_info, source_data, current_file_path):
|
||||
mapping = transfer_shape_keys(source_data, target_obj)
|
||||
verify_transfer(source_data['names'], target_obj)
|
||||
|
||||
# Test quality of 'fat' shape key if it exists
|
||||
for sk_name in source_data['names']:
|
||||
if sk_name == 'fat':
|
||||
test_shape_key_quality(target_obj, sk_name, mapping, source_data)
|
||||
break
|
||||
|
||||
print(f" {'=' * 40}")
|
||||
print(f" ✓ Completed")
|
||||
return True
|
||||
|
||||
def get_body_part_objects():
|
||||
"""Find all mesh objects that are body parts (have age/sex/slot)"""
|
||||
result = []
|
||||
required_props = {'age', 'sex', 'slot'}
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == 'MESH' and obj.data and all(p in obj for p in required_props):
|
||||
result.append(obj)
|
||||
return result
|
||||
|
||||
def round_pos(co, tolerance=0.0001):
|
||||
"""Round a coordinate to tolerance-based buckets for spatial hashing"""
|
||||
return (round(co.x / tolerance),
|
||||
round(co.y / tolerance),
|
||||
round(co.z / tolerance))
|
||||
|
||||
def fix_seams_across_objects(tolerance=0.0001):
|
||||
"""
|
||||
Post-process shape keys to ensure vertices sharing the same basis
|
||||
position (within tolerance) get identical offsets. This fixes seams
|
||||
between body parts and UV seams within the same part.
|
||||
"""
|
||||
body_parts = get_body_part_objects()
|
||||
if not body_parts:
|
||||
print("No body part objects found for seam fixing")
|
||||
return
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print("SEAM FIX: Synchronizing shape key offsets across body parts")
|
||||
print(f"{'=' * 60}")
|
||||
print(f"Found {len(body_parts)} body part objects")
|
||||
|
||||
# Collect shape key names from first object that has them
|
||||
shape_key_names = []
|
||||
for obj in body_parts:
|
||||
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
|
||||
for kb in obj.data.shape_keys.key_blocks:
|
||||
if kb.name != 'Basis':
|
||||
shape_key_names.append(kb.name)
|
||||
break
|
||||
|
||||
if not shape_key_names:
|
||||
print("No shape keys found, skipping seam fix")
|
||||
return
|
||||
|
||||
print(f"Synchronizing {len(shape_key_names)} shape key(s): {', '.join(shape_key_names)}")
|
||||
|
||||
fixed_vertices = 0
|
||||
fixed_groups = 0
|
||||
|
||||
for sk_name in shape_key_names:
|
||||
# Build spatial hash: position_key -> list of (obj, vert_idx, offset)
|
||||
pos_map = {}
|
||||
|
||||
for obj in body_parts:
|
||||
if not obj.data.shape_keys or sk_name not in obj.data.shape_keys.key_blocks:
|
||||
continue
|
||||
|
||||
sk = obj.data.shape_keys.key_blocks[sk_name]
|
||||
for i, v in enumerate(obj.data.vertices):
|
||||
if i >= len(sk.data):
|
||||
continue
|
||||
pos_key = round_pos(v.co, tolerance)
|
||||
offset = sk.data[i].co.copy()
|
||||
|
||||
if pos_key not in pos_map:
|
||||
pos_map[pos_key] = []
|
||||
pos_map[pos_key].append((obj, i, offset))
|
||||
|
||||
# Average offsets for each position group with multiple vertices
|
||||
for pos_key, entries in pos_map.items():
|
||||
if len(entries) < 2:
|
||||
continue
|
||||
|
||||
# Compute average offset
|
||||
avg_offset = mathutils.Vector((0.0, 0.0, 0.0))
|
||||
for obj, idx, offset in entries:
|
||||
avg_offset += offset
|
||||
avg_offset /= len(entries)
|
||||
|
||||
# Apply averaged offset to all vertices in this group
|
||||
for obj, idx, offset in entries:
|
||||
sk = obj.data.shape_keys.key_blocks[sk_name]
|
||||
sk.data[idx].co = avg_offset.copy()
|
||||
fixed_vertices += 1
|
||||
|
||||
fixed_groups += 1
|
||||
|
||||
# Update all meshes
|
||||
for obj in body_parts:
|
||||
if obj.data.shape_keys:
|
||||
for kb in obj.data.shape_keys.key_blocks:
|
||||
kb.data.update()
|
||||
obj.data.update_tag()
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
print(f"Fixed {fixed_vertices} vertices across {fixed_groups} position groups")
|
||||
print(f"Seam fix complete")
|
||||
|
||||
def fix_normals_across_objects(tolerance=0.0001):
|
||||
"""
|
||||
Post-process normals to ensure vertices sharing the same basis
|
||||
position (within tolerance) get identical normals. This fixes
|
||||
lighting seams between body parts.
|
||||
"""
|
||||
body_parts = get_body_part_objects()
|
||||
if not body_parts:
|
||||
print("No body part objects found for normal fixing")
|
||||
return
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print("NORMAL FIX: Synchronizing normals across body parts")
|
||||
print(f"{'=' * 60}")
|
||||
print(f"Found {len(body_parts)} body part objects")
|
||||
|
||||
# Build spatial hash: position_key -> list of normals
|
||||
pos_map = {}
|
||||
|
||||
for obj in body_parts:
|
||||
obj.data.calc_normals()
|
||||
for v in obj.data.vertices:
|
||||
pos_key = round_pos(v.co, tolerance)
|
||||
if pos_key not in pos_map:
|
||||
pos_map[pos_key] = []
|
||||
pos_map[pos_key].append(v.normal.copy())
|
||||
|
||||
# Compute averaged normals for positions with multiple vertices
|
||||
avg_normals = {}
|
||||
for pos_key, normals in pos_map.items():
|
||||
if len(normals) < 2:
|
||||
continue
|
||||
avg = mathutils.Vector((0.0, 0.0, 0.0))
|
||||
for n in normals:
|
||||
avg += n
|
||||
if avg.length > 0.001:
|
||||
avg.normalize()
|
||||
avg_normals[pos_key] = avg
|
||||
|
||||
if not avg_normals:
|
||||
print("No matching vertices found, skipping normal fix")
|
||||
return
|
||||
|
||||
print(f"Found {len(avg_normals)} position groups with matching vertices")
|
||||
|
||||
# Apply averaged normals to each object
|
||||
fixed_loops = 0
|
||||
fixed_verts = 0
|
||||
|
||||
for obj in body_parts:
|
||||
obj.data.use_auto_smooth = True
|
||||
|
||||
if not obj.data.has_custom_normals:
|
||||
obj.data.create_normals_split()
|
||||
|
||||
obj.data.calc_normals_split()
|
||||
|
||||
vert_normal_map = {}
|
||||
for i, v in enumerate(obj.data.vertices):
|
||||
pos_key = round_pos(v.co, tolerance)
|
||||
if pos_key in avg_normals:
|
||||
vert_normal_map[i] = avg_normals[pos_key]
|
||||
|
||||
if not vert_normal_map:
|
||||
continue
|
||||
|
||||
custom_normals = []
|
||||
for loop in obj.data.loops:
|
||||
if loop.vertex_index in vert_normal_map:
|
||||
n = vert_normal_map[loop.vertex_index]
|
||||
custom_normals.append((n.x, n.y, n.z))
|
||||
fixed_loops += 1
|
||||
else:
|
||||
if hasattr(obj.data, 'corner_normals'):
|
||||
cn = obj.data.corner_normals[loop.index]
|
||||
custom_normals.append((cn.vector.x, cn.vector.y, cn.vector.z))
|
||||
else:
|
||||
custom_normals.append((loop.normal.x, loop.normal.y, loop.normal.z))
|
||||
|
||||
obj.data.normals_split_custom_set(custom_normals)
|
||||
fixed_verts += len(vert_normal_map)
|
||||
obj.data.update_tag()
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
print(f"Fixed {fixed_verts} vertices ({fixed_loops} loops) across {len(body_parts)} objects")
|
||||
print(f"Normal fix complete")
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Blender Shape Key Transfer Script - Boundary Velocity Limiting")
|
||||
@@ -1006,7 +1249,14 @@ def main():
|
||||
|
||||
if not target_objects_info:
|
||||
print("\nNo target objects found with required properties")
|
||||
sys.exit(1)
|
||||
print("Skipping shape key transfer. Saving target file as-is.")
|
||||
# Save the target file as-is (no shape keys transferred)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=output_file)
|
||||
print(f"\nSaved output (unchanged): {output_file}")
|
||||
print("=" * 60)
|
||||
print("Script completed (no shape keys transferred)")
|
||||
print("=" * 60)
|
||||
return
|
||||
|
||||
print(f"\nFound {len(target_objects_info)} target objects to process")
|
||||
|
||||
@@ -1041,6 +1291,16 @@ def main():
|
||||
|
||||
print(f"\nProgress saved to: {temp_progress_file}")
|
||||
|
||||
# Post-process: fix seams by synchronizing offsets across body parts
|
||||
print(f"\nLoading final working file for seam fix...")
|
||||
bpy.ops.wm.open_mainfile(filepath=working_file)
|
||||
fix_seams_across_objects()
|
||||
fix_normals_across_objects()
|
||||
|
||||
# Save after seam fix
|
||||
print(f"\nSaving after seam fix...")
|
||||
bpy.ops.wm.save_as_mainfile(filepath=working_file)
|
||||
|
||||
# Copy the final working file to the output location
|
||||
print(f"\nCopying final result to: {output_file}")
|
||||
shutil.copy2(working_file, output_file)
|
||||
|
||||
@@ -354,7 +354,11 @@ class OgreMaterialGenerator(object):
|
||||
else:
|
||||
image_filepath = bpy.path.abspath(image.filepath, library=image.library)
|
||||
image_filepath = os.path.normpath(image_filepath)
|
||||
|
||||
|
||||
if not os.path.isfile(image_filepath):
|
||||
logger.warning("Skipping texture copy: source path is not a file (%s)", image_filepath)
|
||||
return
|
||||
|
||||
# Should we update the file
|
||||
update = False
|
||||
if os.path.isfile(target_filepath):
|
||||
|
||||
@@ -186,26 +186,32 @@ def extra_linear(angle, offset):
|
||||
|
||||
mapping_blend_path = argv[0]
|
||||
mapping_gltf_path = argv[1]
|
||||
mapping_objects = argv[2]
|
||||
mapping_armature_name = argv[3]
|
||||
mapping_outfile = argv[4]
|
||||
# Backward compat: support both 4-arg (blend, gltf, armature, outfile) and 5-arg (blend, gltf, objects, armature, outfile)
|
||||
if len(argv) >= 5:
|
||||
mapping_objects = argv[2]
|
||||
mapping_armature_name = argv[3]
|
||||
mapping_outfile = argv[4]
|
||||
else:
|
||||
mapping_objects = "AUTO"
|
||||
mapping_armature_name = argv[2]
|
||||
mapping_outfile = argv[3]
|
||||
|
||||
#for mapping in [ExportMappingFemale(), ExportMappingMale(), ExportMappingMaleBabyShape(), ExportMappingMaleEdited(), ExportMappingFemaleEdited(), ExportMappingMaleTestShapeEdited(), ExportMappingMaleBaseShapeEdited()]:
|
||||
class CommandLineMapping:
|
||||
blend_path = mapping_blend_path
|
||||
gltf_path = mapping_gltf_path
|
||||
# ogre_scene = "characters/female/vroid-normal-female.scene"
|
||||
inner_path = "Object"
|
||||
# objs = ["male", "Body", "Hair", "Face", "BackHair", "Tops", "Bottoms", "Shoes", "Accessory"]
|
||||
# objs = ["female", "Body", "Hair", "Face", "BackHair", "Tops", "Bottoms", "Shoes", "Accessory"]
|
||||
objs = []
|
||||
armature_name = mapping_armature_name
|
||||
outfile = mapping_outfile
|
||||
default_action = 'default'
|
||||
auto_discover = False
|
||||
def __init__(self):
|
||||
self.objs = [mapping_armature_name]
|
||||
if len(mapping_objects) > 0:
|
||||
if len(mapping_objects) > 0 and mapping_objects != "AUTO":
|
||||
self.objs += [o.strip() for o in mapping_objects.split(";")]
|
||||
else:
|
||||
self.auto_discover = True
|
||||
self.files = []
|
||||
for fobj in self.objs:
|
||||
self.files.append({"name": fobj})
|
||||
@@ -224,11 +230,30 @@ for mapping in[CommandLineMapping()]:
|
||||
bpy.app.driver_namespace["angle_to_linear_x"] = angle_to_linear_x
|
||||
print("Driver setup done...")
|
||||
|
||||
bpy.ops.wm.append(
|
||||
filepath=os.path.join(mapping.blend_path, mapping.inner_path),
|
||||
directory=os.path.join(mapping.blend_path, mapping.inner_path),
|
||||
files=mapping.files)
|
||||
print("Append done...")
|
||||
if mapping.auto_discover:
|
||||
# Load all objects from the blend file so we can discover meshes by custom props
|
||||
with bpy.data.libraries.load(mapping.blend_path) as (data_from, data_to):
|
||||
data_to.objects = data_from.objects
|
||||
for obj in data_to.objects:
|
||||
if obj is not None:
|
||||
try:
|
||||
bpy.context.collection.objects.link(obj)
|
||||
except RuntimeError:
|
||||
pass # Already linked
|
||||
print("Library load done...")
|
||||
|
||||
discovered = []
|
||||
for ob in bpy.data.objects:
|
||||
if ob.type == 'MESH' and all(p in ob.keys() for p in ["age", "sex", "slot"]):
|
||||
discovered.append(ob.name)
|
||||
mapping.objs += discovered
|
||||
print(f"Auto-discovered {len(discovered)} body part objects: {discovered}")
|
||||
else:
|
||||
bpy.ops.wm.append(
|
||||
filepath=os.path.join(mapping.blend_path, mapping.inner_path),
|
||||
directory=os.path.join(mapping.blend_path, mapping.inner_path),
|
||||
files=mapping.files)
|
||||
print("Append done...")
|
||||
|
||||
prepare_armature(mapping)
|
||||
print("Armature done...")
|
||||
@@ -239,6 +264,21 @@ for mapping in[CommandLineMapping()]:
|
||||
bpy.data.objects.remove(ob)
|
||||
elif ob.name.startswith("Face") and ob.name != "Face":
|
||||
bpy.data.objects.remove(ob)
|
||||
# Remove auto-discovered objects that weren't meant for export
|
||||
elif mapping.auto_discover and ob.type == 'MESH' and ob.name not in mapping.objs:
|
||||
if not ob.name.endswith("-noimp"):
|
||||
ob.name = ob.name + "-noimp"
|
||||
|
||||
# Remove .001 suffixed duplicates that may have leaked from consolidate step
|
||||
# These are objects that have the same base name as an object in mapping.objs
|
||||
# but with a .001 suffix (e.g., BodyBottom.001 when BodyBottom is in mapping.objs)
|
||||
if mapping.auto_discover:
|
||||
for ob in list(bpy.data.objects):
|
||||
if ob.type == 'MESH' and ob.name.endswith('.001'):
|
||||
base_name = ob.name[:-4]
|
||||
if base_name in mapping.objs:
|
||||
print(f"Removing duplicate '{ob.name}' (base '{base_name}' already in export list)")
|
||||
bpy.data.objects.remove(ob, do_unlink=True)
|
||||
|
||||
print("Removing original armature and actions...")
|
||||
orig_arm = bpy.data.objects[mapping.armature_name + '_orig']
|
||||
@@ -298,9 +338,18 @@ for mapping in[CommandLineMapping()]:
|
||||
obj = bpy.data.objects.get(name)
|
||||
|
||||
if obj and obj.type == 'MESH':
|
||||
# 1. Rename Mesh Data
|
||||
if not obj.data.name.startswith(prefix):
|
||||
obj.data.name = prefix + obj.data.name
|
||||
# 1. Rename Mesh Data to match object name (not mesh data name)
|
||||
# This prevents .001 suffixed mesh data names (e.g., BodyBottom.001)
|
||||
# from becoming male_BodyBottom.001 and clashing with male_BodyBottom.
|
||||
new_mesh_name = prefix + name
|
||||
if obj.data.name != new_mesh_name:
|
||||
# Check if another mesh data already has this name
|
||||
if new_mesh_name in bpy.data.meshes and bpy.data.meshes[new_mesh_name] != obj.data:
|
||||
old = bpy.data.meshes[new_mesh_name]
|
||||
old.name = new_mesh_name + "_old"
|
||||
print(f"Renamed conflicting mesh data '{old.name}' for object '{name}'")
|
||||
obj.data.name = new_mesh_name
|
||||
print(f"Renamed mesh data from '{obj.data.name}' to '{new_mesh_name}' for object '{name}'")
|
||||
|
||||
# 2. Iterate through all Material Slots on the object
|
||||
for slot in obj.material_slots:
|
||||
@@ -317,17 +366,113 @@ for mapping in[CommandLineMapping()]:
|
||||
save_data[key] = obj[key]
|
||||
if key.startswith("body_"):
|
||||
save_data[key.replace("body_", "", 1)] = obj[key]
|
||||
|
||||
# Export aggregated clothing metadata
|
||||
if "layer" in obj:
|
||||
save_data["layer"] = int(obj["layer"])
|
||||
else:
|
||||
save_data["layer"] = 0
|
||||
|
||||
tags = []
|
||||
if "garments" in obj and obj["garments"]:
|
||||
garments = [g.strip() for g in str(obj["garments"]).split(";") if g.strip()]
|
||||
save_data["garments"] = garments
|
||||
tags.extend(garments)
|
||||
else:
|
||||
save_data["garments"] = []
|
||||
|
||||
if "clothing_tags" in obj and obj["clothing_tags"]:
|
||||
clothing_tags = [t.strip() for t in str(obj["clothing_tags"]).split(";") if t.strip()]
|
||||
tags.extend(clothing_tags)
|
||||
|
||||
# Deduplicate and sort
|
||||
seen = set()
|
||||
unique_tags = []
|
||||
for t in tags:
|
||||
t_lower = t.lower()
|
||||
if t_lower not in seen:
|
||||
seen.add(t_lower)
|
||||
unique_tags.append(t)
|
||||
save_data["tags"] = unique_tags
|
||||
|
||||
# Export shape keys if present
|
||||
# IMPORTANT: Skip "Basis" to match OGRE's pose indexing.
|
||||
# OGRE's blender2ogre exporter skips the Basis shape key (index 0),
|
||||
# so pose index 0 in OGRE = first non-Basis shape key.
|
||||
shape_keys = []
|
||||
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
|
||||
for sk in obj.data.shape_keys.key_blocks:
|
||||
if sk.name != 'Basis':
|
||||
shape_keys.append(sk.name)
|
||||
save_data["shape_keys"] = shape_keys
|
||||
|
||||
save_data["mesh"] = obj.data.name + ".mesh"
|
||||
json_dir = os.path.dirname(mapping.gltf_path)
|
||||
save_file = json_dir + "/body_part_" + obj.data.name + ".json"
|
||||
json_filepath = os.path.join(json_dir, save_file)
|
||||
with open(json_filepath, 'w') as f:
|
||||
json.dump(save_data, f)
|
||||
json.dump(save_data, f, indent=2)
|
||||
|
||||
# Triangulate body part meshes before OGRE export to prevent the exporter's
|
||||
# triangulation from producing mismatched split normals at seams.
|
||||
# BodyTop and BodyBottom have different face topologies, so the exporter's
|
||||
# bmesh triangulation creates slightly different corner normals for matching
|
||||
# vertices. Pre-triangulating ensures the exporter sees already-triangulated
|
||||
# meshes and preserves our custom normals.
|
||||
import bmesh
|
||||
body_part_objs = []
|
||||
for ob in bpy.data.objects:
|
||||
if ob.type == 'MESH' and all(p in ob for p in ["age", "sex", "slot"]):
|
||||
body_part_objs.append(ob)
|
||||
|
||||
print(f"\nTriangulating {len(body_part_objs)} body part meshes for OGRE export...")
|
||||
for obj in body_part_objs:
|
||||
mesh = obj.data
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.triangulate(bm, faces=bm.faces)
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
mesh.calc_normals()
|
||||
print(f" Triangulated '{obj.name}': {len(mesh.polygons)} polygons")
|
||||
|
||||
# Re-run normal fix on triangulated meshes to ensure matching vertices
|
||||
# have identical custom split normals after triangulation.
|
||||
chars_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "characters")
|
||||
if chars_dir not in sys.path:
|
||||
sys.path.insert(0, chars_dir)
|
||||
from transfer_shape_keys import fix_normals_across_objects
|
||||
fix_normals_across_objects()
|
||||
|
||||
armobj = bpy.data.objects.get(mapping.armature_name)
|
||||
armobj.data.name = armobj.name
|
||||
bpy.ops.ogre.export(filepath=mapping.gltf_path.replace(".glb", ".scene"), EX_SELECTED_ONLY=False, EX_SHARED_ARMATURE=True, EX_LOD_GENERATION='0', EX_LOD_DISTANCE=20, EX_LOD_LEVELS=4, EX_GENERATE_TANGENTS='4')
|
||||
|
||||
# Post-process exported OGRE meshes to fix normal/tangent seams between
|
||||
# body parts. Even with identical custom normals in Blender, the OGRE
|
||||
# exporter's calc_tangents() produces slightly different tangents for
|
||||
# matching vertices because BodyTop and BodyBottom have different face
|
||||
# topologies. This causes a visible lighting seam with normal mapping,
|
||||
# especially when shape keys (like "fat") are applied.
|
||||
ogre_export_dir = os.path.dirname(mapping.gltf_path.replace(".glb", ".scene"))
|
||||
fix_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fix_ogre_mesh_seams.py")
|
||||
if os.path.exists(fix_script):
|
||||
print(f"\nPost-processing OGRE meshes in {ogre_export_dir}...")
|
||||
import subprocess
|
||||
import shutil
|
||||
# Use system python3 if available, since sys.executable in Blender
|
||||
# may point to the Blender binary itself, not a Python interpreter.
|
||||
python_exe = shutil.which('python3') or shutil.which('python') or sys.executable
|
||||
result = subprocess.run(
|
||||
[python_exe, fix_script, ogre_export_dir],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
print(result.stdout)
|
||||
if result.returncode != 0:
|
||||
print(f"WARNING: Mesh seam fix failed: {result.stderr}")
|
||||
else:
|
||||
print(f"WARNING: fix_ogre_mesh_seams.py not found at {fix_script}")
|
||||
|
||||
bpy.ops.wm.read_homefile(use_empty=True)
|
||||
time.sleep(2)
|
||||
bpy.ops.wm.quit_blender()
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Post-process exported OGRE mesh files to fix normal/tangent seams
|
||||
between body part meshes at matching vertex positions.
|
||||
|
||||
Also fixes pose normal offsets (shape key normals) so that animated
|
||||
shape keys don't reintroduce seams at runtime.
|
||||
|
||||
Usage: python3 fix_ogre_mesh_seams.py <mesh_directory> [ogre_xml_converter_path]
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
import math
|
||||
|
||||
# Default path to OgreXMLConverter (can be overridden via argument or env var)
|
||||
DEFAULT_OGRE_XML_CONVERTER = os.environ.get(
|
||||
'OGRETOOLS_XML_CONVERTER',
|
||||
'/media/slapin/library/ogre3/ogre-sdk/bin/OgreXMLConverter'
|
||||
)
|
||||
|
||||
# Body part prefixes to process
|
||||
BODY_PREFIXES = ['BodyTop', 'BodyBottom', 'BodyFeet']
|
||||
|
||||
|
||||
def find_converter():
|
||||
"""Find OgreXMLConverter executable."""
|
||||
if len(sys.argv) >= 3:
|
||||
return sys.argv[2]
|
||||
if os.path.exists(DEFAULT_OGRE_XML_CONVERTER):
|
||||
return DEFAULT_OGRE_XML_CONVERTER
|
||||
# Try PATH
|
||||
for path in os.environ.get('PATH', '').split(os.pathsep):
|
||||
exe = os.path.join(path, 'OgreXMLConverter')
|
||||
if os.path.exists(exe):
|
||||
return exe
|
||||
return None
|
||||
|
||||
|
||||
def mesh_to_xml(mesh_path, converter):
|
||||
xml_path = mesh_path + '.xml'
|
||||
result = subprocess.run(
|
||||
[converter, mesh_path, xml_path],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"WARNING: Failed to convert {mesh_path} to XML:")
|
||||
print(result.stdout)
|
||||
print(result.stderr)
|
||||
return None
|
||||
return xml_path
|
||||
|
||||
|
||||
def xml_to_mesh(xml_path, converter):
|
||||
mesh_path = xml_path[:-4] # remove .xml
|
||||
result = subprocess.run(
|
||||
[converter, xml_path, mesh_path],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"WARNING: Failed to convert {xml_path} to binary:")
|
||||
print(result.stdout)
|
||||
print(result.stderr)
|
||||
return None
|
||||
return mesh_path
|
||||
|
||||
|
||||
def parse_mesh_xml(xml_path):
|
||||
"""Parse mesh XML and return (tree, vertices list, poses dict)."""
|
||||
try:
|
||||
tree = ET.parse(xml_path)
|
||||
except ET.ParseError as e:
|
||||
print(f"ERROR: Failed to parse {xml_path}: {e}")
|
||||
return None, None, None
|
||||
|
||||
root = tree.getroot()
|
||||
sharedgeom = root.find('.//sharedgeometry')
|
||||
if sharedgeom is None:
|
||||
return None, None, None
|
||||
|
||||
vbs = sharedgeom.findall('vertexbuffer')
|
||||
if len(vbs) < 1:
|
||||
return None, None, None
|
||||
|
||||
# The first vertexbuffer has positions and normals
|
||||
pos_norm_vb = vbs[0]
|
||||
# The second vertexbuffer (if any) has texcoords and tangents
|
||||
tex_tan_vb = vbs[1] if len(vbs) > 1 else None
|
||||
|
||||
pn_verts = pos_norm_vb.findall('vertex')
|
||||
tt_verts = tex_tan_vb.findall('vertex') if tex_tan_vb is not None else []
|
||||
|
||||
vertices = []
|
||||
for i, pnv in enumerate(pn_verts):
|
||||
pos = pnv.find('position')
|
||||
normal = pnv.find('normal')
|
||||
if pos is None or normal is None:
|
||||
continue
|
||||
|
||||
p = (float(pos.get('x')), float(pos.get('y')), float(pos.get('z')))
|
||||
n = (float(normal.get('x')), float(normal.get('y')), float(normal.get('z')))
|
||||
|
||||
t = None
|
||||
if i < len(tt_verts):
|
||||
tangent = tt_verts[i].find('tangent')
|
||||
if tangent is not None:
|
||||
t = (float(tangent.get('x')), float(tangent.get('y')), float(tangent.get('z')))
|
||||
|
||||
vertices.append({
|
||||
'index': i,
|
||||
'pos': p,
|
||||
'normal': n,
|
||||
'tangent': t,
|
||||
})
|
||||
|
||||
# Parse poses: pose_name -> {vertex_index -> (nx, ny, nz)}
|
||||
poses = {}
|
||||
for pose in root.findall('.//poses/pose'):
|
||||
name = pose.get('name')
|
||||
pose_offsets = {}
|
||||
for po in pose.findall('poseoffset'):
|
||||
idx = int(po.get('index'))
|
||||
nx = float(po.get('nx', 0.0))
|
||||
ny = float(po.get('ny', 0.0))
|
||||
nz = float(po.get('nz', 0.0))
|
||||
pose_offsets[idx] = (nx, ny, nz)
|
||||
poses[name] = pose_offsets
|
||||
|
||||
return tree, vertices, poses
|
||||
|
||||
|
||||
def round_pos(p, tolerance=0.0001):
|
||||
return (
|
||||
round(p[0] / tolerance) * tolerance,
|
||||
round(p[1] / tolerance) * tolerance,
|
||||
round(p[2] / tolerance) * tolerance,
|
||||
)
|
||||
|
||||
|
||||
def vector_normalize(v):
|
||||
length = math.sqrt(v[0]**2 + v[1]**2 + v[2]**2)
|
||||
if length > 0.0001:
|
||||
return (v[0] / length, v[1] / length, v[2] / length)
|
||||
return v
|
||||
|
||||
|
||||
def fix_seams(mesh_dir, converter):
|
||||
"""Fix normal/tangent/pose-normal seams across all body part meshes in directory."""
|
||||
# Find all body part mesh files
|
||||
mesh_files = []
|
||||
for f in os.listdir(mesh_dir):
|
||||
if not f.endswith('.mesh') or f.endswith('.mesh.xml'):
|
||||
continue
|
||||
for prefix in BODY_PREFIXES:
|
||||
if prefix in f:
|
||||
mesh_files.append(os.path.join(mesh_dir, f))
|
||||
break
|
||||
|
||||
if not mesh_files:
|
||||
print(f"No body part meshes found in {mesh_dir}")
|
||||
return 0
|
||||
|
||||
print(f"\n[OGRE Mesh Seam Fix] Found {len(mesh_files)} body part meshes")
|
||||
for mf in mesh_files:
|
||||
print(f" - {os.path.basename(mf)}")
|
||||
|
||||
# Convert all to XML
|
||||
xml_paths = []
|
||||
for mesh_path in mesh_files:
|
||||
print(f"\nConverting {os.path.basename(mesh_path)} to XML...")
|
||||
xml_path = mesh_to_xml(mesh_path, converter)
|
||||
if xml_path:
|
||||
xml_paths.append(xml_path)
|
||||
else:
|
||||
print(f" SKIPPED (conversion failed)")
|
||||
|
||||
if not xml_paths:
|
||||
print("No meshes could be converted to XML, aborting.")
|
||||
return 0
|
||||
|
||||
# Parse all XMLs
|
||||
mesh_data = {}
|
||||
all_pose_names = set()
|
||||
for xml_path in xml_paths:
|
||||
name = os.path.basename(xml_path)[:-9] # remove .mesh.xml
|
||||
tree, vertices, poses = parse_mesh_xml(xml_path)
|
||||
if vertices:
|
||||
mesh_data[name] = {
|
||||
'tree': tree,
|
||||
'vertices': vertices,
|
||||
'poses': poses,
|
||||
'xml_path': xml_path,
|
||||
}
|
||||
all_pose_names.update(poses.keys())
|
||||
else:
|
||||
print(f"WARNING: No vertices found in {name}")
|
||||
|
||||
if not mesh_data:
|
||||
print("No mesh data could be parsed, aborting.")
|
||||
return 0
|
||||
|
||||
# Build global position -> list of (normal, tangent) map for BASIS
|
||||
pos_map = {}
|
||||
for name, data in mesh_data.items():
|
||||
for v in data['vertices']:
|
||||
key = round_pos(v['pos'], 0.0001)
|
||||
if key not in pos_map:
|
||||
pos_map[key] = []
|
||||
pos_map[key].append((v['normal'], v['tangent']))
|
||||
|
||||
# Compute averaged normals and tangents for positions appearing in 2+ meshes
|
||||
avg_data = {}
|
||||
multi_mesh_positions = 0
|
||||
for pos_key, entries in pos_map.items():
|
||||
if len(entries) < 2:
|
||||
continue
|
||||
multi_mesh_positions += 1
|
||||
|
||||
# Average normals
|
||||
avg_n = [0.0, 0.0, 0.0]
|
||||
for n, t in entries:
|
||||
avg_n[0] += n[0]
|
||||
avg_n[1] += n[1]
|
||||
avg_n[2] += n[2]
|
||||
avg_n = vector_normalize(avg_n)
|
||||
|
||||
# Average tangents (only if at least one entry has a tangent)
|
||||
avg_t = [0.0, 0.0, 0.0]
|
||||
tangent_count = 0
|
||||
for n, t in entries:
|
||||
if t is not None:
|
||||
avg_t[0] += t[0]
|
||||
avg_t[1] += t[1]
|
||||
avg_t[2] += t[2]
|
||||
tangent_count += 1
|
||||
|
||||
if tangent_count > 0:
|
||||
avg_t = vector_normalize(avg_t)
|
||||
else:
|
||||
avg_t = None
|
||||
|
||||
avg_data[pos_key] = {
|
||||
'normal': avg_n,
|
||||
'tangent': avg_t,
|
||||
}
|
||||
|
||||
print(f"\nFound {multi_mesh_positions} positions shared across 2+ meshes")
|
||||
print(f"Averaging normals/tangents for {len(avg_data)} positions...")
|
||||
|
||||
# Build global position -> list of pose normal offsets for EACH pose
|
||||
# pose_name -> {pos_key -> list of (nx, ny, nz)}
|
||||
pose_pos_maps = {}
|
||||
for pose_name in all_pose_names:
|
||||
pose_pos_map = {}
|
||||
for name, data in mesh_data.items():
|
||||
if pose_name not in data['poses']:
|
||||
continue
|
||||
pose_offsets = data['poses'][pose_name]
|
||||
for v in data['vertices']:
|
||||
if v['index'] not in pose_offsets:
|
||||
continue
|
||||
key = round_pos(v['pos'], 0.0001)
|
||||
if key not in pose_pos_map:
|
||||
pose_pos_map[key] = []
|
||||
pose_pos_map[key].append(pose_offsets[v['index']])
|
||||
pose_pos_maps[pose_name] = pose_pos_map
|
||||
|
||||
# Compute averaged pose normal offsets for positions appearing in 2+ meshes
|
||||
avg_pose_data = {}
|
||||
for pose_name, pose_pos_map in pose_pos_maps.items():
|
||||
avg_offsets = {}
|
||||
for pos_key, entries in pose_pos_map.items():
|
||||
if len(entries) < 2:
|
||||
continue
|
||||
avg_n = [0.0, 0.0, 0.0]
|
||||
for nx, ny, nz in entries:
|
||||
avg_n[0] += nx
|
||||
avg_n[1] += ny
|
||||
avg_n[2] += nz
|
||||
avg_n = vector_normalize(avg_n)
|
||||
avg_offsets[pos_key] = avg_n
|
||||
if avg_offsets:
|
||||
avg_pose_data[pose_name] = avg_offsets
|
||||
|
||||
if avg_pose_data:
|
||||
print(f"Averaging pose normal offsets for {len(avg_pose_data)} pose(s):")
|
||||
for pose_name in sorted(avg_pose_data.keys()):
|
||||
print(f" - {pose_name}: {len(avg_pose_data[pose_name])} positions")
|
||||
else:
|
||||
print("No pose normal offsets to fix.")
|
||||
|
||||
# Update all meshes with averaged values
|
||||
modified_count = 0
|
||||
for name, data in mesh_data.items():
|
||||
tree = data['tree']
|
||||
vertices = data['vertices']
|
||||
modified = False
|
||||
normal_fixes = 0
|
||||
tangent_fixes = 0
|
||||
pose_normal_fixes = 0
|
||||
|
||||
root = tree.getroot()
|
||||
sharedgeom = root.find('.//sharedgeometry')
|
||||
if sharedgeom is None:
|
||||
continue
|
||||
vbs = sharedgeom.findall('vertexbuffer')
|
||||
if len(vbs) < 1:
|
||||
continue
|
||||
|
||||
pos_norm_vb = vbs[0]
|
||||
tt_verts = vbs[1].findall('vertex') if len(vbs) > 1 else []
|
||||
pn_verts = pos_norm_vb.findall('vertex')
|
||||
|
||||
# Fix basis normals and tangents
|
||||
for v in vertices:
|
||||
key = round_pos(v['pos'], 0.0001)
|
||||
if key not in avg_data:
|
||||
continue
|
||||
|
||||
avg = avg_data[key]
|
||||
|
||||
# Update normal
|
||||
pnv = pn_verts[v['index']]
|
||||
normal = pnv.find('normal')
|
||||
if normal is not None:
|
||||
old_n = (float(normal.get('x')), float(normal.get('y')), float(normal.get('z')))
|
||||
new_n = avg['normal']
|
||||
diff = math.sqrt(
|
||||
(old_n[0]-new_n[0])**2 +
|
||||
(old_n[1]-new_n[1])**2 +
|
||||
(old_n[2]-new_n[2])**2
|
||||
)
|
||||
if diff > 0.000001:
|
||||
normal.set('x', f"{new_n[0]:.6f}")
|
||||
normal.set('y', f"{new_n[1]:.6f}")
|
||||
normal.set('z', f"{new_n[2]:.6f}")
|
||||
normal_fixes += 1
|
||||
|
||||
# Update tangent
|
||||
if avg['tangent'] is not None and v['tangent'] is not None:
|
||||
if v['index'] < len(tt_verts):
|
||||
ttv = tt_verts[v['index']]
|
||||
tangent = ttv.find('tangent')
|
||||
if tangent is not None:
|
||||
old_t = (float(tangent.get('x')), float(tangent.get('y')), float(tangent.get('z')))
|
||||
new_t = avg['tangent']
|
||||
diff = math.sqrt(
|
||||
(old_t[0]-new_t[0])**2 +
|
||||
(old_t[1]-new_t[1])**2 +
|
||||
(old_t[2]-new_t[2])**2
|
||||
)
|
||||
if diff > 0.000001:
|
||||
tangent.set('x', f"{new_t[0]:.6f}")
|
||||
tangent.set('y', f"{new_t[1]:.6f}")
|
||||
tangent.set('z', f"{new_t[2]:.6f}")
|
||||
tangent_fixes += 1
|
||||
|
||||
modified = True
|
||||
|
||||
# Fix pose normal offsets
|
||||
for pose_name, avg_offsets in avg_pose_data.items():
|
||||
if pose_name not in data['poses']:
|
||||
continue
|
||||
# Find the <pose> element
|
||||
pose_elem = None
|
||||
for p in root.findall('.//poses/pose'):
|
||||
if p.get('name') == pose_name:
|
||||
pose_elem = p
|
||||
break
|
||||
if pose_elem is None:
|
||||
continue
|
||||
|
||||
for v in vertices:
|
||||
key = round_pos(v['pos'], 0.0001)
|
||||
if key not in avg_offsets:
|
||||
continue
|
||||
if v['index'] not in data['poses'][pose_name]:
|
||||
continue
|
||||
|
||||
# Find the poseoffset element for this vertex
|
||||
for po in pose_elem.findall('poseoffset'):
|
||||
if int(po.get('index')) == v['index']:
|
||||
old_nx = float(po.get('nx', 0.0))
|
||||
old_ny = float(po.get('ny', 0.0))
|
||||
old_nz = float(po.get('nz', 0.0))
|
||||
new_n = avg_offsets[key]
|
||||
diff = math.sqrt(
|
||||
(old_nx-new_n[0])**2 +
|
||||
(old_ny-new_n[1])**2 +
|
||||
(old_nz-new_n[2])**2
|
||||
)
|
||||
if diff > 0.000001:
|
||||
po.set('nx', f"{new_n[0]:.6f}")
|
||||
po.set('ny', f"{new_n[1]:.6f}")
|
||||
po.set('nz', f"{new_n[2]:.6f}")
|
||||
pose_normal_fixes += 1
|
||||
break
|
||||
|
||||
if pose_normal_fixes > 0:
|
||||
modified = True
|
||||
|
||||
if modified:
|
||||
xml_path = data['xml_path']
|
||||
tree.write(xml_path, encoding='utf-8', xml_declaration=True)
|
||||
print(f" {name}: fixed {normal_fixes} normals, {tangent_fixes} tangents, {pose_normal_fixes} pose normals")
|
||||
modified_count += 1
|
||||
else:
|
||||
print(f" {name}: no changes needed")
|
||||
|
||||
# Convert back to binary
|
||||
print(f"\nConverting {modified_count} modified meshes back to binary...")
|
||||
converted = 0
|
||||
for xml_path in xml_paths:
|
||||
mesh_path = xml_to_mesh(xml_path, converter)
|
||||
if mesh_path:
|
||||
converted += 1
|
||||
# Clean up XML file
|
||||
try:
|
||||
os.remove(xml_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
print(f"[OGRE Mesh Seam Fix] Done. {converted}/{len(xml_paths)} meshes converted.")
|
||||
return converted
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: fix_ogre_mesh_seams.py <mesh_directory> [ogre_xml_converter_path]")
|
||||
sys.exit(1)
|
||||
|
||||
mesh_dir = sys.argv[1]
|
||||
converter = find_converter()
|
||||
|
||||
if not converter:
|
||||
print("ERROR: OgreXMLConverter not found. Set OGRETOOLS_XML_CONVERTER env var or pass path as 2nd argument.")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.isdir(mesh_dir):
|
||||
print(f"ERROR: {mesh_dir} is not a directory")
|
||||
sys.exit(1)
|
||||
|
||||
fix_seams(mesh_dir, converter)
|
||||
@@ -0,0 +1,217 @@
|
||||
# Buoyancy System Analysis
|
||||
|
||||
## Problem: Characters are not affected by buoyancy
|
||||
|
||||
After analyzing the code in `src/features/editScene`, I've identified several potential issues:
|
||||
|
||||
## 1. Character Gravity Factor Issue
|
||||
|
||||
**Root Cause**: Characters have gravity factor set to 0.0f by default.
|
||||
|
||||
In `CharacterSystem.cpp` line 163:
|
||||
```cpp
|
||||
m_physics->setGravityFactor(ch->GetBodyID(), 0.0f);
|
||||
```
|
||||
|
||||
This means characters won't sink into water naturally. The `BuoyancySystem` tries to handle this by setting gravity factor to 1.0f when characters are in water (line 127 in `BuoyancySystem.cpp`), but there may be timing or detection issues.
|
||||
|
||||
## 2. Broadphase Query Area Settings
|
||||
|
||||
The `broadphaseQuery` function in `physics.cpp` uses these settings:
|
||||
|
||||
```cpp
|
||||
JPH::AABox water_box(-JPH::Vec3(1000, 1000, 1000),
|
||||
JPH::Vec3(1000, 0.1f, 1000));
|
||||
water_box.Translate(JPH::Vec3(surface_point));
|
||||
```
|
||||
|
||||
Where `surface_point = position + Ogre::Vector3(0, -0.1f, 0)` (position is the water surface Y level).
|
||||
|
||||
**Dimensions**:
|
||||
- X: -1000 to 1000 (2000 units wide, centered at surface_point.x)
|
||||
- Y: -1000 to 0.1f (1000.1 units tall, but centered 0.1 units BELOW water surface)
|
||||
- Z: -1000 to 1000 (2000 units deep, centered at surface_point.z)
|
||||
|
||||
**Issue**: The water box extends 1000 units BELOW the surface point, but only 0.1 units ABOVE it. Since `surface_point` is 0.1 units below the actual water surface, the box effectively covers:
|
||||
- From 1000.1 units below water surface
|
||||
- To 0.0 units at water surface (not above it)
|
||||
|
||||
This means bodies need to be at or below the water surface to be detected.
|
||||
|
||||
## 3. Character Detection in Broadphase
|
||||
|
||||
Characters are `JPH::Character` objects, not regular dynamic bodies. The broadphase query filters for:
|
||||
- `BroadPhaseLayers::MOVING` layer
|
||||
- `Layers::MOVING` object layer
|
||||
|
||||
Characters should be in these layers, but there may be issues with how character bodies are registered in the broadphase.
|
||||
|
||||
## 4. Debugging Approach
|
||||
|
||||
### 4.1 Enable Debug Logging
|
||||
|
||||
Modify `BuoyancySystem.cpp` to add debug logging:
|
||||
|
||||
```cpp
|
||||
// In update() method, after broadphaseQuery call:
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"BuoyancySystem: Found " + Ogre::StringConverter::toString(m_bodiesInWater.size()) +
|
||||
" bodies in water");
|
||||
|
||||
// In the loop applying buoyancy:
|
||||
for (JPH::BodyID bodyID : m_bodiesInWater) {
|
||||
Ogre::SceneNode *node = m_physics->getSceneNodeFromBodyID(bodyID);
|
||||
if (node) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Body " + Ogre::StringConverter::toString(bodyID.GetIndex()) +
|
||||
" at position: " + Ogre::StringConverter::toString(m_physics->getPosition(bodyID)));
|
||||
}
|
||||
|
||||
// Check if it's a character
|
||||
if (m_physics->bodyIsCharacter(bodyID)) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Body " + Ogre::StringConverter::toString(bodyID.GetIndex()) + " is a character");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Visual Debugging - Draw Water Box
|
||||
|
||||
Add debug rendering to visualize the water detection area:
|
||||
|
||||
```cpp
|
||||
// In BuoyancySystem::update(), after broadphaseQuery:
|
||||
void drawDebugWaterBox(const Ogre::Vector3& waterSurfacePos) {
|
||||
// Create a manual object to visualize the water box
|
||||
static Ogre::ManualObject* waterBoxDebug = nullptr;
|
||||
if (!waterBoxDebug) {
|
||||
waterBoxDebug = m_sceneManager->createManualObject("WaterBoxDebug");
|
||||
Ogre::SceneNode* debugNode = m_sceneManager->getRootSceneNode()->createChildSceneNode();
|
||||
debugNode->attachObject(waterBoxDebug);
|
||||
}
|
||||
|
||||
waterBoxDebug->clear();
|
||||
waterBoxDebug->begin("BaseWhiteNoLighting", Ogre::RenderOperation::OT_LINE_LIST);
|
||||
|
||||
// Water box dimensions (matching broadphaseQuery)
|
||||
float halfSize = 1000.0f;
|
||||
float top = waterSurfacePos.y - 0.1f + 0.1f; // surface_point.y + 0.1f
|
||||
float bottom = waterSurfacePos.y - 0.1f - 1000.0f; // surface_point.y - 1000.0f
|
||||
|
||||
// Draw box edges
|
||||
Ogre::Vector3 corners[8] = {
|
||||
{waterSurfacePos.x - halfSize, bottom, waterSurfacePos.z - halfSize},
|
||||
{waterSurfacePos.x + halfSize, bottom, waterSurfacePos.z - halfSize},
|
||||
{waterSurfacePos.x + halfSize, bottom, waterSurfacePos.z + halfSize},
|
||||
{waterSurfacePos.x - halfSize, bottom, waterSurfacePos.z + halfSize},
|
||||
{waterSurfacePos.x - halfSize, top, waterSurfacePos.z - halfSize},
|
||||
{waterSurfacePos.x + halfSize, top, waterSurfacePos.z - halfSize},
|
||||
{waterSurfacePos.x + halfSize, top, waterSurfacePos.z + halfSize},
|
||||
{waterSurfacePos.x - halfSize, top, waterSurfacePos.z + halfSize}
|
||||
};
|
||||
|
||||
// Bottom square
|
||||
for (int i = 0; i < 4; i++) {
|
||||
waterBoxDebug->position(corners[i]);
|
||||
waterBoxDebug->position(corners[(i+1)%4]);
|
||||
}
|
||||
|
||||
// Top square
|
||||
for (int i = 4; i < 8; i++) {
|
||||
waterBoxDebug->position(corners[i]);
|
||||
waterBoxDebug->position(corners[4 + (i-3)%4]);
|
||||
}
|
||||
|
||||
// Vertical edges
|
||||
for (int i = 0; i < 4; i++) {
|
||||
waterBoxDebug->position(corners[i]);
|
||||
waterBoxDebug->position(corners[i+4]);
|
||||
}
|
||||
|
||||
waterBoxDebug->end();
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Check Character Position Relative to Water
|
||||
|
||||
Add a debug function to check character positions:
|
||||
|
||||
```cpp
|
||||
void debugCharacterPositions() {
|
||||
m_world.query<CharacterComponent, TransformComponent>().each(
|
||||
[&](flecs::entity entity, CharacterComponent &cc, TransformComponent &transform) {
|
||||
if (transform.node) {
|
||||
Ogre::Vector3 worldPos = transform.node->_getDerivedPosition();
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Character entity " + Ogre::StringConverter::toString(entity.id()) +
|
||||
" at Y: " + Ogre::StringConverter::toString(worldPos.y));
|
||||
|
||||
// Check if character has physics body
|
||||
auto it = m_states.find(entity.id());
|
||||
if (it != m_states.end() && it->second.character) {
|
||||
JPH::BodyID bodyID = it->second.character->GetBodyID();
|
||||
Ogre::Vector3 bodyPos = m_physics->getPosition(bodyID);
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Character body at Y: " + Ogre::StringConverter::toString(bodyPos.y) +
|
||||
", gravity factor: " + Ogre::StringConverter::toString(m_physics->getGravityFactor(bodyID)));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Recommended Fixes
|
||||
|
||||
### 5.1 Adjust Water Box Parameters
|
||||
|
||||
The current water box may be too shallow (only 0.1 units at the top). Consider adjusting:
|
||||
|
||||
```cpp
|
||||
// In broadphaseQuery function:
|
||||
JPH::AABox water_box(-JPH::Vec3(1000, 1.0f, 1000), // Increased from 0.1f to 1.0f
|
||||
JPH::Vec3(1000, 1000, 1000)); // Symmetrical above/below
|
||||
```
|
||||
|
||||
This creates a 2-unit tall detection area centered on the surface point.
|
||||
|
||||
### 5.2 Fix Character Gravity Handling
|
||||
|
||||
Modify `BuoyancySystem::update()` to better handle character gravity:
|
||||
|
||||
```cpp
|
||||
// Current issue: characters with gravity factor 0 won't sink into water
|
||||
// Even if buoyancy is applied, they need gravity to sink first
|
||||
|
||||
// Potential fix: Always give characters some minimal gravity when near water
|
||||
// or modify CharacterSystem to not set gravity factor to 0
|
||||
```
|
||||
|
||||
### 5.3 Verify Character Body Registration
|
||||
|
||||
Ensure character bodies are properly registered in the physics system and included in broadphase queries. Check that:
|
||||
1. Characters are added to the physics system (`ch->AddToPhysicsSystem()`)
|
||||
2. They are in the `MOVING` broadphase layer
|
||||
3. Their body IDs are valid for queries
|
||||
|
||||
## 6. Testing Procedure
|
||||
|
||||
1. **Enable debug logging** as shown above
|
||||
2. **Place a character in water** (Y position below water surface)
|
||||
3. **Check console output** for:
|
||||
- Number of bodies detected in water
|
||||
- Character body positions
|
||||
- Gravity factor changes
|
||||
4. **Use visual debug** to see water box
|
||||
5. **Adjust water surface Y** in WaterPhysics component to ensure it's above character position
|
||||
|
||||
## 7. Water Physics Settings
|
||||
|
||||
Default `WaterPhysics` component has:
|
||||
- `waterSurfaceY = -0.1f` (slightly below origin)
|
||||
- `defaultBuoyancy = 1.0f` (neutral buoyancy)
|
||||
- `enabled = true`
|
||||
|
||||
Make sure:
|
||||
1. WaterPhysics entity exists (BuoyancySystem creates one if missing)
|
||||
2. `waterSurfaceY` is above character positions for testing
|
||||
3. Water physics is enabled (`enabled = true`)
|
||||
@@ -0,0 +1,172 @@
|
||||
# Buoyancy System Analysis and Debugging Guide
|
||||
|
||||
## Problem Analysis
|
||||
|
||||
After analyzing the buoyancy system in `src/features/editScene`, I identified several key issues why characters are not affected by buoyancy:
|
||||
|
||||
### 1. **Broadphase Query Area Not Following Camera**
|
||||
The original buoyancy system used a fixed position `(0, waterSurfaceY, 0)` for the broadphase query AABox. This meant the water detection area was static at world origin, not following the camera or characters.
|
||||
|
||||
**Fix Applied**: Modified `BuoyancySystem::update()` to use camera XZ position with water Y position:
|
||||
```cpp
|
||||
Ogre::Vector3 waterSurfacePos(m_cameraPosition.x, waterPhysics->waterSurfaceY, m_cameraPosition.z);
|
||||
```
|
||||
|
||||
### 2. **Character Physics Layer Issue**
|
||||
Characters are created with `Layers::MOVING` but the broadphase query in `physics.cpp` was checking for bodies in specific layers. The query needs to include the MOVING layer.
|
||||
|
||||
**Fix Applied**: Updated `broadphaseQuery` in `physics.cpp` to include `Layers::MOVING`:
|
||||
```cpp
|
||||
if (body->GetMotionType() == JPH::EMotionType::Dynamic &&
|
||||
(body->GetObjectLayer() == Layers::MOVING ||
|
||||
body->GetObjectLayer() == Layers::NON_MOVING)) {
|
||||
```
|
||||
|
||||
### 3. **Character Gravity Factor Management**
|
||||
Characters have gravity factor 0 by default (to prevent sinking into terrain). When they enter water, we need to:
|
||||
1. Save original gravity factor
|
||||
2. Set gravity factor to 1.0 to allow sinking
|
||||
3. Restore original gravity when leaving water
|
||||
|
||||
**Fix Applied**: Added gravity factor caching in `BuoyancySystem`:
|
||||
```cpp
|
||||
// Save original gravity factor if not already saved
|
||||
if (m_characterOriginalGravity.find(bodyID) == m_characterOriginalGravity.end()) {
|
||||
m_characterOriginalGravity[bodyID] = m_physics->getGravityFactor(bodyID);
|
||||
}
|
||||
// Enable gravity for characters in water so they sink
|
||||
m_physics->setGravityFactor(bodyID, 1.0f);
|
||||
```
|
||||
|
||||
### 4. **Water AABox Size Configuration**
|
||||
The broadphase query uses an AABox centered at water surface position with size `(100, 10, 100)`. This may need adjustment based on your scene scale.
|
||||
|
||||
## Debugging Approach
|
||||
|
||||
### 1. **Enable Debug Logging**
|
||||
Run the editor with the `--debug-buoyancy` command line option:
|
||||
```bash
|
||||
./build/Editor --debug-buoyancy
|
||||
```
|
||||
|
||||
This enables verbose logging every 60 frames (about 1 second at 60 FPS) showing:
|
||||
- Water physics state (surface Y, enabled, buoyancy)
|
||||
- Camera position
|
||||
- Water detection center position
|
||||
- All characters and their positions
|
||||
- Bodies detected in water by broadphase query
|
||||
- Character gravity factor cache
|
||||
|
||||
### 2. **Broadphase Query Settings**
|
||||
The water detection area is configured in `src/features/editScene/physics/physics.cpp`:
|
||||
|
||||
```cpp
|
||||
// AABox for water detection (centered at water surface position)
|
||||
// Box extends from (-1000, 1.0, -1000) to (1000, 1000, 1000) relative to surface
|
||||
// Total size: 2000x999x2000 units
|
||||
JPH::AABox water_box(-JPH::Vec3(1000, 1.0f, 1000),
|
||||
JPH::Vec3(1000, 1000, 1000));
|
||||
water_box.Translate(JPH::Vec3(surface_point));
|
||||
```
|
||||
|
||||
**Current Filter Settings**:
|
||||
- Only checks `Layers::MOVING` bodies (line 1587)
|
||||
- Uses `BroadPhaseLayers::MOVING` filter (line 1586)
|
||||
|
||||
**Adjustment Recommendations**:
|
||||
1. **Check character layer**: Ensure characters are in `Layers::MOVING`
|
||||
2. **Adjust box size**: The current 2000x999x2000 box is very large
|
||||
- Reduce 1000 values for smaller detection area
|
||||
- Adjust Y values (1.0f and 1000) for vertical detection range
|
||||
3. **Add NON_MOVING layer**: If characters are in NON_MOVING layer, update filter:
|
||||
```cpp
|
||||
JPH::SpecifiedObjectLayerFilter(Layers::MOVING | Layers::NON_MOVING)
|
||||
```
|
||||
4. **Box follows camera**: The box is translated to `surface_point` which now uses camera XZ position
|
||||
|
||||
### 3. **Water Physics Configuration**
|
||||
Default water settings (in `EditorApp::createDefaultEntities()`):
|
||||
- `waterSurfaceY = -0.1f` (just below ground level)
|
||||
- `defaultBuoyancy = 1.0f` (neutral buoyancy)
|
||||
- `defaultLinearDrag = 0.1f`
|
||||
- `defaultAngularDrag = 0.05f`
|
||||
- `gravity = 9.81f`
|
||||
|
||||
**To adjust**: Use the Water Physics editor UI or modify the WaterPhysics component.
|
||||
|
||||
### 4. **Character Configuration**
|
||||
Ensure characters:
|
||||
1. Have `CharacterComponent` attached
|
||||
2. Are in the `MOVING` physics layer
|
||||
3. Have proper collision shapes
|
||||
4. Are spawned at Y position below water surface for testing
|
||||
|
||||
## Testing Procedure
|
||||
|
||||
1. **Build the project**:
|
||||
```bash
|
||||
cmake --build build --target Editor
|
||||
```
|
||||
|
||||
2. **Run with debug mode**:
|
||||
```bash
|
||||
./build/Editor --debug-buoyancy
|
||||
```
|
||||
|
||||
3. **Create test scene**:
|
||||
- Add water (WaterPhysics entity exists by default)
|
||||
- Spawn characters (use Character spawner in UI)
|
||||
- Position characters below water surface (Y < -0.1)
|
||||
|
||||
4. **Monitor console output** for debug messages showing:
|
||||
- "Bodies in water (broadphase): X"
|
||||
- Character positions and "inWater" status
|
||||
- Gravity factor changes
|
||||
|
||||
5. **Adjust settings as needed**:
|
||||
- Increase water surface Y if characters are above water
|
||||
- Adjust AABox size in physics.cpp
|
||||
- Modify buoyancy/drag coefficients
|
||||
|
||||
## Key Code Changes Made
|
||||
|
||||
1. **BuoyancySystem.cpp/hpp**:
|
||||
- Added camera position tracking
|
||||
- Added gravity factor caching for characters
|
||||
- Added debug logging system
|
||||
- Fixed broadphase query position
|
||||
|
||||
2. **physics.cpp**:
|
||||
- Fixed broadphase query to include MOVING layer
|
||||
- Ensured character bodies are detected
|
||||
|
||||
3. **EditorApp.cpp/hpp**:
|
||||
- Added `--debug-buoyancy` command line option
|
||||
- Added `setDebugBuoyancy()` method
|
||||
- Updated camera position to buoyancy system each frame
|
||||
|
||||
4. **EditorCamera.hpp**:
|
||||
- Added `getPosition()` method
|
||||
|
||||
5. **main.cpp**:
|
||||
- Added command line argument parsing for `--debug-buoyancy`
|
||||
|
||||
## Expected Behavior After Fixes
|
||||
|
||||
1. Characters should sink into water (gravity enabled)
|
||||
2. Buoyancy forces should push characters upward
|
||||
3. Debug logs should show bodies detected in water
|
||||
4. Character gravity should be restored when leaving water
|
||||
5. Water detection area should follow camera movement
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If characters still aren't affected:
|
||||
|
||||
1. **Check debug logs**: Ensure bodies are being detected
|
||||
2. **Verify water surface Y**: Characters must be below this value
|
||||
3. **Check physics layers**: Characters should be in MOVING layer
|
||||
4. **Test with simple objects**: Create a simple box to verify buoyancy works
|
||||
5. **Adjust AABox size**: Increase detection area if characters are far from camera
|
||||
|
||||
The system is now properly configured to detect characters in water and apply buoyancy forces with comprehensive debugging capabilities.
|
||||
@@ -73,8 +73,6 @@ FileSystem=resources/fonts
|
||||
[LuaScripts]
|
||||
FileSystem=lua-scripts
|
||||
|
||||
#[Characters]
|
||||
#FileSystem=./characters
|
||||
[Audio]
|
||||
FileSystem=./audio/gui
|
||||
|
||||
|
||||
@@ -13,6 +13,17 @@ foreach(DIR_NAME ${DIRECTORY_LIST})
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${DIR_NAME})
|
||||
list(APPEND TARGET_PATHS ${CMAKE_CURRENT_BINARY_DIR}/${DIR_NAME})
|
||||
endforeach()
|
||||
set(COPY_FILES main/flare.png)
|
||||
set(TARGET_FILES)
|
||||
foreach(PFILE ${COPY_FILES})
|
||||
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${PFILE}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/${PFILE}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${PFILE}
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${PFILE}
|
||||
)
|
||||
list(APPEND TARGET_FILES ${CMAKE_CURRENT_BINARY_DIR}/${PFILE})
|
||||
endforeach()
|
||||
|
||||
#add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/terrain/world_map.png
|
||||
# COMMAND unzip -o ${CMAKE_CURRENT_SOURCE_DIR}/world_map.kra mergedimage.png -d ${CMAKE_CURRENT_BINARY_DIR}/world_map
|
||||
@@ -31,4 +42,4 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/terrain/brushes.png
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/brushes.kra)
|
||||
list(APPEND TARGET_PATHS ${CMAKE_CURRENT_BINARY_DIR}/terrain/brushes.png)
|
||||
|
||||
add_custom_target(stage_resources ALL DEPENDS ${TARGET_PATHS})
|
||||
add_custom_target(stage_resources ALL DEPENDS ${TARGET_PATHS} ${TARGET_FILES})
|
||||
|
||||
@@ -69,3 +69,27 @@ material Debug/Red2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug material for normal visualization overlay.
|
||||
* Renders on top of everything (depth_check off, depth_write off)
|
||||
* Uses vertex colours so each normal line can have its own colour.
|
||||
* Rendered in overlay queue to appear on top of all geometry.
|
||||
*/
|
||||
material Debug/NormalOverlay
|
||||
{
|
||||
technique
|
||||
{
|
||||
pass
|
||||
{
|
||||
lighting off
|
||||
depth_check off
|
||||
depth_write off
|
||||
ambient 1.0 1.0 1.0 1.0
|
||||
diffuse vertexcolour
|
||||
specular 0.0 0.0 0.0 1.0
|
||||
cull_software none
|
||||
cull_hardware none
|
||||
scene_blend alpha_blend
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
@@ -0,0 +1,19 @@
|
||||
material Examples/Flare
|
||||
{
|
||||
technique
|
||||
{
|
||||
pass
|
||||
{
|
||||
lighting off
|
||||
scene_blend add
|
||||
depth_write off
|
||||
diffuse vertexcolour
|
||||
|
||||
texture_unit
|
||||
{
|
||||
texture flare.png
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
project(features)
|
||||
add_subdirectory(characters)
|
||||
add_subdirectory(sceneEditor)
|
||||
add_subdirectory(editScene)
|
||||
@@ -0,0 +1,33 @@
|
||||
project(characters)
|
||||
find_package(OGRE REQUIRED COMPONENTS Bites Paging Terrain CONFIG)
|
||||
find_package(ZLIB)
|
||||
find_package(SDL2)
|
||||
find_package(assimp REQUIRED CONFIG)
|
||||
find_package(OgreProcedural REQUIRED CONFIG)
|
||||
find_package(pugixml REQUIRED CONFIG)
|
||||
find_package(flecs REQUIRED CONFIG)
|
||||
find_package(Tracy REQUIRED CONFIG)
|
||||
add_executable(demo main.cpp TimeEvents.cpp)
|
||||
target_link_libraries(demo OgreMain OgreBites)
|
||||
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources.cfg"
|
||||
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
add_custom_command(TARGET demo POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_BINARY_DIR}/resources"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/resources"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.mesh"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.mesh"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.skeleton"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.skeleton"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.material"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.material"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/resources/main/blue_jaiqua.jpg"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/resources/main/blue_jaiqua.jpg"
|
||||
DEPENDS "${CMAKE_BINARY_DIR}/resources"
|
||||
COMMENT "Copying generated resources from root build dir to local build dir"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
//
|
||||
// TimeEvents.cpp
|
||||
// OGRE
|
||||
//
|
||||
// Created by Chilly Willy on 11/29/25.
|
||||
//
|
||||
|
||||
#include "TimeEvents.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
|
||||
void TimeEventDispatcher::addEventList(const TimeEventList * list)
|
||||
{
|
||||
if (std::find(mEventLists.begin(), mEventLists.end(), list) == mEventLists.end())
|
||||
{
|
||||
mEventLists.push_back(list);
|
||||
}
|
||||
}
|
||||
|
||||
void TimeEventDispatcher::removeEventList(const TimeEventList * list)
|
||||
{
|
||||
auto i = std::find(mEventLists.begin(), mEventLists.end(), list);
|
||||
if (i != mEventLists.end())
|
||||
{
|
||||
mEventLists.erase(i);
|
||||
}
|
||||
}
|
||||
|
||||
void TimeEventDispatcher::addListener(TimeEventListener * listener)
|
||||
{
|
||||
if (std::find(mListeners.begin(), mListeners.end(), listener) == mListeners.end())
|
||||
{
|
||||
mListeners.push_back(listener);
|
||||
}
|
||||
}
|
||||
|
||||
void TimeEventDispatcher::removeListener(TimeEventListener * listener)
|
||||
{
|
||||
auto i = std::find(mListeners.begin(), mListeners.end(), listener);
|
||||
if (i != mListeners.end())
|
||||
{
|
||||
mListeners.erase(i);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
lastTime, thisTime, n=loops
|
||||
|
||||
lastTime is the thisTime from the last frame and thisTime will be the lastTime in the next frame.
|
||||
Events at those times should only be dispatched once so we should include one and exclude the other.
|
||||
|
||||
If we include thisTime and exclude lastTime, then we need to dispatch events before we start playing,
|
||||
eg when we call setTimePosition(), and we would need to somehow know whether it should be dispatched
|
||||
as Forward or Backward.
|
||||
|
||||
If we include lastTime and exclude thisTime, then we don't need to do anything before playing but
|
||||
we need to include the length/0 when we loop or finish, which requires special handling anyway.
|
||||
|
||||
Looping Forward:
|
||||
next lastTime
|
||||
thisTime < length thisTime lastTime <= t < thisTime
|
||||
thisTime = length 0 lastTime <= t <= length
|
||||
thisTime > length thisTime - n * length lastTime <= t <= length ... 0 <= t < thisTime
|
||||
|
||||
... 0 <= t <= length
|
||||
|
||||
Looping Backward:
|
||||
next lastTime
|
||||
thisTime > 0 thisTime lastTime >= t > thisTime
|
||||
thisTime = 0 0 !!! lastTime >= t >= 0
|
||||
thisTime < 0 thisTime + n * length lastTime >= t >= 0 ... length >= t > thisTime
|
||||
|
||||
... length >= t >= 0
|
||||
|
||||
Not-Looping Forward:
|
||||
next lastTime
|
||||
thisTime < length thisTime lastTime <= t < thisTime
|
||||
thisTime = length length lastTime <= t <= length
|
||||
thisTime > length length lastTime <= t <= length
|
||||
|
||||
|
||||
Not-Looping Backward:
|
||||
next lastTime
|
||||
thisTime > 0 thisTime lastTime >= t > thisTime
|
||||
thisTime = 0 0 lastTime >= t >= 0
|
||||
thisTime < 0 0 lastTime >= t >= 0
|
||||
|
||||
*/
|
||||
|
||||
void TimeEventDispatcher::dispatch(float lastTime, float thisTime, int loops, float length)
|
||||
{
|
||||
if ((loops > 0) || (thisTime > lastTime))
|
||||
{
|
||||
while (loops--)
|
||||
{
|
||||
dispatchForwardInclusive(lastTime, length);
|
||||
lastTime = 0.0f;
|
||||
}
|
||||
if (thisTime >= length)
|
||||
{
|
||||
dispatchForwardInclusive(lastTime, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchForwardExclusive(lastTime, thisTime);
|
||||
}
|
||||
}
|
||||
else if ((loops < 0) || (thisTime < lastTime))
|
||||
{
|
||||
if (lastTime == 0.0f)
|
||||
{
|
||||
/* length mod length = 0 mod length
|
||||
|
||||
Either we've already been looping backward and the last frame just happened
|
||||
to stop on 0, in which case we already triggered its events, or we are just
|
||||
starting to to loop backward, in which case we should not trigger events at
|
||||
0 until we do a full backward run through the animation. If we haven't been
|
||||
looping then we would not get in here because thisTime == lastTime == 0.0f.
|
||||
For the same reason we also know loops < 0.
|
||||
*/
|
||||
lastTime = length;
|
||||
++loops;
|
||||
}
|
||||
while (loops++)
|
||||
{
|
||||
dispatchBackwardInclusive(lastTime, 0.0f);
|
||||
lastTime = length;
|
||||
}
|
||||
if (thisTime <= 0.0f)
|
||||
{
|
||||
dispatchBackwardInclusive(lastTime, 0.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchBackwardExclusive(lastTime, thisTime);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nothing doing
|
||||
}
|
||||
}
|
||||
|
||||
void TimeEventDispatcher::dispatchForwardInclusive(float lastTime, float thisTime)
|
||||
{
|
||||
for (const TimeEventList * events : mEventLists)
|
||||
{
|
||||
for (auto ie = events->begin(); ie != events->end(); ++ie)
|
||||
{
|
||||
if (ie->first >= lastTime && ie->first <= thisTime)
|
||||
{
|
||||
dispatchEvent(ie->second, TED_FORWARD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TimeEventDispatcher::dispatchForwardExclusive(float lastTime, float thisTime)
|
||||
{
|
||||
for (const TimeEventList * events : mEventLists)
|
||||
{
|
||||
for (auto ie = events->begin(); ie != events->end(); ++ie)
|
||||
{
|
||||
if (ie->first >= lastTime && ie->first < thisTime)
|
||||
{
|
||||
dispatchEvent(ie->second, TED_FORWARD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TimeEventDispatcher::dispatchBackwardInclusive(float lastTime, float thisTime)
|
||||
{
|
||||
for (const TimeEventList * events : mEventLists)
|
||||
{
|
||||
for (auto ie = events->rbegin(); ie != events->rend(); ++ie)
|
||||
{
|
||||
if (ie->first <= lastTime && ie->first >= thisTime)
|
||||
{
|
||||
dispatchEvent(ie->second, TED_BACKWARD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TimeEventDispatcher::dispatchBackwardExclusive(float lastTime, float thisTime)
|
||||
{
|
||||
for (const TimeEventList * events : mEventLists)
|
||||
{
|
||||
for (auto ie = events->rbegin(); ie != events->rend(); ++ie)
|
||||
{
|
||||
if (ie->first <= lastTime && ie->first > thisTime)
|
||||
{
|
||||
dispatchEvent(ie->second, TED_BACKWARD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TimeEventDispatcher::dispatchEvent(const std::string & name, TimeEventDirection direction)
|
||||
{
|
||||
for (TimeEventListener * listener : mListeners)
|
||||
{
|
||||
listener->eventOccurred(name, direction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// TimeEvents.h
|
||||
// OGRE
|
||||
//
|
||||
// Created by Chilly Willy on 11/29/25.
|
||||
//
|
||||
|
||||
#ifndef INCLUDE_OGRE_TIME_EVENTS_H
|
||||
#define INCLUDE_OGRE_TIME_EVENTS_H
|
||||
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
|
||||
typedef std::multimap<float, std::string> TimeEventList;
|
||||
|
||||
|
||||
enum TimeEventDirection
|
||||
{
|
||||
TED_FORWARD,
|
||||
TED_BACKWARD,
|
||||
};
|
||||
|
||||
|
||||
class TimeEventListener
|
||||
{
|
||||
public:
|
||||
virtual void eventOccurred(const std::string & name, TimeEventDirection direction) {}
|
||||
};
|
||||
|
||||
|
||||
class TimeEventDispatcher
|
||||
{
|
||||
public:
|
||||
void addEventList(const TimeEventList * list);
|
||||
void removeEventList(const TimeEventList * list);
|
||||
|
||||
void addListener(TimeEventListener * listener);
|
||||
void removeListener(TimeEventListener * listener);
|
||||
|
||||
void dispatch(float lastTime, float thisTime, int loops, float length);
|
||||
|
||||
private:
|
||||
void dispatchForwardInclusive(float lastTime, float thisTime);
|
||||
void dispatchForwardExclusive(float lastTime, float thisTime);
|
||||
void dispatchBackwardInclusive(float lastTime, float thisTime);
|
||||
void dispatchBackwardExclusive(float lastTime, float thisTime);
|
||||
|
||||
void dispatchEvent(const std::string & name, TimeEventDirection direction);
|
||||
|
||||
std::vector<const TimeEventList *> mEventLists;
|
||||
std::vector<TimeEventListener *> mListeners;
|
||||
};
|
||||
|
||||
|
||||
#endif /* INCLUDE_OGRE_TIME_EVENTS_H */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
# Ogre Core Resources
|
||||
[OgreInternal]
|
||||
#FileSystem=./Media/Main
|
||||
FileSystem=resources/main
|
||||
FileSystem=resources/shaderlib
|
||||
#FileSystem=./Media/RTShaderLib
|
||||
FileSystem=resources/terrain
|
||||
|
||||
# Resources required by OgreBites::Trays
|
||||
[Essential]
|
||||
#Zip=./Media/packs/SdkTrays.zip
|
||||
#Zip=./Media/packs/profiler.zip
|
||||
|
||||
## this line will end up in the [Essential] group
|
||||
#FileSystem=./Media/thumbnails
|
||||
|
||||
# Common sample resources needed by many of the samples.
|
||||
# Rarely used resources should be separately loaded by the
|
||||
# samples which require them.
|
||||
[General]
|
||||
FileSystem=skybox
|
||||
FileSystem=resources/buildings
|
||||
FileSystem=resources/buildings/parts/pier
|
||||
FileSystem=resources/buildings/parts/furniture
|
||||
FileSystem=resources/vehicles
|
||||
FileSystem=resources/debug
|
||||
FileSystem=resources/fonts
|
||||
# PBR media must come before the scripts that reference it
|
||||
#FileSystem=./Media/PBR
|
||||
#FileSystem=./Media/PBR/filament
|
||||
|
||||
#FileSystem=./Media/materials/programs/GLSL
|
||||
#FileSystem=./Media/materials/programs/GLSL120
|
||||
#FileSystem=./Media/materials/programs/GLSL150
|
||||
#FileSystem=./Media/materials/programs/GLSL400
|
||||
#FileSystem=./Media/materials/programs/GLSLES
|
||||
#FileSystem=./Media/materials/programs/SPIRV
|
||||
#FileSystem=./Media/materials/programs/Cg
|
||||
#FileSystem=./Media/materials/programs/HLSL
|
||||
#FileSystem=./Media/materials/programs/HLSL_Cg
|
||||
#FileSystem=./Media/materials/scripts
|
||||
#FileSystem=./Media/materials/textures
|
||||
#FileSystem=./Media/materials/textures/terrain
|
||||
#FileSystem=./Media/models
|
||||
#FileSystem=./Media/particle
|
||||
#FileSystem=./Media/DeferredShadingMedia
|
||||
#FileSystem=./Media/DeferredShadingMedia/DeferredShading/post
|
||||
#FileSystem=./Media/PCZAppMedia
|
||||
#FileSystem=./Media/materials/scripts/SSAO
|
||||
#FileSystem=./Media/materials/textures/SSAO
|
||||
#FileSystem=./Media/volumeTerrain
|
||||
#FileSystem=./Media/CSMShadows
|
||||
|
||||
#Zip=./Media/packs/cubemap.zip
|
||||
#Zip=./Media/packs/cubemapsJS.zip
|
||||
#Zip=./Media/packs/dragon.zip
|
||||
#Zip=./Media/packs/fresneldemo.zip
|
||||
#Zip=./Media/packs/ogredance.zip
|
||||
#Zip=./Media/packs/Sinbad.zip
|
||||
#Zip=./Media/packs/skybox.zip
|
||||
#Zip=./Media/volumeTerrain/volumeTerrainBig.zip
|
||||
|
||||
#Zip=./Media/packs/DamagedHelmet.zip
|
||||
#Zip=./Media/packs/filament_shaders.zip
|
||||
|
||||
#[BSPWorld]
|
||||
#Zip=./Media/packs/oa_rpg3dm2.pk3
|
||||
#Zip=./Media/packs/ogretestmap.zip
|
||||
|
||||
# Materials for visual tests
|
||||
#[Tests]
|
||||
#FileSystem=/media/slapin/library/ogre/ogre-sdk/Tests/Media
|
||||
[LuaScripts]
|
||||
FileSystem=lua-scripts
|
||||
|
||||
#[Characters]
|
||||
#FileSystem=./characters
|
||||
[Audio]
|
||||
FileSystem=./audio/gui
|
||||
|
||||
[Water]
|
||||
FileSystem=water
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,24 @@
|
||||
material jaiqua
|
||||
{
|
||||
technique
|
||||
{
|
||||
pass
|
||||
{
|
||||
texture_unit
|
||||
{
|
||||
texture blue_jaiqua.jpg
|
||||
tex_address_mode clamp
|
||||
}
|
||||
|
||||
rtshader_system
|
||||
{
|
||||
// In case the system uses the RTSS, the following line will ensure
|
||||
// that hardware animation is used.
|
||||
// Alternatively, you can derive this information programatically via
|
||||
// HardwareSkinningFactory::prepareEntityForSkinning
|
||||
hardware_skinning 38 2 dual_quaternion true false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,754 @@
|
||||
project(editScene)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
find_package(OGRE REQUIRED COMPONENTS Bites Overlay MeshLodGenerator CONFIG)
|
||||
find_package(flecs REQUIRED CONFIG)
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(Jolt REQUIRED)
|
||||
find_package(OgreProcedural REQUIRED CONFIG)
|
||||
|
||||
# Build RecastNavigation from copied source (RTTI-compatible)
|
||||
add_subdirectory(recastnavigation)
|
||||
|
||||
# Collect all source files
|
||||
set(EDITSCENE_SOURCES
|
||||
main.cpp
|
||||
EditorApp.cpp
|
||||
GameMode.cpp
|
||||
systems/EditorUISystem.cpp
|
||||
systems/SceneSerializer.cpp
|
||||
systems/PhysicsSystem.cpp
|
||||
systems/BuoyancySystem.cpp
|
||||
systems/EditorSunSystem.cpp
|
||||
systems/EditorSkyboxSystem.cpp
|
||||
systems/EditorWaterPlaneSystem.cpp
|
||||
systems/LightSystem.cpp
|
||||
systems/CameraSystem.cpp
|
||||
systems/LodSystem.cpp
|
||||
systems/StaticGeometrySystem.cpp
|
||||
systems/ProceduralTextureSystem.cpp
|
||||
systems/ProceduralMaterialSystem.cpp
|
||||
systems/ProceduralMeshSystem.cpp
|
||||
systems/CellGridSystem.cpp
|
||||
systems/NormalDebugSystem.cpp
|
||||
systems/RoomLayoutSystem.cpp
|
||||
systems/FurnitureLibrary.cpp
|
||||
systems/StartupMenuSystem.cpp
|
||||
systems/PauseMenuSystem.cpp
|
||||
systems/ItemRegistry.cpp
|
||||
systems/ContainerStateRegistry.cpp
|
||||
systems/ItemStateRegistry.cpp
|
||||
systems/SaveLoadSystem.cpp
|
||||
systems/SaveLoadDialog.cpp
|
||||
systems/PlayerControllerSystem.cpp
|
||||
systems/CharacterSlotSystem.cpp
|
||||
systems/OgreEntityHack.cpp
|
||||
systems/CharacterRegistry.cpp
|
||||
systems/MarkovNameGenerator.cpp
|
||||
systems/PregnancySystem.cpp
|
||||
systems/AnimationTreeSystem.cpp
|
||||
systems/BehaviorTreeSystem.cpp
|
||||
systems/NavMeshSystem.cpp
|
||||
recast/TileCacheNavMesh.cpp
|
||||
recast/PartitionedMesh.cpp
|
||||
recast/fastlz.c
|
||||
systems/CharacterSystem.cpp
|
||||
systems/SmartObjectSystem.cpp
|
||||
components/SmartObjectModule.cpp
|
||||
ui/SmartObjectEditor.cpp
|
||||
components/GoapPlannerModule.cpp
|
||||
systems/GoapRunnerSystem.cpp
|
||||
systems/PathFollowingSystem.cpp
|
||||
systems/GoapPlannerSystem.cpp
|
||||
components/GoapRunnerModule.cpp
|
||||
components/PathFollowingModule.cpp
|
||||
ui/GoapRunnerEditor.cpp
|
||||
ui/PathFollowingEditor.cpp
|
||||
ui/GoapPlannerEditor.cpp
|
||||
systems/ActuatorSystem.cpp
|
||||
ui/ActuatorEditor.cpp
|
||||
components/ActuatorModule.cpp
|
||||
systems/EventBus.cpp
|
||||
systems/EventHandlerSystem.cpp
|
||||
ui/EventHandlerEditor.cpp
|
||||
components/EventHandlerModule.cpp
|
||||
systems/PrefabSystem.cpp
|
||||
ui/PrefabInstanceEditor.cpp
|
||||
|
||||
systems/ItemSystem.cpp
|
||||
components/ItemModule.cpp
|
||||
components/InventoryModule.cpp
|
||||
ui/ItemEditor.cpp
|
||||
ui/InventoryEditor.cpp
|
||||
|
||||
ui/TransformEditor.cpp
|
||||
ui/RenderableEditor.cpp
|
||||
ui/PhysicsColliderEditor.cpp
|
||||
ui/RigidBodyEditor.cpp
|
||||
ui/LightEditor.cpp
|
||||
ui/CameraEditor.cpp
|
||||
ui/LodEditor.cpp
|
||||
ui/LodSettingsEditor.cpp
|
||||
ui/StaticGeometryEditor.cpp
|
||||
ui/StaticGeometryMemberEditor.cpp
|
||||
ui/ProceduralTextureEditor.cpp
|
||||
ui/ProceduralMaterialEditor.cpp
|
||||
ui/PrimitiveEditor.cpp
|
||||
ui/TriangleBufferEditor.cpp
|
||||
ui/CharacterSlotsEditor.cpp
|
||||
ui/CharacterIdentityEditor.cpp
|
||||
ui/AnimationTreeEditor.cpp
|
||||
ui/AnimationTreeTemplateEditor.cpp
|
||||
ui/CharacterEditor.cpp
|
||||
ui/CellGridEditor.cpp
|
||||
ui/LotEditor.cpp
|
||||
ui/DistrictEditor.cpp
|
||||
ui/TownEditor.cpp
|
||||
ui/RoofEditor.cpp
|
||||
ui/RoomEditor.cpp
|
||||
|
||||
|
||||
ui/ClearAreaEditor.cpp
|
||||
ui/FurnitureTemplateEditor.cpp
|
||||
ui/StartupMenuEditor.cpp
|
||||
ui/PlayerControllerEditor.cpp
|
||||
ui/BuoyancyInfoEditor.cpp
|
||||
ui/GoapBlackboardEditor.cpp
|
||||
ui/BehaviorTreeEditor.cpp
|
||||
ui/InlineBehaviorTreeEditor.cpp
|
||||
ui/NavMeshEditor.cpp
|
||||
ui/ActionDatabaseEditor.cpp
|
||||
ui/ActionDatabaseSingletonEditor.cpp
|
||||
ui/ActionDebugEditor.cpp
|
||||
ui/ComponentRegistration.cpp
|
||||
components/GoapBlackboard.cpp
|
||||
components/GoapExpression.cpp
|
||||
components/GoapGoal.cpp
|
||||
components/ActionDatabase.cpp
|
||||
components/ActionDatabaseModule.cpp
|
||||
components/ActionDebugModule.cpp
|
||||
components/GoapBlackboardModule.cpp
|
||||
components/NavMeshModule.cpp
|
||||
components/LightModule.cpp
|
||||
components/CameraModule.cpp
|
||||
components/LodModule.cpp
|
||||
components/StaticGeometryModule.cpp
|
||||
components/ProceduralTextureModule.cpp
|
||||
components/ProceduralMaterialModule.cpp
|
||||
components/PrimitiveModule.cpp
|
||||
components/TriangleBufferModule.cpp
|
||||
components/CharacterSlotsModule.cpp
|
||||
components/AnimationTreeModule.cpp
|
||||
components/AnimationTreeTemplateModule.cpp
|
||||
components/AnimationTree.cpp
|
||||
components/CharacterModule.cpp
|
||||
components/CellGridModule.cpp
|
||||
components/CellGridEditorsModule.cpp
|
||||
components/CellGrid.cpp
|
||||
components/StartupMenuModule.cpp
|
||||
components/PlayerControllerModule.cpp
|
||||
systems/DialogueSystem.cpp
|
||||
lua/LuaDialogueApi.cpp
|
||||
lua/LuaItemApi.cpp
|
||||
components/Formula.cpp
|
||||
components/CharacterClassDatabase.cpp
|
||||
systems/CharacterClassSystem.cpp
|
||||
ui/CharacterClassDatabaseEditor.cpp
|
||||
components/BuoyancyInfoModule.cpp
|
||||
components/WaterPhysicsModule.cpp
|
||||
components/WaterPlaneModule.cpp
|
||||
components/SunModule.cpp
|
||||
components/SkyboxModule.cpp
|
||||
camera/EditorCamera.cpp
|
||||
gizmo/Gizmo.cpp
|
||||
gizmo/Cursor3D.cpp
|
||||
physics/physics.cpp
|
||||
lua/LuaState.cpp
|
||||
lua/LuaEntityApi.cpp
|
||||
lua/LuaComponentApi.cpp
|
||||
lua/LuaEventApi.cpp
|
||||
lua/LuaActionApi.cpp
|
||||
lua/LuaBehaviorTreeApi.cpp
|
||||
lua/LuaGameModeApi.cpp
|
||||
lua/LuaCharacterClassApi.cpp
|
||||
lua/LuaCharacterApi.cpp
|
||||
lua/LuaSaveLoadApi.cpp
|
||||
)
|
||||
|
||||
set(EDITSCENE_HEADERS
|
||||
EditorApp.hpp
|
||||
components/Transform.hpp
|
||||
components/Renderable.hpp
|
||||
components/EntityName.hpp
|
||||
components/Relationship.hpp
|
||||
components/PhysicsCollider.hpp
|
||||
components/RigidBody.hpp
|
||||
components/BuoyancyInfo.hpp
|
||||
components/WaterPhysics.hpp
|
||||
components/WaterPlane.hpp
|
||||
components/Sun.hpp
|
||||
components/Skybox.hpp
|
||||
components/Light.hpp
|
||||
components/Camera.hpp
|
||||
components/Lod.hpp
|
||||
components/LodSettings.hpp
|
||||
components/StaticGeometry.hpp
|
||||
components/StaticGeometryMember.hpp
|
||||
components/ProceduralTexture.hpp
|
||||
components/ProceduralMaterial.hpp
|
||||
components/Primitive.hpp
|
||||
components/TriangleBuffer.hpp
|
||||
components/CharacterSlots.hpp
|
||||
components/CharacterIdentity.hpp
|
||||
components/RuntimeMarker.hpp
|
||||
components/AnimationTree.hpp
|
||||
components/Character.hpp
|
||||
components/CellGrid.hpp
|
||||
components/StartupMenu.hpp
|
||||
components/PlayerController.hpp
|
||||
systems/DialogueSystem.hpp
|
||||
lua/LuaDialogueApi.hpp
|
||||
lua/LuaItemApi.hpp
|
||||
components/Formula.hpp
|
||||
components/CharacterClassDatabase.hpp
|
||||
|
||||
ui/CharacterClassDatabaseEditor.hpp
|
||||
systems/CharacterClassSystem.hpp
|
||||
systems/StartupMenuSystem.hpp
|
||||
systems/PauseMenuSystem.hpp
|
||||
systems/ItemRegistry.hpp
|
||||
systems/ContainerStateRegistry.hpp
|
||||
systems/ItemStateRegistry.hpp
|
||||
systems/PlayerControllerSystem.hpp
|
||||
systems/EditorUISystem.hpp
|
||||
systems/CellGridSystem.hpp
|
||||
systems/NormalDebugSystem.hpp
|
||||
systems/RoomLayoutSystem.hpp
|
||||
systems/FurnitureLibrary.hpp
|
||||
systems/ProceduralMaterialSystem.hpp
|
||||
systems/ProceduralMeshSystem.hpp
|
||||
systems/CharacterSlotSystem.hpp
|
||||
systems/CharacterRegistry.hpp
|
||||
systems/PregnancySystem.hpp
|
||||
systems/AnimationTreeSystem.hpp
|
||||
systems/BehaviorTreeSystem.hpp
|
||||
systems/NavMeshSystem.hpp
|
||||
recast/TileCacheNavMesh.hpp
|
||||
recast/PartitionedMesh.hpp
|
||||
recast/fastlz.h
|
||||
systems/CharacterSystem.hpp
|
||||
systems/SmartObjectSystem.hpp
|
||||
components/SmartObject.hpp
|
||||
ui/SmartObjectEditor.hpp
|
||||
components/GoapPlanner.hpp
|
||||
components/PathFollowing.hpp
|
||||
components/GoapRunner.hpp
|
||||
ui/GoapPlannerEditor.hpp
|
||||
ui/GoapRunnerEditor.hpp
|
||||
ui/PathFollowingEditor.hpp
|
||||
systems/PrefabSystem.hpp
|
||||
systems/GoapRunnerSystem.hpp
|
||||
systems/PathFollowingSystem.hpp
|
||||
systems/GoapPlannerSystem.hpp
|
||||
components/Actuator.hpp
|
||||
ui/ActuatorEditor.hpp
|
||||
systems/EventBus.hpp
|
||||
components/EventHandler.hpp
|
||||
systems/EventHandlerSystem.hpp
|
||||
ui/EventHandlerEditor.hpp
|
||||
components/PrefabInstance.hpp
|
||||
ui/PrefabInstanceEditor.hpp
|
||||
|
||||
systems/ItemSystem.hpp
|
||||
components/Item.hpp
|
||||
components/Inventory.hpp
|
||||
ui/ItemEditor.hpp
|
||||
ui/InventoryEditor.hpp
|
||||
|
||||
systems/ProceduralTextureSystem.hpp
|
||||
systems/StaticGeometrySystem.hpp
|
||||
systems/SceneSerializer.hpp
|
||||
systems/SaveLoadSystem.hpp
|
||||
systems/SaveLoadDialog.hpp
|
||||
systems/PhysicsSystem.hpp
|
||||
systems/BuoyancySystem.hpp
|
||||
systems/EditorSunSystem.hpp
|
||||
systems/EditorSkyboxSystem.hpp
|
||||
systems/EditorWaterPlaneSystem.hpp
|
||||
systems/LightSystem.hpp
|
||||
systems/CameraSystem.hpp
|
||||
systems/LodSystem.hpp
|
||||
ui/ComponentEditor.hpp
|
||||
ui/ComponentRegistry.hpp
|
||||
ui/ComponentRegistration.hpp
|
||||
ui/TransformEditor.hpp
|
||||
ui/RenderableEditor.hpp
|
||||
ui/PhysicsColliderEditor.hpp
|
||||
ui/RigidBodyEditor.hpp
|
||||
ui/LightEditor.hpp
|
||||
ui/CameraEditor.hpp
|
||||
ui/LodEditor.hpp
|
||||
ui/LodSettingsEditor.hpp
|
||||
ui/StaticGeometryEditor.hpp
|
||||
ui/StaticGeometryMemberEditor.hpp
|
||||
ui/ProceduralTextureEditor.hpp
|
||||
ui/ProceduralMaterialEditor.hpp
|
||||
ui/PrimitiveEditor.hpp
|
||||
ui/TriangleBufferEditor.hpp
|
||||
ui/CharacterSlotsEditor.hpp
|
||||
ui/CharacterIdentityEditor.hpp
|
||||
ui/AnimationTreeEditor.hpp
|
||||
ui/AnimationTreeTemplateEditor.hpp
|
||||
ui/CharacterEditor.hpp
|
||||
ui/CellGridEditor.hpp
|
||||
ui/LotEditor.hpp
|
||||
ui/DistrictEditor.hpp
|
||||
ui/TownEditor.hpp
|
||||
ui/RoofEditor.hpp
|
||||
ui/RoomEditor.hpp
|
||||
|
||||
|
||||
ui/ClearAreaEditor.hpp
|
||||
ui/FurnitureTemplateEditor.hpp
|
||||
ui/StartupMenuEditor.hpp
|
||||
ui/PlayerControllerEditor.hpp
|
||||
ui/BuoyancyInfoEditor.hpp
|
||||
ui/GoapBlackboardEditor.hpp
|
||||
ui/GoapBlackboardComponentEditor.hpp
|
||||
ui/BehaviorTreeEditor.hpp
|
||||
ui/InlineBehaviorTreeEditor.hpp
|
||||
ui/NavMeshEditor.hpp
|
||||
ui/NavMeshGeometrySourceEditor.hpp
|
||||
ui/ActionDatabaseEditor.hpp
|
||||
ui/ActionDatabaseSingletonEditor.hpp
|
||||
ui/ActionDebugEditor.hpp
|
||||
components/GoapBlackboard.hpp
|
||||
components/GoapExpression.hpp
|
||||
components/NavMesh.hpp
|
||||
components/BehaviorTree.hpp
|
||||
components/GoapAction.hpp
|
||||
components/GoapGoal.hpp
|
||||
components/ActionDatabase.hpp
|
||||
components/ActionDebug.hpp
|
||||
camera/EditorCamera.hpp
|
||||
gizmo/Gizmo.hpp
|
||||
gizmo/Cursor3D.hpp
|
||||
physics/physics.h
|
||||
lua/LuaState.hpp
|
||||
lua/LuaEntityApi.hpp
|
||||
lua/LuaComponentApi.hpp
|
||||
lua/LuaEventApi.hpp
|
||||
lua/LuaActionApi.hpp
|
||||
lua/LuaBehaviorTreeApi.hpp
|
||||
lua/LuaGameModeApi.hpp
|
||||
lua/LuaCharacterClassApi.hpp
|
||||
lua/LuaCharacterApi.hpp
|
||||
lua/LuaSaveLoadApi.hpp
|
||||
)
|
||||
|
||||
add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS})
|
||||
add_dependencies(editSceneEditor morph)
|
||||
|
||||
# Define JPH_DEBUG_RENDERER for physics debug drawing
|
||||
target_compile_definitions(editSceneEditor PRIVATE JPH_DEBUG_RENDERER)
|
||||
|
||||
target_link_libraries(editSceneEditor
|
||||
OgreMain
|
||||
OgreBites
|
||||
OgreOverlay
|
||||
OgreMeshLodGenerator
|
||||
flecs::flecs_static
|
||||
nlohmann_json::nlohmann_json
|
||||
Jolt::Jolt
|
||||
OgreProcedural::OgreProcedural
|
||||
RecastNavigation::Recast
|
||||
RecastNavigation::Detour
|
||||
RecastNavigation::DetourTileCache
|
||||
RecastNavigation::DetourCrowd
|
||||
RecastNavigation::DebugUtils
|
||||
PackageArchive
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(editSceneEditor PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/Recast/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/Detour/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DetourTileCache/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DetourCrowd/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DebugUtils/Include
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lpeg-1.1.0
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: Lua API standalone tests
|
||||
# ---------------------------------------------------------------------------
|
||||
# These standalone tests verify the Lua API functions work correctly.
|
||||
# They do not require OGRE or Flecs - only Lua and the core component types.
|
||||
|
||||
# Test: Entity Lua API
|
||||
add_executable(entity_lua_test
|
||||
tests/entity_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(entity_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(entity_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Component Lua API
|
||||
add_executable(component_lua_test
|
||||
tests/component_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(component_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(component_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Event Lua API
|
||||
add_executable(event_lua_test
|
||||
tests/event_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(event_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(event_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: ActionDatabase Lua API
|
||||
add_executable(action_db_lua_test
|
||||
tests/action_db_lua_test.cpp
|
||||
components/ActionDatabase.cpp
|
||||
components/GoapBlackboard.cpp
|
||||
components/GoapGoal.cpp
|
||||
components/GoapExpression.cpp
|
||||
lua/LuaActionApi.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(action_db_lua_test
|
||||
lua
|
||||
flecs::flecs_static
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
target_include_directories(action_db_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Behavior Tree Lua API
|
||||
add_executable(behavior_tree_lua_test
|
||||
tests/behavior_tree_lua_test.cpp
|
||||
lua/LuaBehaviorTreeApi.cpp
|
||||
lua/LuaGameModeApi.cpp
|
||||
lua/LuaEntityApi.cpp
|
||||
GameMode.cpp
|
||||
components/GoapBlackboard.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(behavior_tree_lua_test
|
||||
lua
|
||||
flecs::flecs_static
|
||||
OgreMain
|
||||
)
|
||||
|
||||
target_include_directories(behavior_tree_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
target_compile_definitions(behavior_tree_lua_test PRIVATE flecs_STATIC)
|
||||
|
||||
# Test: EventParams C++ API (standalone, no Lua dependency)
|
||||
add_executable(event_params_test
|
||||
tests/event_params_test.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(event_params_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(event_params_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Game Mode Lua API
|
||||
add_executable(game_mode_lua_test
|
||||
tests/game_mode_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(game_mode_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(game_mode_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Item / Inventory / Container Lua API
|
||||
add_executable(item_lua_test
|
||||
tests/item_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(item_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(item_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Character Lua API
|
||||
add_executable(character_lua_test
|
||||
tests/character_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(character_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(character_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Dialogue Lua API
|
||||
add_executable(dialogue_lua_test
|
||||
tests/dialogue_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(dialogue_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(dialogue_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Character Class Lua API
|
||||
add_executable(character_class_lua_test
|
||||
tests/character_class_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(character_class_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(character_class_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Save/Load Lua API
|
||||
add_executable(save_load_lua_test
|
||||
tests/save_load_lua_test.cpp
|
||||
tests/lua_test_stubs.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(save_load_lua_test
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(save_load_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Package Archive Library
|
||||
# ---------------------------------------------------------------------------
|
||||
# This implements an Ogre::Archive for .package files (uncompressed indexed
|
||||
# file containers, similar to Unity packages). It can be used from resources.cfg
|
||||
# with "Package=path/to/file.package".
|
||||
|
||||
add_library(PackageArchive STATIC
|
||||
package/OgrePackageArchive.cpp
|
||||
package/OgrePackageArchive.h
|
||||
)
|
||||
|
||||
target_include_directories(PackageArchive PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(PackageArchive PUBLIC
|
||||
OgreMain
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PackageTool - command-line tool for creating/managing .package archives
|
||||
# ---------------------------------------------------------------------------
|
||||
add_executable(PackageTool
|
||||
package/PackageTool.cpp
|
||||
package/OgrePackageArchive.cpp
|
||||
package/OgrePackageArchive.h
|
||||
)
|
||||
|
||||
target_link_libraries(PackageTool
|
||||
PackageArchive
|
||||
OgreMain
|
||||
)
|
||||
|
||||
target_include_directories(PackageTool PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# lua-scripts.package - package all Lua scripts into a single archive
|
||||
# ---------------------------------------------------------------------------
|
||||
# Creates lua-scripts.package from the staged lua-scripts build directory.
|
||||
# Files are stored with paths relative to lua-scripts/ (the "lua-scripts"
|
||||
# directory prefix is stripped from archive paths).
|
||||
#
|
||||
# The staged directory is assembled from two sources:
|
||||
# 1. Main lua-scripts (from ${CMAKE_BINARY_DIR}/lua-scripts, staged by
|
||||
# the stage_lua_scripts target in the main build)
|
||||
# 2. EditScene-specific lua-scripts (from
|
||||
# ${CMAKE_CURRENT_SOURCE_DIR}/lua-scripts, overlaid on top)
|
||||
# The overlay ensures editScene files take precedence over main files
|
||||
# with the same name.
|
||||
set(LUA_SCRIPTS_PACKAGE "${CMAKE_CURRENT_BINARY_DIR}/lua-scripts.package")
|
||||
set(LUA_SCRIPTS_STAGED_DIR "${CMAKE_CURRENT_BINARY_DIR}/lua-scripts")
|
||||
set(LUA_SCRIPTS_EDITSCENE_SRC "${CMAKE_CURRENT_SOURCE_DIR}/lua-scripts")
|
||||
|
||||
# Collect all source files from the main lua-scripts source directory so
|
||||
# that the package is rebuilt when any of them change.
|
||||
# CONFIGURE_DEPENDS makes CMake re-evaluate the glob at build time,
|
||||
# so newly added files are picked up without re-running cmake.
|
||||
file(GLOB_RECURSE LUA_SCRIPTS_MAIN_FILES
|
||||
CONFIGURE_DEPENDS
|
||||
"${CMAKE_SOURCE_DIR}/lua-scripts/*.lua"
|
||||
"${CMAKE_SOURCE_DIR}/lua-scripts/*.ink"
|
||||
)
|
||||
|
||||
# Collect all editScene-specific lua-scripts source files.
|
||||
file(GLOB_RECURSE LUA_SCRIPTS_EDITSCENE_FILES
|
||||
CONFIGURE_DEPENDS
|
||||
"${LUA_SCRIPTS_EDITSCENE_SRC}/*.lua"
|
||||
"${LUA_SCRIPTS_EDITSCENE_SRC}/*.ink"
|
||||
)
|
||||
|
||||
# Custom command to overlay editScene-specific lua-scripts on top of the
|
||||
# staged main lua-scripts directory. This copies files from the editScene
|
||||
# lua-scripts source directory into the staged directory, overlaying on
|
||||
# top of any files already there from the main build.
|
||||
set(LUA_SCRIPTS_OVERLAY_DONE "${CMAKE_CURRENT_BINARY_DIR}/.lua_scripts_overlay_done")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${LUA_SCRIPTS_OVERLAY_DONE}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${LUA_SCRIPTS_EDITSCENE_SRC}"
|
||||
"${LUA_SCRIPTS_STAGED_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E touch "${LUA_SCRIPTS_OVERLAY_DONE}"
|
||||
DEPENDS stage_lua_scripts
|
||||
${LUA_SCRIPTS_EDITSCENE_FILES}
|
||||
COMMENT "Overlaying editScene lua-scripts on ${LUA_SCRIPTS_STAGED_DIR}"
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${LUA_SCRIPTS_PACKAGE}"
|
||||
COMMAND $<TARGET_FILE:PackageTool>
|
||||
create "${LUA_SCRIPTS_PACKAGE}"
|
||||
--exclude "CMakeFiles"
|
||||
--exclude "cmake_install.cmake"
|
||||
--exclude "Makefile"
|
||||
--exclude ".lua_scripts_overlay_done"
|
||||
--exclude "*.lua-"
|
||||
"${LUA_SCRIPTS_STAGED_DIR}"
|
||||
DEPENDS PackageTool
|
||||
"${LUA_SCRIPTS_OVERLAY_DONE}"
|
||||
${LUA_SCRIPTS_MAIN_FILES}
|
||||
COMMENT "Creating lua-scripts.package from ${LUA_SCRIPTS_STAGED_DIR}"
|
||||
)
|
||||
|
||||
add_custom_target(lua_scripts_package ALL
|
||||
DEPENDS "${LUA_SCRIPTS_PACKAGE}"
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Package Archive Test
|
||||
# ---------------------------------------------------------------------------
|
||||
add_executable(PackageArchiveTest
|
||||
tests/package_archive_test.cpp
|
||||
package/OgrePackageArchive.cpp
|
||||
package/OgrePackageArchive.h
|
||||
)
|
||||
|
||||
target_link_libraries(PackageArchiveTest
|
||||
PackageArchive
|
||||
OgreMain
|
||||
)
|
||||
|
||||
target_include_directories(PackageArchiveTest PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/package
|
||||
)
|
||||
|
||||
# Copy local resources (materials, etc.)
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources")
|
||||
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources"
|
||||
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
endif()
|
||||
|
||||
# Copy resources from main build
|
||||
add_custom_command(TARGET editSceneEditor POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_BINARY_DIR}/resources"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/resources"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_BINARY_DIR}/characters"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/characters"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_BINARY_DIR}/lua-scripts"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/lua-scripts"
|
||||
# Overlay editScene-specific lua-scripts on top of main lua-scripts
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${LUA_SCRIPTS_EDITSCENE_SRC}"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/lua-scripts"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/resources.cfg"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/resources.cfg"
|
||||
# Re-copy editScene-specific resources so they aren't overwritten
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/resources"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/resources"
|
||||
COMMENT "Copying resources to editSceneEditor build directory"
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,293 @@
|
||||
#ifndef EDITSCENE_EDITORAPP_HPP
|
||||
#define EDITSCENE_EDITORAPP_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <OgreApplicationContext.h>
|
||||
#include <OgreInput.h>
|
||||
#include <OgreOverlaySystem.h>
|
||||
#include <OgreImGuiOverlay.h>
|
||||
#include <OgreRenderTargetListener.h>
|
||||
#include <flecs.h>
|
||||
#include <memory>
|
||||
#include "lua/LuaState.hpp"
|
||||
|
||||
// Forward declarations
|
||||
class EditorUISystem;
|
||||
class EditorCamera;
|
||||
class EditorPhysicsSystem;
|
||||
class EditorLightSystem;
|
||||
class EditorCameraSystem;
|
||||
class EditorLodSystem;
|
||||
class StaticGeometrySystem;
|
||||
class ProceduralTextureSystem;
|
||||
class ProceduralMaterialSystem;
|
||||
class ProceduralMeshSystem;
|
||||
class CharacterSlotSystem;
|
||||
class AnimationTreeSystem;
|
||||
class BehaviorTreeSystem;
|
||||
class NavMeshSystem;
|
||||
class CharacterSystem;
|
||||
class CellGridSystem;
|
||||
class RoomLayoutSystem;
|
||||
class StartupMenuSystem;
|
||||
class PauseMenuSystem;
|
||||
class DialogueSystem;
|
||||
class PlayerControllerSystem;
|
||||
class BuoyancySystem;
|
||||
class EditorSunSystem;
|
||||
class EditorSkyboxSystem;
|
||||
class EditorWaterPlaneSystem;
|
||||
class NormalDebugSystem;
|
||||
class SmartObjectSystem;
|
||||
class GoapRunnerSystem;
|
||||
class PathFollowingSystem;
|
||||
class GoapPlannerSystem;
|
||||
class ActuatorSystem;
|
||||
class EventHandlerSystem;
|
||||
class ItemSystem;
|
||||
class CharacterClassSystem;
|
||||
class PregnancySystem;
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
* Shared input state for game mode
|
||||
*/
|
||||
struct GameInputState {
|
||||
bool w = false;
|
||||
bool a = false;
|
||||
bool s = false;
|
||||
bool d = false;
|
||||
bool shift = false;
|
||||
bool e = false;
|
||||
bool f = false;
|
||||
bool i = false;
|
||||
bool ePressed = false;
|
||||
bool fPressed = false;
|
||||
bool iPressed = false;
|
||||
float mouseDeltaX = 0.0f;
|
||||
float mouseDeltaY = 0.0f;
|
||||
bool mouseMoved = false;
|
||||
|
||||
void resetPerFrame()
|
||||
{
|
||||
mouseMoved = false;
|
||||
mouseDeltaX = 0.0f;
|
||||
mouseDeltaY = 0.0f;
|
||||
ePressed = false;
|
||||
fPressed = false;
|
||||
iPressed = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* RenderTargetListener for ImGui frame management
|
||||
* Handles NewFrame() in preViewportUpdate and EndFrame() in postViewportUpdate
|
||||
*/
|
||||
class ImGuiRenderListener : public Ogre::RenderTargetListener {
|
||||
public:
|
||||
ImGuiRenderListener(Ogre::ImGuiOverlay *imguiOverlay,
|
||||
EditorUISystem *uiSystem,
|
||||
Ogre::RenderWindow *renderWindow,
|
||||
EditorApp *editorApp);
|
||||
|
||||
void
|
||||
preViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override;
|
||||
void
|
||||
postViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override;
|
||||
|
||||
private:
|
||||
Ogre::ImGuiOverlay *m_imguiOverlay;
|
||||
EditorUISystem *m_uiSystem;
|
||||
Ogre::RenderWindow *m_renderWindow;
|
||||
EditorApp *m_editorApp;
|
||||
|
||||
// Timer for delta time calculation
|
||||
Ogre::Timer m_timer;
|
||||
unsigned long m_lastTime = 0;
|
||||
float m_deltaTime = 0.0f;
|
||||
|
||||
// Frame stats (updated in postViewportUpdate)
|
||||
int m_lastBatchCount = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main application class for the scene editor / game
|
||||
*/
|
||||
class EditorApp : public OgreBites::ApplicationContext,
|
||||
public OgreBites::InputListener {
|
||||
public:
|
||||
enum class GameMode { Editor, Game };
|
||||
enum class GamePlayState { Menu, Playing, Paused };
|
||||
|
||||
EditorApp();
|
||||
virtual ~EditorApp();
|
||||
|
||||
// OgreBites::ApplicationContext overrides
|
||||
void setup() override;
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &evt) override;
|
||||
void locateResources() override;
|
||||
|
||||
// OgreBites::InputListener overrides
|
||||
bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override;
|
||||
bool mousePressed(const OgreBites::MouseButtonEvent &evt) override;
|
||||
bool mouseReleased(const OgreBites::MouseButtonEvent &evt) override;
|
||||
bool keyPressed(const OgreBites::KeyboardEvent &evt) override;
|
||||
bool keyReleased(const OgreBites::KeyboardEvent &evt) override;
|
||||
|
||||
// Scene setup
|
||||
void setupLights();
|
||||
void createGrid();
|
||||
void createAxes();
|
||||
|
||||
// ECS setup
|
||||
void setupECS();
|
||||
void createDefaultEntities();
|
||||
|
||||
// Game mode management
|
||||
void setGameMode(GameMode mode);
|
||||
GameMode getGameMode() const
|
||||
{
|
||||
return m_gameMode;
|
||||
}
|
||||
|
||||
// Debug buoyancy
|
||||
void setDebugBuoyancy(bool enabled);
|
||||
bool getDebugBuoyancy() const
|
||||
{
|
||||
return m_debugBuoyancy;
|
||||
}
|
||||
GamePlayState getGamePlayState() const
|
||||
{
|
||||
return m_gamePlayState;
|
||||
}
|
||||
void setGamePlayState(GamePlayState state);
|
||||
void startNewGame(const Ogre::String &scenePath);
|
||||
void clearScene();
|
||||
|
||||
// Save / Load
|
||||
void saveGame(const std::string &slotPath,
|
||||
const std::string &slotName);
|
||||
void loadGame(const std::string &slotPath);
|
||||
|
||||
// Input access
|
||||
GameInputState &getGameInputState()
|
||||
{
|
||||
return m_gameInput;
|
||||
}
|
||||
|
||||
// Getters
|
||||
flecs::entity getSelectedEntity() const;
|
||||
Ogre::SceneManager *getSceneManager() const
|
||||
{
|
||||
return m_sceneMgr;
|
||||
}
|
||||
flecs::world *getWorld()
|
||||
{
|
||||
return &m_world;
|
||||
}
|
||||
EditorCamera *getEditorCamera() const
|
||||
{
|
||||
return m_camera.get();
|
||||
}
|
||||
AnimationTreeSystem *getAnimationTreeSystem() const
|
||||
{
|
||||
return m_animationTreeSystem.get();
|
||||
}
|
||||
CharacterSlotSystem *getCharacterSlotSystem() const
|
||||
{
|
||||
return m_characterSlotSystem.get();
|
||||
}
|
||||
StartupMenuSystem *getStartupMenuSystem() const
|
||||
{
|
||||
return m_startupMenuSystem.get();
|
||||
}
|
||||
PauseMenuSystem *getPauseMenuSystem() const;
|
||||
DialogueSystem *getDialogueSystem() const;
|
||||
ActuatorSystem *getActuatorSystem() const
|
||||
{
|
||||
return m_actuatorSystem.get();
|
||||
}
|
||||
EventHandlerSystem *getEventHandlerSystem() const
|
||||
{
|
||||
return m_eventHandlerSystem.get();
|
||||
}
|
||||
CharacterClassSystem *getCharacterClassSystem() const
|
||||
{
|
||||
return m_characterClassSystem.get();
|
||||
}
|
||||
PregnancySystem *getPregnancySystem() const
|
||||
{
|
||||
return m_pregnancySystem.get();
|
||||
}
|
||||
Ogre::ImGuiOverlay *getImGuiOverlay() const
|
||||
{
|
||||
return m_imguiOverlay;
|
||||
}
|
||||
|
||||
private:
|
||||
// Ogre objects
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
Ogre::OverlaySystem *m_overlaySystem;
|
||||
Ogre::ImGuiOverlay *m_imguiOverlay;
|
||||
|
||||
// ECS
|
||||
flecs::world m_world;
|
||||
std::vector<flecs::entity> m_defaultEntities;
|
||||
|
||||
// Editor systems
|
||||
std::unique_ptr<EditorUISystem> m_uiSystem;
|
||||
std::unique_ptr<EditorCamera> m_camera;
|
||||
std::unique_ptr<ImGuiRenderListener> m_imguiListener;
|
||||
std::unique_ptr<EditorPhysicsSystem> m_physicsSystem;
|
||||
std::unique_ptr<BuoyancySystem> m_buoyancySystem;
|
||||
std::unique_ptr<EditorSunSystem> m_sunSystem;
|
||||
std::unique_ptr<EditorSkyboxSystem> m_skyboxSystem;
|
||||
std::unique_ptr<EditorWaterPlaneSystem> m_waterPlaneSystem;
|
||||
std::unique_ptr<EditorLightSystem> m_lightSystem;
|
||||
std::unique_ptr<EditorCameraSystem> m_cameraSystem;
|
||||
std::unique_ptr<EditorLodSystem> m_lodSystem;
|
||||
std::unique_ptr<StaticGeometrySystem> m_staticGeometrySystem;
|
||||
std::unique_ptr<ProceduralTextureSystem> m_proceduralTextureSystem;
|
||||
std::unique_ptr<ProceduralMaterialSystem> m_proceduralMaterialSystem;
|
||||
std::unique_ptr<ProceduralMeshSystem> m_proceduralMeshSystem;
|
||||
std::unique_ptr<CharacterSlotSystem> m_characterSlotSystem;
|
||||
std::unique_ptr<AnimationTreeSystem> m_animationTreeSystem;
|
||||
std::unique_ptr<BehaviorTreeSystem> m_behaviorTreeSystem;
|
||||
std::unique_ptr<NavMeshSystem> m_navMeshSystem;
|
||||
std::unique_ptr<CharacterSystem> m_characterSystem;
|
||||
std::unique_ptr<CellGridSystem> m_cellGridSystem;
|
||||
std::unique_ptr<NormalDebugSystem> m_normalDebugSystem;
|
||||
std::unique_ptr<RoomLayoutSystem> m_roomLayoutSystem;
|
||||
std::unique_ptr<SmartObjectSystem> m_smartObjectSystem;
|
||||
std::unique_ptr<GoapRunnerSystem> m_goapRunnerSystem;
|
||||
std::unique_ptr<PathFollowingSystem> m_pathFollowingSystem;
|
||||
std::unique_ptr<GoapPlannerSystem> m_goapPlannerSystem;
|
||||
std::unique_ptr<ActuatorSystem> m_actuatorSystem;
|
||||
std::unique_ptr<EventHandlerSystem> m_eventHandlerSystem;
|
||||
std::unique_ptr<ItemSystem> m_itemSystem;
|
||||
std::unique_ptr<CharacterClassSystem> m_characterClassSystem;
|
||||
std::unique_ptr<PregnancySystem> m_pregnancySystem;
|
||||
|
||||
// Game systems
|
||||
std::unique_ptr<StartupMenuSystem> m_startupMenuSystem;
|
||||
std::unique_ptr<PlayerControllerSystem> m_playerControllerSystem;
|
||||
|
||||
// State
|
||||
uint16_t m_currentModifiers;
|
||||
GameMode m_gameMode = GameMode::Editor;
|
||||
GamePlayState m_gamePlayState = GamePlayState::Menu;
|
||||
GameInputState m_gameInput;
|
||||
bool m_setupComplete = false;
|
||||
bool m_debugBuoyancy = false;
|
||||
float m_playTime = 0.0f;
|
||||
std::string m_currentBaseScene;
|
||||
|
||||
// Lua scripting
|
||||
editScene::LuaState m_lua;
|
||||
|
||||
// Editor visualization nodes
|
||||
Ogre::SceneNode *m_gridNode = nullptr;
|
||||
Ogre::SceneNode *m_axisNode = nullptr;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_EDITORAPP_HPP
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "GameMode.hpp"
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/// Global game mode state.
|
||||
GameMode s_gameMode = GameMode::Editor;
|
||||
|
||||
/// Global gameplay state (only meaningful in game mode).
|
||||
GamePlayState s_gamePlayState = GamePlayState::Menu;
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void setEditSceneGameMode(GameMode mode) noexcept
|
||||
{
|
||||
s_gameMode = mode;
|
||||
}
|
||||
|
||||
void setEditSceneGamePlayState(GamePlayState state) noexcept
|
||||
{
|
||||
s_gamePlayState = state;
|
||||
}
|
||||
|
||||
GameMode getGameMode() noexcept
|
||||
{
|
||||
return s_gameMode;
|
||||
}
|
||||
|
||||
GamePlayState getGamePlayState() noexcept
|
||||
{
|
||||
return s_gamePlayState;
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
@@ -0,0 +1,101 @@
|
||||
#ifndef EDITSCENE_GAMEMODE_HPP
|
||||
#define EDITSCENE_GAMEMODE_HPP
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file GameMode.hpp
|
||||
*
|
||||
* Global game mode query functions for the editScene feature.
|
||||
*
|
||||
* These functions allow any code in the editScene feature to query
|
||||
* whether the application is currently in editor mode or game mode,
|
||||
* and what the current gameplay state is, without needing a direct
|
||||
* pointer to EditorApp.
|
||||
*
|
||||
* The EditorApp sets the current mode via setEditSceneGameMode()
|
||||
* during its lifetime. Code outside the editScene feature should
|
||||
* continue to use EditorApp::getGameMode() / getGamePlayState()
|
||||
* directly.
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* Application mode: editor or game.
|
||||
*/
|
||||
enum class GameMode { Editor, Game };
|
||||
|
||||
/**
|
||||
* Play state when in game mode.
|
||||
*/
|
||||
enum class GamePlayState { Menu, Playing, Paused };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global state management (called by EditorApp)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Set the current game mode. Called by EditorApp on mode changes.
|
||||
*/
|
||||
void setEditSceneGameMode(GameMode mode) noexcept;
|
||||
|
||||
/**
|
||||
* Set the current gameplay state. Called by EditorApp on state changes.
|
||||
*/
|
||||
void setEditSceneGamePlayState(GamePlayState state) noexcept;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return the current application mode.
|
||||
*/
|
||||
GameMode getGameMode() noexcept;
|
||||
|
||||
/**
|
||||
* Return the current gameplay state (only meaningful in game mode).
|
||||
*/
|
||||
GamePlayState getGamePlayState() noexcept;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Predicates
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** True when the application is in editor mode. */
|
||||
inline bool isEditorMode() noexcept
|
||||
{
|
||||
return getGameMode() == GameMode::Editor;
|
||||
}
|
||||
|
||||
/** True when the application is in game mode (any play state). */
|
||||
inline bool isGameMode() noexcept
|
||||
{
|
||||
return getGameMode() == GameMode::Game;
|
||||
}
|
||||
|
||||
/** True when in game mode and the gameplay state is Playing. */
|
||||
inline bool isGamePlaying() noexcept
|
||||
{
|
||||
return getGameMode() == GameMode::Game &&
|
||||
getGamePlayState() == GamePlayState::Playing;
|
||||
}
|
||||
|
||||
/** True when in game mode and the gameplay state is Menu. */
|
||||
inline bool isGameMenu() noexcept
|
||||
{
|
||||
return getGameMode() == GameMode::Game &&
|
||||
getGamePlayState() == GamePlayState::Menu;
|
||||
}
|
||||
|
||||
/** True when in game mode and the gameplay state is Paused. */
|
||||
inline bool isGamePaused() noexcept
|
||||
{
|
||||
return getGameMode() == GameMode::Game &&
|
||||
getGamePlayState() == GamePlayState::Paused;
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_GAMEMODE_HPP
|
||||
@@ -0,0 +1,237 @@
|
||||
#include "EditorCamera.hpp"
|
||||
#include <OgreViewport.h>
|
||||
|
||||
EditorCamera::EditorCamera(Ogre::SceneManager *sceneMgr,
|
||||
Ogre::RenderWindow *window)
|
||||
: m_sceneMgr(sceneMgr)
|
||||
, m_camera(nullptr)
|
||||
, m_cameraNode(nullptr)
|
||||
, m_targetNode(nullptr)
|
||||
, m_position(0, 5, 15)
|
||||
, m_target(0, 0, 0)
|
||||
, m_distance(15.0f)
|
||||
, m_yaw(0.0f)
|
||||
, m_pitch(-20.0f)
|
||||
, m_rotating(false)
|
||||
, m_panning(false)
|
||||
, m_fpsMode(false)
|
||||
, m_lastMouseX(0)
|
||||
, m_lastMouseY(0)
|
||||
, m_keyW(false)
|
||||
, m_keyS(false)
|
||||
, m_keyA(false)
|
||||
, m_keyD(false)
|
||||
, m_keyQ(false)
|
||||
, m_keyE(false)
|
||||
{
|
||||
// Create camera
|
||||
m_camera = sceneMgr->createCamera("EditorCamera");
|
||||
m_camera->setNearClipDistance(0.1f);
|
||||
m_camera->setFarClipDistance(1000.0f);
|
||||
m_camera->setAutoAspectRatio(true);
|
||||
|
||||
// Create camera node
|
||||
m_cameraNode = sceneMgr->getRootSceneNode()->createChildSceneNode(
|
||||
"EditorCameraNode");
|
||||
m_cameraNode->attachObject(m_camera);
|
||||
|
||||
// Create target node
|
||||
m_targetNode = sceneMgr->getRootSceneNode()->createChildSceneNode(
|
||||
"EditorCameraTarget");
|
||||
|
||||
// Setup viewport
|
||||
Ogre::Viewport *vp = window->addViewport(m_camera);
|
||||
vp->setBackgroundColour(Ogre::ColourValue(0.1f, 0.1f, 0.1f));
|
||||
|
||||
// Setup CameraMan for proper camera control (no roll)
|
||||
m_cameraMan = std::make_unique<OgreBites::CameraMan>(m_cameraNode);
|
||||
m_cameraMan->setStyle(OgreBites::CS_ORBIT);
|
||||
m_cameraMan->setTarget(m_targetNode);
|
||||
m_cameraMan->setYawPitchDist(Ogre::Degree(m_yaw), Ogre::Degree(m_pitch),
|
||||
m_distance);
|
||||
|
||||
updateCameraPosition();
|
||||
}
|
||||
|
||||
EditorCamera::~EditorCamera() = default;
|
||||
|
||||
void EditorCamera::update(float deltaTime)
|
||||
{
|
||||
if (m_fpsMode) {
|
||||
updateFPSMovement(deltaTime);
|
||||
}
|
||||
// CameraMan handles the positioning
|
||||
m_cameraMan->setYawPitchDist(Ogre::Degree(m_yaw), Ogre::Degree(m_pitch),
|
||||
m_distance);
|
||||
m_cameraMan->frameRendered(Ogre::FrameEvent{ deltaTime });
|
||||
}
|
||||
|
||||
void EditorCamera::updateFPSMovement(float deltaTime)
|
||||
{
|
||||
// Get camera's forward and right vectors from CameraMan's derived orientation
|
||||
Ogre::Quaternion orientation = m_camera->getDerivedOrientation();
|
||||
Ogre::Vector3 forward = orientation * Ogre::Vector3::UNIT_Z;
|
||||
Ogre::Vector3 right = orientation * Ogre::Vector3::UNIT_X;
|
||||
Ogre::Vector3 up = Ogre::Vector3::UNIT_Y; // World up for Q/E
|
||||
|
||||
// Flatten forward vector to horizontal plane for WSAD movement
|
||||
Ogre::Vector3 forwardHorizontal =
|
||||
Ogre::Vector3(forward.x, 0, forward.z);
|
||||
if (forwardHorizontal.squaredLength() > 0.0001f) {
|
||||
forwardHorizontal.normalise();
|
||||
}
|
||||
|
||||
Ogre::Vector3 movement = Ogre::Vector3::ZERO;
|
||||
|
||||
// WSAD movement (horizontal plane)
|
||||
if (m_keyW)
|
||||
movement -= forwardHorizontal;
|
||||
if (m_keyS)
|
||||
movement += forwardHorizontal;
|
||||
if (m_keyA)
|
||||
movement -= right;
|
||||
if (m_keyD)
|
||||
movement += right;
|
||||
|
||||
// Q/E for vertical movement
|
||||
if (m_keyQ)
|
||||
movement -= up;
|
||||
if (m_keyE)
|
||||
movement += up;
|
||||
|
||||
// Apply movement
|
||||
if (movement.squaredLength() > 0.0001f) {
|
||||
movement.normalise();
|
||||
m_target += movement * FPS_SPEED * deltaTime;
|
||||
m_targetNode->setPosition(m_target);
|
||||
m_cameraMan->setTarget(m_targetNode);
|
||||
}
|
||||
}
|
||||
|
||||
void EditorCamera::handleMouseMove(const OgreBites::MouseMotionEvent &evt)
|
||||
{
|
||||
int dx = evt.x - m_lastMouseX;
|
||||
int dy = evt.y - m_lastMouseY;
|
||||
m_lastMouseX = evt.x;
|
||||
m_lastMouseY = evt.y;
|
||||
|
||||
if (m_rotating) {
|
||||
m_yaw -= dx * ROTATION_SPEED;
|
||||
m_pitch -= dy * ROTATION_SPEED;
|
||||
|
||||
// Clamp pitch
|
||||
if (m_pitch > 89.0f)
|
||||
m_pitch = 89.0f;
|
||||
if (m_pitch < -89.0f)
|
||||
m_pitch = -89.0f;
|
||||
}
|
||||
|
||||
if (m_panning) {
|
||||
// Get right and up vectors from camera's orientation
|
||||
Ogre::Quaternion orientation =
|
||||
m_camera->getDerivedOrientation();
|
||||
Ogre::Vector3 right = orientation * Ogre::Vector3::UNIT_X;
|
||||
Ogre::Vector3 up = orientation * Ogre::Vector3::UNIT_Y;
|
||||
|
||||
Ogre::Vector3 pan = right * (-dx * PAN_SPEED * m_distance) +
|
||||
up * (dy * PAN_SPEED * m_distance);
|
||||
|
||||
m_target += pan;
|
||||
m_targetNode->setPosition(m_target);
|
||||
m_cameraMan->setTarget(m_targetNode);
|
||||
}
|
||||
}
|
||||
|
||||
void EditorCamera::handleMousePress(const OgreBites::MouseButtonEvent &evt)
|
||||
{
|
||||
m_lastMouseX = evt.x;
|
||||
m_lastMouseY = evt.y;
|
||||
|
||||
if (evt.button == OgreBites::BUTTON_RIGHT) {
|
||||
m_rotating = true;
|
||||
m_fpsMode = true; // Enable FPS mode when right mouse is held
|
||||
} else if (evt.button == OgreBites::BUTTON_MIDDLE) {
|
||||
m_panning = true;
|
||||
}
|
||||
}
|
||||
|
||||
void EditorCamera::handleMouseRelease(const OgreBites::MouseButtonEvent &evt)
|
||||
{
|
||||
if (evt.button == OgreBites::BUTTON_RIGHT) {
|
||||
m_rotating = false;
|
||||
m_fpsMode =
|
||||
false; // Disable FPS mode when right mouse is released
|
||||
// Reset key states
|
||||
m_keyW = m_keyS = m_keyA = m_keyD = m_keyQ = m_keyE = false;
|
||||
} else if (evt.button == OgreBites::BUTTON_MIDDLE) {
|
||||
m_panning = false;
|
||||
}
|
||||
}
|
||||
|
||||
void EditorCamera::handleKeyboard(const OgreBites::KeyboardEvent &evt)
|
||||
{
|
||||
// Track key states for FPS movement
|
||||
bool pressed = (evt.type == OgreBites::KEYDOWN);
|
||||
|
||||
switch (evt.keysym.sym) {
|
||||
case 'w':
|
||||
case 'W':
|
||||
m_keyW = pressed;
|
||||
break;
|
||||
case 's':
|
||||
case 'S':
|
||||
m_keyS = pressed;
|
||||
break;
|
||||
case 'a':
|
||||
case 'A':
|
||||
m_keyA = pressed;
|
||||
break;
|
||||
case 'd':
|
||||
case 'D':
|
||||
m_keyD = pressed;
|
||||
break;
|
||||
case 'q':
|
||||
case 'Q':
|
||||
m_keyQ = pressed;
|
||||
break;
|
||||
case 'e':
|
||||
case 'E':
|
||||
m_keyE = pressed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EditorCamera::focusOn(const Ogre::Vector3 &point)
|
||||
{
|
||||
m_target = point;
|
||||
m_targetNode->setPosition(m_target);
|
||||
m_cameraMan->setTarget(m_targetNode);
|
||||
}
|
||||
|
||||
void EditorCamera::setPosition(const Ogre::Vector3 &pos)
|
||||
{
|
||||
m_target = pos;
|
||||
m_targetNode->setPosition(m_target);
|
||||
m_cameraMan->setTarget(m_targetNode);
|
||||
}
|
||||
|
||||
Ogre::Ray EditorCamera::getMouseRay(float screenX, float screenY) const
|
||||
{
|
||||
// Convert pixel coordinates to normalized viewport coordinates (0-1)
|
||||
Ogre::Viewport *viewport = m_camera->getViewport();
|
||||
if (!viewport)
|
||||
return Ogre::Ray();
|
||||
|
||||
float normX = screenX / viewport->getActualWidth();
|
||||
float normY = screenY / viewport->getActualHeight();
|
||||
|
||||
return m_camera->getCameraToViewportRay(normX, normY);
|
||||
}
|
||||
|
||||
void EditorCamera::updateCameraPosition()
|
||||
{
|
||||
// CameraMan handles the positioning
|
||||
m_targetNode->setPosition(m_target);
|
||||
m_cameraMan->setYawPitchDist(Ogre::Degree(m_yaw), Ogre::Degree(m_pitch),
|
||||
m_distance);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#ifndef EDITSCENE_EDITORCAMERA_HPP
|
||||
#define EDITSCENE_EDITORCAMERA_HPP
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <Ogre.h>
|
||||
#include <OgreCamera.h>
|
||||
#include <OgreSceneManager.h>
|
||||
#include <OgreRenderWindow.h>
|
||||
#include <OgreInput.h>
|
||||
#include <OgreCameraMan.h>
|
||||
|
||||
/**
|
||||
* Editor camera controller using OgreBites::CameraMan
|
||||
* Supports orbit (default) and FPS modes
|
||||
*/
|
||||
class EditorCamera {
|
||||
public:
|
||||
EditorCamera(Ogre::SceneManager *sceneMgr, Ogre::RenderWindow *window);
|
||||
~EditorCamera();
|
||||
|
||||
/**
|
||||
* Update camera (called each frame)
|
||||
*/
|
||||
void update(float deltaTime);
|
||||
|
||||
/**
|
||||
* Handle input events
|
||||
*/
|
||||
void handleMouseMove(const OgreBites::MouseMotionEvent &evt);
|
||||
void handleMousePress(const OgreBites::MouseButtonEvent &evt);
|
||||
void handleMouseRelease(const OgreBites::MouseButtonEvent &evt);
|
||||
void handleKeyboard(const OgreBites::KeyboardEvent &evt);
|
||||
|
||||
/**
|
||||
* Get the camera
|
||||
*/
|
||||
Ogre::Camera *getCamera() const
|
||||
{
|
||||
return m_camera;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus camera on a point
|
||||
*/
|
||||
void focusOn(const Ogre::Vector3 &point);
|
||||
|
||||
/**
|
||||
* Set camera position
|
||||
*/
|
||||
void setPosition(const Ogre::Vector3 &pos);
|
||||
|
||||
/**
|
||||
* Get camera position
|
||||
*/
|
||||
Ogre::Vector3 getPosition() const
|
||||
{
|
||||
return m_position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ray from mouse position
|
||||
*/
|
||||
Ogre::Ray getMouseRay(float screenX, float screenY) const;
|
||||
|
||||
/**
|
||||
* Check if in FPS mode
|
||||
*/
|
||||
bool isFPSMode() const
|
||||
{
|
||||
return m_fpsMode;
|
||||
}
|
||||
|
||||
private:
|
||||
void updateCameraPosition();
|
||||
void updateFPSMovement(float deltaTime);
|
||||
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
Ogre::Camera *m_camera;
|
||||
Ogre::SceneNode *m_cameraNode;
|
||||
Ogre::SceneNode *m_targetNode;
|
||||
|
||||
// Use OgreBites::CameraMan for proper camera control
|
||||
std::unique_ptr<OgreBites::CameraMan> m_cameraMan;
|
||||
|
||||
// Camera state
|
||||
Ogre::Vector3 m_position;
|
||||
Ogre::Vector3 m_target;
|
||||
float m_distance;
|
||||
float m_yaw;
|
||||
float m_pitch;
|
||||
|
||||
// Input state
|
||||
bool m_rotating;
|
||||
bool m_panning;
|
||||
bool m_fpsMode;
|
||||
int m_lastMouseX;
|
||||
int m_lastMouseY;
|
||||
|
||||
// Keyboard state for FPS movement
|
||||
bool m_keyW;
|
||||
bool m_keyS;
|
||||
bool m_keyA;
|
||||
bool m_keyD;
|
||||
bool m_keyQ;
|
||||
bool m_keyE;
|
||||
|
||||
// Movement speeds
|
||||
static constexpr float ROTATION_SPEED = 0.3f;
|
||||
static constexpr float PAN_SPEED = 0.01f;
|
||||
static constexpr float FPS_SPEED = 10.0f;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_EDITORCAMERA_HPP
|
||||
@@ -0,0 +1,515 @@
|
||||
#include "ActionDatabase.hpp"
|
||||
#ifndef OGRE_STUB_H
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreResourceGroupManager.h>
|
||||
#endif
|
||||
|
||||
#include <flecs.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Singleton
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ActionDatabase &ActionDatabase::getSingleton()
|
||||
{
|
||||
static ActionDatabase instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
ActionDatabase *ActionDatabase::getSingletonPtr()
|
||||
{
|
||||
return &getSingleton();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Find methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const GoapAction *ActionDatabase::findAction(const Ogre::String &name) const
|
||||
{
|
||||
for (const auto &action : actions) {
|
||||
if (action.name == name)
|
||||
return &action;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GoapAction *ActionDatabase::findAction(const Ogre::String &name)
|
||||
{
|
||||
for (auto &action : actions) {
|
||||
if (action.name == name)
|
||||
return &action;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const GoapGoal *ActionDatabase::findGoal(const Ogre::String &name) const
|
||||
{
|
||||
for (const auto &goal : goals) {
|
||||
if (goal.name == name)
|
||||
return &goal;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GoapGoal *ActionDatabase::findGoal(const Ogre::String &name)
|
||||
{
|
||||
for (auto &goal : goals) {
|
||||
if (goal.name == name)
|
||||
return &goal;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Add or replace
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ActionDatabase::addOrReplaceAction(const GoapAction &action)
|
||||
{
|
||||
for (auto &a : actions) {
|
||||
if (a.name == action.name) {
|
||||
a = action;
|
||||
return;
|
||||
}
|
||||
}
|
||||
actions.push_back(action);
|
||||
}
|
||||
|
||||
void ActionDatabase::addOrReplaceGoal(const GoapGoal &goal)
|
||||
{
|
||||
for (auto &g : goals) {
|
||||
if (g.name == goal.name) {
|
||||
g = goal;
|
||||
return;
|
||||
}
|
||||
}
|
||||
goals.push_back(goal);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Remove methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool ActionDatabase::removeAction(const Ogre::String &name)
|
||||
{
|
||||
for (auto it = actions.begin(); it != actions.end(); ++it) {
|
||||
if (it->name == name) {
|
||||
actions.erase(it);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ActionDatabase::removeGoal(const Ogre::String &name)
|
||||
{
|
||||
for (auto it = goals.begin(); it != goals.end(); ++it) {
|
||||
if (it->name == name) {
|
||||
goals.erase(it);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Selection / validation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const GoapGoal *
|
||||
ActionDatabase::selectBestGoal(const GoapBlackboard &blackboard) const
|
||||
{
|
||||
const GoapGoal *best = nullptr;
|
||||
int bestPriority = -1;
|
||||
|
||||
for (const auto &goal : goals) {
|
||||
if (!goal.isValid(blackboard))
|
||||
continue;
|
||||
if (goal.priority > bestPriority) {
|
||||
bestPriority = goal.priority;
|
||||
best = &goal;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
std::vector<const GoapAction *>
|
||||
ActionDatabase::getValidActions(const GoapBlackboard &blackboard) const
|
||||
{
|
||||
std::vector<const GoapAction *> result;
|
||||
for (const auto &action : actions) {
|
||||
if (action.canRun(blackboard))
|
||||
result.push_back(&action);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Clear
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ActionDatabase::clear()
|
||||
{
|
||||
actions.clear();
|
||||
goals.clear();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ActionDatabaseComponent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ActionDatabaseComponent::syncToSingleton() const
|
||||
{
|
||||
auto &db = ActionDatabase::getSingleton();
|
||||
db.clear();
|
||||
for (const auto &action : actions)
|
||||
db.addOrReplaceAction(action);
|
||||
for (const auto &goal : goals)
|
||||
db.addOrReplaceGoal(goal);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON serialization helpers (local to this translation unit)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static nlohmann::json serializeGoapBlackboard(const GoapBlackboard &bb)
|
||||
{
|
||||
nlohmann::json json;
|
||||
json["bits"] = (uint64_t)bb.bits;
|
||||
json["mask"] = (uint64_t)bb.mask;
|
||||
if (bb.bitmask != ~0ULL)
|
||||
json["bitmask"] = (uint64_t)bb.bitmask;
|
||||
if (!bb.values.empty()) {
|
||||
json["values"] = nlohmann::json::object();
|
||||
for (const auto &pair : bb.values)
|
||||
json["values"][pair.first] = pair.second;
|
||||
}
|
||||
if (!bb.floatValues.empty()) {
|
||||
json["floatValues"] = nlohmann::json::object();
|
||||
for (const auto &pair : bb.floatValues)
|
||||
json["floatValues"][pair.first] = pair.second;
|
||||
}
|
||||
if (!bb.vec3Values.empty()) {
|
||||
json["vec3Values"] = nlohmann::json::object();
|
||||
for (const auto &pair : bb.vec3Values) {
|
||||
nlohmann::json v;
|
||||
v.push_back(pair.second.x);
|
||||
v.push_back(pair.second.y);
|
||||
v.push_back(pair.second.z);
|
||||
json["vec3Values"][pair.first] = v;
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
static void deserializeGoapBlackboard(GoapBlackboard &bb,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
bb.bits = json.value("bits", (uint64_t)0);
|
||||
bb.mask = json.value("mask", (uint64_t)0);
|
||||
bb.bitmask = json.value("bitmask", ~0ULL);
|
||||
bb.values.clear();
|
||||
if (json.contains("values") && json["values"].is_object()) {
|
||||
for (auto &[key, val] : json["values"].items())
|
||||
bb.values[key] = val.get<int>();
|
||||
}
|
||||
bb.floatValues.clear();
|
||||
if (json.contains("floatValues") && json["floatValues"].is_object()) {
|
||||
for (auto &[key, val] : json["floatValues"].items())
|
||||
bb.floatValues[key] = val.get<float>();
|
||||
}
|
||||
bb.vec3Values.clear();
|
||||
if (json.contains("vec3Values") && json["vec3Values"].is_object()) {
|
||||
for (auto &[key, val] : json["vec3Values"].items()) {
|
||||
if (val.is_array() && val.size() >= 3)
|
||||
bb.vec3Values[key] =
|
||||
Ogre::Vector3(val[0].get<float>(),
|
||||
val[1].get<float>(),
|
||||
val[2].get<float>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static nlohmann::json serializeBehaviorTreeNode(const BehaviorTreeNode &node)
|
||||
{
|
||||
nlohmann::json json;
|
||||
json["type"] = node.type;
|
||||
if (!node.name.empty())
|
||||
json["name"] = node.name;
|
||||
if (!node.params.empty())
|
||||
json["params"] = node.params;
|
||||
if (!node.children.empty()) {
|
||||
json["children"] = nlohmann::json::array();
|
||||
for (const auto &child : node.children)
|
||||
json["children"].push_back(
|
||||
serializeBehaviorTreeNode(child));
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
static void deserializeBehaviorTreeNode(BehaviorTreeNode &node,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
node.type = json.value("type", "task");
|
||||
node.name = json.value("name", "");
|
||||
node.params = json.value("params", "");
|
||||
node.children.clear();
|
||||
if (json.contains("children") && json["children"].is_array()) {
|
||||
for (const auto &childJson : json["children"]) {
|
||||
BehaviorTreeNode child;
|
||||
deserializeBehaviorTreeNode(child, childJson);
|
||||
node.children.push_back(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static nlohmann::json serializeGoapAction(const GoapAction &action)
|
||||
{
|
||||
nlohmann::json json;
|
||||
json["name"] = action.name;
|
||||
json["cost"] = action.cost;
|
||||
json["preconditions"] = serializeGoapBlackboard(action.preconditions);
|
||||
json["effects"] = serializeGoapBlackboard(action.effects);
|
||||
if (action.preconditionMask != ~0ULL)
|
||||
json["preconditionMask"] = action.preconditionMask;
|
||||
json["behaviorTree"] = serializeBehaviorTreeNode(action.behaviorTree);
|
||||
if (!action.behaviorTreeName.empty())
|
||||
json["behaviorTreeName"] = action.behaviorTreeName;
|
||||
return json;
|
||||
}
|
||||
|
||||
static void deserializeGoapAction(GoapAction &action,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
action.name = json.value("name", "Unnamed");
|
||||
action.cost = json.value("cost", 1);
|
||||
if (json.contains("preconditions"))
|
||||
deserializeGoapBlackboard(action.preconditions,
|
||||
json["preconditions"]);
|
||||
if (json.contains("effects"))
|
||||
deserializeGoapBlackboard(action.effects, json["effects"]);
|
||||
action.preconditionMask = json.value("preconditionMask", ~0ULL);
|
||||
if (json.contains("behaviorTree"))
|
||||
deserializeBehaviorTreeNode(action.behaviorTree,
|
||||
json["behaviorTree"]);
|
||||
action.behaviorTreeName = json.value("behaviorTreeName", "");
|
||||
}
|
||||
|
||||
static nlohmann::json serializeGoapGoal(const GoapGoal &goal)
|
||||
{
|
||||
nlohmann::json json;
|
||||
json["name"] = goal.name;
|
||||
json["priority"] = goal.priority;
|
||||
json["target"] = serializeGoapBlackboard(goal.target);
|
||||
if (!goal.condition.empty())
|
||||
json["condition"] = goal.condition;
|
||||
return json;
|
||||
}
|
||||
|
||||
static void deserializeGoapGoal(GoapGoal &goal, const nlohmann::json &json)
|
||||
{
|
||||
goal.name = json.value("name", "Unnamed");
|
||||
goal.priority = json.value("priority", 1);
|
||||
if (json.contains("target"))
|
||||
deserializeGoapBlackboard(goal.target, json["target"]);
|
||||
goal.condition = json.value("condition", "");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// saveToJson / loadFromJson
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool ActionDatabase::saveToJson(const std::string &filename)
|
||||
{
|
||||
try {
|
||||
// Resolve the filesystem path from the "General" resource group
|
||||
Ogre::ResourceGroupManager &rgm =
|
||||
Ogre::ResourceGroupManager::getSingleton();
|
||||
const Ogre::ResourceGroupManager::LocationList &locations =
|
||||
rgm.getResourceLocationList("General");
|
||||
if (locations.empty()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::saveToJson: "
|
||||
"no resource locations for group 'General'");
|
||||
return false;
|
||||
}
|
||||
// Use the first location's path
|
||||
std::string dir = locations.begin()->archive->getName();
|
||||
|
||||
std::string filepath = dir + "/" + filename;
|
||||
|
||||
// Backup existing file
|
||||
if (std::filesystem::exists(filepath)) {
|
||||
std::string backup = filepath + ".bak";
|
||||
try {
|
||||
std::filesystem::copy_file(
|
||||
filepath, backup,
|
||||
std::filesystem::copy_options::
|
||||
overwrite_existing);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::saveToJson: "
|
||||
"backup failed: " +
|
||||
Ogre::String(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
const ActionDatabase &db = getSingleton();
|
||||
nlohmann::json root;
|
||||
|
||||
root["actions"] = nlohmann::json::array();
|
||||
for (const auto &action : db.actions)
|
||||
root["actions"].push_back(serializeGoapAction(action));
|
||||
|
||||
root["goals"] = nlohmann::json::array();
|
||||
for (const auto &goal : db.goals)
|
||||
root["goals"].push_back(serializeGoapGoal(goal));
|
||||
|
||||
// Save bit names
|
||||
nlohmann::json bitNames = nlohmann::json::array();
|
||||
for (int i = 0; i < 64; i++) {
|
||||
const char *name = GoapBlackboard::getBitName(i);
|
||||
if (name) {
|
||||
nlohmann::json entry;
|
||||
entry["index"] = i;
|
||||
entry["name"] = name;
|
||||
bitNames.push_back(entry);
|
||||
}
|
||||
}
|
||||
if (!bitNames.empty())
|
||||
root["bitNames"] = bitNames;
|
||||
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::saveToJson: "
|
||||
"failed to open " +
|
||||
filepath);
|
||||
return false;
|
||||
}
|
||||
file << root.dump(4);
|
||||
file.close();
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase saved to " + filepath);
|
||||
return true;
|
||||
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::saveToJson error: " +
|
||||
Ogre::String(e.what()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ActionDatabase::loadFromJson(const std::string &filename)
|
||||
{
|
||||
try {
|
||||
// Resolve the filesystem path from the "General" resource group
|
||||
Ogre::ResourceGroupManager &rgm =
|
||||
Ogre::ResourceGroupManager::getSingleton();
|
||||
const Ogre::ResourceGroupManager::LocationList &locations =
|
||||
rgm.getResourceLocationList("General");
|
||||
if (locations.empty()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::loadFromJson: "
|
||||
"no resource locations for group 'General'");
|
||||
return false;
|
||||
}
|
||||
std::string dir = locations.begin()->archive->getName();
|
||||
|
||||
std::string filepath = dir + "/" + filename;
|
||||
|
||||
if (!std::filesystem::exists(filepath)) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::loadFromJson: "
|
||||
"file not found: " +
|
||||
filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::loadFromJson: "
|
||||
"failed to open " +
|
||||
filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json root;
|
||||
try {
|
||||
file >> root;
|
||||
} catch (const nlohmann::json::parse_error &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::loadFromJson: "
|
||||
"JSON parse error in " +
|
||||
filepath + ": " + Ogre::String(e.what()));
|
||||
return false;
|
||||
}
|
||||
file.close();
|
||||
|
||||
ActionDatabase &db = getSingleton();
|
||||
|
||||
// Load actions (add/replace)
|
||||
if (root.contains("actions") && root["actions"].is_array()) {
|
||||
for (const auto &actionJson : root["actions"]) {
|
||||
GoapAction action;
|
||||
deserializeGoapAction(action, actionJson);
|
||||
db.addOrReplaceAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
// Load goals (add/replace)
|
||||
if (root.contains("goals") && root["goals"].is_array()) {
|
||||
for (const auto &goalJson : root["goals"]) {
|
||||
GoapGoal goal;
|
||||
deserializeGoapGoal(goal, goalJson);
|
||||
db.addOrReplaceGoal(goal);
|
||||
}
|
||||
}
|
||||
|
||||
// Load bit names
|
||||
if (root.contains("bitNames") && root["bitNames"].is_array()) {
|
||||
for (const auto &entry : root["bitNames"]) {
|
||||
if (entry.contains("index") &&
|
||||
entry.contains("name"))
|
||||
GoapBlackboard::setBitName(
|
||||
entry["index"].get<int>(),
|
||||
entry["name"]
|
||||
.get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase loaded from " + filepath);
|
||||
return true;
|
||||
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ActionDatabase::loadFromJson error: " +
|
||||
Ogre::String(e.what()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// reloadFromSceneComponents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ActionDatabase::reloadFromSceneComponents(flecs::world &world)
|
||||
{
|
||||
// First, load from file (if available) — this is done by the caller
|
||||
// before calling this function. Here we just re-sync from scene
|
||||
// entities so that scene-defined actions are applied on top.
|
||||
|
||||
// Iterate all entities with ActionDatabaseComponent
|
||||
world.each([](flecs::entity e, ActionDatabaseComponent &dbComp) {
|
||||
(void)e;
|
||||
dbComp.syncToSingleton();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
#ifndef EDITSCENE_ACTION_DATABASE_HPP
|
||||
#define EDITSCENE_ACTION_DATABASE_HPP
|
||||
#pragma once
|
||||
|
||||
#include "GoapAction.hpp"
|
||||
#include "GoapGoal.hpp"
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
// Forward declaration for reloadFromSceneComponents
|
||||
namespace flecs
|
||||
{
|
||||
class world;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global action database singleton.
|
||||
*
|
||||
* Holds the master list of GOAP actions and goals that characters can use.
|
||||
* This is a singleton accessible from anywhere in the codebase.
|
||||
* The ActionDatabaseComponent on a scene entity stores the actions/goals
|
||||
* and syncs them to the singleton on scene load.
|
||||
*/
|
||||
class ActionDatabase {
|
||||
public:
|
||||
/** Get the singleton instance */
|
||||
static ActionDatabase &getSingleton();
|
||||
static ActionDatabase *getSingletonPtr();
|
||||
|
||||
std::vector<GoapAction> actions;
|
||||
std::vector<GoapGoal> goals;
|
||||
|
||||
// Find an action by name
|
||||
const GoapAction *findAction(const Ogre::String &name) const;
|
||||
GoapAction *findAction(const Ogre::String &name);
|
||||
|
||||
// Find a goal by name
|
||||
const GoapGoal *findGoal(const Ogre::String &name) const;
|
||||
GoapGoal *findGoal(const Ogre::String &name);
|
||||
|
||||
// Add or replace an action by name
|
||||
void addOrReplaceAction(const GoapAction &action);
|
||||
|
||||
// Add or replace a goal by name
|
||||
void addOrReplaceGoal(const GoapGoal &goal);
|
||||
|
||||
// Remove an action by name
|
||||
bool removeAction(const Ogre::String &name);
|
||||
|
||||
// Remove a goal by name
|
||||
bool removeGoal(const Ogre::String &name);
|
||||
|
||||
// Select the best valid goal for a given blackboard
|
||||
// Returns nullptr if no valid goal exists
|
||||
const GoapGoal *selectBestGoal(const GoapBlackboard &blackboard) const;
|
||||
|
||||
// Build a list of actions that can run from a given blackboard state
|
||||
std::vector<const GoapAction *>
|
||||
getValidActions(const GoapBlackboard &blackboard) const;
|
||||
|
||||
// Clear all actions and goals
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* Save the action database to a JSON file.
|
||||
* Creates a backup of the existing file (if any) by appending ".bak".
|
||||
* The file is written to the filesystem path resolved from the
|
||||
* "General" resource group.
|
||||
*
|
||||
* @param filename The filename (e.g. "actions.json").
|
||||
* @return true on success.
|
||||
*/
|
||||
static bool saveToJson(const std::string &filename);
|
||||
|
||||
/**
|
||||
* Load the action database from a JSON file.
|
||||
* The file is located via the "General" resource group.
|
||||
* On failure (file not found, parse error) the error is logged
|
||||
* and the database is left unchanged.
|
||||
*
|
||||
* @param filename The filename (e.g. "actions.json").
|
||||
* @return true on success.
|
||||
*/
|
||||
static bool loadFromJson(const std::string &filename);
|
||||
|
||||
/**
|
||||
* Re-process all ActionDatabaseComponent entities in the given
|
||||
* Flecs world: clear the singleton and re-sync from every entity
|
||||
* that carries the component. This is used after a reload so
|
||||
* scene-defined actions are re-applied on top of the file.
|
||||
*/
|
||||
static void reloadFromSceneComponents(flecs::world &world);
|
||||
|
||||
private:
|
||||
ActionDatabase() = default;
|
||||
~ActionDatabase() = default;
|
||||
ActionDatabase(const ActionDatabase &) = delete;
|
||||
ActionDatabase &operator=(const ActionDatabase &) = delete;
|
||||
};
|
||||
|
||||
/**
|
||||
* Flecs component that stores action database data on a scene entity.
|
||||
* When set on an entity, it syncs its contents to the ActionDatabase singleton.
|
||||
*/
|
||||
struct ActionDatabaseComponent {
|
||||
std::vector<GoapAction> actions;
|
||||
std::vector<GoapGoal> goals;
|
||||
|
||||
/** Sync this component's data to the ActionDatabase singleton */
|
||||
void syncToSingleton() const;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTION_DATABASE_HPP
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "ActionDatabase.hpp"
|
||||
#include "ActionDebug.hpp"
|
||||
#include "BehaviorTree.hpp"
|
||||
#include "GoapAction.hpp"
|
||||
#include "GoapGoal.hpp"
|
||||
#include "GoapBlackboard.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ActionDatabaseEditor.hpp"
|
||||
#include "../ui/BehaviorTreeEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Action Database", "AI", ActionDatabaseComponent,
|
||||
ActionDatabaseEditor)
|
||||
{
|
||||
registry.registerComponent<ActionDatabaseComponent>(
|
||||
"Action Database", "AI",
|
||||
std::make_unique<ActionDatabaseEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ActionDatabaseComponent>())
|
||||
e.set<ActionDatabaseComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ActionDatabaseComponent>())
|
||||
e.remove<ActionDatabaseComponent>();
|
||||
});
|
||||
}
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Behavior Tree", "AI", BehaviorTreeComponent,
|
||||
BehaviorTreeEditor)
|
||||
{
|
||||
registry.registerComponent<BehaviorTreeComponent>(
|
||||
"Behavior Tree", "AI", std::make_unique<BehaviorTreeEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<BehaviorTreeComponent>()) {
|
||||
BehaviorTreeComponent bt;
|
||||
bt.root.type = "sequence";
|
||||
e.set<BehaviorTreeComponent>(bt);
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<BehaviorTreeComponent>())
|
||||
e.remove<BehaviorTreeComponent>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef EDITSCENE_ACTION_DEBUG_HPP
|
||||
#define EDITSCENE_ACTION_DEBUG_HPP
|
||||
#pragma once
|
||||
|
||||
#include "GoapBlackboard.hpp"
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* Per-character action debug component.
|
||||
*
|
||||
* Allows test-running individual actions and inspecting the character's
|
||||
* local blackboard state. Used for debugging AI behavior in the editor.
|
||||
*
|
||||
* Path following animation states have been moved to PathFollowingComponent.
|
||||
*/
|
||||
struct ActionDebug {
|
||||
// Character's local GOAP blackboard
|
||||
GoapBlackboard blackboard;
|
||||
|
||||
// Currently selected action for test-running
|
||||
Ogre::String selectedActionName;
|
||||
|
||||
// Currently selected goal for testing
|
||||
Ogre::String selectedGoalName;
|
||||
|
||||
// Test-run state
|
||||
bool isRunning = false;
|
||||
float runTimer = 0.0f;
|
||||
Ogre::String currentActionName;
|
||||
|
||||
// Debug output
|
||||
Ogre::String lastResult;
|
||||
|
||||
ActionDebug() = default;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTION_DEBUG_HPP
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "ActionDebug.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ActionDebugEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Action Debug", "AI", ActionDebug,
|
||||
ActionDebugEditor)
|
||||
{
|
||||
registry.registerComponent<ActionDebug>(
|
||||
"Action Debug", "AI",
|
||||
std::make_unique<ActionDebugEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ActionDebug>())
|
||||
e.set<ActionDebug>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ActionDebug>())
|
||||
e.remove<ActionDebug>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef EDITSCENE_ACTUATOR_HPP
|
||||
#define EDITSCENE_ACTUATOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Actuator component.
|
||||
*
|
||||
* An interactive object visible only to the player character.
|
||||
* When the player is within radius + height range, an on-screen
|
||||
* prompt appears and the action can be triggered with the action key.
|
||||
*
|
||||
* Unlike SmartObject, Actuators do not use pathfinding or path
|
||||
* following — they are instantaneous interactions.
|
||||
*/
|
||||
struct ActuatorComponent {
|
||||
// Interaction radius in XZ plane
|
||||
float radius = 1.5f;
|
||||
|
||||
// Maximum height difference for interaction
|
||||
float height = 1.8f;
|
||||
|
||||
// Names of GOAP actions (from ActionDatabase) that this actuator provides
|
||||
std::vector<Ogre::String> actionNames;
|
||||
|
||||
// Runtime: cooldown timer (seconds remaining)
|
||||
float cooldownTimer = 0.0f;
|
||||
|
||||
// Runtime: currently executing an action
|
||||
bool isExecuting = false;
|
||||
|
||||
ActuatorComponent() = default;
|
||||
|
||||
explicit ActuatorComponent(float radius_, float height_)
|
||||
: radius(radius_)
|
||||
, height(height_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTUATOR_HPP
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "Actuator.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ActuatorEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Actuator", "Game", ActuatorComponent, ActuatorEditor)
|
||||
{
|
||||
registry.registerComponent<ActuatorComponent>(
|
||||
"Actuator", "Game", std::make_unique<ActuatorEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ActuatorComponent>())
|
||||
e.set<ActuatorComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ActuatorComponent>())
|
||||
e.remove<ActuatorComponent>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "AnimationTree.hpp"
|
||||
|
||||
AnimationTreeComponent::AnimationTreeComponent()
|
||||
: root()
|
||||
, enabled(true)
|
||||
, useRootMotion(false)
|
||||
, dirty(true)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
#ifndef EDITSCENE_ANIMATIONTREE_HPP
|
||||
#define EDITSCENE_ANIMATIONTREE_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* A node in the animation tree.
|
||||
*
|
||||
* Node types:
|
||||
* "output" - Root output node, optional speed multiplier (1 child)
|
||||
* "stateMachine" - Cross-fades between child states, named for lookup
|
||||
* "state" - A named state within a state machine (1 child)
|
||||
* "speed" - Playback speed multiplier (1 child)
|
||||
* "animation" - Leaf referencing an Ogre animation by name
|
||||
*/
|
||||
struct AnimationTreeNode {
|
||||
Ogre::String type = "animation";
|
||||
Ogre::String name;
|
||||
Ogre::String animationName;
|
||||
float speed = 1.0f;
|
||||
float fadeSpeed = 7.5f;
|
||||
std::vector<AnimationTreeNode> children;
|
||||
/* For stateMachine nodes: auto-transition when animation ends */
|
||||
std::unordered_map<Ogre::String, Ogre::String> endTransitions;
|
||||
|
||||
AnimationTreeNode() = default;
|
||||
|
||||
AnimationTreeNode *findChild(const Ogre::String &childName)
|
||||
{
|
||||
for (auto &child : children) {
|
||||
if (child.name == childName)
|
||||
return &child;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AnimationTreeNode *findChild(
|
||||
const Ogre::String &childName) const
|
||||
{
|
||||
for (const auto &child : children) {
|
||||
if (child.name == childName)
|
||||
return &child;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AnimationTreeNode *findStateMachine(const Ogre::String &smName)
|
||||
{
|
||||
if (type == "stateMachine" && name == smName)
|
||||
return this;
|
||||
for (auto &child : children) {
|
||||
auto *found = child.findStateMachine(smName);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AnimationTreeNode *findStateMachine(
|
||||
const Ogre::String &smName) const
|
||||
{
|
||||
if (type == "stateMachine" && name == smName)
|
||||
return this;
|
||||
for (const auto &child : children) {
|
||||
auto *found = child.findStateMachine(smName);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AnimationTreeNode *findState(const Ogre::String &stateName)
|
||||
{
|
||||
if (type == "state" && name == stateName)
|
||||
return this;
|
||||
for (auto &child : children) {
|
||||
auto *found = child.findState(stateName);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AnimationTreeNode *findState(
|
||||
const Ogre::String &stateName) const
|
||||
{
|
||||
if (type == "state" && name == stateName)
|
||||
return this;
|
||||
for (const auto &child : children) {
|
||||
auto *found = child.findState(stateName);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AnimationTreeNode *findAnimationLeaf()
|
||||
{
|
||||
if (type == "animation")
|
||||
return this;
|
||||
for (auto &child : children) {
|
||||
auto *found = child.findAnimationLeaf();
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AnimationTreeNode *findAnimationLeaf() const
|
||||
{
|
||||
if (type == "animation")
|
||||
return this;
|
||||
for (const auto &child : children) {
|
||||
auto *found = child.findAnimationLeaf();
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void collectStateMachines(std::vector<AnimationTreeNode *> &out)
|
||||
{
|
||||
if (type == "stateMachine")
|
||||
out.push_back(this);
|
||||
for (auto &child : children)
|
||||
child.collectStateMachines(out);
|
||||
}
|
||||
|
||||
void collectStateMachines(
|
||||
std::vector<const AnimationTreeNode *> &out) const
|
||||
{
|
||||
if (type == "stateMachine")
|
||||
out.push_back(this);
|
||||
for (const auto &child : children)
|
||||
child.collectStateMachines(out);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Animation tree component for hierarchical state-machine-based animation.
|
||||
*
|
||||
* The tree is evaluated each frame by AnimationTreeSystem to determine
|
||||
* which Ogre AnimationStates are active and their blend weights.
|
||||
*/
|
||||
struct AnimationTreeComponent {
|
||||
AnimationTreeNode root;
|
||||
bool enabled = true;
|
||||
bool useRootMotion = false;
|
||||
bool dirty = true;
|
||||
|
||||
/* If set, the tree root is copied from the named template */
|
||||
Ogre::String templateName;
|
||||
|
||||
/* Runtime: last copied template version (not serialized) */
|
||||
uint64_t templateVersion = 0;
|
||||
|
||||
/* Runtime: current state of each state machine (not serialized) */
|
||||
std::unordered_map<Ogre::String, Ogre::String> currentStates;
|
||||
|
||||
AnimationTreeComponent();
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ANIMATIONTREE_HPP
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "AnimationTree.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/AnimationTreeEditor.hpp"
|
||||
#include "Transform.hpp"
|
||||
|
||||
static AnimationTreeComponent createDefaultTree()
|
||||
{
|
||||
AnimationTreeComponent at;
|
||||
|
||||
at.root.type = "output";
|
||||
at.root.speed = 1.0f;
|
||||
|
||||
AnimationTreeNode sm;
|
||||
sm.type = "stateMachine";
|
||||
sm.name = "main";
|
||||
sm.fadeSpeed = 7.5f;
|
||||
|
||||
AnimationTreeNode state;
|
||||
state.type = "state";
|
||||
state.name = "idle";
|
||||
|
||||
AnimationTreeNode anim;
|
||||
anim.type = "animation";
|
||||
anim.animationName = "idle";
|
||||
|
||||
state.children.push_back(anim);
|
||||
sm.children.push_back(state);
|
||||
at.root.children.push_back(sm);
|
||||
|
||||
return at;
|
||||
}
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering",
|
||||
AnimationTreeComponent, AnimationTreeEditor)
|
||||
{
|
||||
registry.registerComponent<AnimationTreeComponent>(
|
||||
"Animation Tree", "Rendering",
|
||||
std::make_unique<AnimationTreeEditor>(sceneMgr),
|
||||
/* Adder */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<TransformComponent>()) {
|
||||
TransformComponent transform;
|
||||
transform.node =
|
||||
sceneMgr->getRootSceneNode()
|
||||
->createChildSceneNode();
|
||||
e.set<TransformComponent>(transform);
|
||||
}
|
||||
AnimationTreeComponent at = createDefaultTree();
|
||||
at.dirty = true;
|
||||
e.set<AnimationTreeComponent>(at);
|
||||
},
|
||||
/* Remover */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
(void)sceneMgr;
|
||||
e.remove<AnimationTreeComponent>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef EDITSCENE_ANIMATIONTREETEMPLATE_HPP
|
||||
#define EDITSCENE_ANIMATIONTREETEMPLATE_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Template marker for reusable animation trees.
|
||||
*
|
||||
* Entities with this component serve as shared animation tree templates.
|
||||
* They should also have an AnimationTreeComponent for editing the tree.
|
||||
* Other entities reference the template by name via
|
||||
* AnimationTreeComponent::templateName.
|
||||
*/
|
||||
struct AnimationTreeTemplate {
|
||||
Ogre::String name;
|
||||
uint64_t version = 1;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ANIMATIONTREETEMPLATE_HPP
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "AnimationTreeTemplate.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/AnimationTreeTemplateEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Animation Tree Template", "Animation",
|
||||
AnimationTreeTemplate, AnimationTreeTemplateEditor)
|
||||
{
|
||||
registry.registerComponent<AnimationTreeTemplate>(
|
||||
AnimationTreeTemplate_name, AnimationTreeTemplate_group,
|
||||
std::make_unique<AnimationTreeTemplateEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<AnimationTreeTemplate>()) {
|
||||
e.set<AnimationTreeTemplate>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<AnimationTreeTemplate>()) {
|
||||
e.remove<AnimationTreeTemplate>();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
#ifndef EDITSCENE_BEHAVIOR_TREE_HPP
|
||||
#define EDITSCENE_BEHAVIOR_TREE_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Data-driven behavior tree node for AI action execution.
|
||||
*
|
||||
* Node types:
|
||||
* "sequence" - Execute children in order until one fails
|
||||
* "selector" - Execute children in order until one succeeds
|
||||
* "invert" - Invert the result of a single child
|
||||
* "task" - Leaf action (references a named task)
|
||||
* "check" - Leaf condition (references a named check)
|
||||
* "debugPrint" - Leaf: prints 'name' to console once when active
|
||||
* "setAnimationState"- Leaf: sets animation state (name="SM/State")
|
||||
* "isAnimationEnded" - Leaf check: true if anim in state machine ended
|
||||
* "setBit" - Leaf: sets blackboard bit (name=bit, params=0/1)
|
||||
* "checkBit" - Leaf check: true if blackboard bit is set
|
||||
* "setValue" - Leaf: sets blackboard value (name=key, params=val)
|
||||
* "checkValue" - Leaf check: blackboard comparison (name=key, params="op val")
|
||||
* "blackboardDump" - Leaf: dumps entire blackboard to log
|
||||
* "delay" - Leaf: waits for N seconds (params=seconds as float)
|
||||
* "teleportToChild" - Leaf: teleports character to a named child entity
|
||||
* of the Smart Object being interacted with.
|
||||
* name = child entity name to teleport to.
|
||||
* The character is positioned at the child's absolute
|
||||
* world transform (position + orientation).
|
||||
* "disablePhysics" - Leaf: removes character's JPH::BodyID from physics
|
||||
* system so physics no longer interferes with animation.
|
||||
* "enablePhysics" - Leaf: re-adds character's JPH::BodyID to physics
|
||||
* system to restore physics simulation.
|
||||
*
|
||||
* --- Item / Inventory nodes ---
|
||||
* "hasItem" - Leaf check: true if character's inventory has an item
|
||||
* matching the given itemId (name=itemId).
|
||||
* "hasItemByName" - Leaf check: true if character's inventory has an item
|
||||
* matching the given itemName (name=itemName).
|
||||
* "countItem" - Leaf check: true if character's inventory has at least
|
||||
* N of itemId (name=itemId, params=count as int).
|
||||
* "pickupItem" - Leaf: picks up the nearest ItemComponent entity within
|
||||
* range into the character's inventory.
|
||||
* name=itemId filter (optional, empty = any).
|
||||
* "dropItem" - Leaf: drops an item from inventory into the world.
|
||||
* name=itemId, params=count (optional, default 1).
|
||||
* "useItem" - Leaf: uses an item from inventory (executes its
|
||||
* useAction behavior tree). name=itemId.
|
||||
* "addItemToInventory"- Leaf: adds an item directly to character's inventory
|
||||
* (for quest rewards, etc.).
|
||||
* params="itemId,itemName,itemType,count,weight,value"
|
||||
*
|
||||
* --- Lua node ---
|
||||
* "luaTask" - Leaf: calls a registered Lua function.
|
||||
* name = registered node handler name.
|
||||
* params = "key=val,key2=val2" passed to the Lua function.
|
||||
* The Lua function receives (entity_id, params_table)
|
||||
* and must return "success", "failure", or "running".
|
||||
* Register handlers via:
|
||||
* ecs.behavior_tree.register_node("name", function)
|
||||
*/
|
||||
struct BehaviorTreeNode {
|
||||
Ogre::String type = "task";
|
||||
Ogre::String name; // Action/condition name, or message, or SM/State
|
||||
Ogre::String params; // Optional extra parameters
|
||||
std::vector<BehaviorTreeNode> children;
|
||||
|
||||
BehaviorTreeNode() = default;
|
||||
|
||||
BehaviorTreeNode *findChild(const Ogre::String &childName)
|
||||
{
|
||||
for (auto &child : children) {
|
||||
if (child.name == childName)
|
||||
return &child;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const BehaviorTreeNode *findChild(const Ogre::String &childName) const
|
||||
{
|
||||
for (const auto &child : children) {
|
||||
if (child.name == childName)
|
||||
return &child;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool canHaveChildren() const
|
||||
{
|
||||
return type == "sequence" || type == "selector" ||
|
||||
type == "invert";
|
||||
}
|
||||
|
||||
bool isLeaf() const
|
||||
{
|
||||
return type == "task" || type == "check" ||
|
||||
type == "debugPrint" || type == "setAnimationState" ||
|
||||
type == "isAnimationEnded" || type == "setBit" ||
|
||||
type == "checkBit" || type == "setValue" ||
|
||||
type == "checkValue" || type == "blackboardDump" ||
|
||||
type == "delay" || type == "teleportToChild" ||
|
||||
type == "disablePhysics" || type == "enablePhysics" ||
|
||||
type == "sendEvent" || type == "hasItem" ||
|
||||
type == "hasItemByName" || type == "countItem" ||
|
||||
type == "pickupItem" || type == "dropItem" ||
|
||||
type == "useItem" || type == "addItemToInventory" ||
|
||||
type == "luaTask";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Behavior tree asset component.
|
||||
*
|
||||
* Can be attached to an entity to define a reusable behavior tree,
|
||||
* or referenced by name from a GoapAction.
|
||||
*/
|
||||
struct BehaviorTreeComponent {
|
||||
BehaviorTreeNode root;
|
||||
Ogre::String treeName;
|
||||
bool enabled = true;
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty()
|
||||
{
|
||||
dirty = true;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_BEHAVIOR_TREE_HPP
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef EDITSCENE_BUOYANCYINFO_HPP
|
||||
#define EDITSCENE_BUOYANCYINFO_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* BuoyancyInfo component
|
||||
* Provides per-entity buoyancy settings for water physics
|
||||
* If an entity has this component, it will use these settings
|
||||
* Otherwise, default settings from the buoyancy system will be used
|
||||
*/
|
||||
struct BuoyancyInfo {
|
||||
// Enable/disable buoyancy for this entity
|
||||
bool enabled = true;
|
||||
|
||||
// Buoyancy strength (0 = no buoyancy, 1 = neutral buoyancy, >1 = floats)
|
||||
float buoyancy = 1.0f;
|
||||
|
||||
// Linear drag when submerged (0 = no drag, 1 = full drag)
|
||||
float linearDrag = 0.1f;
|
||||
|
||||
// Angular drag when submerged (0 = no drag, 1 = full drag)
|
||||
float angularDrag = 0.05f;
|
||||
|
||||
// Water surface Y level for this entity (world space)
|
||||
// If not set (0), uses global water level from buoyancy system
|
||||
float waterSurfaceY = 0.0f;
|
||||
|
||||
// Submergedness threshold (0-1) - how much of the body must be submerged
|
||||
// before buoyancy is applied (0 = any contact, 1 = fully submerged)
|
||||
float submergedThreshold = 0.3f;
|
||||
|
||||
// Use custom water surface level (if false, uses global water level)
|
||||
bool useCustomWaterLevel = false;
|
||||
|
||||
// Mark component as dirty (needs update)
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty()
|
||||
{
|
||||
dirty = true;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_BUOYANCYINFO_HPP
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "BuoyancyInfo.hpp"
|
||||
#include "Transform.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/BuoyancyInfoEditor.hpp"
|
||||
|
||||
// Register BuoyancyInfo component
|
||||
REGISTER_COMPONENT("Buoyancy Info", BuoyancyInfo, BuoyancyInfoEditor)
|
||||
{
|
||||
registry.registerComponent<BuoyancyInfo>(
|
||||
"Buoyancy Info", std::make_unique<BuoyancyInfoEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<BuoyancyInfo>()) {
|
||||
e.set<BuoyancyInfo>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<BuoyancyInfo>()) {
|
||||
e.remove<BuoyancyInfo>();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef EDITSCENE_CAMERA_HPP
|
||||
#define EDITSCENE_CAMERA_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Camera component - attaches an Ogre::Camera to the entity's SceneNode
|
||||
*/
|
||||
struct CameraComponent {
|
||||
// Camera properties
|
||||
float fovY = 45.0f; // Vertical field of view in degrees
|
||||
float nearClip = 0.1f; // Near clip distance
|
||||
float farClip = 1000.0f; // Far clip distance
|
||||
float aspectRatio = 16.0f / 9.0f; // Aspect ratio (width/height)
|
||||
|
||||
// For orthographic camera
|
||||
bool orthographic = false;
|
||||
float orthoWidth = 10.0f; // Width for orthographic view
|
||||
float orthoHeight = 10.0f; // Height for orthographic view
|
||||
|
||||
// The Ogre camera object (created by CameraSystem)
|
||||
Ogre::Camera* camera = nullptr;
|
||||
|
||||
// For preview (optional RTT - created on demand)
|
||||
Ogre::RenderTarget* previewTarget = nullptr;
|
||||
Ogre::TexturePtr previewTexture;
|
||||
bool showPreview = false;
|
||||
int previewWidth = 400;
|
||||
int previewHeight = 300;
|
||||
|
||||
void markDirty() { needsRebuild = true; }
|
||||
bool needsRebuild = true;
|
||||
bool needsPreviewUpdate = false;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CAMERA_HPP
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "Camera.hpp"
|
||||
#include "Transform.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/CameraEditor.hpp"
|
||||
#include "../systems/CameraSystem.hpp"
|
||||
|
||||
// Register Camera component
|
||||
REGISTER_COMPONENT("Camera", CameraComponent, CameraEditor)
|
||||
{
|
||||
registry.registerComponent<CameraComponent>(
|
||||
"Camera",
|
||||
std::make_unique<CameraEditor>(sceneMgr, renderWindow),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<CameraComponent>()) {
|
||||
// Camera requires Transform
|
||||
if (!e.has<TransformComponent>()) {
|
||||
// Auto-add transform if missing
|
||||
TransformComponent transform;
|
||||
transform.node = sceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
e.set<TransformComponent>(transform);
|
||||
}
|
||||
e.set<CameraComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<CameraComponent>()) {
|
||||
auto& camera = e.get_mut<CameraComponent>();
|
||||
// Clean up Ogre camera and preview resources
|
||||
if (camera.camera) {
|
||||
// Clean up preview
|
||||
if (camera.previewTarget) {
|
||||
camera.previewTarget->removeAllListeners();
|
||||
}
|
||||
if (camera.previewTexture) {
|
||||
Ogre::TextureManager::getSingleton().remove(
|
||||
camera.previewTexture->getName());
|
||||
}
|
||||
|
||||
// Detach and destroy camera
|
||||
Ogre::SceneNode* parent = camera.camera->getParentSceneNode();
|
||||
if (parent) {
|
||||
parent->detachObject(camera.camera);
|
||||
}
|
||||
sceneMgr->destroyCamera(camera.camera);
|
||||
camera.camera = nullptr;
|
||||
}
|
||||
e.remove<CameraComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
#include "CellGrid.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
Cell* CellGridComponent::findCell(int x, int y, int z)
|
||||
{
|
||||
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
|
||||
for (auto& cell : cells) {
|
||||
if (cell.getKey() == key) {
|
||||
return &cell;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Cell* CellGridComponent::findCell(int x, int y, int z) const
|
||||
{
|
||||
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
|
||||
for (const auto& cell : cells) {
|
||||
if (cell.getKey() == key) {
|
||||
return &cell;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Cell& CellGridComponent::getOrCreateCell(int x, int y, int z)
|
||||
{
|
||||
Cell* existing = findCell(x, y, z);
|
||||
if (existing) {
|
||||
return *existing;
|
||||
}
|
||||
Cell newCell;
|
||||
newCell.x = x;
|
||||
newCell.y = y;
|
||||
newCell.z = z;
|
||||
cells.push_back(newCell);
|
||||
return cells.back();
|
||||
}
|
||||
|
||||
void CellGridComponent::removeCell(int x, int y, int z)
|
||||
{
|
||||
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
|
||||
auto it = std::remove_if(cells.begin(), cells.end(),
|
||||
[key](const Cell& c) { return c.getKey() == key; });
|
||||
cells.erase(it, cells.end());
|
||||
}
|
||||
|
||||
FurnitureCell* CellGridComponent::findFurnitureCell(int x, int y, int z)
|
||||
{
|
||||
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
|
||||
for (auto& cell : furnitureCells) {
|
||||
if (cell.getKey() == key) {
|
||||
return &cell;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FurnitureCell& CellGridComponent::getOrCreateFurnitureCell(int x, int y, int z)
|
||||
{
|
||||
FurnitureCell* existing = findFurnitureCell(x, y, z);
|
||||
if (existing) {
|
||||
return *existing;
|
||||
}
|
||||
FurnitureCell newCell;
|
||||
newCell.x = x;
|
||||
newCell.y = y;
|
||||
newCell.z = z;
|
||||
furnitureCells.push_back(newCell);
|
||||
return furnitureCells.back();
|
||||
}
|
||||
|
||||
void CellGridComponent::removeFurnitureCell(int x, int y, int z)
|
||||
{
|
||||
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
|
||||
auto it = std::remove_if(furnitureCells.begin(), furnitureCells.end(),
|
||||
[key](const FurnitureCell& c) { return c.getKey() == key; });
|
||||
furnitureCells.erase(it, furnitureCells.end());
|
||||
}
|
||||
|
||||
void CellGridComponent::clear()
|
||||
{
|
||||
cells.clear();
|
||||
furnitureCells.clear();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
Ogre::Vector3 CellGridComponent::cellToWorld(int x, int y, int z) const
|
||||
{
|
||||
return Ogre::Vector3(
|
||||
x * cellSize,
|
||||
y * cellHeight,
|
||||
z * cellSize
|
||||
);
|
||||
}
|
||||
|
||||
void CellGridComponent::worldToCell(const Ogre::Vector3& worldPos, int& x, int& y, int& z) const
|
||||
{
|
||||
x = Ogre::Math::Floor(worldPos.x / cellSize);
|
||||
y = Ogre::Math::Floor(worldPos.y / cellHeight);
|
||||
z = Ogre::Math::Floor(worldPos.z / cellSize);
|
||||
}
|
||||
|
||||
const char* CellGridComponent::getFlagName(uint64_t flag)
|
||||
{
|
||||
switch (flag) {
|
||||
case CellFlags::Floor: return "Floor";
|
||||
case CellFlags::Ceiling: return "Ceiling";
|
||||
case CellFlags::WallXNeg: return "Ext Wall X-";
|
||||
case CellFlags::WallXPos: return "Ext Wall X+";
|
||||
case CellFlags::WallZPos: return "Ext Wall Z+";
|
||||
case CellFlags::WallZNeg: return "Ext Wall Z-";
|
||||
case CellFlags::DoorXNeg: return "Ext Door X-";
|
||||
case CellFlags::DoorXPos: return "Ext Door X+";
|
||||
case CellFlags::DoorZPos: return "Ext Door Z+";
|
||||
case CellFlags::DoorZNeg: return "Ext Door Z-";
|
||||
case CellFlags::WindowXNeg: return "Ext Window X-";
|
||||
case CellFlags::WindowXPos: return "Ext Window X+";
|
||||
case CellFlags::WindowZPos: return "Ext Window Z+";
|
||||
case CellFlags::WindowZNeg: return "Ext Window Z-";
|
||||
case CellFlags::IntWallXNeg: return "Int Wall X-";
|
||||
case CellFlags::IntWallXPos: return "Int Wall X+";
|
||||
case CellFlags::IntWallZPos: return "Int Wall Z+";
|
||||
case CellFlags::IntWallZNeg: return "Int Wall Z-";
|
||||
case CellFlags::IntDoorXNeg: return "Int Door X-";
|
||||
case CellFlags::IntDoorXPos: return "Int Door X+";
|
||||
case CellFlags::IntDoorZPos: return "Int Door Z+";
|
||||
case CellFlags::IntDoorZNeg: return "Int Door Z-";
|
||||
case CellFlags::IntWindowXNeg: return "Int Window X-";
|
||||
case CellFlags::IntWindowXPos: return "Int Window X+";
|
||||
case CellFlags::IntWindowZPos: return "Int Window Z+";
|
||||
case CellFlags::IntWindowZNeg: return "Int Window Z-";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t CellGridComponent::getFlagByName(const std::string& name)
|
||||
{
|
||||
auto flags = getAllFlags();
|
||||
for (const auto& [flag, flagName] : flags) {
|
||||
if (name == flagName) {
|
||||
return flag;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<std::pair<uint64_t, const char*>> CellGridComponent::getAllFlags()
|
||||
{
|
||||
return {
|
||||
{ CellFlags::Floor, "Floor" },
|
||||
{ CellFlags::Ceiling, "Ceiling" },
|
||||
{ CellFlags::WallXNeg, "Ext Wall X-" },
|
||||
{ CellFlags::WallXPos, "Ext Wall X+" },
|
||||
{ CellFlags::WallZPos, "Ext Wall Z+" },
|
||||
{ CellFlags::WallZNeg, "Ext Wall Z-" },
|
||||
{ CellFlags::DoorXNeg, "Ext Door X-" },
|
||||
{ CellFlags::DoorXPos, "Ext Door X+" },
|
||||
{ CellFlags::DoorZPos, "Ext Door Z+" },
|
||||
{ CellFlags::DoorZNeg, "Ext Door Z-" },
|
||||
{ CellFlags::WindowXNeg, "Ext Window X-" },
|
||||
{ CellFlags::WindowXPos, "Ext Window X+" },
|
||||
{ CellFlags::WindowZPos, "Ext Window Z+" },
|
||||
{ CellFlags::WindowZNeg, "Ext Window Z-" },
|
||||
{ CellFlags::IntWallXNeg, "Int Wall X-" },
|
||||
{ CellFlags::IntWallXPos, "Int Wall X+" },
|
||||
{ CellFlags::IntWallZPos, "Int Wall Z+" },
|
||||
{ CellFlags::IntWallZNeg, "Int Wall Z-" },
|
||||
{ CellFlags::IntDoorXNeg, "Int Door X-" },
|
||||
{ CellFlags::IntDoorXPos, "Int Door X+" },
|
||||
{ CellFlags::IntDoorZPos, "Int Door Z+" },
|
||||
{ CellFlags::IntDoorZNeg, "Int Door Z-" },
|
||||
{ CellFlags::IntWindowXNeg, "Int Window X-" },
|
||||
{ CellFlags::IntWindowXPos, "Int Window X+" },
|
||||
{ CellFlags::IntWindowZPos, "Int Window Z+" },
|
||||
{ CellFlags::IntWindowZNeg, "Int Window Z-" },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <chrono>
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* @brief Cell flags for building geometry
|
||||
*
|
||||
* Each cell in the 3D grid can have these features:
|
||||
* - Floor/Ceiling
|
||||
* - External walls (4 directions)
|
||||
* - External doors/windows (4 directions each)
|
||||
* - Internal walls (4 directions)
|
||||
* - Internal doors/windows (4 directions each)
|
||||
*/
|
||||
namespace CellFlags {
|
||||
constexpr uint64_t Floor = 1ULL << 0;
|
||||
constexpr uint64_t Ceiling = 1ULL << 1;
|
||||
// External walls
|
||||
constexpr uint64_t WallXNeg = 1ULL << 2; // Wall on -X side
|
||||
constexpr uint64_t WallXPos = 1ULL << 3; // Wall on +X side
|
||||
constexpr uint64_t WallZPos = 1ULL << 4; // Wall on +Z side
|
||||
constexpr uint64_t WallZNeg = 1ULL << 5; // Wall on -Z side
|
||||
// External doors
|
||||
constexpr uint64_t DoorXNeg = 1ULL << 6;
|
||||
constexpr uint64_t DoorXPos = 1ULL << 7;
|
||||
constexpr uint64_t DoorZPos = 1ULL << 8;
|
||||
constexpr uint64_t DoorZNeg = 1ULL << 9;
|
||||
// External windows
|
||||
constexpr uint64_t WindowXNeg = 1ULL << 10;
|
||||
constexpr uint64_t WindowXPos = 1ULL << 11;
|
||||
constexpr uint64_t WindowZPos = 1ULL << 12;
|
||||
constexpr uint64_t WindowZNeg = 1ULL << 13;
|
||||
// Internal walls
|
||||
constexpr uint64_t IntWallXNeg = 1ULL << 14;
|
||||
constexpr uint64_t IntWallXPos = 1ULL << 15;
|
||||
constexpr uint64_t IntWallZPos = 1ULL << 16;
|
||||
constexpr uint64_t IntWallZNeg = 1ULL << 17;
|
||||
// Internal doors
|
||||
constexpr uint64_t IntDoorXNeg = 1ULL << 18;
|
||||
constexpr uint64_t IntDoorXPos = 1ULL << 19;
|
||||
constexpr uint64_t IntDoorZPos = 1ULL << 20;
|
||||
constexpr uint64_t IntDoorZNeg = 1ULL << 21;
|
||||
// Internal windows
|
||||
constexpr uint64_t IntWindowXNeg = 1ULL << 22;
|
||||
constexpr uint64_t IntWindowXPos = 1ULL << 23;
|
||||
constexpr uint64_t IntWindowZPos = 1ULL << 24;
|
||||
constexpr uint64_t IntWindowZNeg = 1ULL << 25;
|
||||
|
||||
// Masks
|
||||
constexpr uint64_t AllExternalWalls = WallXNeg | WallXPos | WallZPos | WallZNeg;
|
||||
constexpr uint64_t AllInternalWalls = IntWallXNeg | IntWallXPos | IntWallZPos | IntWallZNeg;
|
||||
constexpr uint64_t AllWalls = AllExternalWalls | AllInternalWalls;
|
||||
constexpr uint64_t AllDoors = DoorXNeg | DoorXPos | DoorZPos | DoorZNeg |
|
||||
IntDoorXNeg | IntDoorXPos | IntDoorZPos | IntDoorZNeg;
|
||||
constexpr uint64_t AllWindows = WindowXNeg | WindowXPos | WindowZPos | WindowZNeg |
|
||||
IntWindowXNeg | IntWindowXPos | IntWindowZPos | IntWindowZNeg;
|
||||
|
||||
// Combined masks for corners (walls + doors + windows in each direction)
|
||||
constexpr uint64_t AllXNeg = WallXNeg | DoorXNeg | WindowXNeg;
|
||||
constexpr uint64_t AllXPos = WallXPos | DoorXPos | WindowXPos;
|
||||
constexpr uint64_t AllZPos = WallZPos | DoorZPos | WindowZPos;
|
||||
constexpr uint64_t AllZNeg = WallZNeg | DoorZNeg | WindowZNeg;
|
||||
constexpr uint64_t AllIntXNeg = IntWallXNeg | IntDoorXNeg | IntWindowXNeg;
|
||||
constexpr uint64_t AllIntXPos = IntWallXPos | IntDoorXPos | IntWindowXPos;
|
||||
constexpr uint64_t AllIntZPos = IntWallZPos | IntDoorZPos | IntWindowZPos;
|
||||
constexpr uint64_t AllIntZNeg = IntWallZNeg | IntDoorZNeg | IntWindowZNeg;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A single cell in the 3D grid
|
||||
*/
|
||||
struct Cell {
|
||||
int x = 0, y = 0, z = 0;
|
||||
uint64_t flags = 0;
|
||||
|
||||
// Generate unique key for this cell
|
||||
int64_t getKey() const {
|
||||
// Support -1024 to 1024 range for each axis
|
||||
return (int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z;
|
||||
}
|
||||
|
||||
bool hasFlag(uint64_t flag) const { return (flags & flag) != 0; }
|
||||
void setFlag(uint64_t flag) { flags |= flag; }
|
||||
void clearFlag(uint64_t flag) { flags &= ~flag; }
|
||||
void toggleFlag(uint64_t flag) { flags ^= flag; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Furniture placement in a cell
|
||||
*/
|
||||
struct FurnitureCell {
|
||||
int x = 0, y = 0, z = 0;
|
||||
std::vector<std::string> tags;
|
||||
std::string furnitureType;
|
||||
int rotation = 0; // 0-3, representing 0, 90, 180, 270 degrees
|
||||
|
||||
int64_t getKey() const {
|
||||
return (int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Cell grid for procedural building generation
|
||||
*
|
||||
* This component stores a sparse 3D grid of cells that define
|
||||
* building geometry (walls, floors, doors, windows).
|
||||
*
|
||||
* Used by: House/Lot generation, dungeon generation
|
||||
*/
|
||||
struct CellGridComponent {
|
||||
// Grid dimensions (in cells)
|
||||
int width = 10; // X dimension
|
||||
int height = 1; // Y dimension (floors)
|
||||
int depth = 10; // Z dimension
|
||||
|
||||
// Cell size in world units
|
||||
float cellSize = 4.0f;
|
||||
float cellHeight = 4.0f;
|
||||
|
||||
// The cell data (sparse storage)
|
||||
std::vector<Cell> cells;
|
||||
std::vector<FurnitureCell> furnitureCells;
|
||||
|
||||
// Generation script (Lua or custom format)
|
||||
std::string generationScript;
|
||||
|
||||
// Texture rectangle names for different parts (from ProceduralTexture)
|
||||
std::string floorRectName;
|
||||
std::string ceilingRectName;
|
||||
std::string extWallRectName;
|
||||
std::string intWallRectName;
|
||||
// Frame texture rectangles
|
||||
std::string extDoorFrameRectName;
|
||||
std::string intDoorFrameRectName;
|
||||
std::string extWindowFrameRectName;
|
||||
std::string intWindowFrameRectName;
|
||||
|
||||
// Roof texture rectangles
|
||||
std::string roofTopRectName;
|
||||
std::string roofSideRectName;
|
||||
|
||||
// Physics properties for generated colliders
|
||||
float friction = 0.5f;
|
||||
|
||||
// Dirty flag - triggers rebuild
|
||||
bool dirty = true;
|
||||
unsigned int version = 0;
|
||||
|
||||
// Find cell at position (returns nullptr if not found)
|
||||
Cell* findCell(int x, int y, int z);
|
||||
const Cell* findCell(int x, int y, int z) const;
|
||||
|
||||
// Get or create cell at position
|
||||
Cell& getOrCreateCell(int x, int y, int z);
|
||||
|
||||
// Remove cell at position
|
||||
void removeCell(int x, int y, int z);
|
||||
|
||||
// Find furniture cell
|
||||
FurnitureCell* findFurnitureCell(int x, int y, int z);
|
||||
|
||||
// Get or create furniture cell
|
||||
FurnitureCell& getOrCreateFurnitureCell(int x, int y, int z);
|
||||
|
||||
// Remove furniture cell
|
||||
void removeFurnitureCell(int x, int y, int z);
|
||||
|
||||
// Clear all cells
|
||||
void clear();
|
||||
|
||||
// Mark for rebuild
|
||||
void markDirty() { dirty = true; version++; }
|
||||
|
||||
// Convert local cell position to world position
|
||||
Ogre::Vector3 cellToWorld(int x, int y, int z) const;
|
||||
|
||||
// Convert world position to cell coordinates
|
||||
void worldToCell(const Ogre::Vector3& worldPos, int& x, int& y, int& z) const;
|
||||
|
||||
// Get bit name for editor display
|
||||
static const char* getFlagName(uint64_t flag);
|
||||
static uint64_t getFlagByName(const std::string& name);
|
||||
static std::vector<std::pair<uint64_t, const char*>> getAllFlags();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Room definition within a cell grid
|
||||
*
|
||||
* Rooms are rectangular areas that can be connected by doors.
|
||||
* This is the ECS version of the Lua room() function.
|
||||
*/
|
||||
struct RoomComponent {
|
||||
// Room bounds (in cell coordinates)
|
||||
// A room from (minX, minZ) to (maxX-1, maxZ-1) inclusive
|
||||
int minX = 0, minY = 0, minZ = 0;
|
||||
int maxX = 1, maxY = 1, maxZ = 1; // exclusive max (size = max - min)
|
||||
|
||||
// Room name/tag for furniture placement rules
|
||||
std::string roomType; // e.g., "bedroom", "kitchen", "hallway"
|
||||
std::vector<std::string> tags;
|
||||
|
||||
// Generation flags
|
||||
bool createFloor = true; // Create floor cells
|
||||
bool createCeiling = true; // Create ceiling cells
|
||||
bool createInteriorWalls = true; // Create iwallx-/+, iwallz-/+ around the room
|
||||
bool createWindows = false; // Convert exterior-facing walls to windows
|
||||
bool fillRoomWithFurniture = false; // Automatically place furniture based on tags
|
||||
unsigned int furnitureSeed = 42; // Seed for deterministic furniture placement
|
||||
float furnitureYOffset = 0.05f; // Y offset for all furniture in this room
|
||||
|
||||
// Dirty flag - triggers regeneration of cell grid
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
|
||||
// Helper to get size
|
||||
int getSizeX() const { return maxX - minX; }
|
||||
int getSizeY() const { return maxY - minY; }
|
||||
int getSizeZ() const { return maxZ - minZ; }
|
||||
|
||||
// Check if a cell position is inside this room
|
||||
bool contains(int x, int y, int z) const {
|
||||
return x >= minX && x < maxX &&
|
||||
y >= minY && y < maxY &&
|
||||
z >= minZ && z < maxZ;
|
||||
}
|
||||
|
||||
// Check if a cell is on the room edge (for wall placement)
|
||||
bool isOnEdge(int x, int z) const {
|
||||
return x == minX || x == maxX - 1 || z == minZ || z == maxZ - 1;
|
||||
}
|
||||
|
||||
// Get the side of the room this cell is on (0=Z-, 1=Z+, 2=X-, 3=X+, -1=not on edge)
|
||||
int getEdgeSide(int x, int z) const {
|
||||
if (!isOnEdge(x, z)) return -1;
|
||||
if (z == minZ) return 0; // Z- (north)
|
||||
if (z == maxZ - 1) return 1; // Z+ (south)
|
||||
if (x == minX) return 2; // X- (west)
|
||||
if (x == maxX - 1) return 3; // X+ (east)
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Persistent unique ID for this room (survives save/load)
|
||||
// If empty, will be auto-generated during serialization
|
||||
std::string persistentId;
|
||||
|
||||
// Connected room persistent IDs (bidirectional connections)
|
||||
// Using persistent IDs instead of entity IDs because entity IDs change on save/load
|
||||
std::vector<std::string> connectedRoomIds;
|
||||
|
||||
// Exit doors for outside-facing walls (0=Z-, 1=Z+, 2=X-, 3=X+)
|
||||
// An exit is always a door placed on a wall that faces outside (not connected to another room)
|
||||
bool exits[4] = { false, false, false, false }; // Z-, Z+, X-, X+
|
||||
|
||||
// Generate a persistent ID if one doesn't exist
|
||||
void ensurePersistentId(flecs::entity_t entityId = 0) {
|
||||
if (persistentId.empty()) {
|
||||
// Generate unique ID based on timestamp + random + optional entity ID
|
||||
auto now = std::chrono::high_resolution_clock::now();
|
||||
auto nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
now.time_since_epoch()).count();
|
||||
persistentId = "room_" + std::to_string(nanos);
|
||||
if (entityId != 0) {
|
||||
persistentId += "_" + std::to_string(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a connection to another room (bidirectional, by persistent ID)
|
||||
void addConnection(const std::string& otherRoomId) {
|
||||
// Check if not already connected
|
||||
for (const auto& id : connectedRoomIds) {
|
||||
if (id == otherRoomId) return;
|
||||
}
|
||||
connectedRoomIds.push_back(otherRoomId);
|
||||
}
|
||||
|
||||
// Remove a connection to another room
|
||||
void removeConnection(const std::string& otherRoomId) {
|
||||
connectedRoomIds.erase(
|
||||
std::remove(connectedRoomIds.begin(), connectedRoomIds.end(), otherRoomId),
|
||||
connectedRoomIds.end()
|
||||
);
|
||||
}
|
||||
|
||||
// Check if connected to a room (by persistent ID)
|
||||
bool isConnectedTo(const std::string& otherRoomId) const {
|
||||
for (const auto& id : connectedRoomIds) {
|
||||
if (id == otherRoomId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper to set all exits at once
|
||||
void setAllExits(bool value) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
exits[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to set a specific exit
|
||||
void setExit(int side, bool value) {
|
||||
if (side >= 0 && side < 4) {
|
||||
exits[side] = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Room exits - defines external exits from a room
|
||||
*
|
||||
* This replaces the Lua create_exit0/1/2/3() functions.
|
||||
* Creates external walls/doors/windows on the room edges that face outside.
|
||||
*/
|
||||
/**
|
||||
* @brief Clear Area - clears cells in a region before room generation
|
||||
*
|
||||
* This replaces the Lua clear_area() and clear_furniture_area() functions.
|
||||
* It clears all cells within the specified bounds before any room generation happens.
|
||||
* This component is processed first in the RoomLayoutSystem.
|
||||
*/
|
||||
struct ClearAreaComponent {
|
||||
// Area bounds (in cell coordinates, inclusive min, exclusive max)
|
||||
int minX = 0, minY = 0, minZ = 0;
|
||||
int maxX = 1, maxY = 1, maxZ = 1;
|
||||
|
||||
// What to clear
|
||||
bool clearCells = true; // Clear cell flags (walls, floors, etc.)
|
||||
bool clearFurniture = true; // Clear furniture placements
|
||||
bool clearRoofs = false; // Clear roof definitions (children with RoofComponent)
|
||||
bool clearRooms = false; // Remove existing room entities (children with RoomComponent)
|
||||
|
||||
// Processed flag - cleared each time dirty is set
|
||||
bool processed = false;
|
||||
|
||||
// Dirty flag - clear again when true
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
|
||||
// Helper to set bounds
|
||||
void setBounds(int x1, int z1, int x2, int z2, int y = 0) {
|
||||
minX = x1; minZ = z1; minY = y;
|
||||
maxX = x2; maxZ = z2; maxY = y + 1;
|
||||
}
|
||||
|
||||
// Helper to get size
|
||||
int getSizeX() const { return maxX - minX; }
|
||||
int getSizeY() const { return maxY - minY; }
|
||||
int getSizeZ() const { return maxZ - minZ; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Roof definition for a lot
|
||||
*/
|
||||
struct RoofComponent {
|
||||
enum Type {
|
||||
Flat = 0,
|
||||
Normal = 1,
|
||||
Normal2 = 2,
|
||||
Cone = 3,
|
||||
Cylinder = 4
|
||||
};
|
||||
|
||||
Type type = Flat;
|
||||
|
||||
// Position (in cell coordinates)
|
||||
int posX = 0, posY = 0, posZ = 0;
|
||||
|
||||
// Size (in cells)
|
||||
int sizeX = 1, sizeZ = 1;
|
||||
|
||||
// Offset from cell position (world units)
|
||||
float offsetX = 0.0f, offsetY = 0.0f, offsetZ = 0.0f;
|
||||
|
||||
// Height parameters
|
||||
float baseHeight = 0.5f;
|
||||
float maxHeight = 0.5f;
|
||||
|
||||
// Dirty flag - triggers rebuild when changed
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Lot component - represents a single house/building lot
|
||||
*
|
||||
* A lot contains:
|
||||
* - Cell grid (walls, floors, etc.)
|
||||
* - Room definitions
|
||||
* - Roof definitions
|
||||
* - Furniture placement rules
|
||||
*/
|
||||
struct LotComponent {
|
||||
// Lot dimensions (in cells)
|
||||
int width = 10;
|
||||
int depth = 10;
|
||||
|
||||
// Elevation offset from district
|
||||
float elevation = 0.0f;
|
||||
|
||||
// Rotation around district center (degrees)
|
||||
float angle = 0.0f;
|
||||
|
||||
// Position offset (for manual placement)
|
||||
float offsetX = 0.0f, offsetZ = 0.0f;
|
||||
|
||||
// Template name if this lot was created from a template
|
||||
std::string templateName;
|
||||
|
||||
// Procedural material for lot base (optional - falls back to District, then Town)
|
||||
flecs::entity proceduralMaterialEntity;
|
||||
std::string proceduralMaterialEntityId; // For serialization
|
||||
|
||||
// Texture rectangle name from ProceduralTexture for UV mapping
|
||||
std::string textureRectName;
|
||||
|
||||
// Physics properties for generated colliders
|
||||
float friction = 0.5f;
|
||||
|
||||
// Dirty flag
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief District component - contains multiple lots arranged in a circle
|
||||
*/
|
||||
struct DistrictComponent {
|
||||
// District radius (distance from center to lots)
|
||||
float radius = 50.0f;
|
||||
|
||||
// Elevation offset
|
||||
float elevation = 0.0f;
|
||||
|
||||
// Height of district base (for plaza)
|
||||
float height = 0.2f;
|
||||
|
||||
// Is this a plaza (circular base)?
|
||||
bool isPlaza = false;
|
||||
|
||||
// Lot templates for this district
|
||||
std::vector<std::string> lotTemplateNames;
|
||||
|
||||
// Procedural material for plaza (references entity with ProceduralMaterialComponent)
|
||||
flecs::entity proceduralMaterialEntity;
|
||||
std::string proceduralMaterialEntityId; // For serialization
|
||||
|
||||
// Texture rectangle name from ProceduralTexture for UV mapping
|
||||
std::string textureRectName;
|
||||
|
||||
// Physics properties for generated colliders
|
||||
float friction = 0.5f;
|
||||
|
||||
// Dirty flag
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Town component - top-level container
|
||||
*/
|
||||
struct TownComponent {
|
||||
std::string townName = "New Town";
|
||||
|
||||
// Color rectangles for texture atlas
|
||||
struct ColorRect {
|
||||
float left = 0.0f, top = 0.0f, right = 1.0f, bottom = 1.0f;
|
||||
Ogre::ColourValue color = Ogre::ColourValue::White;
|
||||
};
|
||||
std::unordered_map<std::string, ColorRect> colorRects;
|
||||
|
||||
// Material name (created from color rects)
|
||||
std::string materialName;
|
||||
|
||||
// Procedural material for town (used by districts and lots)
|
||||
flecs::entity proceduralMaterialEntity;
|
||||
std::string proceduralMaterialEntityId; // For serialization
|
||||
|
||||
// Texture rectangle name from ProceduralTexture for UV mapping
|
||||
std::string textureRectName;
|
||||
|
||||
// Dirty flag
|
||||
bool dirty = true;
|
||||
bool materialDirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
void markMaterialDirty() { materialDirty = true; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Furniture template - defines a furniture item
|
||||
*/
|
||||
struct FurnitureTemplateComponent {
|
||||
std::string templateName;
|
||||
std::vector<std::string> tags; // e.g., "bed", "essential", "bedroom"
|
||||
|
||||
// Mesh/model info
|
||||
std::string meshName;
|
||||
std::string materialName;
|
||||
|
||||
// Placement rules
|
||||
bool requiresWall = false;
|
||||
bool requiresFloor = true;
|
||||
bool blocksPath = true;
|
||||
|
||||
// Offset from cell center
|
||||
float offsetX = 0.0f, offsetY = 0.0f, offsetZ = 0.0f;
|
||||
|
||||
// Random selection weight
|
||||
float weight = 1.0f;
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
#include "CellGrid.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/CellGridEditor.hpp"
|
||||
#include "../ui/LotEditor.hpp"
|
||||
#include "../ui/DistrictEditor.hpp"
|
||||
#include "../ui/TownEditor.hpp"
|
||||
#include "../ui/RoofEditor.hpp"
|
||||
#include "../ui/RoomEditor.hpp"
|
||||
|
||||
#include "../ui/ClearAreaEditor.hpp"
|
||||
#include "../ui/FurnitureTemplateEditor.hpp"
|
||||
|
||||
// Register CellGrid component
|
||||
REGISTER_COMPONENT_GROUP("Cell Grid", "Cell Grid", CellGridComponent, CellGridEditor)
|
||||
{
|
||||
registry.registerComponent<CellGridComponent>(
|
||||
"Cell Grid", "Cell Grid",
|
||||
std::make_unique<CellGridEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<CellGridComponent>()) {
|
||||
e.set<CellGridComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<CellGridComponent>()) {
|
||||
e.remove<CellGridComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register Lot component
|
||||
REGISTER_COMPONENT_GROUP("Lot", "Cell Grid", LotComponent, LotEditor)
|
||||
{
|
||||
registry.registerComponent<LotComponent>(
|
||||
"Lot", "Cell Grid",
|
||||
std::make_unique<LotEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<LotComponent>()) {
|
||||
e.set<LotComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<LotComponent>()) {
|
||||
e.remove<LotComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register District component
|
||||
REGISTER_COMPONENT_GROUP("District", "Cell Grid", DistrictComponent, DistrictEditor)
|
||||
{
|
||||
registry.registerComponent<DistrictComponent>(
|
||||
"District", "Cell Grid",
|
||||
std::make_unique<DistrictEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<DistrictComponent>()) {
|
||||
e.set<DistrictComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<DistrictComponent>()) {
|
||||
e.remove<DistrictComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register Town component
|
||||
REGISTER_COMPONENT_GROUP("Town", "Cell Grid", TownComponent, TownEditor)
|
||||
{
|
||||
registry.registerComponent<TownComponent>(
|
||||
"Town", "Cell Grid",
|
||||
std::make_unique<TownEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<TownComponent>()) {
|
||||
e.set<TownComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<TownComponent>()) {
|
||||
e.remove<TownComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register Roof component
|
||||
REGISTER_COMPONENT_GROUP("Roof", "Cell Grid", RoofComponent, RoofEditor)
|
||||
{
|
||||
registry.registerComponent<RoofComponent>(
|
||||
"Roof", "Cell Grid",
|
||||
std::make_unique<RoofEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<RoofComponent>()) {
|
||||
e.set<RoofComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<RoofComponent>()) {
|
||||
e.remove<RoofComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register Room component (in Room Layout group)
|
||||
REGISTER_COMPONENT_GROUP("Room", "Room Layout", RoomComponent, RoomEditor)
|
||||
{
|
||||
registry.registerComponent<RoomComponent>(
|
||||
"Room", "Room Layout",
|
||||
std::make_unique<RoomEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<RoomComponent>()) {
|
||||
e.set<RoomComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<RoomComponent>()) {
|
||||
e.remove<RoomComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register FurnitureTemplate component
|
||||
REGISTER_COMPONENT_GROUP("Furniture Template", "Cell Grid", FurnitureTemplateComponent, FurnitureTemplateEditor)
|
||||
{
|
||||
registry.registerComponent<FurnitureTemplateComponent>(
|
||||
"Furniture Template", "Cell Grid",
|
||||
std::make_unique<FurnitureTemplateEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<FurnitureTemplateComponent>()) {
|
||||
e.set<FurnitureTemplateComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<FurnitureTemplateComponent>()) {
|
||||
e.remove<FurnitureTemplateComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register ClearArea component (in Room Layout group)
|
||||
REGISTER_COMPONENT_GROUP("Clear Area", "Room Layout", ClearAreaComponent, ClearAreaEditor)
|
||||
{
|
||||
registry.registerComponent<ClearAreaComponent>(
|
||||
"Clear Area", "Room Layout",
|
||||
std::make_unique<ClearAreaEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<ClearAreaComponent>()) {
|
||||
e.set<ClearAreaComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<ClearAreaComponent>()) {
|
||||
e.remove<ClearAreaComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Note: ExteriorGenerationComponent has been removed.
|
||||
// Exterior generation now automatically runs at the end of the RoomLayoutSystem pipeline.
|
||||
// This mirrors the original Lua behavior where create_exterior() was called after all rooms were defined.
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "CellGridModule.hpp"
|
||||
#include "CellGrid.hpp"
|
||||
#include "GeneratedPhysicsTag.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <Ogre.h>
|
||||
|
||||
namespace CellGridModule {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
void registerComponents(flecs::world& world)
|
||||
{
|
||||
// Register components without .member<> for complex types (std::string, std::vector)
|
||||
// Flecs doesn't have built-in reflection for these, but JSON serialization handles them
|
||||
|
||||
// CellGridComponent
|
||||
world.component<CellGridComponent>();
|
||||
|
||||
// Cell struct
|
||||
world.component<Cell>();
|
||||
|
||||
// FurnitureCell struct
|
||||
world.component<FurnitureCell>();
|
||||
|
||||
// LotComponent
|
||||
world.component<LotComponent>();
|
||||
|
||||
// DistrictComponent
|
||||
world.component<DistrictComponent>();
|
||||
|
||||
// TownComponent
|
||||
world.component<TownComponent>();
|
||||
|
||||
// TownComponent::ColorRect
|
||||
world.component<TownComponent::ColorRect>();
|
||||
|
||||
// RoomComponent
|
||||
world.component<RoomComponent>();
|
||||
|
||||
// RoofComponent
|
||||
world.component<RoofComponent>();
|
||||
|
||||
// FurnitureTemplateComponent
|
||||
world.component<FurnitureTemplateComponent>();
|
||||
world.component<GeneratedPhysicsTag>();
|
||||
}
|
||||
|
||||
} // namespace CellGridModule
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
|
||||
namespace CellGridModule {
|
||||
void registerComponents(flecs::world& world);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef EDITSCENE_CHARACTER_HPP
|
||||
#define EDITSCENE_CHARACTER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Character physics component
|
||||
*
|
||||
* Attaches a Jolt JPH::Character (kinematic capsule) to the entity.
|
||||
* The entity may also have CharacterSlotsComponent; the character
|
||||
* physics lives on the same entity as the visual character.
|
||||
*
|
||||
* Child entities can add extra collision shapes via PhysicsColliderComponent.
|
||||
*/
|
||||
struct CharacterComponent {
|
||||
/* Capsule dimensions */
|
||||
float radius = 0.3f;
|
||||
float height = 1.8f; /* cylinder height (excluding spherical caps) */
|
||||
|
||||
/* Offset from the entity's scene node */
|
||||
Ogre::Vector3 offset = Ogre::Vector3::ZERO;
|
||||
|
||||
/* Current linear velocity (m/s), applied each frame by CharacterSystem */
|
||||
Ogre::Vector3 linearVelocity = Ogre::Vector3::ZERO;
|
||||
|
||||
/* Enable/disable physics character */
|
||||
bool enabled = true;
|
||||
|
||||
/* Physics was explicitly disabled (e.g. by behavior tree node).
|
||||
* When true, the character's JPH::BodyID is removed from the physics
|
||||
* system but the JPH::Character object is kept alive so it can be
|
||||
* re-added later. This is separate from 'enabled' which controls
|
||||
* whether the character system processes this entity at all. */
|
||||
bool physicsDisabled = false;
|
||||
|
||||
/* Dirty flag — triggers rebuild of the Jolt character */
|
||||
bool dirty = true;
|
||||
|
||||
/* When true, the scene node position is driven by root motion
|
||||
* (AnimationTreeSystem), not by physics. The physics character
|
||||
* position is synced to match the scene node each frame, and
|
||||
* physics does NOT write its position back to the scene node. */
|
||||
bool useRootMotion = false;
|
||||
|
||||
/* Floor detection: raycast downward to find ground before enabling gravity */
|
||||
bool hasFloor = false;
|
||||
float floorCheckDistance = 2.0f;
|
||||
bool useGravity = true;
|
||||
|
||||
float getHalfHeight() const
|
||||
{
|
||||
return height * 0.5f;
|
||||
}
|
||||
float getTotalHeight() const
|
||||
{
|
||||
return height + 2.0f * radius;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTER_HPP
|
||||
@@ -0,0 +1,442 @@
|
||||
#include "CharacterClassDatabase.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreResourceGroupManager.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Singleton
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
CharacterClassDatabase &CharacterClassDatabase::getSingleton()
|
||||
{
|
||||
static CharacterClassDatabase instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
CharacterClassDatabase *CharacterClassDatabase::getSingletonPtr()
|
||||
{
|
||||
return &getSingleton();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lookup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const CharacterClassDatabase::StatDef *
|
||||
CharacterClassDatabase::findStat(const Ogre::String &name) const
|
||||
{
|
||||
auto it = m_stats.find(name);
|
||||
if (it != m_stats.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const CharacterClassDatabase::SkillDef *
|
||||
CharacterClassDatabase::findSkill(const Ogre::String &name) const
|
||||
{
|
||||
auto it = m_skills.find(name);
|
||||
if (it != m_skills.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const CharacterClassDatabase::NeedDef *
|
||||
CharacterClassDatabase::findNeed(const Ogre::String &name) const
|
||||
{
|
||||
auto it = m_needs.find(name);
|
||||
if (it != m_needs.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const CharacterClassDatabase::ClassDef *
|
||||
CharacterClassDatabase::findClass(const Ogre::String &name) const
|
||||
{
|
||||
auto it = m_classes.find(name);
|
||||
if (it != m_classes.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CharacterClassDatabase::StatDef *
|
||||
CharacterClassDatabase::findStat(const Ogre::String &name)
|
||||
{
|
||||
auto it = m_stats.find(name);
|
||||
if (it != m_stats.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CharacterClassDatabase::SkillDef *
|
||||
CharacterClassDatabase::findSkill(const Ogre::String &name)
|
||||
{
|
||||
auto it = m_skills.find(name);
|
||||
if (it != m_skills.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CharacterClassDatabase::NeedDef *
|
||||
CharacterClassDatabase::findNeed(const Ogre::String &name)
|
||||
{
|
||||
auto it = m_needs.find(name);
|
||||
if (it != m_needs.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CharacterClassDatabase::ClassDef *
|
||||
CharacterClassDatabase::findClass(const Ogre::String &name)
|
||||
{
|
||||
auto it = m_classes.find(name);
|
||||
if (it != m_classes.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutators
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CharacterClassDatabase::addOrReplaceStat(const StatDef &def)
|
||||
{
|
||||
bool wasNew = m_stats.find(def.name) == m_stats.end();
|
||||
m_stats[def.name] = def;
|
||||
if (wasNew)
|
||||
m_statNames.push_back(def.name);
|
||||
}
|
||||
|
||||
void CharacterClassDatabase::addOrReplaceSkill(const SkillDef &def)
|
||||
{
|
||||
bool wasNew = m_skills.find(def.name) == m_skills.end();
|
||||
m_skills[def.name] = def;
|
||||
if (wasNew)
|
||||
m_skillNames.push_back(def.name);
|
||||
}
|
||||
|
||||
void CharacterClassDatabase::addOrReplaceNeed(const NeedDef &def)
|
||||
{
|
||||
bool wasNew = m_needs.find(def.name) == m_needs.end();
|
||||
m_needs[def.name] = def;
|
||||
if (wasNew)
|
||||
m_needNames.push_back(def.name);
|
||||
}
|
||||
|
||||
void CharacterClassDatabase::addOrReplaceClass(const ClassDef &def)
|
||||
{
|
||||
bool wasNew = m_classes.find(def.name) == m_classes.end();
|
||||
m_classes[def.name] = def;
|
||||
if (wasNew)
|
||||
m_classNames.push_back(def.name);
|
||||
}
|
||||
|
||||
bool CharacterClassDatabase::removeStat(const Ogre::String &name)
|
||||
{
|
||||
if (m_stats.erase(name) == 0)
|
||||
return false;
|
||||
auto it = std::remove(m_statNames.begin(), m_statNames.end(),
|
||||
name);
|
||||
m_statNames.erase(it, m_statNames.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CharacterClassDatabase::removeSkill(const Ogre::String &name)
|
||||
{
|
||||
if (m_skills.erase(name) == 0)
|
||||
return false;
|
||||
auto it = std::remove(m_skillNames.begin(), m_skillNames.end(),
|
||||
name);
|
||||
m_skillNames.erase(it, m_skillNames.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CharacterClassDatabase::removeNeed(const Ogre::String &name)
|
||||
{
|
||||
if (m_needs.erase(name) == 0)
|
||||
return false;
|
||||
auto it = std::remove(m_needNames.begin(), m_needNames.end(),
|
||||
name);
|
||||
m_needNames.erase(it, m_needNames.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CharacterClassDatabase::removeClass(const Ogre::String &name)
|
||||
{
|
||||
if (m_classes.erase(name) == 0)
|
||||
return false;
|
||||
auto it = std::remove(m_classNames.begin(), m_classNames.end(),
|
||||
name);
|
||||
m_classNames.erase(it, m_classNames.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
void CharacterClassDatabase::clear()
|
||||
{
|
||||
m_stats.clear();
|
||||
m_skills.clear();
|
||||
m_needs.clear();
|
||||
m_classes.clear();
|
||||
m_statNames.clear();
|
||||
m_skillNames.clear();
|
||||
m_needNames.clear();
|
||||
m_classNames.clear();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Runtime helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int64_t CharacterClassDatabase::computeXPForLevel(int level,
|
||||
const ClassDef &cls) const
|
||||
{
|
||||
return static_cast<int64_t>(cls.xpForLevel.evaluate(level));
|
||||
}
|
||||
|
||||
int CharacterClassDatabase::computePointsForLevel(int level,
|
||||
const ClassDef &cls) const
|
||||
{
|
||||
return static_cast<int>(cls.pointsPerLevel.evaluate(level));
|
||||
}
|
||||
|
||||
int CharacterClassDatabase::computeStatGrowth(const Ogre::String &stat,
|
||||
int level,
|
||||
const ClassDef &cls) const
|
||||
{
|
||||
auto it = cls.statGrowth.find(stat);
|
||||
if (it == cls.statGrowth.end())
|
||||
return 0;
|
||||
return static_cast<int>(it->second.evaluate(level));
|
||||
}
|
||||
|
||||
int CharacterClassDatabase::computeSkillGrowth(const Ogre::String &skill,
|
||||
int level,
|
||||
const ClassDef &cls) const
|
||||
{
|
||||
auto it = cls.skillGrowth.find(skill);
|
||||
if (it == cls.skillGrowth.end())
|
||||
return 0;
|
||||
return static_cast<int>(it->second.evaluate(level));
|
||||
}
|
||||
|
||||
int CharacterClassDatabase::computeStatCost(int currentValue,
|
||||
const ClassDef &cls) const
|
||||
{
|
||||
return static_cast<int>(cls.statCost.evaluate(0, static_cast<double>(currentValue)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON serialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool CharacterClassDatabase::saveToJson(const std::string &filename)
|
||||
{
|
||||
auto &db = getSingleton();
|
||||
nlohmann::json json;
|
||||
json["version"] = 1;
|
||||
|
||||
// Stats
|
||||
json["stat_definitions"] = nlohmann::json::object();
|
||||
for (const auto &name : db.m_statNames) {
|
||||
const auto &def = db.m_stats.at(name);
|
||||
nlohmann::json j;
|
||||
j["display_name"] = def.displayName;
|
||||
j["kind"] = (def.kind == StatKind::Attribute) ? "attribute" :
|
||||
"resource_pool";
|
||||
j["min"] = def.minValue;
|
||||
j["max"] = def.maxValue;
|
||||
json["stat_definitions"][name] = j;
|
||||
}
|
||||
|
||||
// Skills
|
||||
json["skill_definitions"] = nlohmann::json::object();
|
||||
for (const auto &name : db.m_skillNames) {
|
||||
const auto &def = db.m_skills.at(name);
|
||||
nlohmann::json j;
|
||||
j["display_name"] = def.displayName;
|
||||
j["max"] = def.maxValue;
|
||||
json["skill_definitions"][name] = j;
|
||||
}
|
||||
|
||||
// Needs
|
||||
json["need_definitions"] = nlohmann::json::object();
|
||||
for (const auto &name : db.m_needNames) {
|
||||
const auto &def = db.m_needs.at(name);
|
||||
nlohmann::json j;
|
||||
j["display_name"] = def.displayName;
|
||||
j["max"] = def.maxValue;
|
||||
j["accumulation_rate"] = def.accumulationRate;
|
||||
j["low_threshold"] = def.lowThreshold;
|
||||
j["high_threshold"] = def.highThreshold;
|
||||
j["bit_name"] = def.bitName;
|
||||
json["need_definitions"][name] = j;
|
||||
}
|
||||
|
||||
// Classes
|
||||
json["classes"] = nlohmann::json::object();
|
||||
for (const auto &name : db.m_classNames) {
|
||||
const auto &cls = db.m_classes.at(name);
|
||||
nlohmann::json j;
|
||||
j["description"] = cls.description;
|
||||
j["primary_stats"] = cls.primaryStats;
|
||||
j["base_stats"] = cls.baseStats;
|
||||
j["base_skills"] = cls.baseSkills;
|
||||
j["base_needs"] = cls.baseNeeds;
|
||||
j["xp_for_level"] = cls.xpForLevel.getExpression();
|
||||
j["points_per_level"] = cls.pointsPerLevel.getExpression();
|
||||
j["stat_cost"] = cls.statCost.getExpression();
|
||||
j["stat_growth"] = nlohmann::json::object();
|
||||
for (const auto &pair : cls.statGrowth)
|
||||
j["stat_growth"][pair.first] =
|
||||
pair.second.getExpression();
|
||||
j["skill_growth"] = nlohmann::json::object();
|
||||
for (const auto &pair : cls.skillGrowth)
|
||||
j["skill_growth"][pair.first] =
|
||||
pair.second.getExpression();
|
||||
json["classes"][name] = j;
|
||||
}
|
||||
|
||||
try {
|
||||
std::filesystem::path outPath =
|
||||
std::filesystem::current_path() / filename;
|
||||
std::ofstream f(outPath);
|
||||
if (!f.is_open()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CharacterClassDatabase: Failed to open " +
|
||||
filename + " for writing");
|
||||
return false;
|
||||
}
|
||||
f << json.dump(4);
|
||||
return true;
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CharacterClassDatabase: Exception saving " + filename);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool CharacterClassDatabase::loadFromJson(const std::string &filename)
|
||||
{
|
||||
auto &db = getSingleton();
|
||||
db.clear();
|
||||
|
||||
std::filesystem::path inPath =
|
||||
std::filesystem::current_path() / filename;
|
||||
std::ifstream f(inPath);
|
||||
if (!f.is_open()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CharacterClassDatabase: Could not load " + filename +
|
||||
", using defaults.");
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json json;
|
||||
try {
|
||||
f >> json;
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CharacterClassDatabase: JSON parse error in " +
|
||||
filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stats
|
||||
if (json.contains("stat_definitions") &&
|
||||
json["stat_definitions"].is_object()) {
|
||||
for (auto &[key, val] : json["stat_definitions"].items()) {
|
||||
StatDef def;
|
||||
def.name = key;
|
||||
def.displayName = val.value("display_name", key);
|
||||
Ogre::String kindStr = val.value("kind", "attribute");
|
||||
def.kind = (kindStr == "resource_pool") ? StatKind::ResourcePool :
|
||||
StatKind::Attribute;
|
||||
def.minValue = val.value("min", 1);
|
||||
def.maxValue = val.value("max", 999);
|
||||
db.addOrReplaceStat(def);
|
||||
}
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (json.contains("skill_definitions") &&
|
||||
json["skill_definitions"].is_object()) {
|
||||
for (auto &[key, val] : json["skill_definitions"].items()) {
|
||||
SkillDef def;
|
||||
def.name = key;
|
||||
def.displayName = val.value("display_name", key);
|
||||
def.maxValue = val.value("max", 100);
|
||||
db.addOrReplaceSkill(def);
|
||||
}
|
||||
}
|
||||
|
||||
// Needs
|
||||
if (json.contains("need_definitions") &&
|
||||
json["need_definitions"].is_object()) {
|
||||
for (auto &[key, val] : json["need_definitions"].items()) {
|
||||
NeedDef def;
|
||||
def.name = key;
|
||||
def.displayName = val.value("display_name", key);
|
||||
def.maxValue = val.value("max", 1000);
|
||||
def.accumulationRate = val.value("accumulation_rate", 0.0f);
|
||||
def.lowThreshold = val.value("low_threshold", 0);
|
||||
def.highThreshold = val.value("high_threshold", 1000);
|
||||
def.bitName = val.value("bit_name", "");
|
||||
// Backward compat: old files used low_bit_name / high_bit_name
|
||||
if (def.bitName.empty()) {
|
||||
def.bitName = val.value("high_bit_name", "");
|
||||
if (def.bitName.empty())
|
||||
def.bitName = val.value("low_bit_name", "");
|
||||
}
|
||||
db.addOrReplaceNeed(def);
|
||||
}
|
||||
}
|
||||
|
||||
// Classes
|
||||
if (json.contains("classes") && json["classes"].is_object()) {
|
||||
for (auto &[key, val] : json["classes"].items()) {
|
||||
ClassDef cls;
|
||||
cls.name = key;
|
||||
cls.description = val.value("description", "");
|
||||
if (val.contains("primary_stats") &&
|
||||
val["primary_stats"].is_array()) {
|
||||
for (const auto &item : val["primary_stats"])
|
||||
cls.primaryStats.push_back(
|
||||
item.get<std::string>());
|
||||
}
|
||||
if (val.contains("base_stats") &&
|
||||
val["base_stats"].is_object()) {
|
||||
for (auto &[k, v] : val["base_stats"].items())
|
||||
cls.baseStats[k] = v.get<int>();
|
||||
}
|
||||
if (val.contains("base_skills") &&
|
||||
val["base_skills"].is_object()) {
|
||||
for (auto &[k, v] : val["base_skills"].items())
|
||||
cls.baseSkills[k] = v.get<int>();
|
||||
}
|
||||
if (val.contains("base_needs") &&
|
||||
val["base_needs"].is_object()) {
|
||||
for (auto &[k, v] : val["base_needs"].items())
|
||||
cls.baseNeeds[k] = v.get<int>();
|
||||
}
|
||||
cls.xpForLevel = Formula(val.value("xp_for_level", "0"));
|
||||
cls.pointsPerLevel =
|
||||
Formula(val.value("points_per_level", "0"));
|
||||
cls.statCost = Formula(val.value("stat_cost", "1"));
|
||||
if (val.contains("stat_growth") &&
|
||||
val["stat_growth"].is_object()) {
|
||||
for (auto &[k, v] : val["stat_growth"].items())
|
||||
cls.statGrowth[k] =
|
||||
Formula(v.get<std::string>());
|
||||
}
|
||||
if (val.contains("skill_growth") &&
|
||||
val["skill_growth"].is_object()) {
|
||||
for (auto &[k, v] : val["skill_growth"].items())
|
||||
cls.skillGrowth[k] =
|
||||
Formula(v.get<std::string>());
|
||||
}
|
||||
db.addOrReplaceClass(cls);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
#ifndef EDITSCENE_CHARACTER_CLASS_DATABASE_HPP
|
||||
#define EDITSCENE_CHARACTER_CLASS_DATABASE_HPP
|
||||
#pragma once
|
||||
|
||||
#include "Formula.hpp"
|
||||
#include <Ogre.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Global character class database singleton.
|
||||
*
|
||||
* Holds the master list of stat/skill/need definitions and class templates.
|
||||
* This is a singleton accessible from anywhere in the codebase.
|
||||
* Persisted to character_class.json, read-only in game mode.
|
||||
*/
|
||||
class CharacterClassDatabase {
|
||||
public:
|
||||
static CharacterClassDatabase &getSingleton();
|
||||
static CharacterClassDatabase *getSingletonPtr();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Definitions
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
enum class StatKind {
|
||||
Attribute, // permanent value (strength, agility)
|
||||
ResourcePool // depletable pool (health, stamina, mana)
|
||||
};
|
||||
|
||||
struct StatDef {
|
||||
Ogre::String name;
|
||||
Ogre::String displayName;
|
||||
StatKind kind = StatKind::Attribute;
|
||||
int minValue = 1;
|
||||
int maxValue = 999;
|
||||
};
|
||||
|
||||
struct SkillDef {
|
||||
Ogre::String name;
|
||||
Ogre::String displayName;
|
||||
int maxValue = 100;
|
||||
};
|
||||
|
||||
struct NeedDef {
|
||||
Ogre::String name;
|
||||
Ogre::String displayName;
|
||||
int maxValue = 1000;
|
||||
float accumulationRate = 0.0f;
|
||||
int lowThreshold = 0; // clear bit when need <= this
|
||||
int highThreshold = 1000; // set bit when need >= this
|
||||
Ogre::String bitName; // GOAP blackboard bit (hysteresis)
|
||||
};
|
||||
|
||||
struct ClassDef {
|
||||
Ogre::String name;
|
||||
Ogre::String description;
|
||||
std::vector<Ogre::String> primaryStats;
|
||||
std::unordered_map<Ogre::String, int> baseStats;
|
||||
std::unordered_map<Ogre::String, int> baseSkills;
|
||||
std::unordered_map<Ogre::String, int> baseNeeds;
|
||||
Formula xpForLevel;
|
||||
Formula pointsPerLevel;
|
||||
Formula statCost;
|
||||
std::unordered_map<Ogre::String, Formula> statGrowth;
|
||||
std::unordered_map<Ogre::String, Formula> skillGrowth;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Lookup
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
const StatDef *findStat(const Ogre::String &name) const;
|
||||
const SkillDef *findSkill(const Ogre::String &name) const;
|
||||
const NeedDef *findNeed(const Ogre::String &name) const;
|
||||
const ClassDef *findClass(const Ogre::String &name) const;
|
||||
|
||||
StatDef *findStat(const Ogre::String &name);
|
||||
SkillDef *findSkill(const Ogre::String &name);
|
||||
NeedDef *findNeed(const Ogre::String &name);
|
||||
ClassDef *findClass(const Ogre::String &name);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Mutators
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
void addOrReplaceStat(const StatDef &def);
|
||||
void addOrReplaceSkill(const SkillDef &def);
|
||||
void addOrReplaceNeed(const NeedDef &def);
|
||||
void addOrReplaceClass(const ClassDef &def);
|
||||
|
||||
bool removeStat(const Ogre::String &name);
|
||||
bool removeSkill(const Ogre::String &name);
|
||||
bool removeNeed(const Ogre::String &name);
|
||||
bool removeClass(const Ogre::String &name);
|
||||
|
||||
void clear();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Lists
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
const std::vector<Ogre::String> &getStatNames() const
|
||||
{
|
||||
return m_statNames;
|
||||
}
|
||||
const std::vector<Ogre::String> &getSkillNames() const
|
||||
{
|
||||
return m_skillNames;
|
||||
}
|
||||
const std::vector<Ogre::String> &getNeedNames() const
|
||||
{
|
||||
return m_needNames;
|
||||
}
|
||||
const std::vector<Ogre::String> &getClassNames() const
|
||||
{
|
||||
return m_classNames;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Persistence
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static bool saveToJson(const std::string &filename);
|
||||
static bool loadFromJson(const std::string &filename);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Runtime helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
int64_t computeXPForLevel(int level, const ClassDef &cls) const;
|
||||
int computePointsForLevel(int level, const ClassDef &cls) const;
|
||||
int computeStatGrowth(const Ogre::String &stat, int level,
|
||||
const ClassDef &cls) const;
|
||||
int computeSkillGrowth(const Ogre::String &skill, int level,
|
||||
const ClassDef &cls) const;
|
||||
int computeStatCost(int currentValue, const ClassDef &cls) const;
|
||||
|
||||
private:
|
||||
CharacterClassDatabase() = default;
|
||||
|
||||
std::unordered_map<Ogre::String, StatDef> m_stats;
|
||||
std::unordered_map<Ogre::String, SkillDef> m_skills;
|
||||
std::unordered_map<Ogre::String, NeedDef> m_needs;
|
||||
std::unordered_map<Ogre::String, ClassDef> m_classes;
|
||||
|
||||
std::vector<Ogre::String> m_statNames;
|
||||
std::vector<Ogre::String> m_skillNames;
|
||||
std::vector<Ogre::String> m_needNames;
|
||||
std::vector<Ogre::String> m_classNames;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTER_CLASS_DATABASE_HPP
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef EDITSCENE_CHARACTERIDENTITY_HPP
|
||||
#define EDITSCENE_CHARACTERIDENTITY_HPP
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Links a spawned entity to a global character registry entry.
|
||||
*
|
||||
* When a character entity is created from a prefab or manually,
|
||||
* this component stores the persistent registry ID so the entity
|
||||
* knows who it is in the social graph.
|
||||
*/
|
||||
struct CharacterIdentityComponent {
|
||||
uint64_t registryId = 0;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTERIDENTITY_HPP
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "Character.hpp"
|
||||
#include "../ui/CharacterEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Character Physics", "Physics",
|
||||
CharacterComponent, CharacterEditor)
|
||||
{
|
||||
registry.registerComponent<CharacterComponent>(
|
||||
"Character Physics", "Physics", std::make_unique<CharacterEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<CharacterComponent>())
|
||||
e.set<CharacterComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<CharacterComponent>())
|
||||
e.remove<CharacterComponent>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef EDITSCENE_CHARACTERSLOTS_HPP
|
||||
#define EDITSCENE_CHARACTERSLOTS_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Selection criteria for a single character slot.
|
||||
* Layer 0 (nude base) is always implicit.
|
||||
* Layer 1 and 2 are selected via combo boxes.
|
||||
*/
|
||||
struct SlotSelection {
|
||||
Ogre::String layer1Mesh; // "none" or exact mesh name for layer 1
|
||||
Ogre::String layer2Mesh; // "none" or exact mesh name for layer 2
|
||||
Ogre::String explicitMesh; // backward-compat override
|
||||
};
|
||||
|
||||
/**
|
||||
* Multi-slot mesh component for character parts sharing a skeleton.
|
||||
* The "face" slot (or first available slot) serves as the master skeleton.
|
||||
*
|
||||
* Age is now stored in CharacterRegistry::CharacterRecord::age
|
||||
* and should be retrieved from there when needed.
|
||||
*/
|
||||
struct CharacterSlotsComponent {
|
||||
Ogre::String sex = "male";
|
||||
|
||||
/* Backward-compat: old mesh-name map. Deserialized into slotSelections on load. */
|
||||
std::unordered_map<Ogre::String, Ogre::String> slots;
|
||||
|
||||
/* Per-slot layer selections (runtime) */
|
||||
std::unordered_map<Ogre::String, SlotSelection> slotSelections;
|
||||
|
||||
bool dirty = true;
|
||||
|
||||
/* Runtime: master entity with shared skeleton (set by CharacterSlotSystem) */
|
||||
Ogre::Entity *masterEntity = nullptr;
|
||||
|
||||
/**
|
||||
* Front-facing axis for this character model.
|
||||
* Most models face -Z (NEGATIVE_UNIT_Z), but some face +Z.
|
||||
*/
|
||||
Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
|
||||
CharacterSlotsComponent() = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global shape key weights for a character.
|
||||
* Same name applies to all slots uniformly.
|
||||
*/
|
||||
struct CharacterShapeKeysComponent {
|
||||
std::unordered_map<Ogre::String, float> weights;
|
||||
bool dirty = true;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTERSLOTS_HPP
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "CharacterSlots.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/CharacterSlotsEditor.hpp"
|
||||
#include "../systems/CharacterSlotSystem.hpp"
|
||||
#include "Transform.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Character Slots", "Rendering",
|
||||
CharacterSlotsComponent, CharacterSlotsEditor)
|
||||
{
|
||||
CharacterSlotSystem::loadCatalog();
|
||||
|
||||
registry.registerComponent<CharacterSlotsComponent>(
|
||||
"Character Slots",
|
||||
"Rendering",
|
||||
std::make_unique<CharacterSlotsEditor>(sceneMgr),
|
||||
/* Adder */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<TransformComponent>()) {
|
||||
TransformComponent transform;
|
||||
transform.node =
|
||||
sceneMgr->getRootSceneNode()
|
||||
->createChildSceneNode();
|
||||
e.set<TransformComponent>(transform);
|
||||
}
|
||||
CharacterSlotsComponent cs;
|
||||
cs.dirty = true;
|
||||
e.set<CharacterSlotsComponent>(cs);
|
||||
},
|
||||
/* Remover */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
(void)sceneMgr;
|
||||
e.remove<CharacterSlotsComponent>();
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef EDITSCENE_EDITORMARKER_HPP
|
||||
#define EDITSCENE_EDITORMARKER_HPP
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Marker component for entities created in the editor
|
||||
* Used to filter out flecs internal entities (components, systems, etc.)
|
||||
*/
|
||||
struct EditorMarkerComponent {
|
||||
// Empty marker component
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_EDITORMARKER_HPP
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef EDITSCENE_ENTITYNAME_HPP
|
||||
#define EDITSCENE_ENTITYNAME_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Name component for Flecs entities
|
||||
* Used for display in the hierarchy window
|
||||
*/
|
||||
struct EntityNameComponent {
|
||||
Ogre::String name;
|
||||
|
||||
EntityNameComponent() = default;
|
||||
|
||||
explicit EntityNameComponent(const Ogre::String &n)
|
||||
: name(n)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ENTITYNAME_HPP
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef EDITSCENE_EVENT_HANDLER_HPP
|
||||
#define EDITSCENE_EVENT_HANDLER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Event-driven behavior tree handler component.
|
||||
*
|
||||
* When the specified event is received, the referenced GoapAction's
|
||||
* behavior tree is executed for this entity. Event parameters
|
||||
* (EventParams) are injected into the entity's GoapBlackboard before
|
||||
* the tree runs and cleaned up when the tree completes.
|
||||
*/
|
||||
struct EventHandlerComponent {
|
||||
Ogre::String eventName;
|
||||
Ogre::String actionName;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_EVENT_HANDLER_HPP
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "EventHandler.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/EventHandlerEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Event Handler", "Game", EventHandlerComponent,
|
||||
EventHandlerEditor)
|
||||
{
|
||||
registry.registerComponent<EventHandlerComponent>(
|
||||
"Event Handler", "Game",
|
||||
std::make_unique<EventHandlerEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<EventHandlerComponent>())
|
||||
e.set<EventHandlerComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<EventHandlerComponent>())
|
||||
e.remove<EventHandlerComponent>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,739 @@
|
||||
#ifndef EDITSCENE_EVENT_PARAMS_HPP
|
||||
#define EDITSCENE_EVENT_PARAMS_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cassert>
|
||||
|
||||
/**
|
||||
* @file EventParams.hpp
|
||||
* @brief Tagged union type for event parameters.
|
||||
*
|
||||
* A C++11-compatible, RTTI-free tagged union that supports:
|
||||
* - Entity ID (uint64_t)
|
||||
* - Integer (int64_t)
|
||||
* - Float (float)
|
||||
* - Double (double)
|
||||
* - String (std::string)
|
||||
* - Array of entity IDs (std::vector<uint64_t>)
|
||||
* - Array of integers (std::vector<int64_t>)
|
||||
* - Array of floats (std::vector<float>)
|
||||
* - Array of doubles (std::vector<double>)
|
||||
* - Array of strings (std::vector<std::string>)
|
||||
*
|
||||
* Named parameters are stored as a map of string -> EventValue,
|
||||
* where EventValue is a tagged union of the above types.
|
||||
*/
|
||||
|
||||
// Forward declaration for friend function
|
||||
struct lua_State;
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EventValue: A single tagged-union value
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct EventValue {
|
||||
enum Type {
|
||||
NIL = 0,
|
||||
ENTITY_ID,
|
||||
INT,
|
||||
FLOAT,
|
||||
DOUBLE,
|
||||
STRING,
|
||||
ENTITY_ID_ARRAY,
|
||||
INT_ARRAY,
|
||||
FLOAT_ARRAY,
|
||||
DOUBLE_ARRAY,
|
||||
STRING_ARRAY
|
||||
};
|
||||
|
||||
Type type;
|
||||
|
||||
union {
|
||||
uint64_t asEntityId;
|
||||
int64_t asInt;
|
||||
float asFloat;
|
||||
double asDouble;
|
||||
};
|
||||
|
||||
// Heap-allocated data (strings and arrays)
|
||||
// We use raw pointers to avoid std::unique_ptr (C++11 compatible)
|
||||
std::string *strPtr;
|
||||
void *arrayPtr; // points to std::vector<T>*
|
||||
size_t arraySize;
|
||||
|
||||
EventValue()
|
||||
: type(NIL)
|
||||
, asEntityId(0)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(nullptr)
|
||||
, arraySize(0)
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(uint64_t entityId)
|
||||
: type(ENTITY_ID)
|
||||
, asEntityId(entityId)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(nullptr)
|
||||
, arraySize(0)
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(int64_t val)
|
||||
: type(INT)
|
||||
, asInt(val)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(nullptr)
|
||||
, arraySize(0)
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(int val)
|
||||
: type(INT)
|
||||
, asInt(static_cast<int64_t>(val))
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(nullptr)
|
||||
, arraySize(0)
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(float val)
|
||||
: type(FLOAT)
|
||||
, asFloat(val)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(nullptr)
|
||||
, arraySize(0)
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(double val)
|
||||
: type(DOUBLE)
|
||||
, asDouble(val)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(nullptr)
|
||||
, arraySize(0)
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(const std::string &val)
|
||||
: type(STRING)
|
||||
, asEntityId(0)
|
||||
, strPtr(new std::string(val))
|
||||
, arrayPtr(nullptr)
|
||||
, arraySize(0)
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(const char *val)
|
||||
: type(STRING)
|
||||
, asEntityId(0)
|
||||
, strPtr(new std::string(val ? val : ""))
|
||||
, arrayPtr(nullptr)
|
||||
, arraySize(0)
|
||||
{
|
||||
}
|
||||
|
||||
// Array constructors
|
||||
explicit EventValue(const std::vector<uint64_t> &arr)
|
||||
: type(ENTITY_ID_ARRAY)
|
||||
, asEntityId(0)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(new std::vector<uint64_t>(arr))
|
||||
, arraySize(arr.size())
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(const std::vector<int64_t> &arr)
|
||||
: type(INT_ARRAY)
|
||||
, asEntityId(0)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(new std::vector<int64_t>(arr))
|
||||
, arraySize(arr.size())
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(const std::vector<int> &arr)
|
||||
: type(INT_ARRAY)
|
||||
, asEntityId(0)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(new std::vector<int64_t>(arr.begin(), arr.end()))
|
||||
, arraySize(arr.size())
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(const std::vector<float> &arr)
|
||||
: type(FLOAT_ARRAY)
|
||||
, asEntityId(0)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(new std::vector<float>(arr))
|
||||
, arraySize(arr.size())
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(const std::vector<double> &arr)
|
||||
: type(DOUBLE_ARRAY)
|
||||
, asEntityId(0)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(new std::vector<double>(arr))
|
||||
, arraySize(arr.size())
|
||||
{
|
||||
}
|
||||
|
||||
explicit EventValue(const std::vector<std::string> &arr)
|
||||
: type(STRING_ARRAY)
|
||||
, asEntityId(0)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(new std::vector<std::string>(arr))
|
||||
, arraySize(arr.size())
|
||||
{
|
||||
}
|
||||
|
||||
// Copy constructor
|
||||
EventValue(const EventValue &other)
|
||||
: type(other.type)
|
||||
, asEntityId(other.asEntityId)
|
||||
, strPtr(nullptr)
|
||||
, arrayPtr(nullptr)
|
||||
, arraySize(other.arraySize)
|
||||
{
|
||||
copyHeapData(other);
|
||||
}
|
||||
|
||||
// Copy assignment
|
||||
EventValue &operator=(const EventValue &other)
|
||||
{
|
||||
if (this != &other) {
|
||||
destroyHeapData();
|
||||
type = other.type;
|
||||
asEntityId = other.asEntityId;
|
||||
arraySize = other.arraySize;
|
||||
strPtr = nullptr;
|
||||
arrayPtr = nullptr;
|
||||
copyHeapData(other);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Move constructor
|
||||
EventValue(EventValue &&other) noexcept : type(other.type),
|
||||
asEntityId(other.asEntityId),
|
||||
strPtr(other.strPtr),
|
||||
arrayPtr(other.arrayPtr),
|
||||
arraySize(other.arraySize)
|
||||
{
|
||||
other.type = NIL;
|
||||
other.strPtr = nullptr;
|
||||
other.arrayPtr = nullptr;
|
||||
other.arraySize = 0;
|
||||
}
|
||||
|
||||
// Move assignment
|
||||
EventValue &operator=(EventValue &&other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
destroyHeapData();
|
||||
type = other.type;
|
||||
asEntityId = other.asEntityId;
|
||||
strPtr = other.strPtr;
|
||||
arrayPtr = other.arrayPtr;
|
||||
arraySize = other.arraySize;
|
||||
other.type = NIL;
|
||||
other.strPtr = nullptr;
|
||||
other.arrayPtr = nullptr;
|
||||
other.arraySize = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~EventValue()
|
||||
{
|
||||
destroyHeapData();
|
||||
}
|
||||
|
||||
// --- Accessors ---
|
||||
|
||||
Type getType() const
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
uint64_t getEntityId() const
|
||||
{
|
||||
assert(type == ENTITY_ID);
|
||||
return asEntityId;
|
||||
}
|
||||
|
||||
int64_t getInt() const
|
||||
{
|
||||
assert(type == INT);
|
||||
return asInt;
|
||||
}
|
||||
|
||||
float getFloat() const
|
||||
{
|
||||
assert(type == FLOAT);
|
||||
return asFloat;
|
||||
}
|
||||
|
||||
double getDouble() const
|
||||
{
|
||||
assert(type == DOUBLE);
|
||||
return asDouble;
|
||||
}
|
||||
|
||||
const std::string &getString() const
|
||||
{
|
||||
assert(type == STRING && strPtr != nullptr);
|
||||
return *strPtr;
|
||||
}
|
||||
|
||||
const std::vector<uint64_t> &getEntityIdArray() const
|
||||
{
|
||||
assert(type == ENTITY_ID_ARRAY && arrayPtr != nullptr);
|
||||
return *static_cast<const std::vector<uint64_t> *>(arrayPtr);
|
||||
}
|
||||
|
||||
const std::vector<int64_t> &getIntArray() const
|
||||
{
|
||||
assert(type == INT_ARRAY && arrayPtr != nullptr);
|
||||
return *static_cast<const std::vector<int64_t> *>(arrayPtr);
|
||||
}
|
||||
|
||||
const std::vector<float> &getFloatArray() const
|
||||
{
|
||||
assert(type == FLOAT_ARRAY && arrayPtr != nullptr);
|
||||
return *static_cast<const std::vector<float> *>(arrayPtr);
|
||||
}
|
||||
|
||||
const std::vector<double> &getDoubleArray() const
|
||||
{
|
||||
assert(type == DOUBLE_ARRAY && arrayPtr != nullptr);
|
||||
return *static_cast<const std::vector<double> *>(arrayPtr);
|
||||
}
|
||||
|
||||
const std::vector<std::string> &getStringArray() const
|
||||
{
|
||||
assert(type == STRING_ARRAY && arrayPtr != nullptr);
|
||||
return *static_cast<const std::vector<std::string> *>(arrayPtr);
|
||||
}
|
||||
|
||||
// --- Convenience: get numeric value as double ---
|
||||
double asNumeric() const
|
||||
{
|
||||
switch (type) {
|
||||
case INT:
|
||||
return static_cast<double>(asInt);
|
||||
case FLOAT:
|
||||
return static_cast<double>(asFloat);
|
||||
case DOUBLE:
|
||||
return asDouble;
|
||||
case ENTITY_ID:
|
||||
return static_cast<double>(asEntityId);
|
||||
default:
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Equality ---
|
||||
bool operator==(const EventValue &other) const
|
||||
{
|
||||
if (type != other.type)
|
||||
return false;
|
||||
switch (type) {
|
||||
case NIL:
|
||||
return true;
|
||||
case ENTITY_ID:
|
||||
return asEntityId == other.asEntityId;
|
||||
case INT:
|
||||
return asInt == other.asInt;
|
||||
case FLOAT:
|
||||
return asFloat == other.asFloat;
|
||||
case DOUBLE:
|
||||
return asDouble == other.asDouble;
|
||||
case STRING:
|
||||
return strPtr && other.strPtr &&
|
||||
*strPtr == *other.strPtr;
|
||||
case ENTITY_ID_ARRAY:
|
||||
return arrayPtr && other.arrayPtr &&
|
||||
getEntityIdArray() == other.getEntityIdArray();
|
||||
case INT_ARRAY:
|
||||
return arrayPtr && other.arrayPtr &&
|
||||
getIntArray() == other.getIntArray();
|
||||
case FLOAT_ARRAY:
|
||||
return arrayPtr && other.arrayPtr &&
|
||||
getFloatArray() == other.getFloatArray();
|
||||
case DOUBLE_ARRAY:
|
||||
return arrayPtr && other.arrayPtr &&
|
||||
getDoubleArray() == other.getDoubleArray();
|
||||
case STRING_ARRAY:
|
||||
return arrayPtr && other.arrayPtr &&
|
||||
getStringArray() == other.getStringArray();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator!=(const EventValue &other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
private:
|
||||
void copyHeapData(const EventValue &other)
|
||||
{
|
||||
if (other.type == STRING && other.strPtr) {
|
||||
strPtr = new std::string(*other.strPtr);
|
||||
} else if (other.type == ENTITY_ID_ARRAY && other.arrayPtr) {
|
||||
arrayPtr = new std::vector<uint64_t>(
|
||||
*static_cast<const std::vector<uint64_t> *>(
|
||||
other.arrayPtr));
|
||||
} else if (other.type == INT_ARRAY && other.arrayPtr) {
|
||||
arrayPtr = new std::vector<int64_t>(
|
||||
*static_cast<const std::vector<int64_t> *>(
|
||||
other.arrayPtr));
|
||||
} else if (other.type == FLOAT_ARRAY && other.arrayPtr) {
|
||||
arrayPtr = new std::vector<float>(
|
||||
*static_cast<const std::vector<float> *>(
|
||||
other.arrayPtr));
|
||||
} else if (other.type == DOUBLE_ARRAY && other.arrayPtr) {
|
||||
arrayPtr = new std::vector<double>(
|
||||
*static_cast<const std::vector<double> *>(
|
||||
other.arrayPtr));
|
||||
} else if (other.type == STRING_ARRAY && other.arrayPtr) {
|
||||
arrayPtr = new std::vector<std::string>(
|
||||
*static_cast<const std::vector<std::string> *>(
|
||||
other.arrayPtr));
|
||||
}
|
||||
}
|
||||
|
||||
void destroyHeapData()
|
||||
{
|
||||
if (type == STRING) {
|
||||
delete strPtr;
|
||||
} else if (type == ENTITY_ID_ARRAY) {
|
||||
delete static_cast<std::vector<uint64_t> *>(arrayPtr);
|
||||
} else if (type == INT_ARRAY) {
|
||||
delete static_cast<std::vector<int64_t> *>(arrayPtr);
|
||||
} else if (type == FLOAT_ARRAY) {
|
||||
delete static_cast<std::vector<float> *>(arrayPtr);
|
||||
} else if (type == DOUBLE_ARRAY) {
|
||||
delete static_cast<std::vector<double> *>(arrayPtr);
|
||||
} else if (type == STRING_ARRAY) {
|
||||
delete static_cast<std::vector<std::string> *>(
|
||||
arrayPtr);
|
||||
}
|
||||
strPtr = nullptr;
|
||||
arrayPtr = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EventParams: A map of named EventValue entries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class EventParams {
|
||||
public:
|
||||
EventParams() = default;
|
||||
|
||||
// --- Set values ---
|
||||
|
||||
void setEntityId(const std::string &key, uint64_t val)
|
||||
{
|
||||
m_values[key] = EventValue(val);
|
||||
}
|
||||
|
||||
void setInt(const std::string &key, int64_t val)
|
||||
{
|
||||
m_values[key] = EventValue(val);
|
||||
}
|
||||
|
||||
void setFloat(const std::string &key, float val)
|
||||
{
|
||||
m_values[key] = EventValue(val);
|
||||
}
|
||||
|
||||
void setDouble(const std::string &key, double val)
|
||||
{
|
||||
m_values[key] = EventValue(val);
|
||||
}
|
||||
|
||||
void setString(const std::string &key, const std::string &val)
|
||||
{
|
||||
m_values[key] = EventValue(val);
|
||||
}
|
||||
|
||||
void setEntityIdArray(const std::string &key,
|
||||
const std::vector<uint64_t> &val)
|
||||
{
|
||||
m_values[key] = EventValue(val);
|
||||
}
|
||||
|
||||
void setIntArray(const std::string &key,
|
||||
const std::vector<int64_t> &val)
|
||||
{
|
||||
m_values[key] = EventValue(val);
|
||||
}
|
||||
|
||||
void setFloatArray(const std::string &key,
|
||||
const std::vector<float> &val)
|
||||
{
|
||||
m_values[key] = EventValue(val);
|
||||
}
|
||||
|
||||
void setDoubleArray(const std::string &key,
|
||||
const std::vector<double> &val)
|
||||
{
|
||||
m_values[key] = EventValue(val);
|
||||
}
|
||||
|
||||
void setStringArray(const std::string &key,
|
||||
const std::vector<std::string> &val)
|
||||
{
|
||||
m_values[key] = EventValue(val);
|
||||
}
|
||||
|
||||
// --- Get values ---
|
||||
|
||||
bool has(const std::string &key) const
|
||||
{
|
||||
return m_values.find(key) != m_values.end();
|
||||
}
|
||||
|
||||
const EventValue *get(const std::string &key) const
|
||||
{
|
||||
auto it = m_values.find(key);
|
||||
if (it != m_values.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EventValue *get(const std::string &key)
|
||||
{
|
||||
auto it = m_values.find(key);
|
||||
if (it != m_values.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// --- Typed getters with defaults ---
|
||||
|
||||
uint64_t getEntityId(const std::string &key,
|
||||
uint64_t defaultVal = 0) const
|
||||
{
|
||||
auto v = get(key);
|
||||
if (v && v->getType() == EventValue::ENTITY_ID)
|
||||
return v->getEntityId();
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
int64_t getInt(const std::string &key, int64_t defaultVal = 0) const
|
||||
{
|
||||
auto v = get(key);
|
||||
if (v && v->getType() == EventValue::INT)
|
||||
return v->getInt();
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
float getFloat(const std::string &key, float defaultVal = 0.0f) const
|
||||
{
|
||||
auto v = get(key);
|
||||
if (v && v->getType() == EventValue::FLOAT)
|
||||
return v->getFloat();
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
double getDouble(const std::string &key, double defaultVal = 0.0) const
|
||||
{
|
||||
auto v = get(key);
|
||||
if (v && v->getType() == EventValue::DOUBLE)
|
||||
return v->getDouble();
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
std::string getString(const std::string &key,
|
||||
const std::string &defaultVal = "") const
|
||||
{
|
||||
auto v = get(key);
|
||||
if (v && v->getType() == EventValue::STRING)
|
||||
return v->getString();
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
// --- Remove ---
|
||||
|
||||
void remove(const std::string &key)
|
||||
{
|
||||
m_values.erase(key);
|
||||
}
|
||||
|
||||
// --- Clear ---
|
||||
|
||||
void clear()
|
||||
{
|
||||
m_values.clear();
|
||||
}
|
||||
|
||||
// --- Size ---
|
||||
|
||||
size_t size() const
|
||||
{
|
||||
return m_values.size();
|
||||
}
|
||||
|
||||
bool empty() const
|
||||
{
|
||||
return m_values.empty();
|
||||
}
|
||||
|
||||
// --- Iteration ---
|
||||
|
||||
typedef std::unordered_map<std::string, EventValue>::const_iterator
|
||||
ConstIterator;
|
||||
typedef std::unordered_map<std::string, EventValue>::iterator Iterator;
|
||||
|
||||
ConstIterator begin() const
|
||||
{
|
||||
return m_values.begin();
|
||||
}
|
||||
ConstIterator end() const
|
||||
{
|
||||
return m_values.end();
|
||||
}
|
||||
Iterator begin()
|
||||
{
|
||||
return m_values.begin();
|
||||
}
|
||||
Iterator end()
|
||||
{
|
||||
return m_values.end();
|
||||
}
|
||||
|
||||
// --- Merge ---
|
||||
|
||||
void merge(const EventParams &other)
|
||||
{
|
||||
for (const auto &pair : other.m_values)
|
||||
m_values[pair.first] = pair.second;
|
||||
}
|
||||
|
||||
// --- Equality ---
|
||||
|
||||
bool operator==(const EventParams &other) const
|
||||
{
|
||||
return m_values == other.m_values;
|
||||
}
|
||||
|
||||
bool operator!=(const EventParams &other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
// --- Dump for debugging ---
|
||||
|
||||
std::string dump() const
|
||||
{
|
||||
std::string result = "EventParams:\n";
|
||||
for (const auto &pair : m_values) {
|
||||
result += " " + pair.first + " = ";
|
||||
switch (pair.second.getType()) {
|
||||
case EventValue::NIL:
|
||||
result += "nil";
|
||||
break;
|
||||
case EventValue::ENTITY_ID:
|
||||
result += "entity:" +
|
||||
std::to_string(
|
||||
pair.second.getEntityId());
|
||||
break;
|
||||
case EventValue::INT:
|
||||
result += std::to_string(pair.second.getInt());
|
||||
break;
|
||||
case EventValue::FLOAT:
|
||||
result +=
|
||||
std::to_string(pair.second.getFloat());
|
||||
break;
|
||||
case EventValue::DOUBLE:
|
||||
result +=
|
||||
std::to_string(pair.second.getDouble());
|
||||
break;
|
||||
case EventValue::STRING:
|
||||
result += "'" + pair.second.getString() + "'";
|
||||
break;
|
||||
case EventValue::ENTITY_ID_ARRAY: {
|
||||
result += "[";
|
||||
const auto &arr =
|
||||
pair.second.getEntityIdArray();
|
||||
for (size_t i = 0; i < arr.size(); i++) {
|
||||
if (i > 0)
|
||||
result += ", ";
|
||||
result += "e:" + std::to_string(arr[i]);
|
||||
}
|
||||
result += "]";
|
||||
break;
|
||||
}
|
||||
case EventValue::INT_ARRAY: {
|
||||
result += "[";
|
||||
const auto &arr = pair.second.getIntArray();
|
||||
for (size_t i = 0; i < arr.size(); i++) {
|
||||
if (i > 0)
|
||||
result += ", ";
|
||||
result += std::to_string(arr[i]);
|
||||
}
|
||||
result += "]";
|
||||
break;
|
||||
}
|
||||
case EventValue::FLOAT_ARRAY: {
|
||||
result += "[";
|
||||
const auto &arr = pair.second.getFloatArray();
|
||||
for (size_t i = 0; i < arr.size(); i++) {
|
||||
if (i > 0)
|
||||
result += ", ";
|
||||
result += std::to_string(arr[i]);
|
||||
}
|
||||
result += "]";
|
||||
break;
|
||||
}
|
||||
case EventValue::DOUBLE_ARRAY: {
|
||||
result += "[";
|
||||
const auto &arr = pair.second.getDoubleArray();
|
||||
for (size_t i = 0; i < arr.size(); i++) {
|
||||
if (i > 0)
|
||||
result += ", ";
|
||||
result += std::to_string(arr[i]);
|
||||
}
|
||||
result += "]";
|
||||
break;
|
||||
}
|
||||
case EventValue::STRING_ARRAY: {
|
||||
result += "[";
|
||||
const auto &arr = pair.second.getStringArray();
|
||||
for (size_t i = 0; i < arr.size(); i++) {
|
||||
if (i > 0)
|
||||
result += ", ";
|
||||
result += "'" + arr[i] + "'";
|
||||
}
|
||||
result += "]";
|
||||
break;
|
||||
}
|
||||
}
|
||||
result += "\n";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Allow LuaEventApi to access m_values directly for efficiency
|
||||
friend EventParams readEventParams(lua_State *L, int idx);
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, EventValue> m_values;
|
||||
};
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_EVENT_PARAMS_HPP
|
||||
@@ -0,0 +1,192 @@
|
||||
#include "Formula.hpp"
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <OgreLogManager.h>
|
||||
|
||||
Formula::Formula(const Ogre::String &expr)
|
||||
: m_expression(expr)
|
||||
{
|
||||
m_valid = !expr.empty();
|
||||
}
|
||||
|
||||
double Formula::evaluate(const std::unordered_map<std::string, double> &vars) const
|
||||
{
|
||||
if (!m_valid || m_expression.empty())
|
||||
return 0.0;
|
||||
|
||||
Parser p;
|
||||
p.s = m_expression.c_str();
|
||||
p.vars = &vars;
|
||||
try {
|
||||
return p.parseExpression();
|
||||
} catch (...) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
double Formula::evaluate(int level) const
|
||||
{
|
||||
std::unordered_map<std::string, double> vars;
|
||||
vars["level"] = static_cast<double>(level);
|
||||
return evaluate(vars);
|
||||
}
|
||||
|
||||
double Formula::evaluate(int level, double current) const
|
||||
{
|
||||
std::unordered_map<std::string, double> vars;
|
||||
vars["level"] = static_cast<double>(level);
|
||||
vars["current"] = current;
|
||||
return evaluate(vars);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parser implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void Formula::Parser::skipWhitespace()
|
||||
{
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
}
|
||||
|
||||
bool Formula::Parser::consume(char c)
|
||||
{
|
||||
skipWhitespace();
|
||||
if (*s == c) {
|
||||
s++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
double Formula::Parser::parseExpression()
|
||||
{
|
||||
double value = parseTerm();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (consume('+')) {
|
||||
value += parseTerm();
|
||||
} else if (consume('-')) {
|
||||
value -= parseTerm();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
double Formula::Parser::parseTerm()
|
||||
{
|
||||
double value = parsePower();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (consume('*')) {
|
||||
value *= parsePower();
|
||||
} else if (consume('/')) {
|
||||
double rhs = parsePower();
|
||||
if (rhs != 0.0)
|
||||
value /= rhs;
|
||||
else
|
||||
value = 0.0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
double Formula::Parser::parsePower()
|
||||
{
|
||||
double value = parseUnary();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (consume('^')) {
|
||||
value = std::pow(value, parseUnary());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
double Formula::Parser::parseUnary()
|
||||
{
|
||||
skipWhitespace();
|
||||
if (consume('+'))
|
||||
return parseUnary();
|
||||
if (consume('-'))
|
||||
return -parseUnary();
|
||||
return parsePrimary();
|
||||
}
|
||||
|
||||
double Formula::Parser::parsePrimary()
|
||||
{
|
||||
skipWhitespace();
|
||||
|
||||
// Number
|
||||
if (std::isdigit(*s) || (*s == '.' && std::isdigit(*(s + 1)))) {
|
||||
char *end = nullptr;
|
||||
double val = std::strtod(s, &end);
|
||||
s = end;
|
||||
return val;
|
||||
}
|
||||
|
||||
// Parenthesized expression
|
||||
if (consume('(')) {
|
||||
double val = parseExpression();
|
||||
skipWhitespace();
|
||||
if (!consume(')'))
|
||||
return 0.0;
|
||||
return val;
|
||||
}
|
||||
|
||||
// Identifier (variable or function)
|
||||
if (std::isalpha(*s) || *s == '_') {
|
||||
const char *start = s;
|
||||
while (std::isalnum(*s) || *s == '_')
|
||||
s++;
|
||||
std::string name(start, static_cast<size_t>(s - start));
|
||||
|
||||
skipWhitespace();
|
||||
if (consume('(')) {
|
||||
// Function call
|
||||
std::vector<double> args;
|
||||
if (!consume(')')) {
|
||||
args.push_back(parseExpression());
|
||||
while (consume(',')) {
|
||||
args.push_back(parseExpression());
|
||||
}
|
||||
skipWhitespace();
|
||||
if (!consume(')'))
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if (name == "floor" && args.size() >= 1)
|
||||
return std::floor(args[0]);
|
||||
if (name == "ceil" && args.size() >= 1)
|
||||
return std::ceil(args[0]);
|
||||
if (name == "min" && args.size() >= 2)
|
||||
return args[0] < args[1] ? args[0] : args[1];
|
||||
if (name == "max" && args.size() >= 2)
|
||||
return args[0] > args[1] ? args[0] : args[1];
|
||||
if (name == "clamp" && args.size() >= 3) {
|
||||
if (args[0] < args[1])
|
||||
return args[1];
|
||||
if (args[0] > args[2])
|
||||
return args[2];
|
||||
return args[0];
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Variable lookup
|
||||
if (vars) {
|
||||
auto it = vars->find(name);
|
||||
if (it != vars->end())
|
||||
return it->second;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifndef EDITSCENE_FORMULA_HPP
|
||||
#define EDITSCENE_FORMULA_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* Lightweight expression evaluator for RPG formulas.
|
||||
*
|
||||
* Supports:
|
||||
* Variables: level, current, base, value
|
||||
* Operators: +, -, *, /, ^, ()
|
||||
* Functions: floor(x), ceil(x), min(a,b), max(a,b), clamp(x,lo,hi)
|
||||
*
|
||||
* Example: "level * level * 100 + floor(current / 10)"
|
||||
*/
|
||||
class Formula {
|
||||
public:
|
||||
Formula() = default;
|
||||
explicit Formula(const Ogre::String &expr);
|
||||
|
||||
bool isValid() const { return m_valid; }
|
||||
const Ogre::String &getExpression() const { return m_expression; }
|
||||
|
||||
/**
|
||||
* Evaluate the formula with given variable bindings.
|
||||
*
|
||||
* @param vars Map of variable names to values.
|
||||
* @return The computed result. Returns 0.0 if formula is invalid.
|
||||
*/
|
||||
double evaluate(const std::unordered_map<std::string, double> &vars) const;
|
||||
|
||||
/**
|
||||
* Convenience: evaluate with just a level.
|
||||
*/
|
||||
double evaluate(int level) const;
|
||||
|
||||
/**
|
||||
* Convenience: evaluate with level and current value.
|
||||
*/
|
||||
double evaluate(int level, double current) const;
|
||||
|
||||
private:
|
||||
Ogre::String m_expression;
|
||||
bool m_valid = false;
|
||||
|
||||
// Recursive descent parser
|
||||
struct Parser {
|
||||
const char *s;
|
||||
const std::unordered_map<std::string, double> *vars;
|
||||
|
||||
double parseExpression();
|
||||
double parseTerm();
|
||||
double parsePower();
|
||||
double parseUnary();
|
||||
double parsePrimary();
|
||||
|
||||
void skipWhitespace();
|
||||
bool consume(char c);
|
||||
};
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_FORMULA_HPP
|
||||
@@ -0,0 +1,11 @@
|
||||
#ifndef EDITSCENE_GENERATEDPHYSICSTAG_HPP
|
||||
#define EDITSCENE_GENERATEDPHYSICSTAG_HPP
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Marker component for entities auto-generated by systems (e.g. physics colliders).
|
||||
* Entities with this tag are hidden from the editor hierarchy and not serialized.
|
||||
*/
|
||||
struct GeneratedPhysicsTag {};
|
||||
|
||||
#endif // EDITSCENE_GENERATEDPHYSICSTAG_HPP
|
||||
@@ -0,0 +1,74 @@
|
||||
#ifndef EDITSCENE_GOAP_ACTION_HPP
|
||||
#define EDITSCENE_GOAP_ACTION_HPP
|
||||
#pragma once
|
||||
|
||||
#include "GoapBlackboard.hpp"
|
||||
#include "BehaviorTree.hpp"
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* A GOAP action definition.
|
||||
*
|
||||
* Actions live in the ActionDatabase and can be executed by any character.
|
||||
* Each action has preconditions (required blackboard state),
|
||||
* effects (resulting blackboard state), a cost, and a behavior tree.
|
||||
*/
|
||||
struct GoapAction {
|
||||
Ogre::String name;
|
||||
int cost = 1;
|
||||
|
||||
// GOAP preconditions and effects
|
||||
GoapBlackboard preconditions;
|
||||
GoapBlackboard effects;
|
||||
|
||||
// Bitmask for precondition checking. Only bits set here are compared.
|
||||
// Defaults to all 1s (check all bits).
|
||||
uint64_t preconditionMask = ~0ULL;
|
||||
|
||||
// Behavior tree to execute when this action is selected
|
||||
BehaviorTreeNode behaviorTree;
|
||||
|
||||
// Optional: reference to a named behavior tree asset
|
||||
Ogre::String behaviorTreeName;
|
||||
|
||||
GoapAction() = default;
|
||||
|
||||
explicit GoapAction(const Ogre::String &name_, int cost_ = 1)
|
||||
: name(name_)
|
||||
, cost(cost_)
|
||||
{
|
||||
}
|
||||
|
||||
// Check if the given blackboard satisfies this action's preconditions.
|
||||
// Only bits in preconditionMask are compared.
|
||||
bool canRun(const GoapBlackboard &blackboard) const
|
||||
{
|
||||
// Fast-path: if mask covers all set bits, use standard check
|
||||
if (preconditionMask == ~0ULL)
|
||||
return blackboard.satisfies(preconditions);
|
||||
|
||||
// Masked check: only compare bits in preconditionMask
|
||||
uint64_t relevantBits = preconditionMask;
|
||||
uint64_t bbBits = blackboard.bits & relevantBits;
|
||||
uint64_t preBits = preconditions.bits & relevantBits;
|
||||
uint64_t bbMask = blackboard.mask & relevantBits;
|
||||
uint64_t preMask = preconditions.mask & relevantBits;
|
||||
|
||||
// All precondition bits must be present in blackboard
|
||||
if ((bbMask & preMask) != preMask)
|
||||
return false;
|
||||
if ((bbBits & preMask) != preBits)
|
||||
return false;
|
||||
|
||||
// Check integer values
|
||||
for (const auto &kv : preconditions.values) {
|
||||
if (!blackboard.hasValue(kv.first))
|
||||
return false;
|
||||
if (blackboard.getValue(kv.first) != kv.second)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_ACTION_HPP
|
||||
@@ -0,0 +1,227 @@
|
||||
#include "GoapBlackboard.hpp"
|
||||
#include <cstdlib>
|
||||
|
||||
std::array<std::string, 64> &GoapBlackboard::getBitNameRegistry()
|
||||
{
|
||||
static std::array<std::string, 64> registry;
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
for (auto &s : registry)
|
||||
s.clear();
|
||||
initialized = true;
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
void GoapBlackboard::setBitName(int index, const std::string &name)
|
||||
{
|
||||
if (index < 0 || index >= 64)
|
||||
return;
|
||||
getBitNameRegistry()[index] = name;
|
||||
}
|
||||
|
||||
const char *GoapBlackboard::getBitName(int index)
|
||||
{
|
||||
if (index < 0 || index >= 64)
|
||||
return nullptr;
|
||||
const auto &name = getBitNameRegistry()[index];
|
||||
return name.empty() ? nullptr : name.c_str();
|
||||
}
|
||||
|
||||
int GoapBlackboard::findBitByName(const std::string &name)
|
||||
{
|
||||
if (name.empty())
|
||||
return -1;
|
||||
const auto ®istry = getBitNameRegistry();
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (registry[i] == name)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool GoapBlackboard::satisfies(const GoapBlackboard &other) const
|
||||
{
|
||||
// Only compare bits that both sides consider relevant
|
||||
uint64_t relevantMask = bitmask & other.bitmask;
|
||||
|
||||
// Check bits: for every bit set in other's mask (within relevant mask),
|
||||
// our bit must match
|
||||
uint64_t commonMask = mask & other.mask & relevantMask;
|
||||
if ((bits & commonMask) != (other.bits & commonMask))
|
||||
return false;
|
||||
|
||||
// Also check bits that other has set but we don't (within relevant mask)
|
||||
uint64_t missingMask = other.mask & ~mask & relevantMask;
|
||||
if (missingMask) {
|
||||
// Other requires bits we don't have set -> fail
|
||||
// But only if other has those bits set to 1
|
||||
if ((other.bits & missingMask) != 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check values: for every key in other, our value must match
|
||||
for (const auto &pair : other.values) {
|
||||
auto it = values.find(pair.first);
|
||||
if (it == values.end()) {
|
||||
// If we don't have the key, only satisfy if target is 0
|
||||
if (pair.second != 0)
|
||||
return false;
|
||||
} else if (it->second != pair.second) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GoapBlackboard::apply(const GoapBlackboard &other)
|
||||
{
|
||||
// Apply bit effects
|
||||
bits = (bits & ~other.mask) | (other.bits & other.mask);
|
||||
mask |= other.mask;
|
||||
|
||||
// Apply value effects
|
||||
for (const auto &pair : other.values)
|
||||
values[pair.first] = pair.second;
|
||||
}
|
||||
|
||||
bool GoapBlackboard::getScalarValue(const std::string &key,
|
||||
float &out) const
|
||||
{
|
||||
auto itf = floatValues.find(key);
|
||||
if (itf != floatValues.end()) {
|
||||
out = itf->second;
|
||||
return true;
|
||||
}
|
||||
auto iti = values.find(key);
|
||||
if (iti != values.end()) {
|
||||
out = static_cast<float>(iti->second);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int GoapBlackboard::distanceTo(const GoapBlackboard &target,
|
||||
bool ignoreValues) const
|
||||
{
|
||||
int distance = 0;
|
||||
|
||||
// Only compare bits that both sides consider relevant
|
||||
uint64_t relevantMask = bitmask & target.bitmask;
|
||||
|
||||
// Bit differences (within relevant mask)
|
||||
uint64_t commonMask = mask & target.mask & relevantMask;
|
||||
distance += __builtin_popcountll((bits ^ target.bits) & commonMask);
|
||||
|
||||
// Bits target cares about but we don't have (within relevant mask)
|
||||
uint64_t missingInUs = target.mask & ~mask & relevantMask;
|
||||
distance += __builtin_popcountll(target.bits & missingInUs);
|
||||
|
||||
// Bits we care about but target doesn't (within relevant mask)
|
||||
uint64_t missingInTarget = mask & ~target.mask & relevantMask;
|
||||
distance += __builtin_popcountll(bits & missingInTarget);
|
||||
|
||||
if (ignoreValues)
|
||||
return distance;
|
||||
|
||||
// Value differences (int only — planner ignores float/vec3)
|
||||
for (const auto &pair : target.values) {
|
||||
auto it = values.find(pair.first);
|
||||
if (it == values.end())
|
||||
distance += std::abs(pair.second);
|
||||
else
|
||||
distance += std::abs(it->second - pair.second);
|
||||
}
|
||||
|
||||
// Values we have that target doesn't (may need to be cleared)
|
||||
for (const auto &pair : values) {
|
||||
if (target.values.find(pair.first) == target.values.end())
|
||||
distance += std::abs(pair.second);
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
Ogre::String GoapBlackboard::dump() const
|
||||
{
|
||||
Ogre::String result = "Blackboard:\n";
|
||||
|
||||
result += " Bits:\n";
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (hasBit(i)) {
|
||||
const char *name = getBitName(i);
|
||||
if (name)
|
||||
result += " " + Ogre::String(name) + " = " +
|
||||
(getBit(i) ? "true" : "false") + "\n";
|
||||
else
|
||||
result += " bit[" +
|
||||
Ogre::StringConverter::toString(i) + "] = " +
|
||||
(getBit(i) ? "true" : "false") + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!values.empty()) {
|
||||
result += " Int values:\n";
|
||||
for (const auto &pair : values)
|
||||
result += " " + pair.first + " = " +
|
||||
Ogre::StringConverter::toString(pair.second) +
|
||||
"\n";
|
||||
}
|
||||
|
||||
if (!floatValues.empty()) {
|
||||
result += " Float values:\n";
|
||||
for (const auto &pair : floatValues)
|
||||
result += " " + pair.first + " = " +
|
||||
Ogre::StringConverter::toString(pair.second) +
|
||||
"\n";
|
||||
}
|
||||
|
||||
if (!vec3Values.empty()) {
|
||||
result += " Vec3 values:\n";
|
||||
for (const auto &pair : vec3Values)
|
||||
result += " " + pair.first + " = (" +
|
||||
Ogre::StringConverter::toString(pair.second.x) +
|
||||
", " +
|
||||
Ogre::StringConverter::toString(pair.second.y) +
|
||||
", " +
|
||||
Ogre::StringConverter::toString(pair.second.z) +
|
||||
")\n";
|
||||
}
|
||||
|
||||
if (!stringValues.empty()) {
|
||||
result += " String values:\n";
|
||||
for (const auto &pair : stringValues)
|
||||
result += " " + pair.first + " = " + pair.second + "\n";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void GoapBlackboard::merge(const GoapBlackboard &other)
|
||||
{
|
||||
// Merge bits
|
||||
bits = (bits & ~other.mask) | (other.bits & other.mask);
|
||||
mask |= other.mask;
|
||||
|
||||
// Merge values
|
||||
for (const auto &pair : other.values)
|
||||
values[pair.first] = pair.second;
|
||||
for (const auto &pair : other.floatValues)
|
||||
floatValues[pair.first] = pair.second;
|
||||
for (const auto &pair : other.vec3Values)
|
||||
vec3Values[pair.first] = pair.second;
|
||||
for (const auto &pair : other.stringValues)
|
||||
stringValues[pair.first] = pair.second;
|
||||
}
|
||||
|
||||
std::vector<int> GoapBlackboard::getSetBits() const
|
||||
{
|
||||
std::vector<int> result;
|
||||
uint64_t m = mask;
|
||||
while (m) {
|
||||
int bit = __builtin_ctzll(m);
|
||||
result.push_back(bit);
|
||||
m &= m - 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
#ifndef EDITSCENE_GOAP_BLACKBOARD_HPP
|
||||
#define EDITSCENE_GOAP_BLACKBOARD_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Lightweight GOAP blackboard for action preconditions, effects,
|
||||
* and per-character runtime state.
|
||||
*
|
||||
* Uses a 64-bit bitfield for fast boolean flag checks (used by GOAP planner).
|
||||
* Supports int, float, and Vector3 values (int is used for preconditions/effects;
|
||||
* float/vec3 are for behavior-tree-driven character state).
|
||||
*/
|
||||
struct GoapBlackboard {
|
||||
// Boolean flags: 64 bits available. These are used by the GOAP planner.
|
||||
uint64_t bits = 0;
|
||||
uint64_t mask = 0; // which bits are actually set
|
||||
|
||||
// Bitmask for comparison: only bits set here are compared.
|
||||
// Defaults to all 1s (compare all bits).
|
||||
uint64_t bitmask = ~0ULL;
|
||||
|
||||
// Named integer values (health, hunger, etc.) — used by preconditions/effects
|
||||
std::unordered_map<std::string, int> values;
|
||||
|
||||
// Named float values — runtime character state
|
||||
std::unordered_map<std::string, float> floatValues;
|
||||
|
||||
// Named Vector3 values — runtime character state
|
||||
std::unordered_map<std::string, Ogre::Vector3> vec3Values;
|
||||
|
||||
// Named string values — event params, tags, etc.
|
||||
std::unordered_map<std::string, Ogre::String> stringValues;
|
||||
|
||||
GoapBlackboard() = default;
|
||||
|
||||
/* --- Bit accessors --- */
|
||||
void setBit(int index, bool value)
|
||||
{
|
||||
if (index < 0 || index >= 64)
|
||||
return;
|
||||
uint64_t bit = 1ULL << index;
|
||||
mask |= bit;
|
||||
if (value)
|
||||
bits |= bit;
|
||||
else
|
||||
bits &= ~bit;
|
||||
}
|
||||
|
||||
bool getBit(int index) const
|
||||
{
|
||||
if (index < 0 || index >= 64)
|
||||
return false;
|
||||
return (bits >> index) & 1ULL;
|
||||
}
|
||||
|
||||
bool hasBit(int index) const
|
||||
{
|
||||
if (index < 0 || index >= 64)
|
||||
return false;
|
||||
return (mask >> index) & 1ULL;
|
||||
}
|
||||
|
||||
void clearBit(int index)
|
||||
{
|
||||
if (index < 0 || index >= 64)
|
||||
return;
|
||||
uint64_t bit = 1ULL << index;
|
||||
mask &= ~bit;
|
||||
bits &= ~bit;
|
||||
}
|
||||
|
||||
/* --- Integer value accessors (backward compat) --- */
|
||||
void setValue(const std::string &key, int value)
|
||||
{
|
||||
values[key] = value;
|
||||
}
|
||||
|
||||
int getValue(const std::string &key, int defaultValue = 0) const
|
||||
{
|
||||
auto it = values.find(key);
|
||||
if (it != values.end())
|
||||
return it->second;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool hasValue(const std::string &key) const
|
||||
{
|
||||
return values.find(key) != values.end();
|
||||
}
|
||||
|
||||
void removeValue(const std::string &key)
|
||||
{
|
||||
values.erase(key);
|
||||
}
|
||||
|
||||
/* --- Float value accessors --- */
|
||||
void setFloatValue(const std::string &key, float value)
|
||||
{
|
||||
floatValues[key] = value;
|
||||
}
|
||||
|
||||
float getFloatValue(const std::string &key,
|
||||
float defaultValue = 0.0f) const
|
||||
{
|
||||
auto it = floatValues.find(key);
|
||||
if (it != floatValues.end())
|
||||
return it->second;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool hasFloatValue(const std::string &key) const
|
||||
{
|
||||
return floatValues.find(key) != floatValues.end();
|
||||
}
|
||||
|
||||
void removeFloatValue(const std::string &key)
|
||||
{
|
||||
floatValues.erase(key);
|
||||
}
|
||||
|
||||
/* --- Vector3 value accessors --- */
|
||||
void setVec3Value(const std::string &key, const Ogre::Vector3 &value)
|
||||
{
|
||||
vec3Values[key] = value;
|
||||
}
|
||||
|
||||
Ogre::Vector3 getVec3Value(
|
||||
const std::string &key,
|
||||
const Ogre::Vector3 &defaultValue = Ogre::Vector3::ZERO) const
|
||||
{
|
||||
auto it = vec3Values.find(key);
|
||||
if (it != vec3Values.end())
|
||||
return it->second;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool hasVec3Value(const std::string &key) const
|
||||
{
|
||||
return vec3Values.find(key) != vec3Values.end();
|
||||
}
|
||||
|
||||
void removeVec3Value(const std::string &key)
|
||||
{
|
||||
vec3Values.erase(key);
|
||||
}
|
||||
|
||||
/* --- String value accessors --- */
|
||||
void setStringValue(const std::string &key, const Ogre::String &value)
|
||||
{
|
||||
stringValues[key] = value;
|
||||
}
|
||||
|
||||
Ogre::String getStringValue(const std::string &key,
|
||||
const Ogre::String &defaultValue = "") const
|
||||
{
|
||||
auto it = stringValues.find(key);
|
||||
if (it != stringValues.end())
|
||||
return it->second;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool hasStringValue(const std::string &key) const
|
||||
{
|
||||
return stringValues.find(key) != stringValues.end();
|
||||
}
|
||||
|
||||
void removeStringValue(const std::string &key)
|
||||
{
|
||||
stringValues.erase(key);
|
||||
}
|
||||
|
||||
/* --- Merge another blackboard into this one --- */
|
||||
void merge(const GoapBlackboard &other);
|
||||
|
||||
/* --- Generic scalar lookup (tries int then float) --- */
|
||||
bool getScalarValue(const std::string &key, float &out) const;
|
||||
|
||||
/* --- GOAP methods --- */
|
||||
bool satisfies(const GoapBlackboard &other) const;
|
||||
void apply(const GoapBlackboard &other);
|
||||
int distanceTo(const GoapBlackboard &target,
|
||||
bool ignoreValues = false) const;
|
||||
|
||||
/* --- Utility --- */
|
||||
bool isValid() const
|
||||
{
|
||||
return mask != 0 || !values.empty() || !floatValues.empty() ||
|
||||
!vec3Values.empty();
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
bits = 0;
|
||||
mask = 0;
|
||||
values.clear();
|
||||
floatValues.clear();
|
||||
vec3Values.clear();
|
||||
stringValues.clear();
|
||||
}
|
||||
|
||||
Ogre::String dump() const;
|
||||
|
||||
// Bit naming: global registry for human-readable bit names
|
||||
static std::array<std::string, 64> &getBitNameRegistry();
|
||||
static void setBitName(int index, const std::string &name);
|
||||
static const char *getBitName(int index);
|
||||
static int findBitByName(const std::string &name);
|
||||
|
||||
// List all set bit indices
|
||||
std::vector<int> getSetBits() const;
|
||||
|
||||
// Equality
|
||||
bool operator==(const GoapBlackboard &other) const
|
||||
{
|
||||
return bits == other.bits && mask == other.mask &&
|
||||
bitmask == other.bitmask &&
|
||||
values == other.values &&
|
||||
floatValues == other.floatValues &&
|
||||
vec3Values == other.vec3Values &&
|
||||
stringValues == other.stringValues;
|
||||
}
|
||||
|
||||
bool operator!=(const GoapBlackboard &other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_BLACKBOARD_HPP
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "GoapBlackboard.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/GoapBlackboardComponentEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Blackboard", "AI", GoapBlackboard,
|
||||
GoapBlackboardComponentEditor)
|
||||
{
|
||||
registry.registerComponent<GoapBlackboard>(
|
||||
"Blackboard", "AI",
|
||||
std::make_unique<GoapBlackboardComponentEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<GoapBlackboard>())
|
||||
e.set<GoapBlackboard>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<GoapBlackboard>())
|
||||
e.remove<GoapBlackboard>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
#include "GoapExpression.hpp"
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
void GoapExpression::skipWhitespace()
|
||||
{
|
||||
while (*m_pos == ' ' || *m_pos == '\t' || *m_pos == '\n' ||
|
||||
*m_pos == '\r')
|
||||
m_pos++;
|
||||
}
|
||||
|
||||
bool GoapExpression::match(const char *s)
|
||||
{
|
||||
skipWhitespace();
|
||||
size_t len = strlen(s);
|
||||
if (strncmp(m_pos, s, len) == 0) {
|
||||
m_pos += len;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GoapExpression::Node *GoapExpression::parsePrimary()
|
||||
{
|
||||
skipWhitespace();
|
||||
|
||||
// Parenthesized expression
|
||||
if (match("(")) {
|
||||
Node *node = parseExpression();
|
||||
if (!node)
|
||||
return nullptr;
|
||||
if (!match(")")) {
|
||||
setError("Expected ')'");
|
||||
delete node;
|
||||
return nullptr;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// Integer literal
|
||||
if (isdigit(*m_pos) || (*m_pos == '-' && isdigit(m_pos[1]))) {
|
||||
bool negative = false;
|
||||
if (*m_pos == '-') {
|
||||
negative = true;
|
||||
m_pos++;
|
||||
}
|
||||
int value = 0;
|
||||
while (isdigit(*m_pos)) {
|
||||
value = value * 10 + (*m_pos - '0');
|
||||
m_pos++;
|
||||
}
|
||||
Node *node = new Node(Node::Value);
|
||||
node->value = negative ? -value : value;
|
||||
return node;
|
||||
}
|
||||
|
||||
// Variable name
|
||||
if (isalpha(*m_pos) || *m_pos == '_') {
|
||||
std::string name;
|
||||
while (isalnum(*m_pos) || *m_pos == '_') {
|
||||
name += *m_pos;
|
||||
m_pos++;
|
||||
}
|
||||
Node *node = new Node(Node::Variable);
|
||||
node->name = name;
|
||||
return node;
|
||||
}
|
||||
|
||||
setError("Unexpected character in expression");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GoapExpression::Node *GoapExpression::parseComparison()
|
||||
{
|
||||
Node *left = parsePrimary();
|
||||
if (!left)
|
||||
return nullptr;
|
||||
|
||||
skipWhitespace();
|
||||
if (match("==")) {
|
||||
Node *right = parsePrimary();
|
||||
if (!right) {
|
||||
delete left;
|
||||
return nullptr;
|
||||
}
|
||||
Node *node = new Node(Node::Equal);
|
||||
node->left = left;
|
||||
node->right = right;
|
||||
return node;
|
||||
} else if (match("!=")) {
|
||||
Node *right = parsePrimary();
|
||||
if (!right) {
|
||||
delete left;
|
||||
return nullptr;
|
||||
}
|
||||
Node *node = new Node(Node::NotEqual);
|
||||
node->left = left;
|
||||
node->right = right;
|
||||
return node;
|
||||
} else if (match("<=")) {
|
||||
Node *right = parsePrimary();
|
||||
if (!right) {
|
||||
delete left;
|
||||
return nullptr;
|
||||
}
|
||||
Node *node = new Node(Node::LessEqual);
|
||||
node->left = left;
|
||||
node->right = right;
|
||||
return node;
|
||||
} else if (match(">=")) {
|
||||
Node *right = parsePrimary();
|
||||
if (!right) {
|
||||
delete left;
|
||||
return nullptr;
|
||||
}
|
||||
Node *node = new Node(Node::GreaterEqual);
|
||||
node->left = left;
|
||||
node->right = right;
|
||||
return node;
|
||||
} else if (match("<")) {
|
||||
Node *right = parsePrimary();
|
||||
if (!right) {
|
||||
delete left;
|
||||
return nullptr;
|
||||
}
|
||||
Node *node = new Node(Node::Less);
|
||||
node->left = left;
|
||||
node->right = right;
|
||||
return node;
|
||||
} else if (match(">")) {
|
||||
Node *right = parsePrimary();
|
||||
if (!right) {
|
||||
delete left;
|
||||
return nullptr;
|
||||
}
|
||||
Node *node = new Node(Node::Greater);
|
||||
node->left = left;
|
||||
node->right = right;
|
||||
return node;
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
GoapExpression::Node *GoapExpression::parseNot()
|
||||
{
|
||||
skipWhitespace();
|
||||
if (match("!")) {
|
||||
Node *child = parseNot();
|
||||
if (!child)
|
||||
return nullptr;
|
||||
Node *node = new Node(Node::Not);
|
||||
node->left = child;
|
||||
return node;
|
||||
}
|
||||
return parseComparison();
|
||||
}
|
||||
|
||||
GoapExpression::Node *GoapExpression::parseAnd()
|
||||
{
|
||||
Node *left = parseNot();
|
||||
if (!left)
|
||||
return nullptr;
|
||||
|
||||
while (true) {
|
||||
if (match("&&")) {
|
||||
Node *right = parseNot();
|
||||
if (!right) {
|
||||
delete left;
|
||||
return nullptr;
|
||||
}
|
||||
Node *node = new Node(Node::And);
|
||||
node->left = left;
|
||||
node->right = right;
|
||||
left = node;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
GoapExpression::Node *GoapExpression::parseExpression()
|
||||
{
|
||||
Node *left = parseAnd();
|
||||
if (!left)
|
||||
return nullptr;
|
||||
|
||||
while (true) {
|
||||
if (match("||")) {
|
||||
Node *right = parseAnd();
|
||||
if (!right) {
|
||||
delete left;
|
||||
return nullptr;
|
||||
}
|
||||
Node *node = new Node(Node::Or);
|
||||
node->left = left;
|
||||
node->right = right;
|
||||
left = node;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
bool GoapExpression::parse(const char *expr)
|
||||
{
|
||||
clear();
|
||||
m_expr = expr;
|
||||
m_pos = expr;
|
||||
m_root = parseExpression();
|
||||
if (!m_root)
|
||||
return false;
|
||||
skipWhitespace();
|
||||
if (*m_pos != '\0') {
|
||||
setError("Unexpected trailing characters");
|
||||
clear();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int GoapExpression::evalNode(Node *node, const GoapBlackboard &bb) const
|
||||
{
|
||||
if (!node)
|
||||
return 0;
|
||||
|
||||
switch (node->type) {
|
||||
case Node::Value:
|
||||
return node->value;
|
||||
case Node::Variable:
|
||||
return bb.getValue(node->name, 0);
|
||||
case Node::Equal:
|
||||
return evalNode(node->left, bb) == evalNode(node->right, bb) ? 1 : 0;
|
||||
case Node::NotEqual:
|
||||
return evalNode(node->left, bb) != evalNode(node->right, bb) ? 1 : 0;
|
||||
case Node::Less:
|
||||
return evalNode(node->left, bb) < evalNode(node->right, bb) ? 1 : 0;
|
||||
case Node::Greater:
|
||||
return evalNode(node->left, bb) > evalNode(node->right, bb) ? 1 : 0;
|
||||
case Node::LessEqual:
|
||||
return evalNode(node->left, bb) <= evalNode(node->right, bb) ? 1 : 0;
|
||||
case Node::GreaterEqual:
|
||||
return evalNode(node->left, bb) >= evalNode(node->right, bb) ? 1 : 0;
|
||||
case Node::And:
|
||||
return evalNode(node->left, bb) && evalNode(node->right, bb) ? 1 : 0;
|
||||
case Node::Or:
|
||||
return evalNode(node->left, bb) || evalNode(node->right, bb) ? 1 : 0;
|
||||
case Node::Not:
|
||||
return !evalNode(node->left, bb) ? 1 : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool GoapExpression::evaluate(const GoapBlackboard &blackboard) const
|
||||
{
|
||||
if (!m_root)
|
||||
return false;
|
||||
return evalNode(m_root, blackboard) != 0;
|
||||
}
|
||||
|
||||
void GoapExpression::setError(const char *msg)
|
||||
{
|
||||
m_error = msg;
|
||||
if (m_pos && m_expr) {
|
||||
m_error += " at position ";
|
||||
m_error += std::to_string(m_pos - m_expr);
|
||||
m_error += " near \"";
|
||||
m_error += std::string(m_pos, strnlen(m_pos, 20));
|
||||
m_error += "\"";
|
||||
}
|
||||
}
|
||||
|
||||
void GoapExpression::clear()
|
||||
{
|
||||
delete m_root;
|
||||
m_root = nullptr;
|
||||
m_expr = nullptr;
|
||||
m_pos = nullptr;
|
||||
m_error.clear();
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
#ifndef EDITSCENE_GOAP_EXPRESSION_HPP
|
||||
#define EDITSCENE_GOAP_EXPRESSION_HPP
|
||||
#pragma once
|
||||
|
||||
#include "GoapBlackboard.hpp"
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Simple expression evaluator for GOAP goal conditions.
|
||||
*
|
||||
* Supports:
|
||||
* - Variable names (looked up in blackboard values, default 0)
|
||||
* - Integer literals
|
||||
* - Comparisons: ==, !=, <, >, <=, >=
|
||||
* - Boolean operators: &&, ||
|
||||
* - Parentheses for grouping
|
||||
* - Unary negation: !
|
||||
*
|
||||
* Example: "health > 20 && (hunger > 50 || have_food == 1)"
|
||||
*/
|
||||
class GoapExpression {
|
||||
public:
|
||||
GoapExpression() = default;
|
||||
|
||||
// Parse an expression string. Returns true on success.
|
||||
bool parse(const char *expr);
|
||||
|
||||
// Evaluate the parsed expression against a blackboard.
|
||||
// Returns false if expression was not parsed successfully.
|
||||
bool evaluate(const GoapBlackboard &blackboard) const;
|
||||
|
||||
// Get last error message
|
||||
const std::string &getError() const { return m_error; }
|
||||
|
||||
private:
|
||||
struct Node {
|
||||
enum Type {
|
||||
Value, // integer literal
|
||||
Variable, // blackboard variable name
|
||||
Equal,
|
||||
NotEqual,
|
||||
Less,
|
||||
Greater,
|
||||
LessEqual,
|
||||
GreaterEqual,
|
||||
And,
|
||||
Or,
|
||||
Not
|
||||
} type;
|
||||
int value = 0; // for Value
|
||||
std::string name; // for Variable
|
||||
Node *left = nullptr;
|
||||
Node *right = nullptr;
|
||||
|
||||
Node(Type t)
|
||||
: type(t)
|
||||
{
|
||||
}
|
||||
~Node()
|
||||
{
|
||||
delete left;
|
||||
delete right;
|
||||
}
|
||||
};
|
||||
|
||||
const char *m_expr = nullptr;
|
||||
const char *m_pos = nullptr;
|
||||
std::string m_error;
|
||||
Node *m_root = nullptr;
|
||||
|
||||
void skipWhitespace();
|
||||
bool match(const char *s);
|
||||
Node *parseExpression(); // ||
|
||||
Node *parseAnd(); // &&
|
||||
Node *parseNot(); // !
|
||||
Node *parseComparison(); // ==, !=, <, >, <=, >=
|
||||
Node *parsePrimary(); // value, variable, (expr)
|
||||
|
||||
int evalNode(Node *node, const GoapBlackboard &bb) const;
|
||||
|
||||
void setError(const char *msg);
|
||||
void clear();
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_EXPRESSION_HPP
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "GoapGoal.hpp"
|
||||
#include "GoapExpression.hpp"
|
||||
|
||||
bool GoapGoal::isSatisfied(const GoapBlackboard &blackboard) const
|
||||
{
|
||||
return blackboard.satisfies(target);
|
||||
}
|
||||
|
||||
bool GoapGoal::isValid(const GoapBlackboard &blackboard) const
|
||||
{
|
||||
// If already satisfied, not a valid goal to pursue
|
||||
if (isSatisfied(blackboard))
|
||||
return false;
|
||||
|
||||
// Evaluate condition if present
|
||||
if (!condition.empty()) {
|
||||
GoapExpression expr;
|
||||
if (expr.parse(condition.c_str())) {
|
||||
return expr.evaluate(blackboard);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef EDITSCENE_GOAP_GOAL_HPP
|
||||
#define EDITSCENE_GOAP_GOAL_HPP
|
||||
#pragma once
|
||||
|
||||
#include "GoapBlackboard.hpp"
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* A GOAP goal definition.
|
||||
*
|
||||
* Goals are selected based on priority and validity.
|
||||
* The target blackboard defines the desired world state.
|
||||
* The condition string provides additional runtime validity checks
|
||||
* using a simple expression language against blackboard values.
|
||||
*/
|
||||
struct GoapGoal {
|
||||
Ogre::String name;
|
||||
int priority = 1;
|
||||
|
||||
// Target blackboard state to achieve
|
||||
GoapBlackboard target;
|
||||
|
||||
// Optional condition expression (e.g. "health > 20 && hunger > 50")
|
||||
// If empty, the goal is always considered for validity checking
|
||||
Ogre::String condition;
|
||||
|
||||
GoapGoal() = default;
|
||||
|
||||
explicit GoapGoal(const Ogre::String &name_, int priority_ = 1)
|
||||
: name(name_)
|
||||
, priority(priority_)
|
||||
{
|
||||
}
|
||||
|
||||
// Check if the goal is already satisfied by the given blackboard
|
||||
bool isSatisfied(const GoapBlackboard &blackboard) const;
|
||||
|
||||
// Check if the goal is valid for the given blackboard
|
||||
// (condition evaluates to true and goal is not already satisfied)
|
||||
bool isValid(const GoapBlackboard &blackboard) const;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_GOAL_HPP
|
||||
@@ -0,0 +1,99 @@
|
||||
#ifndef EDITSCENE_GOAP_PLANNER_HPP
|
||||
#define EDITSCENE_GOAP_PLANNER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <queue>
|
||||
|
||||
/**
|
||||
* GOAP Planner component.
|
||||
*
|
||||
* Holds a curated list of action and goal names from an ActionDatabase,
|
||||
* plus configuration for smart-object action discovery.
|
||||
* The planner resolves names against an ActionDatabase at runtime.
|
||||
*
|
||||
* The actionNames and goalNames lists act as external references:
|
||||
* prefabs can store them even when the ActionDatabase is not
|
||||
* part of the prefab itself.
|
||||
*/
|
||||
struct GoapPlannerComponent {
|
||||
// Selected action names from ActionDatabase
|
||||
std::vector<Ogre::String> actionNames;
|
||||
|
||||
// Selected goal names from ActionDatabase
|
||||
std::vector<Ogre::String> goalNames;
|
||||
|
||||
// Maximum distance to search for smart objects with matching actions
|
||||
float smartObjectDistance = 50.0f;
|
||||
|
||||
// Whether to include smart object actions in planning
|
||||
bool includeSmartObjects = true;
|
||||
|
||||
// Optional reference to an external ActionDatabase entity by name.
|
||||
Ogre::String actionDatabaseRef;
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Runtime plan queue (not serialized)
|
||||
// -----------------------------------------------------------------
|
||||
struct Plan {
|
||||
std::vector<Ogre::String> actions;
|
||||
int totalCost = 0;
|
||||
Ogre::String goalName;
|
||||
};
|
||||
std::vector<Plan> planQueue;
|
||||
|
||||
// Planner status
|
||||
enum class Status {
|
||||
Idle, // No planning requested
|
||||
Planning, // Currently planning
|
||||
PlansAvailable, // One or more plans in queue
|
||||
NoPlanFound // Planning finished but no valid plan found
|
||||
};
|
||||
Status status = Status::Idle;
|
||||
|
||||
// Goal name that was used for the current plan batch
|
||||
Ogre::String currentGoalName;
|
||||
|
||||
// Planning control
|
||||
bool planDirty = true;
|
||||
int maxPlans = 3; // stop after generating this many plans
|
||||
|
||||
// Planning progress (for status display)
|
||||
int plansGenerated = 0;
|
||||
int nodesExplored = 0;
|
||||
|
||||
GoapPlannerComponent() = default;
|
||||
|
||||
void clearPlans()
|
||||
{
|
||||
planQueue.clear();
|
||||
plansGenerated = 0;
|
||||
status = Status::Idle;
|
||||
}
|
||||
|
||||
// Pop the cheapest plan from the queue
|
||||
Plan popCheapestPlan()
|
||||
{
|
||||
if (planQueue.empty())
|
||||
return Plan();
|
||||
size_t bestIdx = 0;
|
||||
for (size_t i = 1; i < planQueue.size(); i++) {
|
||||
if (planQueue[i].totalCost < planQueue[bestIdx].totalCost)
|
||||
bestIdx = i;
|
||||
}
|
||||
Plan result = std::move(planQueue[bestIdx]);
|
||||
planQueue.erase(planQueue.begin() + bestIdx);
|
||||
if (planQueue.empty() && status == Status::PlansAvailable)
|
||||
status = Status::Idle;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool hasPlans() const
|
||||
{
|
||||
return !planQueue.empty();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_PLANNER_HPP
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "GoapPlanner.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/GoapPlannerEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("GOAP Planner", "AI", GoapPlannerComponent,
|
||||
GoapPlannerEditor)
|
||||
{
|
||||
registry.registerComponent<GoapPlannerComponent>(
|
||||
"GOAP Planner", "AI",
|
||||
std::make_unique<GoapPlannerEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<GoapPlannerComponent>())
|
||||
e.set<GoapPlannerComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<GoapPlannerComponent>())
|
||||
e.remove<GoapPlannerComponent>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef EDITSCENE_GOAP_RUNNER_HPP
|
||||
#define EDITSCENE_GOAP_RUNNER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* GOAP Runner component.
|
||||
*
|
||||
* Executes plans from GoapPlannerComponent.
|
||||
* For normal actions: runs the action's behavior tree.
|
||||
* For smart object actions: pathfinds to the smart object and executes.
|
||||
* After plan completion, marks the planner dirty for replanning.
|
||||
*/
|
||||
struct GoapRunnerComponent {
|
||||
// Current plan execution state
|
||||
enum class State {
|
||||
Idle, // No plan running
|
||||
RunningAction, // Executing a normal action
|
||||
MovingToSmartObject, // Pathfinding to a smart object
|
||||
ExecutingSmartObject, // Executing smart object action
|
||||
PlanComplete // Plan finished, waiting for replan
|
||||
};
|
||||
|
||||
State state = State::Idle;
|
||||
|
||||
// Index of current action in the plan
|
||||
int currentActionIndex = 0;
|
||||
|
||||
// Name of the currently executing action
|
||||
Ogre::String currentActionName;
|
||||
|
||||
// Timer for action execution
|
||||
float actionTimer = 0.0f;
|
||||
|
||||
// Entity ID of target smart object (if applicable)
|
||||
uint64_t targetSmartObjectId = 0;
|
||||
|
||||
// Active plan actions (copied from planner when plan starts)
|
||||
std::vector<Ogre::String> planActions;
|
||||
|
||||
// Whether to auto-replan after completion
|
||||
bool autoReplan = true;
|
||||
|
||||
GoapRunnerComponent() = default;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_RUNNER_HPP
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "GoapRunner.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/GoapRunnerEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("GOAP Runner", "AI", GoapRunnerComponent,
|
||||
GoapRunnerEditor)
|
||||
{
|
||||
registry.registerComponent<GoapRunnerComponent>(
|
||||
"GOAP Runner", "AI",
|
||||
std::make_unique<GoapRunnerEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<GoapRunnerComponent>())
|
||||
e.set<GoapRunnerComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<GoapRunnerComponent>())
|
||||
e.remove<GoapRunnerComponent>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef EDITSCENE_INWATER_HPP
|
||||
#define EDITSCENE_INWATER_HPP
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Tag component indicating the entity is currently in water.
|
||||
* Automatically added/removed by BuoyancySystem.
|
||||
*/
|
||||
struct InWater {
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_INWATER_HPP
|
||||
@@ -0,0 +1,141 @@
|
||||
#ifndef EDITSCENE_INVENTORY_HPP
|
||||
#define EDITSCENE_INVENTORY_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
#include "../systems/ItemRegistry.hpp"
|
||||
|
||||
/**
|
||||
* A single slot in an inventory.
|
||||
* Stores a reference to an item entity (if the item is a world entity)
|
||||
* or stores item data directly for items that exist only in inventory.
|
||||
*/
|
||||
struct InventorySlot {
|
||||
// Flecs entity ID of the item (0 if slot is empty or no world entity)
|
||||
flecs::entity_t itemEntity = 0;
|
||||
|
||||
// Item registry key
|
||||
std::string itemId;
|
||||
|
||||
// Stack size
|
||||
int stackSize = 0;
|
||||
|
||||
bool isEmpty() const
|
||||
{
|
||||
return itemId.empty() && itemEntity == 0;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
itemEntity = 0;
|
||||
itemId.clear();
|
||||
stackSize = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Inventory component.
|
||||
*
|
||||
* Attached to a character entity to hold items.
|
||||
* Can also be attached to container entities (chests, barrels, etc.)
|
||||
* to define their contents.
|
||||
*/
|
||||
struct InventoryComponent {
|
||||
// Maximum number of slots
|
||||
int maxSlots = 20;
|
||||
|
||||
// Current slots
|
||||
std::vector<InventorySlot> slots;
|
||||
|
||||
// Total weight of all items (computed)
|
||||
float totalWeight = 0.0f;
|
||||
|
||||
// Maximum weight capacity (0 = unlimited)
|
||||
float maxWeight = 50.0f;
|
||||
|
||||
// Whether this inventory is a container (chest, barrel, etc.)
|
||||
bool isContainer = false;
|
||||
|
||||
// Whether this inventory is currently open (for containers being browsed)
|
||||
bool isOpen = false;
|
||||
|
||||
// Persistent ID for scene containers (empty for character inventories)
|
||||
std::string containerId;
|
||||
|
||||
InventoryComponent() = default;
|
||||
|
||||
explicit InventoryComponent(int maxSlots_)
|
||||
: maxSlots(maxSlots_)
|
||||
{
|
||||
slots.reserve(maxSlots_);
|
||||
}
|
||||
|
||||
/** Find the first empty slot index, or -1 if full. */
|
||||
int findEmptySlot() const
|
||||
{
|
||||
for (int i = 0; i < maxSlots; i++) {
|
||||
if (i >= (int)slots.size())
|
||||
return i;
|
||||
if (slots[i].isEmpty())
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Find a slot containing an item with the given itemId. */
|
||||
int findItem(const std::string &itemId) const
|
||||
{
|
||||
for (int i = 0; i < (int)slots.size(); i++) {
|
||||
if (!slots[i].isEmpty() && slots[i].itemId == itemId)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Count total number of items (sum of stack sizes). */
|
||||
int countItems() const
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty())
|
||||
count += slot.stackSize;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Count how many of a specific itemId are in the inventory. */
|
||||
int countItem(const std::string &itemId) const
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty() && slot.itemId == itemId)
|
||||
count += slot.stackSize;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Check if inventory has at least one of a specific itemId. */
|
||||
bool hasItem(const std::string &itemId) const
|
||||
{
|
||||
return findItem(itemId) >= 0;
|
||||
}
|
||||
|
||||
/** Recalculate total weight from registry. */
|
||||
void recalculateWeight()
|
||||
{
|
||||
totalWeight = 0.0f;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty())
|
||||
totalWeight +=
|
||||
ItemRegistry::getSingleton().getWeight(slot.itemId) *
|
||||
slot.stackSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_INVENTORY_HPP
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "Inventory.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/InventoryEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Inventory", "Game", InventoryComponent,
|
||||
InventoryEditor)
|
||||
{
|
||||
registry.registerComponent<InventoryComponent>(
|
||||
"Inventory", "Game", std::make_unique<InventoryEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<InventoryComponent>())
|
||||
e.set<InventoryComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<InventoryComponent>())
|
||||
e.remove<InventoryComponent>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef EDITSCENE_ITEM_HPP
|
||||
#define EDITSCENE_ITEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Item reference component.
|
||||
*
|
||||
* Attached to a world entity that represents a pickable / interactable item.
|
||||
* All item properties (name, type, weight, etc.) are stored in
|
||||
* the ItemRegistry singleton and looked up by itemId.
|
||||
*
|
||||
* action: Optional GOAP action name to execute on interact (E key).
|
||||
* If empty, the item is picked up into inventory.
|
||||
* instanceId: Optional unique ID for global state tracking (save/load).
|
||||
* disabled: Set to true when the item has been picked up / consumed.
|
||||
*/
|
||||
struct ItemComponent {
|
||||
// Registry key for this item definition
|
||||
Ogre::String itemId;
|
||||
|
||||
// Stack size for this world entity
|
||||
int stackSize = 1;
|
||||
|
||||
// Optional action to execute on interact (instead of pickup)
|
||||
Ogre::String action;
|
||||
|
||||
// Unique instance ID for global state tracking
|
||||
Ogre::String instanceId;
|
||||
|
||||
// True when item has been picked up or consumed
|
||||
bool disabled = false;
|
||||
|
||||
ItemComponent() = default;
|
||||
|
||||
explicit ItemComponent(const Ogre::String &id, int stack = 1)
|
||||
: itemId(id)
|
||||
, stackSize(stack)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ITEM_HPP
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "Item.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ItemEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Item", "Game", ItemComponent, ItemEditor)
|
||||
{
|
||||
registry.registerComponent<ItemComponent>(
|
||||
"Item", "Game", std::make_unique<ItemEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ItemComponent>())
|
||||
e.set<ItemComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ItemComponent>())
|
||||
e.remove<ItemComponent>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef EDITSCENE_LIGHT_HPP
|
||||
#define EDITSCENE_LIGHT_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Light component - attaches an Ogre::Light to the entity's SceneNode
|
||||
*/
|
||||
struct LightComponent {
|
||||
enum class LightType {
|
||||
Point, // Omnidirectional light
|
||||
Directional, // Parallel rays (sun/moon)
|
||||
Spotlight // Cone-shaped light
|
||||
};
|
||||
|
||||
LightType lightType = LightType::Point;
|
||||
|
||||
// Common properties
|
||||
Ogre::ColourValue diffuseColor{0.8f, 0.8f, 0.8f};
|
||||
Ogre::ColourValue specularColor{0.5f, 0.5f, 0.5f};
|
||||
float intensity = 1.0f;
|
||||
|
||||
// Attenuation (for Point and Spot)
|
||||
float range = 100.0f;
|
||||
float constantAttenuation = 1.0f;
|
||||
float linearAttenuation = 0.0f;
|
||||
float quadraticAttenuation = 0.0f;
|
||||
|
||||
// Spotlight specific
|
||||
float spotlightInnerAngle = 30.0f; // Degrees
|
||||
float spotlightOuterAngle = 45.0f; // Degrees
|
||||
float spotlightFalloff = 1.0f;
|
||||
|
||||
// Direction (for Directional and Spot, relative to node orientation)
|
||||
Ogre::Vector3 direction{0, -1, 0};
|
||||
|
||||
// Cast shadows
|
||||
bool castShadows = true;
|
||||
|
||||
// The Ogre light object (created by LightSystem)
|
||||
Ogre::Light* light = nullptr;
|
||||
|
||||
void markDirty() { needsRebuild = true; }
|
||||
bool needsRebuild = true;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_LIGHT_HPP
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "Light.hpp"
|
||||
#include "Transform.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/LightEditor.hpp"
|
||||
#include "../systems/LightSystem.hpp"
|
||||
|
||||
// Register Light component
|
||||
REGISTER_COMPONENT("Light", LightComponent, LightEditor)
|
||||
{
|
||||
registry.registerComponent<LightComponent>(
|
||||
"Light",
|
||||
std::make_unique<LightEditor>(),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<LightComponent>()) {
|
||||
// Light requires Transform
|
||||
if (!e.has<TransformComponent>()) {
|
||||
// Auto-add transform if missing
|
||||
TransformComponent transform;
|
||||
transform.node = sceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
e.set<TransformComponent>(transform);
|
||||
}
|
||||
e.set<LightComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<LightComponent>()) {
|
||||
auto& light = e.get_mut<LightComponent>();
|
||||
// Clean up Ogre light
|
||||
if (light.light) {
|
||||
Ogre::SceneNode* parent = light.light->getParentSceneNode();
|
||||
if (parent) {
|
||||
parent->detachObject(light.light);
|
||||
}
|
||||
sceneMgr->destroyLight(light.light);
|
||||
light.light = nullptr;
|
||||
}
|
||||
e.remove<LightComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef EDITSCENE_LOD_HPP
|
||||
#define EDITSCENE_LOD_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
#include <string>
|
||||
#include "LodSettings.hpp"
|
||||
|
||||
/**
|
||||
* LodComponent - Per-entity LOD configuration
|
||||
*
|
||||
* This component references a LodSettingsComponent by its settingsId
|
||||
* for persistent identification across scene loads.
|
||||
*/
|
||||
struct LodComponent {
|
||||
// Reference to the settings by ID (persistent across scene loads)
|
||||
std::string settingsId;
|
||||
|
||||
// Runtime entity reference (resolved from settingsId)
|
||||
flecs::entity settingsEntity = flecs::entity::null();
|
||||
|
||||
// Per-entity distance multiplier (scales all LOD distances)
|
||||
float distanceMultiplier = 1.0f;
|
||||
|
||||
// Whether LOD has been applied to the mesh
|
||||
bool lodApplied = false;
|
||||
|
||||
// Last settings version that was applied
|
||||
uint32_t appliedVersion = 0;
|
||||
|
||||
// Dirty flag for regenerating LOD
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
|
||||
// Helper to check if LOD needs to be regenerated
|
||||
bool needsUpdate() const { return dirty; }
|
||||
|
||||
// Helper to check if settings reference is valid
|
||||
bool hasValidSettings() const {
|
||||
return settingsEntity.is_alive() &&
|
||||
settingsEntity.has<LodSettingsComponent>();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_LOD_HPP
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "Lod.hpp"
|
||||
#include "LodSettings.hpp"
|
||||
#include "Renderable.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/LodEditor.hpp"
|
||||
#include "../ui/LodSettingsEditor.hpp"
|
||||
|
||||
// Register LodSettings component
|
||||
REGISTER_COMPONENT("LOD Settings", LodSettingsComponent, LodSettingsEditor)
|
||||
{
|
||||
registry.registerComponent<LodSettingsComponent>(
|
||||
"LOD Settings",
|
||||
std::make_unique<LodSettingsEditor>(),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<LodSettingsComponent>()) {
|
||||
auto settings = LodSettingsComponent();
|
||||
// Create default LOD levels
|
||||
settings.createDefaultLevels();
|
||||
e.set<LodSettingsComponent>(settings);
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<LodSettingsComponent>()) {
|
||||
e.remove<LodSettingsComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register Lod component
|
||||
REGISTER_COMPONENT("LOD", LodComponent, LodEditor)
|
||||
{
|
||||
registry.registerComponent<LodComponent>(
|
||||
"LOD",
|
||||
std::make_unique<LodEditor>(),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<LodComponent>()) {
|
||||
// LOD requires Renderable
|
||||
if (!e.has<RenderableComponent>()) {
|
||||
// Auto-add renderable if missing (will need mesh assignment later)
|
||||
e.set<RenderableComponent>({});
|
||||
}
|
||||
e.set<LodComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<LodComponent>()) {
|
||||
e.remove<LodComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user