Source code for core.animation

import json
import os
import pygame
from core.resources import ResourceManager

[docs] class AnimationClip: def __init__(self, name: str): self.name = name self.type = "spritesheet" # "spritesheet" or "images" self.fps = 12.0 self.loop = True # Spritesheet params self.sheet_path = "" self.frame_width = 32 self.frame_height = 32 self.margin = 0 self.spacing = 0 self.start_frame = 0 self.frame_count = 0 # Image sequence params self.image_paths = [] # Runtime data self.frames = []
[docs] def load_frames(self): self.frames = [] if self.type == "spritesheet" and self.sheet_path: all_frames = ResourceManager.slice_spritesheet( self.sheet_path, self.frame_width, self.frame_height, 0, self.margin, self.spacing ) if all_frames: clip_start = max(0, int(self.start_frame)) if clip_start < len(all_frames): if self.frame_count and self.frame_count > 0: clip_end = min(len(all_frames), clip_start + int(self.frame_count)) else: clip_end = len(all_frames) self.frames = all_frames[clip_start:clip_end] elif self.type == "images" and self.image_paths: for path in self.image_paths: image = ResourceManager.load_image(path) if image: self.frames.append(image)
[docs] def to_data(self) -> dict: data = { "type": self.type, "fps": self.fps, "loop": self.loop } if self.type == "spritesheet": data.update({ "sheet_path": ResourceManager.portable_path(self.sheet_path) if self.sheet_path else self.sheet_path, "frame_width": self.frame_width, "frame_height": self.frame_height, "margin": self.margin, "spacing": self.spacing, "start_frame": self.start_frame, "frame_count": self.frame_count }) elif self.type == "images": data.update({ "image_paths": [ResourceManager.portable_path(p) if p else p for p in self.image_paths] }) return data
[docs] @staticmethod def from_data(name: str, data: dict) -> 'AnimationClip': clip = AnimationClip(name) clip.type = data.get("type", "spritesheet") clip.fps = data.get("fps", 12.0) clip.loop = data.get("loop", True) if clip.type == "spritesheet": clip.sheet_path = data.get("sheet_path", "") clip.frame_width = data.get("frame_width", 32) clip.frame_height = data.get("frame_height", 32) clip.margin = data.get("margin", 0) clip.spacing = data.get("spacing", 0) clip.start_frame = data.get("start_frame", 0) clip.frame_count = data.get("frame_count", 0) elif clip.type == "images": clip.image_paths = data.get("image_paths", []) return clip
[docs] class AnimationNode: def __init__(self, name: str, clip_path: str = "", position: tuple = (0, 0)): self.name = name self.clip_path = clip_path self.position = position # Editor position self.clip = None # Runtime AnimationClip instance
[docs] class AnimationTransition: def __init__(self, from_node: str, to_node: str, conditions: list = None, trigger: str = "", on_finish: bool = False): self.from_node = from_node self.to_node = to_node self.conditions = conditions or [] self.trigger = str(trigger or "") self.on_finish = bool(on_finish)
[docs] class AnimationController: ROOT_NODE_NAME = "Root" def __init__(self): self.nodes = {} self.transitions = [] self.default_node = None self.parameters = {} self.nodes[self.ROOT_NODE_NAME] = AnimationNode(self.ROOT_NODE_NAME, "", (80, 120))
[docs] def add_node(self, name: str, clip_path: str, position: tuple = (0, 0)): if not name: return if name == self.ROOT_NODE_NAME and name in self.nodes: return self.nodes[name] = AnimationNode(name, clip_path, position) self._refresh_default_node()
[docs] def add_transition(self, from_node: str, to_node: str, conditions: list = None, trigger: str = "", on_finish: bool = False): if from_node not in self.nodes or to_node not in self.nodes: return False if to_node == self.ROOT_NODE_NAME: return False if from_node == self.ROOT_NODE_NAME: self.transitions = [ t for t in self.transitions if t.from_node != self.ROOT_NODE_NAME ] for transition in self.transitions: if transition.from_node == from_node and transition.to_node == to_node: return False self.transitions.append(AnimationTransition(from_node, to_node, conditions, trigger, on_finish)) self._refresh_default_node() return True
[docs] def remove_node(self, name: str): if name == self.ROOT_NODE_NAME or name not in self.nodes: return False del self.nodes[name] self.transitions = [ t for t in self.transitions if t.from_node != name and t.to_node != name ] self._refresh_default_node() return True
[docs] def rename_node(self, old_name: str, new_name: str): if not old_name or not new_name: return False if old_name == self.ROOT_NODE_NAME or old_name not in self.nodes: return False if new_name in self.nodes: return False node = self.nodes.pop(old_name) node.name = new_name self.nodes[new_name] = node for transition in self.transitions: if transition.from_node == old_name: transition.from_node = new_name if transition.to_node == old_name: transition.to_node = new_name self._refresh_default_node() return True
[docs] def get_default_state(self): for transition in self.transitions: if transition.from_node == self.ROOT_NODE_NAME and transition.to_node in self.nodes: return transition.to_node return None
def _normalize(self): idle_migrated = False if self.ROOT_NODE_NAME not in self.nodes: if "Idle" in self.nodes: node = self.nodes.pop("Idle") node.name = self.ROOT_NODE_NAME node.clip_path = "" self.nodes[self.ROOT_NODE_NAME] = node idle_migrated = True else: self.nodes[self.ROOT_NODE_NAME] = AnimationNode(self.ROOT_NODE_NAME, "", (80, 120)) self.nodes[self.ROOT_NODE_NAME].clip_path = "" valid_names = set(self.nodes.keys()) normalized = [] has_root_transition = False for transition in self.transitions: from_node = transition.from_node to_node = transition.to_node if idle_migrated: if from_node == "Idle": from_node = self.ROOT_NODE_NAME if to_node == "Idle": to_node = self.ROOT_NODE_NAME if from_node not in valid_names or to_node not in valid_names: continue if to_node == self.ROOT_NODE_NAME: continue if from_node == self.ROOT_NODE_NAME: if has_root_transition: continue has_root_transition = True if any(t.from_node == from_node and t.to_node == to_node for t in normalized): continue normalized.append( AnimationTransition( from_node, to_node, transition.conditions, transition.trigger, transition.on_finish ) ) self.transitions = normalized self._refresh_default_node() def _refresh_default_node(self): self.default_node = self.get_default_state()
[docs] def to_data(self) -> dict: self._normalize() return { "default_node": self.default_node, "nodes": [ { "name": node.name, "clip_path": ResourceManager.portable_path(node.clip_path) if node.clip_path else node.clip_path, "position": node.position } for node in self.nodes.values() ], "transitions": [ { "from": t.from_node, "to": t.to_node, "conditions": t.conditions, "trigger": t.trigger, "on_finish": t.on_finish } for t in self.transitions ], "parameters": self.parameters }
[docs] @staticmethod def from_data(data: dict) -> 'AnimationController': ctrl = AnimationController() ctrl.nodes = {} ctrl.transitions = [] ctrl.parameters = data.get("parameters", {}) # Only migrate legacy "Idle" → "Root" when no explicit Root node exists has_explicit_root = any( nd.get("name") == ctrl.ROOT_NODE_NAME for nd in data.get("nodes", []) ) for node_data in data.get("nodes", []): node_name = node_data.get("name") if not node_name: continue if not has_explicit_root and node_name == "Idle": node_name = ctrl.ROOT_NODE_NAME if node_name in ctrl.nodes: continue clip_path = node_data.get("clip_path", "") if node_name == ctrl.ROOT_NODE_NAME: clip_path = "" ctrl.nodes[node_name] = AnimationNode( node_name, clip_path, tuple(node_data.get("position", (0, 0))) ) for trans_data in data.get("transitions", []): from_node = trans_data.get("from") to_node = trans_data.get("to") if not has_explicit_root: if from_node == "Idle": from_node = ctrl.ROOT_NODE_NAME if to_node == "Idle": to_node = ctrl.ROOT_NODE_NAME ctrl.add_transition( from_node, to_node, trans_data.get("conditions"), trans_data.get("trigger", ""), trans_data.get("on_finish", False) ) ctrl._normalize() return ctrl