Add bubble patches and bubbles (no collision or drowning yet)

This commit is contained in:
Lucas S. Vieira 2024-12-19 23:35:14 -03:00
parent 888b363aa2
commit 69c885beb1
11 changed files with 240 additions and 13 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

@ -426,6 +426,60 @@ frames = [
] ]
loopback = 0 loopback = 0
# ============
[bubble_patch]
[[bubble_patch.animations]]
id = 0
name = "default"
duration = 16
frames = [
[16, 206, 16, 16],
[32, 206, 16, 16],
]
loopback = 0
# ============
[bubble]
[[bubble.animations]]
id = 0
name = "small"
duration = 8
frames = [
[ 2, 194, 4, 4],
[ 8, 192, 8, 8],
]
loopback = 1
[[bubble.animations]]
id = 1
name = "medium"
duration = 8
frames = [
[ 2, 194, 4, 4],
[ 8, 192, 8, 8],
[18, 194, 12, 12],
]
loopback = 2
[[bubble.animations]]
id = 2
name = "big"
duration = 8
frames = [
[ 2, 194, 4, 4],
[ 8, 192, 8, 8],
[18, 194, 12, 12],
[ 0, 200, 16, 16],
[ 0, 222, 23, 23],
[48, 172, 32, 32],
]
loopback = 5
# ============ # ============
# Dummy objects # Dummy objects
# ============ # ============

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.11.0" name="objects_common" tilewidth="64" tileheight="64" tilecount="16" columns="4"> <tileset version="1.10" tiledversion="1.11.0" name="objects_common" tilewidth="64" tileheight="64" tilecount="20" columns="4">
<image source="obj_common_designtiles.png" width="256" height="256"/> <image source="obj_common_designtiles.png" width="256" height="320"/>
<tile id="0" type="ring"/> <tile id="0" type="ring"/>
<tile id="1" type="monitor"> <tile id="1" type="monitor">
<properties> <properties>
@ -52,4 +52,10 @@
<tile id="13" type="explosion"/> <tile id="13" type="explosion"/>
<tile id="14" type="monitor_image"/> <tile id="14" type="monitor_image"/>
<tile id="15" type="shield"/> <tile id="15" type="shield"/>
<tile id="16" type="bubble_patch">
<properties>
<property name="frequency" type="int" value="0"/>
</properties>
</tile>
<tile id="17" type="bubble"/>
</tileset> </tileset>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="255" height="31" tilewidth="128" tileheight="128" infinite="0" nextlayerid="4" nextobjectid="109"> <map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="255" height="31" tilewidth="128" tileheight="128" infinite="0" nextlayerid="4" nextobjectid="112">
<tileset firstgid="1" source="128x128.tsx"/> <tileset firstgid="1" source="128x128.tsx"/>
<tileset firstgid="57" source="../COMMON/objects_common.tsx"/> <tileset firstgid="57" source="../COMMON/objects_common.tsx"/>
<layer id="1" name="LAYER0" width="255" height="31"> <layer id="1" name="LAYER0" width="255" height="31">
@ -115,6 +115,16 @@
<object id="105" gid="65" x="2961.5" y="834" width="64" height="64"/> <object id="105" gid="65" x="2961.5" y="834" width="64" height="64"/>
<object id="106" gid="61" x="3888" y="1104" width="64" height="64"/> <object id="106" gid="61" x="3888" y="1104" width="64" height="64"/>
<object id="108" gid="61" x="1024" y="848" width="64" height="64" rotation="90"/> <object id="108" gid="61" x="1024" y="848" width="64" height="64" rotation="90"/>
<object id="109" gid="73" x="1767" y="960" width="64" height="64">
<properties>
<property name="frequency" type="int" value="0"/>
</properties>
</object>
<object id="111" gid="73" x="1097" y="896" width="64" height="64">
<properties>
<property name="frequency" type="int" value="1"/>
</properties>
</object>
</objectgroup> </objectgroup>
</group> </group>
</map> </map>

View file

