Skip to main content

[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