diff --git a/assets/levels/COMMON/obj_common_designtiles.png b/assets/levels/COMMON/obj_common_designtiles.png index c5ccce4..9e02535 100644 Binary files a/assets/levels/COMMON/obj_common_designtiles.png and b/assets/levels/COMMON/obj_common_designtiles.png differ diff --git a/assets/levels/COMMON/objects_common.toml b/assets/levels/COMMON/objects_common.toml index 9c7ca73..6849fb7 100644 --- a/assets/levels/COMMON/objects_common.toml +++ b/assets/levels/COMMON/objects_common.toml @@ -426,6 +426,60 @@ frames = [ ] 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 # ============ diff --git a/assets/levels/COMMON/objects_common.tsx b/assets/levels/COMMON/objects_common.tsx index 90f099d..d601e9a 100644 --- a/assets/levels/COMMON/objects_common.tsx +++ b/assets/levels/COMMON/objects_common.tsx @@ -1,6 +1,6 @@ - - + + @@ -52,4 +52,10 @@ + + + + + + diff --git a/assets/levels/R5/Z1.tmx b/assets/levels/R5/Z1.tmx index 75fe4bd..2fcb266 100644 --- a/assets/levels/R5/Z1.tmx +++ b/assets/levels/R5/Z1.tmx @@ -1,5 +1,5 @@ - + @@ -115,6 +115,16 @@ + + + + + + + + + + diff --git a/include/object.h b/include/object.h index e2f6c7d..8b26d48 100644 --- a/include/object.h +++ b/include/object.h @@ -27,6 +27,8 @@ typedef enum { OBJ_EXPLOSION = 0x0a, OBJ_MONITOR_IMAGE = 0x0b, OBJ_SHIELD = 0x0c, + OBJ_BUBBLE_PATCH = 0x0d, + OBJ_BUBBLE = 0x0e, } ObjectType; #define MASK_FLIP_FLIPX 0x1 // Flip on X axis diff --git a/include/object_state.h b/include/object_state.h index cece979..e678303 100644 --- a/include/object_state.h +++ b/include/object_state.h @@ -36,6 +36,20 @@ typedef struct { uint8_t kind; } 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 { uint16_t animation; uint8_t frame; diff --git a/src/object_state.c b/src/object_state.c index 111cdd0..f52eab8 100644 --- a/src/object_state.c +++ b/src/object_state.c @@ -99,6 +99,13 @@ load_object_placement(const char *filename, void *lvl_data) extra = alloc_arena_malloc(&_level_arena, sizeof(MonitorExtra)); ((MonitorExtra *)extra)->kind = get_byte(bytes, &b); 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 @@ -289,7 +296,8 @@ begin_render_routine: sort_prim(poly, ((state->id == OBJ_RING) || (state->id == OBJ_SHIELD) - || (state->id == OBJ_EXPLOSION)) + || (state->id == OBJ_EXPLOSION) + || (state->id == OBJ_BUBBLE)) ? OTZ_LAYER_OBJECTS : OTZ_LAYER_PLAYER); diff --git a/src/object_state_update.c b/src/object_state_update.c index 1e4ea3a..3c2c90e 100644 --- a/src/object_state_update.c +++ b/src/object_state_update.c @@ -35,6 +35,7 @@ extern int debug_mode; extern uint8_t level_ring_count; extern uint32_t level_score_count; extern uint8_t level_finished; +extern int32_t level_water_y; // 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 _shield_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. 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_SHIELD: _shield_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; } } + + +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 + } +} diff --git a/src/screen_level.c b/src/screen_level.c index 9c825fa..1a35112 100644 --- a/src/screen_level.c +++ b/src/screen_level.c @@ -97,6 +97,9 @@ screen_level_load() demo_init(); + // init proper RNG per level + srand(get_global_frames()); + // If it is a demo or we're recording, skip title card if(level_mode == LEVEL_MODE_DEMO || level_mode == LEVEL_MODE_RECORD) { diff --git a/tools/cookobj/datatypes.py b/tools/cookobj/datatypes.py index a347a30..a26f975 100644 --- a/tools/cookobj/datatypes.py +++ b/tools/cookobj/datatypes.py @@ -36,13 +36,11 @@ class ObjectId(Enum): SPRING_RED_DIAGONAL = 0x07 SWITCH = 0x08 GOAL_SIGN = 0x09 - - # Certain objects simply cannot be placed. - # This happens when the object in question is a particle - # or effect. - EXPLOSION = 0x0a - MONITOR_IMAGE = 0x0b - SHIELD = 0x0c + EXPLOSION = 0x0A + MONITOR_IMAGE = 0x0B + SHIELD = 0x0C + BUBBLE_PATCH = 0x0D + BUBBLE = 0x0E @staticmethod def get(name): @@ -60,6 +58,8 @@ class ObjectId(Enum): "explosion": ObjectId.EXPLOSION, "monitor_image": ObjectId.MONITOR_IMAGE, "shield": ObjectId.SHIELD, + "bubble_patch": ObjectId.BUBBLE_PATCH, + "bubble": ObjectId.BUBBLE, } result = switch.get(name.lower()) assert result is not None, f"Unknown common object {name}" @@ -230,7 +230,15 @@ class MonitorProperties: 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 diff --git a/tools/cookobj/parsers.py b/tools/cookobj/parsers.py index 063d28b..dea73b3 100644 --- a/tools/cookobj/parsers.py +++ b/tools/cookobj/parsers.py @@ -170,7 +170,12 @@ def parse_object_group( m = MonitorProperties() m.kind = MonitorKind.get(prop.get("value")).value 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( # f"Object type {current_ts.object_types[p.otype + current_ts.firstgid].name if p.otype >= 0 else 'DUMMY'}" # )