@ -27,6 +27,8 @@ typedef enum {
OBJ_EXPLOSION = 0x0a, OBJ_EXPLOSION = 0x0a,
OBJ_MONITOR_IMAGE = 0x0b, OBJ_MONITOR_IMAGE = 0x0b,
OBJ_SHIELD = 0x0c, OBJ_SHIELD = 0x0c,
OBJ_BUBBLE_PATCH = 0x0d,
OBJ_BUBBLE = 0x0e,
} ObjectType; } ObjectType;
#define MASK_FLIP_FLIPX 0x1 // Flip on X axis #define MASK_FLIP_FLIPX 0x1 // Flip on X axis

View file

@ -36,6 +36,20 @@ typedef struct {
uint8_t kind; uint8_t kind;
} MonitorExtra; } MonitorExtra;
typedef struct {
// Defined during design
uint8_t frequency;
// Runtime variables
uint8_t timer; // Decrementing random timer. idle=[128, 255]; producing=[0, 31]
uint8_t state; // 0 = idle, 1 = producing
uint8_t num_bubbles; // num=[1, 6] at every producing delay end
uint8_t bubble_set; // bubble set picked at random=[0, 3]
uint8_t cycle; // large bubble cycle counter
uint8_t bubble_idx;
uint8_t produced_big; // Whether a big bubble was produced this cycle
} BubblePatchExtra;
typedef struct { typedef struct {
uint16_t animation; uint16_t animation;
uint8_t frame; uint8_t frame;

View file

@ -99,6 +99,13 @@ load_object_placement(const char *filename, void *lvl_data)
extra = alloc_arena_malloc(&_level_arena, sizeof(MonitorExtra)); extra = alloc_arena_malloc(&_level_arena, sizeof(MonitorExtra));
((MonitorExtra *)extra)->kind = get_byte(bytes, &b); ((MonitorExtra *)extra)->kind = get_byte(bytes, &b);
break; break;
case OBJ_BUBBLE_PATCH:
extra = alloc_arena_malloc(&_level_arena, sizeof(BubblePatchExtra));
((BubblePatchExtra *)extra)->frequency = get_byte(bytes, &b);
// Start timer at a low timer so we start with idle instead of producing
((BubblePatchExtra *)extra)->timer = 8;
break;
} }
// Get chunk at position // Get chunk at position
@ -289,7 +296,8 @@ begin_render_routine:
sort_prim(poly, sort_prim(poly,
((state->id == OBJ_RING) ((state->id == OBJ_RING)
|| (state->id == OBJ_SHIELD) || (state->id == OBJ_SHIELD)
|| (state->id == OBJ_EXPLOSION)) || (state->id == OBJ_EXPLOSION)
|| (state->id == OBJ_BUBBLE))
? OTZ_LAYER_OBJECTS ? OTZ_LAYER_OBJECTS
: OTZ_LAYER_PLAYER); : OTZ_LAYER_PLAYER);

View file

