'OHRRPGCE - Animation system '(C) Copyright 1997-2025 James Paige, Ralph Versteegen, and the OHRRPGCE Developers 'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability. #include "config.bi" #include "util.bi" #include "const.bi" #include "uiconst.bi" #include "common.bi" #include "allmodex.bi" #include "reload.bi" #include "slices.bi" #include "loading.bi" #include "sliceedit.bi" #include "slice_propnames.bi" using Reload dim shared ms_per_frame as integer = 55 ' A single global document for all Animation.opsnode RELOAD nodes (they are orphaned). ' Access via get_anim_doc(). dim shared _anim_doc as DocPtr ' Map from property names (interned strings: zstring ptr, does not own a copy) to ' SlicePropertyNumber index. dim shared slice_property_numbers as HashTable ' Number of loops/non-forwards branches that can occur in an animation without a ' wait before it's considered to be stuck in an infinite loop. const ANIMATION_LOOPLIMIT = 10 ' Short names used for listing an animation redim anim_op_names(animOpLAST) as string anim_op_names(animOpWait) = "wait" anim_op_names(animOpWaitMS) = "wait" anim_op_names(animOpFrame) = "frame" anim_op_names(animOpRepeat) = "repeat" anim_op_names(animOpSetOffset) = "set offset" anim_op_names(animOpRelOffset) = "add offset" anim_op_names(animOpPlayFrameGroup) = "frame group" anim_op_names(animOpSetProp) = "set" anim_op_names(animOpSwitchAnim) ="switch to" anim_op_names(animOpTween) = "tween" ' Short names used for RELOAD serialisation redim anim_op_node_names(animOpLAST) as string anim_op_node_names(animOpWait) = "wait" anim_op_node_names(animOpWaitMS) = "waitms" anim_op_node_names(animOpFrame) = "frame" anim_op_node_names(animOpRepeat) = "repeat" anim_op_node_names(animOpSetOffset) = "setoffset" anim_op_node_names(animOpRelOffset) = "addoffset" anim_op_node_names(animOpPlayFrameGroup) = "framegroup" anim_op_node_names(animOpSetProp) = "setprop" anim_op_node_names(animOpSwitchAnim) ="switchanim" anim_op_node_names(animOpTween) = "tween" ' Descriptive captions redim anim_op_fullnames(animOpLAST) as string anim_op_fullnames(animOpWait) = "Wait (num frames)" anim_op_fullnames(animOpWaitMS) = "Wait (seconds)" anim_op_fullnames(animOpFrame) = "Set frame" anim_op_fullnames(animOpRepeat) = "Repeat animation" anim_op_fullnames(animOpSetOffset) = "Move to offset (unimp)" anim_op_fullnames(animOpRelOffset) = "Add to offset (unimp)" anim_op_fullnames(animOpPlayFrameGroup) = "Play frame group" anim_op_fullnames(animOpSetProp) = "Set" anim_op_fullnames(animOpSwitchAnim) ="Switch to animation" anim_op_fullnames(animOpTween) = "Tween (blend keyframes)" ' Built-in variants have names starting with :, which are not allowed for user names dim shared builtin_variants_table(4) as IntStrPair ' => { _ ' (varUp, "up"), _ ' (varRight, "right"), _ ' (varDown, "down"), _ ' (varLeft, "left"), _ ' (varHurt, "hurt") _ ' } ' '(varSelected, "selected"), _ ' '(varHover, "hover"), _ ' '(varDisabled, "disabled"), _ dim builtin_animations(...) as AnimVariantInfo => { _ (@"idle", acAny, @"Default when no other animation running, e.g. standing still"), _ (@"walk", acActor, @"Normal/default movement"), _ (@"retreat", acBattler, @"Walking back to start after attacking"), _ (@"flee", acBattler, @"Fleeing from battle"), _ (@"victory", acBatHero, @"Victory dance after a battle"), _ (@"walkinplace", acWalkabout, @"NPCs with Walk-in-Place movetype"), _ (@"talk", acWalkabout, @"For heroes and NPCs, when an NPC shows a textbox"), _ (@"attack", acBattler, @"Default attack animation"), _ (@"cast", acBattler, @"`Cast' attack animation"), _ (@"useitem", acBatHero, @"Doing an attack linked to an item"), _ /'(@"jump", acBattler, @"`Jump' attack animation"), '/ _ /'(@"land", acBattler, @"`Land' attack animation"), '/ _ (@"hurt", acBattler, @"When hit by an attack"), _ /'(@"healed", acBattler, @"When healed in a battle"), '/ _ /' die disabled; need to figure out how to handle or supersede enemy dissolve '/ _ /'(@"die", acBattler, @"Used instead of `hurt' when dying"), (happens before enemy dissolve) '/ _ (@"dead", acBatHero, @"Heroes with 0 HP"), _ /'(@"revive", acBatHero, @"When HP increases above 0") '/ _ (@"selected", acAny, @"Selected in a menu"), _ (NULL, 0, NULL) _ } dim builtin_anim_variants(...) as AnimVariantInfo => { _ (@"(none)", acAny, @"Used if there's no more specific animation"), _ (@"up", acActor, @"Facing direction"), _ (@"right", acActor, @"Facing direction"), _ (@"down", acActor, @"Facing direction"), _ (@"left", acActor, @"Facing direction"), _ (@"weak", acBattler, @"HP less than the Weak threshold (see Battle System Options)"), _ /'(@"missed", acBattler, @"Performing or hit by an attack that missed"), '/ _ /'(@"resisted", acBattler, @"Performing or hit by an attack that failed"), '/ _ (NULL, 0, NULL) _ } function get_anim_doc() as DocPtr if _anim_doc = NULL then _anim_doc = CreateDocument() return _anim_doc end function ' Creates the default global animations for a sprite type in an existing AnimationSet. ' TODO: very unfinished. These could be defined in a RELOAD data file instead of by code. sub spriteset_default_global_animations(byref animset as AnimationSet, sprtype as SpriteType) debuginfo "Using default global animations for " & sprite_sizes(sprtype).name dim n as NodePtr #define op(opname) n = .append(opname) #define arg(argname, argvalue) AppendChildNode(n, argname, argvalue) animset.delete_all_animations() if sprtype = sprTypeHero then with *animset.new_animation("idle") op(animOpFrame) arg("frame", frameSTAND) '0 end with with *animset.new_animation("walk") op(animOpPlayFrameGroup) arg("framegroup", 0) '(frameSTAND + frameSTEP) arg("ms", 110) op(animOpRepeat) end with with *animset.new_animation("attack") op(animOpPlayFrameGroup) op(animOpPlayFrameGroup) arg("framegroup", 1) '(frameATTACKA + frameATTACKB) arg("ms", 110) op(animOpFrame) arg("frame", frameSTAND) '0 end with end if if sprtype = sprTypeWalkabout then with *animset.new_animation("walk", "up") op(animOpPlayFrameGroup) arg("framegroup", 0) arg("ms", 110) op(animOpRepeat) end with with *animset.new_animation("walk", "right") op(animOpPlayFrameGroup) arg("framegroup", 1) arg("ms", 110) op(animOpRepeat) end with with *animset.new_animation("walk", "down") op(animOpPlayFrameGroup) arg("framegroup", 2) arg("ms", 110) op(animOpRepeat) end with with *animset.new_animation("walk", "left") op(animOpPlayFrameGroup) arg("framegroup", 3) arg("ms", 110) op(animOpRepeat) end with with *animset.new_animation("selected") op(animOpSwitchAnim) arg("anim", "walk") arg("variant", "down") end with end if end sub sub set_animation_framerate(ms as integer) ' We bound to 5-200 because set_speedcontrol does the same thing ms_per_frame = bound(ms, 5, 200) end sub function get_animation_framerate() as integer return ms_per_frame end function function ms_to_frames(ms as integer) as integer if ms <= 0 then return 0 return large(1, CINT(ms / ms_per_frame)) 'Round to nearest end function function frames_to_ms(frames as integer) as integer return frames * ms_per_frame end function '========================================================================================== ' Slice properties '========================================================================================== declare sub set_slice_blend_property(sl as Slice ptr, propnum as SlicePropertyNumber, drawopts as DrawOptions, datnode as NodePtr) local sub setup_slice_property_numbers() slice_property_numbers.construct(31) slice_property_numbers.key_is_opaque_ptr = YES for idx as integer = 0 to ubound(slice_property_names) dim prop as zstring ptr = intern_string(slice_property_names(idx)) slice_property_numbers.set(prop, idx) next end sub ' Sets a property (such as "w") or pseudo-property (such as "screen_x") of a slice to ' value(s) taken from datnode. ' Rather than just raw writes to slice data, this should aim to provide a higher-level ' interface comparable to script commands such as "set opacity" or "move slice to" which ' modify multiple data at once. ' There can be multiple properties for the same thing (e.g. "frame" and "frameid"). ' datnode could contain more than one value, but there's no examples of that yet. ' If a property name isn't matched, we do nothing, and don't detect that case. sub set_slice_property(sl as Slice ptr, prop as zstring ptr, datnode as NodePtr) if slice_property_numbers.constructed = NO then setup_slice_property_numbers end if dim propnum as SlicePropertyNumber propnum = slice_property_numbers.get_int(intern_string(prop), _INVALID_PROP) dim intval as integer = datnode..int dim boolval as bool = intval <> 0 ' == datnode..bool ' When possible, if there are slice types with comparable properties, ' they should accept the same property names. ' Animation properties are the same as the node names used in .slices with the following exceptions: ' Ellipse: use "col" and "bgcol" instead of "bordercol" and "fillcol", to match Rect, Line and Text slices ' Rectangle: use "col" and "bgcol" instead of "fg" and "bg", ditto ' Rectangle: use "translucent" instead of "trans" to not match Sprite "trans" transparency setting ' Layout: use "padding" instead of "padding0" to match Panel "padding" if propnum < _LAST_GENERIC_PROP then ' Properties applicable to all slice types select case as const propnum case _x sl->X = intval case _y sl->Y = intval case _screen_x RefreshSliceScreenPos sl sl->X += intval - (sl->ScreenX + SliceXAnchor(sl)) case _screen_y RefreshSliceScreenPos sl sl->Y += intval - (sl->ScreenY + SliceYAnchor(sl)) case _w ' "set slice width" allows negative width, so we will too. sl->Width = intval slice_edit_updates sl, @sl->Width case _h sl->Height = intval slice_edit_updates sl, @sl->Height case _cover ' Not exposed to users yet if in_bound(intval, 0, 3) then sl->CoverChildren = intval slice_edit_updates sl, @sl->CoverChildren end if case _fill sl->Fill = boolval slice_edit_updates sl, @sl->Fill case _fillmode if in_bound(intval, 0, 2) then sl->FillMode = intval slice_edit_updates sl, @sl->FillMode end if case _vis sl->Visible = boolval case _paused ' Doesn't make sense for an animation to pause itself, but ' set_slice_property might get called elsewhere. sl->Paused = boolval case _clip sl->Clip = boolval case _alignh if in_bound(intval, alignLeft, alignRight) then sl->AlignHoriz = intval end if case _alignv if in_bound(intval, alignLeft, alignRight) then sl->AlignVert = intval end if case _anchorh if in_bound(intval, alignLeft, alignRight) then sl->AnchorHoriz = intval end if case _anchorv if in_bound(intval, alignLeft, alignRight) then sl->AnchorVert = intval end if case _clamph if in_bound(intval, alignLeft, alignBoth) then sl->ClampHoriz = intval end if case _clampv if in_bound(intval, alignLeft, alignBoth) then sl->ClampVert = intval end if case _clamptoscreen sl->ClampToScreen = boolval case _padt sl->PaddingTop = intval case _padr sl->PaddingRight = intval case _padb sl->PaddingBottom = intval case _padl sl->PaddingLeft = intval case _autosort if in_bound(intval, 0, slAutoSortLAST) then sl->AutoSort = intval end if case _sort sl->Sorter = intval case _lookup ' TODO: allow other lookups in some cases if sl->Lookup >= 0 andalso intval >= 0 then sl->Lookup = intval end if case _vtickx sl->VelTicks.X = bound(intval, -1, 999999) sl->TargTicks = 0 case _vx sl->Velocity.X = bound(intval, -999999, 999999) if sl->VelTicks.X = 0 then sl->VelTicks.X = -1 sl->TargTicks = 0 end if case _vticky sl->VelTicks.Y = bound(intval, -1, 999999) sl->TargTicks = 0 case _vy sl->Velocity.Y = bound(intval, -999999, 999999) if sl->VelTicks.Y = 0 then sl->VelTicks.Y = -1 sl->TargTicks = 0 end if case _ttick sl->TargTicks = bound(intval, 0, 999999) sl->VelTicks = 0 case _tx sl->Targ.X = intval sl->VelTicks = 0 case _ty sl->Targ.Y = intval sl->VelTicks = 0 end select else ' Properties which apply to only some types (usually only one) select case sl->SliceType case slMap dim dat as MapSliceData ptr = sl->MapData set_slice_blend_property(sl, propnum, dat->drawopts, datnode) case slSprite dim dat as SpriteSliceData ptr = sl->SpriteData ' Various sprite data that's not editable by users is missing select case as const propnum case _frame dat->set_frame(sl, intval) case _frameid dat->set_frameid(sl, intval) case _sprtype if in_bound(intval, 0, sprTypeLastPickable) then ChangeSpriteSlice sl, intval end if case _rec 'if in_bound(intval, 0, sprite_sizes(dat->spritetype).lastrec) then ' Handles bounds checking ChangeSpriteSlice sl, , intval case _pal if in_bound(intval, -1, gen(genMaxPal)) then ChangeSpriteSlice sl, , , intval end if case _trans dat->trans = boolval case _fliph dat->flipHoriz = boolval case _flipv dat->flipVert = boolval case _dissolving dat->dissolving = boolval case _d_type if in_bound(intval, 0, dissolveTypeMax) then dat->d_type = intval dat->dissolving = YES end if case _d_time if in_bound(intval, -1, 999999) then dat->d_time = intval end if case _d_tick if in_bound(intval, 0, 999999) then dat->d_tick = intval dat->dissolving = YES end if case _d_back dat->d_back = boolval case else set_slice_blend_property(sl, propnum, dat->drawopts, datnode) end select case slRectangle dim dat as RectangleSliceData ptr = sl->RectData select case as const propnum case _style if in_bound(intval, -1, 14) then ChangeRectangleSlice sl, intval end if case _bgcol 'bg if in_bound(intval, LowColorCode(), 255) then ChangeRectangleSlice sl, , intval end if case _col 'fg if in_bound(intval, LowColorCode(), 255) then ChangeRectangleSlice sl, , , intval end if case _border if in_bound(intval, -2, 14) then ChangeRectangleSlice sl, , , , intval end if case _raw_box_border if in_bound(intval, 0, gen(genMaxBoxBorder)) then ChangeRectangleSlice sl, , , , , , , intval end if case _translucent if in_bound(intval, 0, transLAST) then dat->translucent = intval end if case _fuzzfactor if in_bound(intval, 0, 99) then dat->fuzzfactor = intval end if case _fz_zoom dat->fuzz_zoom = bound(intval, 1, 10000) case _fz_stationary dat->fuzz_stationary = boolval end select case slLine dim dat as LineSliceData ptr = sl->LineData select case propnum case _col if in_bound(intval, LowColorCode(), 255) then dat->col = intval end if end select case slText dim dat as TextSliceData Ptr = sl->TextData select case propnum case _s ChangeTextSlice sl, datnode..str case _col if in_bound(intval, LowColorCode(), 255) then dat->col = intval end if case _bgcol if in_bound(intval, LowColorCode(), 255) then dat->bgcol = intval end if case _outline 'dat->outline = boolval ChangeTextSlice sl, , , boolval case _wrap ChangeTextSlice sl, , , , boolval end select case slGrid dim dat as GridSliceData Ptr = sl->GridData select case propnum case _rows if in_bound(intval, 0, 99) then dat->rows = intval end if case _cols if in_bound(intval, 0, 99) then dat->cols = intval end if case _show dat->show = boolval end select case slEllipse dim dat as EllipseSliceData Ptr = sl->EllipseData select case propnum case _col 'bordercol if in_bound(intval, LowColorCode(), 255) then dat->bordercol = intval end if case _bgcol 'fillcol if in_bound(intval, LowColorCode(), 255) then dat->fillcol = intval end if end select case slScroll dim dat as ScrollSliceData Ptr = sl->ScrollData select case propnum case _style ' Note that Rectangle style allows -1 for "None" if in_bound(intval, 0, 14) then dat->style = intval end if case _check_depth dat->check_depth = large(0, intval) end select case slSelect dim dat as SelectSliceData Ptr = sl->SelectData select case propnum case _index dat->index = large(0, intval) end select case slPanel dim dat as PanelSliceData Ptr = sl->PanelData select case propnum case _vertical dat->vertical = boolval case _primary if in_bound(intval, 0, 1) then dat->primary = intval end if case _percent if in_bound(datnode..float, 0.0, 1.0) then dat->percent = intval end if case _pixels dat->pixels = large(intval, 0) case _padding dat->padding = large(intval, 0) end select case slLayout dim dat as LayoutSliceData Ptr = sl->LayoutData select case propnum case _dir0 if in_bound(intval, 0, 3) then dat->primary_dir = intval end if case _dir1 if in_bound(intval, 0, 3) then dat->secondary_dir = intval end if case _justified dat->justified = boolval case _last_row_justified dat->last_row_justified = boolval case _skip_hidden dat->skip_hidden = boolval case _row_align if in_bound(intval, alignLeft, alignRight) then dat->row_alignment = intval end if case _cell_align if in_bound(intval, alignLeft, alignRight) then dat->cell_alignment = intval end if case _padding 'padding0 dat->primary_padding = intval case _padding1 dat->secondary_padding = intval case _min_breadth dat->min_row_breadth = large(0, intval) end select end select end if end sub sub set_slice_blend_property(sl as Slice ptr, propnum as SlicePropertyNumber, drawopts as DrawOptions, datnode as NodePtr) dim intval as integer = datnode..int select case propnum case _blending drawopts.with_blending = datnode..bool case _opacity drawopts.opacity = bound(datnode..float, 0.0, 1.0) if drawopts.opacity < 1.0 then drawopts.with_blending = YES case _blend_mode if in_bound(intval, 0, blendModeLAST) then drawopts.blend_mode = intval drawopts.with_blending = YES end if case _mod_r drawopts.argbModifier.R = bound(intval, 0, 255) drawopts.with_blending = YES case _mod_g drawopts.argbModifier.G = bound(intval, 0, 255) drawopts.with_blending = YES case _mod_b drawopts.argbModifier.B = bound(intval, 0, 255) drawopts.with_blending = YES end select end sub ' Set a slice property as the interpolation of two value nodes. ' x is a value from 0.0 (to use value0) to 1.0 (to use value1) sub interpolate_slice_property(sl as Slice ptr, prop as zstring ptr, x as double, value0 as NodePtr, value1 as NodePtr) dim as NodePtr intervalue = CreateNode(value0, "value") select case NodeType(value0) case rltInt dim vals(1) as integer vals(0) = value0..int vals(1) = value1..int SetContent intervalue, vals(0) + x * (vals(1) - vals(0)) case rltFloat dim vals(1) as double vals(0) = value0..float vals(1) = value1..float SetContent intervalue, vals(0) + x * (vals(1) - vals(0)) case rltString, rltInternString ' Cute text interpolation: first erase the text character by characer ' down to the common prefix, then add the value1 suffix. dim vals(1) as string vals(0) = value0..str vals(1) = value1..str dim matchlen as integer = length_matching(vals(0), vals(1)) dim edit_dist as integer = len(vals(0)) + len(vals(1)) - 2 * matchlen + 1 dim curchar as integer = edit_dist * x dim prefix0len as integer = len(vals(0)) - curchar ' TODO: will need reimplementation to support text markup if prefix0len >= matchlen then SetContent intervalue, left(vals(0), prefix0len) else SetContent intervalue, left(vals(1), 2 * matchlen - prefix0len) end if end select set_slice_property sl, prop, intervalue FreeNode intervalue end sub '========================================================================================== ' Animation '========================================================================================== ' constructor Animation() ' reference() ' opsnode = CreateNode(get_anim_doc) ' end constructor constructor Animation(name as string, variant as string = "") this.name = name this.variant = variant reference() this.opsnode = CreateNode(get_anim_doc, "ops") end constructor destructor Animation() '? " Animation.destructor(" & hex(@this) & " " & name & " " & variant & ")" FreeNode(this.opsnode) end destructor function Animation.reference() as Animation ptr refcount += 1 '? "Animation.reference(" & hex(@this) & " " & name & " " & variant & "): refc=" & refcount return @this end function sub Animation.dereference() refcount -= 1 '? "Animation.dereference(" & hex(@this) & " " & name & " " & variant & "): refc=" & refcount BUG_IF(refcount < 0, "Too many Animation.dereference()") if refcount = 0 then delete @this end if end sub ' Deep copy function Animation.duplicate() as Animation ptr '? "Animation.duplicate(" & hex(@this) & " " & name & " " & variant & "):" dim ret as Animation ptr = new Animation(name, variant) ret->refcount = 1 FreeNode ret->opsnode ret->opsnode = CloneNodeTree(opsnode, get_anim_doc) return ret end function sub Animation.replace_ops(copy_from as NodePtr) FreeNode(opsnode) opsnode = CloneNodeTree(copy_from, get_anim_doc) end sub function Animation.append(optype as AnimOpType) as NodePtr BUG_IF(opsnode = 0, "opsnode null", 0) return AppendChildNode(opsnode, anim_op_node_names(optype), optype) end function ' Changes the type (operator) of an op while leaving its children unmodified, which ' is useful if the new type takes the same args. sub Animation.mutate_op(op as NodePtr, optype as AnimOpType) SetContent(op, optype) RenameNode(op, anim_op_node_names(optype)) end sub '========================================================================================== ' AnimationSet '========================================================================================== ' Create deep copies of animations; shared_set/fallback_set are shared. ' The result has refcount 1 (comparable to reference()). ' When this is called on a SpriteSet you get an AnimationSet. You can't duplicate a SpriteSet ' (well, it could be implemented with frame_duplicate) function AnimationSet.duplicate() as AnimationSet ptr dim ret as AnimationSet ptr = new AnimationSet ret->refcount = 1 ret->slice_specific = slice_specific ret->name = name if shared_set then ret->shared_set = shared_set->reference() end if if fallback_set then ret->fallback_set = fallback_set->reference() end if v_new ret->animations for idx as integer = 0 to v_len(animations) - 1 v_append ret->animations, animations[idx]->duplicate() next return ret end function destructor AnimationSet() 'If deleting an AnimationSet that's a SpriteSet, noone should still be playing its animations! '(The Animations can remain referenced, but the Frames might be gone) delete_all_animations(YES) 'check_no_references = YES unload_shared_animsets end destructor sub AnimationSet.unload_shared_animsets() animset_unload @shared_set animset_unload @fallback_set end sub ' Increment refcount. function AnimationSet.reference() as AnimationSet ptr refcount += 1 DEBUG_ANIM_CACHE(? "AnimationSet.reference(" & hex(@this) & " " & name & "): refc=" & refcount) return @this end function ' Recommended to call the animset_unload() wrapper instead sub AnimationSet.dereference() refcount -= 1 DEBUG_ANIM_CACHE(? "AnimationSet.dereference(" & hex(@this) & " " & name & "): refc=" & refcount) BUG_IF(refcount < 0, "Negative refc") if refcount = 0 then delete @this end if end sub ' Decrement refcount and delete on zero sub animset_unload(pp as AnimationSet ptr ptr) if *pp then (*pp)->dereference() *pp = NULL end if end sub sub AnimationSet.delete_all_animations(check_no_references as bool = NO) for idx as integer = 0 to v_len(animations) - 1 if check_no_references then BUG_IF(animations[idx]->refcount <> 1, "Leaked reference to animation") end if animations[idx]->dereference() animations[idx] = 0 next v_free animations end sub ' animvariant can contain a trailing space sub split_animvariant(animvariant as string, byref animname as string, byref variant as string) dim spacepos as integer = instr(animvariant, " ") if spacepos then animname = left(animvariant, spacepos - 1) variant = mid(animvariant, spacepos + 1) else animname = animvariant variant = "" end if end sub ' Searches for an animation with a certain name, or NULL if there's no match. ' If exact=YES, the variant must match exactly, otherwise looks for best match. ' If recurse=YES, go through the chain of shared_sets and then the chain of ' fallback_sets as if they were all concatenated. But we don't recurse from a ' shared_set to its fallback_set or vice versa. This is because fallback_set ' is intended for spriteset-specific animations and shared_set for looking at ' some ancestor slice for shared defaults, but we don't want to use defaults ' from ancestor's spriteset. ' animvariant is either just the name of the animation, or the ' name plus an optional variant separated by a space, e.g. "walk upleft", "walk ", "walk". ' The nearest match is picked amongst animations which match the name: ' - prefer variant as specified ' - then prefer an animation with blank variant ' - then prefer the first animation (with that name) function AnimationSet.find_animation(animvariant as string, exact as bool = NO, recurse as bool = YES) as Animation ptr dim as string animname, variant split_animvariant animvariant, animname, variant return find_animation_recurse(animname, variant, exact, recurse, recurse, 0) end function function AnimationSet.find_animation_recurse(animname as string, variant as string, exact as bool, recurse1 as bool, recurse2 as bool, byref _best_score as integer) as Animation ptr dim as Animation ptr best_match, anim, best_fallback for idx as integer = 0 to v_len(animations) - 1 anim = animations[idx] if anim->name = animname then ' Right name, check how good the match of variants is dim score as integer if anim->variant = variant then score = 1000 elseif exact then score = 0 elseif len(anim->variant) = 0 then 'Prefer nonvariant animations score = 10 else 'Otherwise, default to the first variant score = 1 end if if _best_score < score then best_match = anim _best_score = score end if end if next if recurse1 andalso shared_set then ' When called recursively, returns null unless a better match ' than _best_score was found, which is updated. best_fallback = shared_set->find_animation_recurse(animname, variant, exact, YES, NO, _best_score) if best_fallback then best_match = best_fallback end if if recurse2 andalso fallback_set then best_fallback = fallback_set->find_animation_recurse(animname, variant, exact, NO, YES, _best_score) if best_fallback then best_match = best_fallback end if return best_match end function ' Get exactly this animvariant and do not recurse function AnimationSet.get_animation(animvariant as string) as Animation ptr return find_animation(animvariant, YES, NO) end function ' Append a new blank animation and return pointer function AnimationSet.new_animation(name as string = "", variant as string = "") as Animation ptr dim ret as Animation ptr = new Animation(name, variant) if animations = NULL then v_new animations end if v_append animations, ret return ret end function sub AnimationSet.delete_animation(anim as Animation ptr) if anim then anim->dereference() v_remove animations, anim end if end sub '========================================================================================== ' AnimationState '========================================================================================== constructor AnimationState(rhs as AnimationState) memcpy(@this, @rhs, sizeof(this)) if anim then anim->reference() end constructor constructor AnimationState(sl as Slice ptr) this.sl = sl sl->GetAnimations() 'Ensure nonnull ' Maybe we need to keep our own reference to sl->Animations in case it's replaced? end constructor destructor AnimationState() set_anim(NULL) 'Dec refcount end destructor ' Lookup an animation and start it. See AnimationSet.find_animation() for documentation ' of animvariant (animation name plus optional variant). ' Normally an animation specifies how many times it loops (unimplemented), or ends in Repeat ' to loop forever. loopcount <> 0 overrides this, giving a fixed number of ' times to play, or < 0 to repeat forever function AnimationState.start_animation(animvariant as string, loopcount as integer = 0) as Animation ptr start_animation(sl->Animations->find_animation(animvariant), loopcount) return anim end function ' OK for anim to be NULL function AnimationState.start_animation(anim as Animation ptr, loopcount as integer = 0) as Animation ptr anim_wait = 0 anim_advanced = YES anim_loop = loopcount anim_looplimit = ANIMATION_LOOPLIMIT set_anim(anim) if anim then curop = FirstChild(anim->opsnode) else curop = NULL end if return anim end function ' Start an animation if it's not already playing. If it is, change the loopcount (if not -1). ' Stop if the animation doesn't exist. function AnimationState.switch_animation(animvariant as string, loopcount as integer = -1) as bool return switch_animation(sl->Animations->find_animation(animvariant), loopcount) end function ' OK for anim to be NULL function AnimationState.switch_animation(to_anim as Animation ptr, loopcount as integer = -1) as bool if to_anim <> anim then start_animation(to_anim, loopcount) return YES elseif loopcount > -1 then anim_loop = loopcount end if return NO end function ' Doesn't reset the sprite. sub AnimationState.stop_animation() set_anim(NULL) anim_wait = 0 curop = NULL end sub sub AnimationState.set_anim(newanim as Animation ptr) if anim then anim->dereference() end if if newanim then newanim->reference() end if anim = newanim end sub ' Resets everything that an animation might change, but doesn't stop it ' TODO: requires keeping an undo log... which could be shared with the slice editor! sub AnimationState.reset() /' sl->Pos = 0 if sl->SliceType = slSprite then sl->SpriteData->frame = 0 end if '/ end sub ' Advance time until the next wait, skipping the current one, and returns number of ms that the wait was for. ' Returns -1 and does nothing if not waiting, -2 on error. ' The return value ought to be independent of ms_per_frame ' Note: any time already spent on the current wait is ignored. function AnimationState.skip_wait() as integer if anim = NULL orelse curop = NULL then return -2 ' Look at the current op instead of anim_wait, because it might be a wait ' which we haven't looked at yet. declare as NodePtr curop select case curop..int case animOpWait, animOpWaitMS, animOpPlayFrameGroup dim ret as integer = curop."ms" anim_wait = ms_to_frames(ret) if animate() = NO then ret = -2 ' Until next wait return ret case animOpTween ' TODO: don't know yet how to handle this for gifs 'var lastfr = LastChild(curop, "keyframe") 'if lastfr = NULL then return -1 case else return -1 end select end function ' Calculate and apply the current state of a tween. ' curop is a animOpTween node with "key" and "keyframe" children, which are assumed to be sorted by time. ' curtime is in ms. ' Returns true if curtime is before the end of the tween. function tween_keyframes(sl as Slice ptr, curop as NodePtr, curtime as integer) as bool dim as NodePtr prevframe, nextframe prevframe = FirstChild(curop, "keyframe") nextframe = prevframe if prevframe = NULL then return NO nextframe = NextSibling(prevframe, "keyframe") if nextframe = NULL orelse curtime < prevframe..int then set_slice_property sl, curop."key".zstring, prevframe."value".ptr return NO end if ' Find the interval containing curtime dim times(1) as integer while nextframe times(0) = prevframe..int times(1) = nextframe..int if curtime >= times(0) andalso curtime <= times(1) then var diff = times(1) - times(0) dim x as double = 0 if diff then x = (curtime - times(0)) / diff end if ' The first keyframe might have a time > 0ms. Don't extrapolate if x < 0 then x = 0 interpolate_slice_property sl, curop."key".zstring, x, prevframe."value".ptr, nextframe."value".ptr return YES end if prevframe = nextframe nextframe = NextSibling(nextframe, "keyframe") wend return NO end function ' Advance the animation by one op. ' Returns true on success or finished animation, false on error. ' Sets anim = NULL on error or finished animation. ' Does not check for infinite loops; caller must do that. function AnimationState.animate_step() as AnimationState.StepResult declare as NodePtr curop '? "animate_step: curop", curop, curop..name, curop..int if anim = NULL then return stepError ' This happens only if the animation doesn't end with Repeat if curop = NULL then anim_looplimit -= 1 ' anim_loop = 0 means default number of loops ' Also refuse to loop if empty. if anim_loop = 0 or anim_loop = 1 orelse NumChildren(anim->opsnode) = 0 then return stepEnd end if if anim_loop > 0 then anim_loop -= 1 curop = FirstChild(anim->opsnode) end if declare as NodePtr curop select case curop..int case animOpWait, animOpWaitMS ' These two opcodes are identical, differing only in how ' they are treated by the editor anim_wait += 1 if anim_wait > ms_to_frames(curop."ms") then anim_wait = 0 else return stepWait end if case animOpFrame if sl->SliceType <> slSprite then exit select 'Skip op sl->SpriteData->set_frame(sl, curop."frame") case animOpSetProp set_slice_property sl, curop."key".zstring, curop."value".ptr case animOpRepeat ' If a loop count was specified when playing the animation, ' then only loop that many times, otherwise repeat forever if anim_loop > 0 then anim_loop -= 1 if anim_loop = 0 then return stepEnd end if end if curop = FirstChild(anim->opsnode) anim_looplimit -= 1 return stepNext case animOpSetOffset /' offset.x = curop."x" offset.y = curop."y" '/ case animOpRelOffset /' offset.x += curop."x" offset.y += curop."y" '/ case animOpPlayFrameGroup if sl->SliceType <> slSprite then exit select 'Skip op if anim_advanced then ' Set initial frame if sl->SpriteData->set_frameid(sl, curop."framegroup" * 100, YES) = -1 then 'debug "frame group missing" exit select end if end if anim_wait += 1 if anim_wait > ms_to_frames(curop."ms") then '? "animOpPlayFrameGroup: wait done" ' Next frame dim as integer frameid, frameidx frameid = sl->SpriteData->get_frameid(sl) + 1 frameidx = sl->SpriteData->set_frameid(sl, frameid, YES) 'exact=YES '? "animOpPlayFrameGroup: advance to id=" & frameid & " idx=" & frameidx if frameidx = -1 then anim_wait = 0 'Done else anim_wait = 1 'Start wait return stepWait end if else '? "animOpPlayFrameGroup: waiting" return stepWait end if case animOpSwitchAnim ' TODO: should have a way to play an animation in shared_set/fallback_set ' switch_animation resets the looplimit, but we instead decrement it dim looplimit as integer = anim_looplimit - 1 if switch_animation(curop."anim".str & " " & curop."variant".str) then anim_looplimit = looplimit end if case animOpTween if tween_keyframes(sl, curop, frames_to_ms(anim_wait)) = NO then anim_wait = 0 'Done else anim_wait += 1 return stepWait end if case animOpUnknown ' An opcode from a future version not recognised by load_animation_node, ' which already printed an error. Skip it. case else ' Shouldn't happen showbug "bad animation opcode " & curop..name & " " & curop..int & " in '" & anim->name & "'" return stepError end select curop = NextSibling(curop) 'NULL if finished return stepNext end function ' Advance time by one tick. True on success or finished (anim is now NULL!), false on an error/infinite loop function AnimationState.animate() as bool if anim = NULL then return NO while anim_looplimit > 0 select case animate_step() case stepError stop_animation() return NO case stepWait anim_looplimit = ANIMATION_LOOPLIMIT 'Reset anim_advanced = NO return YES case stepNext ' animate_step advanced curop anim_advanced = YES case stepEnd stop_animation() return YES end select wend ' Exceeded the loop limit debug "animation '" & anim->name & "' got stuck in an infinite loop" stop_animation() return NO end function