[ACTOR]
# =============================================================================
# ACTOR (FRAMEWORK FILE) VERSION 2025-08-30
# =============================================================================
# AN ACTOR DISPLAYS AN ANIMATED SPRITE CLIP, WITH CUSTOMIZABLE THINKING.
# ACTORS ARE MANAGED BY THE ENGINE. IF YOU KNOW HOW TO MAKE YOUR OWN ENGINE,
# YOU COULD DELETE THIS FILE.
TYPE ACTOR_FACTORY IS FUNC(): ACTOR
# -----------------------------------------------------------------------------
CLASS ACTOR EXTENDS CLASS_ID_BASE
VAR CLIP: CLIP
VAR X: INT, Y: INT
# IF SCREEN_SPACE=TRUE, THEN THE CAMERA DOES NOT AFFECT THIS SPRITE
VAR SCREEN_SPACE: BOOL
VAR VISIBLE: BOOL
VAR FLIP_X: BOOL, FLIP_Y: BOOL, ROTATE: BOOL
VAR THEME: BYTE
# ASSIGNED TO IO_SPRITE.LAYER, WHICH CAN BE USED TO MOVE THE SPRITE BEHIND
# A TILEMAP
VAR LAYER: BYTE # 0..3; DEFAULT IS 1
# WITHIN A LAYER, SPRITES WITH A HIGHER DEPTH APPEAR BEHIND OTHER SPRITES
VAR _DEPTH: BYTE # 0..3; DEFAULT IS 2
# RENDER() USES A DOUBLE-LINKED LIST WITH ABILITY TO MOVE NODES AROUND
VAR _RENDERLIST_PREV_LINK: ACTOR
VAR _RENDERLIST_NEXT_LINK: ACTOR
# THINK() USES A SINGLE-LINKED LIST THAT DOES NOT ACTUALLY REMOVE ITEMS
# UNTIL AFTER THE ITERATOR HAS FINISHED
VAR _THINKLIST_NEXT_LINK: ACTOR
VAR _LAST_CLIP: CLIP
VAR _FRAME_INDEX: BYTE
VAR _DURATION: BYTE # SET TO 0 WHEN ASSIGNING A NEW INDEX
# ---------------------------------------------------------------------------
FUNC NEW()
BASE.NEW()
1 -> .LAYER
2 -> ._DEPTH
TRUE -> .VISIBLE
END FUNC
# ---------------------------------------------------------------------------
# INITIALIZE THIS ACTOR USING DATA FROM AN "S_ACTOR_SPOT" SYMBOL.
NEW HOOK APPLY_SPOT(SPOT: S_ACTOR_SPOT)
SPOT.X -> .X
SPOT.Y -> .Y
SPOT.STARTING_CLIP -> .CLIP
SPOT.THEME -> .THEME
SPOT.FLIP_X -> .FLIP_X
SPOT.FLIP_Y -> .FLIP_Y
SPOT.ROTATE -> .ROTATE
END HOOK
# ---------------------------------------------------------------------------
# THIS HOOK GETS CALLED BY THE ENGINE AFTER THE ACTOR IS ADDED TO THE
# THINKLIST. IT IS MAINLY FOR ACTORS THAT GET REMOVED AND THEN RE-ADDED.
# (NAMING CONVENTION: THE "ON_" PREFIX INDICATES A FUNCTION THAT IS CALLED
# BY SOME OTHER SYSTEM TO NOTIFY THAT AN EVENT HAS OCCURRED.)
NEW HOOK ON_ADDED()
END HOOK
# ---------------------------------------------------------------------------
# THIS HOOK GETS CALLED BY ENGINE::THINK() ONCE EVERY RENDERING FRAME.
# USE IT TO MOVE THE ACTOR AROUND ON THE SCREEN OR PERFORM OTHER BEHAVIORS.
NEW HOOK THINK()
END HOOK
# ---------------------------------------------------------------------------
# THIS HOOK GETS CALLED BY THE ENGINE AFTER THE ACTOR IS REMOVED FROM
# THE THINKLIST. USE IT TO CLEAN UP ANY RELATED OBJECTS.
NEW HOOK ON_REMOVED()
END HOOK
# ---------------------------------------------------------------------------
FUNC IS_ADDED(): BOOL
VAR RESULT: BOOL
._RENDERLIST_PREV_LINK <> NULL -> RESULT
RETURN RESULT
END FUNC
# ---------------------------------------------------------------------------
FUNC SET_DEPTH(DEPTH: BYTE)
IF DEPTH >= 4
DO KERNEL::FAIL("INVALID DEPTH")
IF ._DEPTH <> DEPTH THEN
DEPTH -> ._DEPTH
IF ._RENDERLIST_PREV_LINK <> NULL THEN
# SET_DEPTH() DOES NOT TRIGGER ON_ADDED() OR ON_REMOVED() EVENTS
ENGINE::_REMOVE_ACTOR(SELF)
# THINKLIST IS PRESERVED BECAUSE _PRUNE_THINKLIST() IS NOT CALLED HERE
ENGINE::_ADD_ACTOR(SELF, FALSE)
END IF
END IF
END FUNC
# ---------------------------------------------------------------------------
FUNC START_CLIP_FRAME(FRAME_INDEX: BYTE)
.CLIP -> ._LAST_CLIP
FRAME_INDEX -> ._FRAME_INDEX
# 0 INDICATES THAT WE'RE LOADING A NEW FRAME
0 -> ._DURATION
END FUNC
# ---------------------------------------------------------------------------
FUNC FREEZE_CLIP_FRAME(FRAME_INDEX: BYTE)
.CLIP -> ._LAST_CLIP
FRAME_INDEX -> ._FRAME_INDEX
# 255 INDICATES MEANS ANIMATION IS DISABLED
255 -> ._DURATION
END FUNC
# ---------------------------------------------------------------------------
FUNC RENDER(SPRITE_INDEX: INT, FRAME_DELTA: PAIR)
VAR IO_SPRITE: IO_SPRITE
IO::SPRITES[SPRITE_INDEX] -> IO_SPRITE
IF .CLIP = NULL THEN
# HIDE THE SPRITE
0 -> IO_SPRITE.PIXELS_ADDRESS
ELSE
# NOTE THAT ANIMATION CONTINUES NORMALLY WHEN .VISIBLE = FALSE
IF .CLIP <> ._LAST_CLIP THEN
# IF THE CLIP HAS CHANGED, RESET THE ANIMATION
.START_CLIP_FRAME(0)
END IF
VAR FRAMES: CLIP_FRAME[]
.CLIP.FRAMES -> FRAMES
# IF THE CURRENT FRAME WAS SET, MAKE SURE IT IS VALID
IF ._FRAME_INDEX >= FRAMES.SIZE
DO 0 -> ._FRAME_INDEX
VAR P: PAIR
IF FRAME_DELTA >= 30 THEN
# THE VIDEO SYSTEM RENDERS AT 30 FRAME/SEC; IF MORE THAN ONE SECOND
# HAS ELAPSED, THEN TRUNCATE FRAME_DELTA TO AVOID TOO MUCH LOOPING.
30 -> P
ELSE
FRAME_DELTA -> P
END IF
# TODO: OPTIMIZE THIS LOOP BY ITERATING ._FRAME_INDEX INSTEAD OF T
LOOP
IF P <= 0
DO DROP # FINISHED RENDERING
IF ._DURATION = 255
DO DROP # ANIMATION IS DISABLED
IF ._DURATION > 1 THEN
# COUNT DOWN
TO_BYTE(._DURATION - 1) -> ._DURATION
ELSE
# (IF DURATION=0 THEN WE ARE LOADING THE FIRST FRAME)
IF ._DURATION <> 0 THEN
# COUNTED DOWN TO 1, TIME FOR THE NEXT FRAME
TO_BYTE(._FRAME_INDEX + 1) -> ._FRAME_INDEX
IF ._FRAME_INDEX >= FRAMES.SIZE
DO 0 -> ._FRAME_INDEX
END IF
FRAMES[._FRAME_INDEX].DURATION -> ._DURATION
IF ._DURATION = 0 THEN
# 0 IS OUR SPECIAL CODE FOR LOADING A NEW FRAME,
# SO IF THE CLIP SPECIFIED THAT, MAKE IT A SYNONYM FOR 255
255 -> ._DURATION
END IF
END IF
TO_PAIR(P - 1) -> P
END LOOP
VAR X: INT, Y: INT
VAR X_PAIR: PAIR, Y_PAIR: PAIR
.X -> X
.Y -> Y
IF NOT .SCREEN_SPACE THEN
X - ENGINE::CAMERA_X -> X
Y - ENGINE::CAMERA_Y -> Y
END IF
TO_PAIR(X) -> X_PAIR
TO_PAIR(Y) -> Y_PAIR
# ALSO HIDE THE ACTOR IF TO_PAIR() OVERFLOWS
IF .VISIBLE AND X_PAIR = X AND Y_PAIR = Y THEN
# DISPLAY THE CURRENT FRAME (+3 SKIPS ARRAY SIZE)
TO_ADDRESS(FRAMES[._FRAME_INDEX].PIXELS) + 3 -> IO_SPRITE.PIXELS_ADDRESS
X_PAIR -> IO_SPRITE.X
Y_PAIR -> IO_SPRITE.Y
.CLIP.WIDTH -> IO_SPRITE.WIDTH
.CLIP.HEIGHT -> IO_SPRITE.HEIGHT
IF .LAYER >= 4
DO KERNEL::FAIL("BAD LAYER")
VAR FLAGS: BYTE
.LAYER -> FLAGS
IF .FLIP_X
DO TO_BYTE(FLAGS + 4) -> FLAGS
IF .FLIP_Y
DO TO_BYTE(FLAGS + 8) -> FLAGS
IF .ROTATE
DO TO_BYTE(FLAGS + 16) -> FLAGS
FLAGS -> IO_SPRITE.FLAGS
.THEME -> IO_SPRITE.THEME
ELSE
0 -> IO_SPRITE.PIXELS_ADDRESS
END IF
END IF
END FUNC
END CLASS