@ -35,6 +35,7 @@ extern int debug_mode;
extern uint8_t level_ring_count; extern uint8_t level_ring_count;
extern uint32_t level_score_count; extern uint32_t level_score_count;
extern uint8_t level_finished; extern uint8_t level_finished;
extern int32_t level_water_y;
// Object-specific definitions // Object-specific definitions
@ -53,6 +54,8 @@ static void _explosion_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
static void _monitor_image_update(ObjectState *state, ObjectTableEntry *, VECTOR *); static void _monitor_image_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
static void _shield_update(ObjectState *state, ObjectTableEntry *, VECTOR *); static void _shield_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
static void _switch_update(ObjectState *state, ObjectTableEntry *, VECTOR *); static void _switch_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
static void _bubble_patch_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
static void _bubble_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
// Player hitbox information. Calculated once per frame. // Player hitbox information. Calculated once per frame.
static int32_t player_vx, player_vy; // Top left corner of player hitbox static int32_t player_vx, player_vy; // Top left corner of player hitbox
@ -115,6 +118,8 @@ object_update(ObjectState *state, ObjectTableEntry *typedata, VECTOR *pos)
case OBJ_MONITOR_IMAGE: _monitor_image_update(state, typedata, pos); break; case OBJ_MONITOR_IMAGE: _monitor_image_update(state, typedata, pos); break;
case OBJ_SHIELD: _shield_update(state, typedata, pos); break; case OBJ_SHIELD: _shield_update(state, typedata, pos); break;
case OBJ_SWITCH: _switch_update(state, typedata, pos); break; case OBJ_SWITCH: _switch_update(state, typedata, pos); break;
case OBJ_BUBBLE_PATCH: _bubble_patch_update(state, typedata, pos); break;
case OBJ_BUBBLE: _bubble_update(state, typedata, pos); break;
} }
} }
@ -653,3 +658,115 @@ _switch_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos)
state->props &= ~OBJ_FLAG_SWITCH_PRESSED; state->props &= ~OBJ_FLAG_SWITCH_PRESSED;
} }
} }
static void
_bubble_patch_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos)
{
// Fixed bubble patch sets
static const uint8_t bubble_size_sets[4][6] = {
{0, 0, 0, 0, 1, 0},
{0, 0, 0, 1, 0, 0},
{1, 0, 1, 0, 0, 0},
{0, 1, 0, 0, 1, 0},
};
BubblePatchExtra *extra = state->extra;
extra->timer--;
if(extra->timer == 0) {
// Flip production state between idle/producing.
// Since num_bubbles is always 0 when idle, we use this as shortcut
if(extra->num_bubbles == 0) {
extra->state = (extra->state + 1) % 2;
if(extra->state == 0) {
// Flipped from producing to idle. Pick a random delay [128; 255]
extra->timer = 128 + (rand() % 128);
extra->cycle = (extra->cycle + 1) % (extra->frequency + 1);
extra->produced_big = 0;
} else {
// Flipped from idle to producing
extra->num_bubbles = 1 + (rand() % 6); // Random [1; 6] bubbles
extra->bubble_set = rand() % 4; // Random bubble set [0; 4]
extra->bubble_idx = 0;
extra->timer = rand() % 32; // Random [0; 31] frames
}
} else {
// Produce a bubble according to constants
PoolObject *bubble = object_pool_create(OBJ_BUBBLE);
if(bubble) {
bubble->state.anim_state.animation =
bubble_size_sets[extra->bubble_set][extra->bubble_idx++];
bubble->freepos.vx = (pos->vx << 12) - (8 << 12) + ((rand() % 16) << 12);
bubble->freepos.vy = (pos->vy << 12);
// If this is a big bubble cycle, however, we may want to turn
// the current bubble into a big one
if((extra->cycle == extra->frequency) && !extra->produced_big) {
// Roll a dice with 1/4 of chance, but if this is a big
// bubble cycle and we're at the last bubble, make it big
// regardless!
if((extra->num_bubbles == 1) || !(rand() % 4)) {
extra->produced_big = 1;
bubble->state.anim_state.animation = 2;
}
}
}
extra->timer = rand() % 32; // Random [0; 31] frames
extra->num_bubbles--;
}
}
}
static void
_bubble_update(ObjectState *state, ObjectTableEntry *, VECTOR *)
{
// NOTE: this object can only exist as a free object. Do not insist.
// FIRST OFF: If way too far from camera, destroy it
if((state->freepos->vx < camera.pos.vx - (SCREEN_XRES << 13))
|| (state->freepos->vx > camera.pos.vx + (SCREEN_XRES << 13))
|| (state->freepos->vy < camera.pos.vy - (SCREEN_YRES << 13))
|| (state->freepos->vy > camera.pos.vy + (SCREEN_YRES << 13))
|| (state->timer >= 256)) {
state->props |= OBJ_FLAG_DESTROYED;
return;
}
// A bubble can be of three diameters: small (8), medium (12) or big (32).
// Animation dictates object diameter.
int32_t diameter = 0;
switch(state->anim_state.animation) {
default:
case 0: diameter = 8; break; // small (breath)
case 1: diameter = 12; break; // medium
case 2: diameter = 32; break; // big
}
// Bubbles should always be ascending with a 0.5 speed (-0x800).
state->freepos->vy -= 0x800;
// Bubbles also sway back-and-forth in a sine-like movement.
// x = initial_x + 8 * sin(timer / 128.0)
// When the bubble's top interact with water surface, destroy it
if(state->freepos->vy - (diameter << 12) <= level_water_y) {
state->props |= OBJ_FLAG_DESTROYED;
return;
}
// Bubbles can also only be interacted when big and on last animation frame.
// There is no destruction animation because of VRAM constraints; we still
// technically have a whole area available to add it, but I felt like I
// shouldn't create a whole new texture this time just because of a single
// bubble frame.
if(state->anim_state.animation == 2 && state->anim_state.frame == 5) {
// TODO
}
}

