forked from LeenkxTeam/LNXSDK
295 lines
7.2 KiB
Haxe
295 lines
7.2 KiB
Haxe
package iron.object;
|
|
|
|
import iron.App;
|
|
import iron.data.SceneFormat;
|
|
import iron.system.Time;
|
|
import haxe.ds.Map;
|
|
|
|
|
|
class Tilesheet {
|
|
|
|
public var tileX: Float = 0.0;
|
|
public var tileY: Float = 0.0;
|
|
public var flipX: Bool = false;
|
|
public var flipY: Bool = false;
|
|
public var paused: Bool = false;
|
|
public var frame: Int = 0;
|
|
public var actions: Array<TTilesheetAction>;
|
|
public var action: TTilesheetAction = null;
|
|
|
|
public var ready: Bool = false;
|
|
var time: Float = 0.0;
|
|
var onActionComplete: Void->Void = null;
|
|
var onReady: Void->Void = null;
|
|
var onEvent: String->Void = null; // Callback for tilesheet events
|
|
var prevFrame: Int = -1; // Track previous frame to detect changes
|
|
var owner: MeshObject = null;
|
|
var currentMesh: MeshObject = null;
|
|
var meshCache: Map<String, MeshObject> = new Map();
|
|
var pendingAction: String = null;
|
|
var pendingOnComplete: Void->Void = null;
|
|
|
|
public function new(tilesheetData: TTilesheetData, ownerObject: MeshObject = null) {
|
|
owner = ownerObject;
|
|
actions = tilesheetData.actions;
|
|
|
|
pendingAction = tilesheetData.start_action;
|
|
if ((pendingAction == null || pendingAction == "") && actions.length > 0) {
|
|
pendingAction = actions[0].name;
|
|
}
|
|
|
|
flipX = tilesheetData.flipx;
|
|
flipY = tilesheetData.flipy;
|
|
|
|
// If no actions need mesh swapping, ready immediately
|
|
var hasMeshActions: Bool = false;
|
|
for (a in actions) {
|
|
if (a.mesh != null && a.mesh != "") {
|
|
hasMeshActions = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hasMeshActions) {
|
|
ready = true;
|
|
if (pendingAction != null) {
|
|
playAction(pendingAction);
|
|
pendingAction = null;
|
|
}
|
|
if (onReady != null) onReady();
|
|
}
|
|
}
|
|
|
|
public function update() {
|
|
if (App.pauseUpdates) return;
|
|
|
|
if (!ready) {
|
|
if (tryInitialize()) {
|
|
ready = true;
|
|
if (pendingAction != null) {
|
|
playAction(pendingAction, pendingOnComplete);
|
|
pendingAction = null;
|
|
pendingOnComplete = null;
|
|
}
|
|
if (onReady != null) onReady();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (paused || action == null || action.start >= action.end) return;
|
|
|
|
time += Time.renderDelta;
|
|
|
|
var frameTime = 1 / action.framerate;
|
|
var framesToAdvance = 0;
|
|
|
|
while (time >= frameTime) {
|
|
time -= frameTime;
|
|
framesToAdvance++;
|
|
}
|
|
|
|
if (framesToAdvance > 0) {
|
|
setFrame(frame + framesToAdvance);
|
|
}
|
|
}
|
|
|
|
function tryInitialize(): Bool {
|
|
if (owner == null) return false;
|
|
|
|
// If no children, use the owner mesh itself
|
|
if (owner.children == null || owner.children.length == 0) {
|
|
if (owner.data != null && !meshCache.exists(owner.data.name)) {
|
|
meshCache.set(owner.data.name, owner);
|
|
// Also cache by object name for flexible lookup
|
|
if (owner.name != owner.data.name) {
|
|
meshCache.set(owner.name, owner);
|
|
}
|
|
}
|
|
} else {
|
|
// Use child meshes for mesh swapping
|
|
for (child in owner.children) {
|
|
if (Std.isOfType(child, MeshObject)) {
|
|
var meshChild = cast(child, MeshObject);
|
|
if (meshChild.data != null && !meshCache.exists(meshChild.data.name)) {
|
|
meshCache.set(meshChild.data.name, meshChild);
|
|
meshChild.visible = false;
|
|
// Also cache by object name for flexible lookup
|
|
if (meshChild.name != meshChild.data.name) {
|
|
meshCache.set(meshChild.name, meshChild);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (a in actions) {
|
|
if (a.mesh != null && a.mesh != "" && !meshCache.exists(a.mesh)) {
|
|
if (findMatchingMesh(a.mesh) == null) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** Find mesh by base name pattern (handles linked objects with different suffixes). */
|
|
function findMatchingMesh(actionMeshName: String): MeshObject {
|
|
var baseName = actionMeshName;
|
|
|
|
// Strip "Mesh" prefix if present
|
|
if (StringTools.startsWith(baseName, "Mesh")) {
|
|
baseName = baseName.substr(4);
|
|
}
|
|
|
|
// Strip suffix after underscore (e.g., "_character.blend")
|
|
var idx = baseName.indexOf("_");
|
|
if (idx > 0) baseName = baseName.substr(0, idx);
|
|
|
|
for (meshName in meshCache.keys()) {
|
|
if (meshName.indexOf(baseName) != -1) {
|
|
var mesh = meshCache.get(meshName);
|
|
meshCache.set(actionMeshName, mesh); // Cache alias
|
|
return mesh;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function play(action_ref: String, onActionComplete: Void->Void = null) {
|
|
if (actions == null) return;
|
|
|
|
if (!ready) {
|
|
pendingAction = action_ref;
|
|
pendingOnComplete = onActionComplete;
|
|
return;
|
|
}
|
|
|
|
playAction(action_ref, onActionComplete);
|
|
}
|
|
|
|
public function notifyOnReady(callback: Void->Void) {
|
|
onReady = callback;
|
|
if (ready) onReady();
|
|
}
|
|
|
|
public function notifyOnEvent(callback: String->Void) {
|
|
onEvent = callback;
|
|
}
|
|
|
|
function playAction(action_ref: String, onComplete: Void->Void = null) {
|
|
if (action != null && action.name == action_ref) {
|
|
paused = false;
|
|
return;
|
|
}
|
|
|
|
onActionComplete = onComplete;
|
|
|
|
for (a in actions) {
|
|
if (a.name == action_ref) {
|
|
action = a;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (action == null) return;
|
|
|
|
if (action.mesh != null && action.mesh != "") {
|
|
var targetMesh = meshCache.get(action.mesh);
|
|
if (targetMesh != null && targetMesh != currentMesh) {
|
|
swapMesh(targetMesh);
|
|
}
|
|
}
|
|
|
|
prevFrame = -1; // Reset previous frame for new action
|
|
setFrame(action.start);
|
|
paused = false;
|
|
time = 0.0;
|
|
}
|
|
|
|
function swapMesh(meshObj: MeshObject) {
|
|
if (owner == null || meshObj == null) return;
|
|
currentMesh = meshObj;
|
|
if (meshObj.data != null) owner.setData(meshObj.data);
|
|
if (meshObj.materials != null) owner.materials = meshObj.materials;
|
|
}
|
|
|
|
function setFrame(f: Int) {
|
|
frame = f;
|
|
|
|
if (frame > action.end && action.start < action.end) {
|
|
// Check for events on last frame before completing
|
|
checkEvents(prevFrame, action.end);
|
|
if (onActionComplete != null) onActionComplete();
|
|
if (action.loop) {
|
|
prevFrame = -1; // Reset for loop
|
|
setFrame(action.start);
|
|
} else {
|
|
paused = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check for events between previous frame and current frame
|
|
checkEvents(prevFrame, frame);
|
|
prevFrame = frame;
|
|
|
|
var tx = frame % action.tilesx;
|
|
var ty = Std.int(frame / action.tilesx);
|
|
tileX = tx / action.tilesx;
|
|
tileY = ty / action.tilesy;
|
|
}
|
|
|
|
/** Check and fire events for frames between fromFrame (exclusive) and toFrame (inclusive). */
|
|
function checkEvents(fromFrame: Int, toFrame: Int) {
|
|
if (onEvent == null || action == null || action.events == null) return;
|
|
|
|
// Convert to action-relative frame numbers
|
|
var relativeFrom = fromFrame - action.start;
|
|
var relativeTo = toFrame - action.start;
|
|
|
|
for (evt in action.events) {
|
|
// Fire event if it falls in the range (fromFrame, toFrame]
|
|
if (evt.frame > relativeFrom && evt.frame <= relativeTo) {
|
|
onEvent(evt.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function pause() {
|
|
paused = true;
|
|
}
|
|
|
|
public function resume() {
|
|
paused = false;
|
|
}
|
|
|
|
public function remove() {
|
|
ready = false;
|
|
action = null;
|
|
actions = null;
|
|
owner = null;
|
|
currentMesh = null;
|
|
pendingAction = null;
|
|
pendingOnComplete = null;
|
|
onEvent = null;
|
|
prevFrame = -1;
|
|
meshCache.clear();
|
|
}
|
|
|
|
public function setFrameOffset(frame: Int) {
|
|
if (action == null) return;
|
|
setFrame(action.start + frame);
|
|
paused = false;
|
|
}
|
|
|
|
public function getFrameOffset(): Int {
|
|
return action != null ? frame - action.start : 0;
|
|
}
|
|
|
|
public function getTilesX(): Int {
|
|
return action != null ? action.tilesx : 1;
|
|
}
|
|
|
|
public function getTilesY(): Int {
|
|
return action != null ? action.tilesy : 1;
|
|
}
|
|
}
|