[actor]
# =============================================================================
# ACTOR (FRAMEWORK FILE) VERSION 2026-04-19
# =============================================================================
# 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
# ---------------------------------------------------------------------------
constructor()
base()
.layer <- 1
._depth <- 2
.visible <- true
end constructor
# ---------------------------------------------------------------------------
# Initialize this actor using data from an "s_actor_spot" symbol.
new hook apply_spot(spot: s_actor_spot)
.x <- spot.x
.y <- spot.y
.clip <- spot.starting_clip
.theme <- spot.theme
.flip_x <- spot.flip_x
.flip_y <- spot.flip_y
.rotate <- spot.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
result <- ._renderlist_prev_link <> null
return result
end func
# ---------------------------------------------------------------------------
func set_depth(depth: byte)
if depth >= 4 then
kernel::fail_code(20) # function was called with an invalid argument
end if
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)
._last_clip <- .clip
._frame_index <- frame_index
# 0 indicates that we're loading a new frame
._duration <- 0
end func
# ---------------------------------------------------------------------------
func freeze_clip_frame(frame_index: byte)
._last_clip <- .clip
._frame_index <- frame_index
# 255 indicates means animation is disabled
._duration <- 255
end func
# ---------------------------------------------------------------------------
func render(sprite_index: int, frame_delta: pair)
var io_sprite: io_sprite
io_sprite <- io::sprites[sprite_index]
if .clip = null then
# Hide the sprite
io_sprite.pixels_address <- 0
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[]
frames <- .clip.frames
# If the current frame was set, make sure it is valid
if ._frame_index >= frames.size
do ._frame_index <- 0
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.
p <- 30
else
p <- frame_delta
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
._duration <- to_byte(._duration - 1)
else
# (if duration=0 then we are loading the first frame)
if ._duration <> 0 then
# counted down to 1, time for the next frame
._frame_index <- to_byte(._frame_index + 1)
if ._frame_index >= frames.size
do ._frame_index <- 0
end if
._duration <- frames[._frame_index].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
._duration <- 255
end if
end if
p <- to_pair(p - 1)
end loop
var x: int, y: int
var x_pair: pair, y_pair: pair
x <- .x
y <- .y
if not .screen_space then
x <- x - engine::camera_x
y <- y - engine::camera_y
end if
x_pair <- to_pair(x)
y_pair <- to_pair(y)
# 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)
io_sprite.pixels_address <- to_address(frames[._frame_index].pixels) + 3
io_sprite.x <- x_pair
io_sprite.y <- y_pair
io_sprite.width <- .clip.width
io_sprite.height <- .clip.height
if .layer >= 4
do kernel::fail_code(20) # function was called with an invalid argument
var flags: byte
flags <- .layer
if .flip_x
do flags <- to_byte(flags + 4)
if .flip_y
do flags <- to_byte(flags + 8)
if .rotate
do flags <- to_byte(flags + 16)
io_sprite.flags <- flags
io_sprite.theme <- .theme
else
io_sprite.pixels_address <- 0
end if
end if
end func
end class