Skip to main content

[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