View file

@ -97,6 +97,9 @@ screen_level_load()
demo_init(); demo_init();
// init proper RNG per level
srand(get_global_frames());
// If it is a demo or we're recording, skip title card // If it is a demo or we're recording, skip title card
if(level_mode == LEVEL_MODE_DEMO if(level_mode == LEVEL_MODE_DEMO
|| level_mode == LEVEL_MODE_RECORD) { || level_mode == LEVEL_MODE_RECORD) {

View file

@ -36,13 +36,11 @@ class ObjectId(Enum):
SPRING_RED_DIAGONAL = 0x07 SPRING_RED_DIAGONAL = 0x07
SWITCH = 0x08 SWITCH = 0x08
GOAL_SIGN = 0x09 GOAL_SIGN = 0x09
EXPLOSION = 0x0A
# Certain objects simply cannot be placed. MONITOR_IMAGE = 0x0B
# This happens when the object in question is a particle SHIELD = 0x0C
# or effect. BUBBLE_PATCH = 0x0D
EXPLOSION = 0x0a BUBBLE = 0x0E
MONITOR_IMAGE = 0x0b
SHIELD = 0x0c
@staticmethod @staticmethod
def get(name): def get(name):
@ -60,6 +58,8 @@ class ObjectId(Enum):
"explosion": ObjectId.EXPLOSION, "explosion": ObjectId.EXPLOSION,
"monitor_image": ObjectId.MONITOR_IMAGE, "monitor_image": ObjectId.MONITOR_IMAGE,
"shield": ObjectId.SHIELD, "shield": ObjectId.SHIELD,
"bubble_patch": ObjectId.BUBBLE_PATCH,
"bubble": ObjectId.BUBBLE,
} }
result = switch.get(name.lower()) result = switch.get(name.lower())
assert result is not None, f"Unknown common object {name}" assert result is not None, f"Unknown common object {name}"
@ -230,7 +230,15 @@ class MonitorProperties:
f.write(c_ubyte(self.kind)) f.write(c_ubyte(self.kind))
ObjectProperties = MonitorProperties | None @dataclass
class BubblePatchProperties:
frequency: int = 0
def write_to(self, f):
f.write(c_ubyte(self.frequency))
ObjectProperties = MonitorProperties | BubblePatchProperties | None
@dataclass @dataclass

View file

@ -170,7 +170,12 @@ def parse_object_group(
m = MonitorProperties() m = MonitorProperties()
m.kind = MonitorKind.get(prop.get("value")).value m.kind = MonitorKind.get(prop.get("value")).value
p.properties = m p.properties = m
pass elif p.otype == ObjectId.BUBBLE_PATCH.value:
prop = props.find("property")
bp = BubblePatchProperties()
# Get first available value
bp.frequency = int(prop.get("value"))
p.properties = bp
# print( # print(
# f"Object type {current_ts.object_types[p.otype + current_ts.firstgid].name if p.otype >= 0 else 'DUMMY'}" # f"Object type {current_ts.object_types[p.otype + current_ts.firstgid].name if p.otype >= 0 else 'DUMMY'}"
# ) # )