aboutsummaryrefslogtreecommitdiff
path: root/game/src/game.c
diff options
context:
space:
mode:
authorJuan J. Martinez <jjm@usebox.net>2020-12-30 19:07:31 +0000
committerJuan J. Martinez <jjm@usebox.net>2020-12-30 19:23:41 +0000
commit2682bc5d1d864341aaeb42a449db73c3ecd16d70 (patch)
tree9116764364b4ee0ce7f6037305077807b57776de /game/src/game.c
downloadubox-msx-lib-2682bc5d1d864341aaeb42a449db73c3ecd16d70.tar.gz
ubox-msx-lib-2682bc5d1d864341aaeb42a449db73c3ecd16d70.zip
Initial import1.0
Diffstat (limited to 'game/src/game.c')
-rw-r--r--game/src/game.c437
1 files changed, 437 insertions, 0 deletions
diff --git a/game/src/game.c b/game/src/game.c
new file mode 100644
index 0000000..bc52b2a
--- /dev/null
+++ b/game/src/game.c
@@ -0,0 +1,437 @@
+#include <stdint.h>
+#include <string.h>
+
+#include "ubox.h"
+#include "spman.h"
+#include "mplayer.h"
+
+#include "aux.h"
+#include "main.h"
+
+#define LOCAL
+#include "game.h"
+
+// generated
+#include "map.h"
+#include "player.h"
+#include "enemy.h"
+
+void init_map_entities()
+{
+ const uint8_t *m = cur_map;
+ uint8_t typ, last = 0;
+ uint16_t i;
+
+ // init sprite and patterns
+ spman_init();
+
+ // this sets everything to 0, which is useful as
+ // entity ET_UNUSED is 0
+ memset(entities, 0, sizeof(struct entity) * MAX_ENTITIES);
+
+ // get to the beginning of the entities:
+ // map size + 3 bytes of header (the map size and the entities size)
+ m += (uint16_t)(m[0] | m[1] << 8) + 3;
+
+ // the entity list ends with 255
+ while (*m != 0xff)
+ {
+ // first byte is type + direction flag on MSB
+ // remove MSB
+ typ = m[0] & (~DIR_FLAG);
+
+ entities[last].type = typ;
+ entities[last].x = m[1];
+ entities[last].y = m[2];
+ // in the map: param is 1 (int) to look left
+ entities[last].dir = m[0] & DIR_FLAG ? 1 : 0;
+
+ switch (typ)
+ {
+ // can be only one; always first entity
+ // because our entities are sorted by type!
+ case ET_PLAYER:
+ // 3 frames x 2 sprites = 6
+ entities[last].pat = spman_alloc_pat(PAT_PLAYER, player_sprite[0], 6, 0);
+ spman_alloc_pat(PAT_PLAYER_FLIP, player_sprite[0], 6, 1);
+ entities[last].update = update_player;
+ break;
+
+ case ET_ENEMY:
+ // 3 frames
+ entities[last].pat = spman_alloc_pat(PAT_ENEMY, enemy_sprite[0], 3, 0);
+ spman_alloc_pat(PAT_ENEMY_FLIP, enemy_sprite[0], 3, 1);
+ entities[last].update = update_enemy;
+ break;
+ }
+
+ // next entity
+ last++;
+
+ // all our entities are 3 bytes
+ m += 3;
+ }
+
+ // count how many batteries are in the map
+ batteries = 0;
+ for (i = 0; i < MAP_W * MAP_H; ++i)
+ if (cur_map_data[i] == BATTERY_TILE)
+ batteries++;
+
+}
+
+void draw_map()
+{
+ // draw the map!
+ //
+ // - is not compressed (which limits how many maps we can store)
+ // - our map is just the tile numbers
+ //
+ // so we do it in one call by coping our map to the backtround tile map
+ // addr in the VDP VRAM
+ ubox_wait_vsync();
+ ubox_write_vm((uint8_t *)0x1800, MAP_W * MAP_H, cur_map_data);
+}
+
+void draw_hud()
+{
+ uint8_t i;
+
+ put_text(0, 21, "LIVES");
+
+ for (i = 0; i < MAX_LIVES; ++i)
+ if (i < lives)
+ // our hearts tile
+ ubox_put_tile(1 + i, 22, 193);
+ else
+ ubox_put_tile(1 + i, 22, WHITESPACE_TILE);
+}
+
+// x and y in pixels
+void erase_battery(uint8_t x, uint8_t y)
+{
+ uint8_t t;
+ int8_t mod;
+
+ // find out the bg tile to use
+
+ // border of the map
+ if ((x >> 3) == 0)
+ mod = 1;
+ else
+ mod = -1;
+
+ switch (cur_map_data[mod + (x >> 3) + (y >> 3) * MAP_W])
+ {
+ case 12:
+ t = 13;
+ break;
+ case 13:
+ t = 12;
+ break;
+ default:
+ // this is next to a wall
+ if (mod == 1)
+ t = 13;
+ else
+ t = 12;
+ break;
+ }
+
+ // change the map data so we don't pick it up again
+ cur_map_data[(x >> 3) + (y >> 3) * MAP_W] = t;
+
+ // erase on the screen
+ ubox_put_tile(x >> 3, y >> 3, t);
+ ubox_put_tile(x >> 3, (y >> 3) - 1, t);
+}
+
+// x and y in pixels
+uint8_t is_map_blocked(uint8_t x, uint8_t y)
+{
+ return cur_map_data[(x >> 3) + (y >> 3) * MAP_W] < LAST_SOLID_TILE + 1;
+}
+
+// x and y in pixels
+uint8_t is_map_battery(uint8_t x, uint8_t y)
+{
+ return cur_map_data[(x >> 3) + (y >> 3) * MAP_W] == BATTERY_TILE;
+}
+
+// x and y in pixels; always check the bottom tile!
+uint8_t is_map_elevator_down(uint8_t x, uint8_t y)
+{
+ uint8_t t = cur_map_data[(x >> 3) + (y >> 3) * MAP_W];
+
+ // first check the elevator platform comparing tiles
+ if (t != 9 && t != 10)
+ return 0;
+
+ // then check the elevator tube comparing tiles
+ t = cur_map_data[(x >> 3) + ((y >> 3) - 1) * MAP_W];
+ return (t != 14 && t != 15);
+}
+
+// x and y in pixels; always check the bottom tile!
+uint8_t is_map_elevator_up(uint8_t x, uint8_t y)
+{
+ uint8_t t = cur_map_data[(x >> 3) + (y >> 3) * MAP_W];
+
+ // first check the elevator platform comparing tiles
+ if (t != 9 && t != 10)
+ return 0;
+
+ // then check the elevator tube comparing tiles
+ t = cur_map_data[(x >> 3) + ((y >> 3) - 1) * MAP_W];
+ return (t == 14 || t == 15);
+}
+
+void update_enemy()
+{
+ // check for the player; if alive and not invulnerable!
+ // we use small hit boxes
+ if (lives && !invuln
+ && entities[0].x + 6 < self->x + 10 && self->x + 6 < entities[0].x + 10
+ && self->y == entities[0].y)
+ {
+ // change direction
+ self->dir ^= 1;
+
+ // remove one life (is more like "hits")
+ lives--;
+ draw_hud();
+ invuln = INVUL_TIME;
+
+ if (!lives)
+ {
+ // different sound effects if is game over
+ mplayer_init(SONG, SONG_SILENCE);
+ mplayer_play_effect_p(EFX_DEAD, EFX_CHAN_NO, 0);
+ gameover_delay = GAMEOVER_DELAY;
+ }
+ else
+ mplayer_play_effect_p(EFX_HIT, EFX_CHAN_NO, 0);
+ }
+
+ // left or right?
+ if (self->dir)
+ {
+ // change direction
+ if (self->x == 2 || is_map_blocked(self->x, self->y + 15))
+ self->dir ^= 1;
+ else
+ self->x -= 1;
+ }
+ else
+ {
+ // change direction
+ if (self->x == 255 - 16 || is_map_blocked(self->x + 15, self->y + 15))
+ self->dir ^= 1;
+ else
+ self->x += 1;
+ }
+
+ // update the walking animation
+ if (self->delay++ == FRAME_WAIT)
+ {
+ self->delay = 0;
+ if (++self->frame == WALK_CYCLE)
+ self->frame = 0;
+ }
+
+ // allocate the sprites
+ sp.x = self->x;
+ // y on the screen starts in 255
+ sp.y = self->y - 1;
+ // find which pattern to show
+ sp.pattern = self->pat + (walk_frames[self->frame] + self->dir * 3) * 4;
+ // red
+ sp.attr = 9;
+ spman_alloc_sprite(&sp);
+}
+
+void update_player()
+{
+ // to know if we need to update the walk animation
+ uint8_t moved = 0;
+
+ // player is dead
+ if (!lives)
+ return;
+
+ // decrease counter if set
+ if (invuln)
+ invuln--;
+
+ if (control & UBOX_MSX_CTL_RIGHT)
+ {
+ self->dir = DIR_RIGHT;
+ moved = 1;
+
+ // wrap horizontally
+ if (self->x == 255 - 16)
+ self->x = 0;
+ // check if not solid, using bottom right
+ else if (!is_map_blocked(self->x + 15, self->y + 15))
+ self->x += 2;
+ }
+
+ if (control & UBOX_MSX_CTL_LEFT)
+ {
+ self->dir = DIR_LEFT;
+ moved = 1;
+
+ // wrap horizontally
+ if (self->x == 2)
+ self->x = (uint8_t)(255 - 16);
+ // check if not solid, using bottom left
+ else if (!is_map_blocked(self->x, self->y + 15))
+ self->x -= 2;
+ }
+
+ // are we touching a battery?
+ // use bottom center
+ if (is_map_battery(self->x + 8, self->y + 15))
+ {
+ mplayer_play_effect_p(EFX_BATTERY, EFX_CHAN_NO, 0);
+ batteries--;
+ erase_battery(self->x + 8, self->y + 15);
+ }
+
+ if (control & UBOX_MSX_CTL_FIRE1)
+ {
+ // use flags to prevent repeat: the player will
+ // have to release fire to use the elevator again
+ if (!self->flags)
+ {
+ self->flags = 1;
+
+ // check elevator down; using bottom middle
+ if (is_map_elevator_down(self->x + 8, self->y + 16))
+ {
+ mplayer_play_effect_p(EFX_ELEVATOR, EFX_CHAN_NO, 0);
+ self->y += 8 * 4;
+ }
+ // then elevator up; using bottom middle
+ else if (is_map_elevator_up(self->x + 8, self->y + 16))
+ {
+ mplayer_play_effect_p(EFX_ELEVATOR, EFX_CHAN_NO, 0);
+ self->y -= 8 * 4;
+ }
+ }
+ }
+ else
+ {
+ if (self->flags)
+ self->flags = 0;
+ }
+
+ // it moved, or at least pushed against a wall
+ if (moved)
+ {
+ // update the walking animation
+ if (self->delay++ == FRAME_WAIT)
+ {
+ self->delay = 0;
+ if (++self->frame == WALK_CYCLE)
+ self->frame = 0;
+ }
+ }
+ else
+ {
+ // just stand
+ if (self->frame)
+ {
+ self->frame = 0;
+ self->delay = 0;
+ }
+ }
+
+ // if we are invulnerable, don't draw odd frames
+ // and we get a nice blinking effect
+ if (invuln & 1)
+ return;
+
+ // allocate the player sprites; fixed so they never flicker
+ sp.x = self->x;
+ // y on the screen starts in 255
+ sp.y = self->y - 1;
+ // find which pattern to show
+ sp.pattern = self->pat + (walk_frames[self->frame] + self->dir * 3) * 8;
+ // green
+ sp.attr = 12;
+ spman_alloc_fixed_sprite(&sp);
+ // second one is 4 patterns away (16x16 sprites)
+ sp.pattern += 4;
+ // white
+ sp.attr = 15;
+ spman_alloc_fixed_sprite(&sp);
+}
+
+void run_game()
+{
+ // init some variables; look at game.h for description
+ lives = MAX_LIVES;
+ invuln = 0;
+ gameover_delay = 0;
+
+ ubox_disable_screen();
+
+ ubox_fill_screen(WHITESPACE_TILE);
+
+ // we only have one map, select it
+ cur_map = map[0];
+ // copy map data into RAM, we will modify it
+ // map data starts on byte 3 (skip map data size and entities size)
+ memcpy(cur_map_data, cur_map + 3, MAP_W * MAP_H);
+
+ // init entities before drawing
+ init_map_entities();
+ draw_map();
+
+ draw_hud();
+
+ ubox_enable_screen();
+
+ mplayer_init(SONG, SONG_IN_GAME);
+
+ // our game loop
+ while (1)
+ {
+ // exit the game
+ if (ubox_read_keys(7) == UBOX_MSX_KEY_ESC)
+ break;
+
+ // game completed!
+ if (!batteries)
+ break;
+
+ // we are in the gameover delay
+ if (gameover_delay)
+ {
+ // if finished, exit
+ if (--gameover_delay == 0)
+ break;
+ }
+
+ // read the selected control
+ control = ubox_read_ctl(ctl);
+
+ // update all the entities:
+ // - self is a pointer to THIS entity
+ // - because we don't create/destroy entities dynamically
+ // when we found one that is unused we are done
+ for (self = entities; self->type; self++)
+ self->update();
+
+ // ensure we wait to our desired update rate
+ ubox_wait();
+ // update sprites on screen
+ spman_update();
+ }
+
+ // stop the in game music
+ mplayer_init(SONG, SONG_IN_GAME);
+ // hide all the sprites before going back to the menu
+ spman_hide_all_sprites();
+}