diff options
Diffstat (limited to 'src/game.c')
-rw-r--r-- | src/game.c | 660 |
1 files changed, 660 insertions, 0 deletions
diff --git a/src/game.c b/src/game.c new file mode 100644 index 0000000..552471b --- /dev/null +++ b/src/game.c @@ -0,0 +1,660 @@ +/* + * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com> + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <SDL.h> +#include <assert.h> + +#include <ix2.h> +#include <pad.h> +#include <play.h> +#include <stage.h> + +#include "adult-node.h" +#include "baby-node.h" +#include "bb2f.h" +#include "bb3f.h" +#include "digit-node.h" +#include "glad.h" +#include "m4f.h" +#include "m4f-3dx.h" +#include "m4f-bbx.h" +#include "macros.h" +#include "plasma-node.h" +#include "sars.h" +#include "sfx.h" +#include "tv-node.h" +#include "v2f.h" +#include "virus-node.h" + +#define GAME_NUM_VIRUSES 8 +#define GAME_NUM_BABIES 10 + +#define GAME_VIRUS_SPEED .01f +#define GAME_ADULT_SPEED .04f + +#define GAME_VIRUS_DELAY_MS 20 +#define GAME_VIRUS_TIMER PLAY_TICKS_TIMER1 + +#define GAME_TV_DELAY_MS 3000 +#define GAME_TV_TIMER PLAY_TICKS_TIMER3 + +#define GAME_KBD_DELAY_MS 20 +#define GAME_KBD_TIMER PLAY_TICKS_TIMER4 + +#define GAME_OVER_DELAY_MS 1000 +#define GAME_OVER_TIMER PLAY_TICKS_TIMER2 + +#define GAME_BABY_SCALE (v3f_t){.05f, .05f, .05f} +#define GAME_ADULT_SCALE (v3f_t){.07f, .07f, .07f} +#define GAME_TV_SCALE (v3f_t){.15f, .15f, .15f} +#define GAME_VIRUS_SCALE (v3f_t){.05f, .05f, .05f} +#define GAME_DIGITS_SCALE (v3f_t){.05f, .05f, .05f} + +/* every entity just starts with a unit cube AABB and is transformed with a matrix into its position, + * so here's a convenient aabb to feed into those transformations as needed. + */ +static const bb3f_t any_aabb = { .min = { -1.f -1.f, -1.f}, .max = { 1.f, 1.f, 1.f } }; + +typedef enum game_state_t { + GAME_STATE_PLAYING, + GAME_STATE_OVER, + GAME_STATE_OVER_DELAY, + GAME_STATE_OVER_WAITING +} game_state_t; + +typedef enum entity_type_t { + ENTITY_TYPE_BABY, + ENTITY_TYPE_ADULT, + ENTITY_TYPE_VIRUS, + ENTITY_TYPE_TV, +} entity_type_t; + +typedef union entity_t entity_t; + +typedef struct entity_any_t { + entity_type_t type; + stage_t *node; + ix2_object_t *ix2_object; + v2f_t position; + v3f_t scale; + m4f_t model_x; + bb2f_t aabb_x; +} entity_any_t; + +typedef struct baby_t { + entity_any_t entity; +} baby_t; + +typedef struct virus_t { + entity_any_t entity; +} virus_t; + +typedef struct adult_t { + entity_any_t entity; + unsigned rescues; + unsigned frozen:1; + entity_t *holding; +} adult_t; + +typedef struct tv_t { + entity_any_t entity; +} tv_t; + +union entity_t { + entity_any_t any; + baby_t baby; + virus_t virus; + adult_t adult; + tv_t tv; +}; + + +typedef struct game_t { + game_state_t state; + + stage_t *stage; + stage_t *game_node; + stage_t *babies_node; + stage_t *viruses_node; + stage_t *plasma_node; + stage_t *score_node; + ix2_t *ix2; + pad_t *pad; + + adult_t *adult; + tv_t *tv; + virus_t *viruses[GAME_NUM_VIRUSES]; + m4f_t score_digits_x[10]; +} game_t; + + +static inline float randf(void) +{ + return 2.f / RAND_MAX * rand() - 1.f; +} + + +/* update the entity's transformation and position in the index */ +static void entity_update_x(game_t *game, entity_any_t *entity) +{ + + entity->model_x = m4f_identity(); + entity->model_x = m4f_translate(&entity->model_x, &(v3f_t){entity->position.x, entity->position.y, 0.f}); + entity->model_x = m4f_scale(&entity->model_x, &entity->scale); + + /* apply the entities transform to any_aabb to get the current transformed aabb, cache it in the + * entity in case a search needs to be done... */ + m4f_mult_bb3f_bb2f(&entity->model_x, &any_aabb, &entity->aabb_x); + + if (!(entity->ix2_object)) { + entity->ix2_object = ix2_object_new(game->ix2, NULL, NULL, &entity->aabb_x, entity); + } else { + entity->ix2_object = ix2_object_move(game->ix2, entity->ix2_object, NULL, NULL, &entity->aabb_x); + } + + fatal_if(!entity->ix2_object, "Unable to update ix2 object"); +} + + +/* this is unnecessary copy and paste junk, but I'm really falling asleep here + * and need to get shit working before I pass out. + */ + +static adult_t * adult_new(game_t *game, stage_t *parent) +{ + adult_t *adult; + + adult = pad_get(game->pad, sizeof(entity_t)); + fatal_if(!adult, "unale to allocate adult_t"); + + adult->entity.type = ENTITY_TYPE_ADULT; + adult->entity.node = adult_node_new(&(stage_conf_t){ .parent = parent, .name = "adult", .layer = 3, .alpha = 1.f }, &adult->entity.model_x); + adult->entity.scale = GAME_ADULT_SCALE; + entity_update_x(game, &adult->entity); + + return adult; +} + + + +static baby_t * baby_new(game_t *game, stage_t *parent) +{ + baby_t *baby; + + baby = pad_get(game->pad, sizeof(entity_t)); + fatal_if(!baby, "unale to allocate baby_t"); + + baby->entity.type = ENTITY_TYPE_BABY; + baby->entity.node = baby_node_new(&(stage_conf_t){ .parent = parent, .name = "baby", .active = 1, .alpha = 1.f }, &baby->entity.model_x); + baby->entity.scale = GAME_BABY_SCALE; + baby->entity.position.x = randf(); + baby->entity.position.y = randf(); + entity_update_x(game, &baby->entity); + + return baby; +} + + +static tv_t * tv_new(game_t *game, stage_t *parent) +{ + tv_t *tv; + + tv = pad_get(game->pad, sizeof(entity_t)); + fatal_if(!tv, "unale to allocate tv_t"); + + tv->entity.type = ENTITY_TYPE_TV; + tv->entity.node = tv_node_new(&(stage_conf_t){ .parent = parent, .name = "tv", .layer = 1, .alpha = 1.f }, &tv->entity.model_x); + tv->entity.scale = GAME_TV_SCALE; + entity_update_x(game, &tv->entity); + + return tv; +} + + +static void randomize_virus(virus_t *virus) +{ + virus->entity.position.y = randf(); + virus->entity.position.x = randf(); +} + + +static virus_t * virus_new(game_t *game, stage_t *parent) +{ + virus_t *virus; + + virus = pad_get(game->pad, sizeof(entity_t)); + fatal_if(!virus, "unale to allocate virus_t"); + + virus->entity.type = ENTITY_TYPE_VIRUS; + virus->entity.node = virus_node_new(&(stage_conf_t){ .parent = parent, .name = "virus", .alpha = 1.f }, &virus->entity.model_x); + virus->entity.scale = GAME_VIRUS_SCALE; + randomize_virus(virus); + entity_update_x(game, &virus->entity); + + return virus; +} + + +static void reset_virus(virus_t *virus) +{ + stage_set_active(virus->entity.node, 0); + randomize_virus(virus); +} + + +typedef struct virus_search_t { + game_t *game; + virus_t *virus; +} virus_search_t; + +static ix2_search_status_t virus_search(void *cb_context, ix2_object_t *ix2_object, v2f_t *ix2_object_position, bb2f_t *ix2_object_aabb, void *object) +{ + virus_search_t *search = cb_context; + entity_t *entity = object; + + switch (entity->any.type) { + case ENTITY_TYPE_BABY: + /* convert baby into inanimate virus (off the viruses array) */ + stage_free(entity->any.node); + entity->any.node = virus_node_new(&(stage_conf_t){ .parent = search->game->viruses_node, .name = "baby-virus", .active = 1, .alpha = 1.f }, &entity->any.model_x); + sfx_play(sfx.baby_infected); + entity->any.type = ENTITY_TYPE_VIRUS; + + /* add new baby */ + (void) baby_new(search->game, search->game->babies_node); + + /* reset virus */ + reset_virus(search->virus); + + /* stop searching */ + return IX2_SEARCH_STOP_HIT; + + case ENTITY_TYPE_ADULT: + /* convert adult into inanimate virus (of the viruses array) */ + stage_free(entity->any.node); + entity->any.node = virus_node_new(&(stage_conf_t){ .parent = search->game->viruses_node, .name = "adult-virus", .active = 1, .alpha = 1.f }, &entity->any.model_x); + sfx_play(sfx.adult_infected); + search->game->state = GAME_STATE_OVER; + return IX2_SEARCH_STOP_HIT; + + case ENTITY_TYPE_VIRUS: + case ENTITY_TYPE_TV: + return IX2_SEARCH_MORE_MISS; + + default: + assert(0); + } + + /* XXX: I'm not really caring about the return value for now */ +} + + +/* animate the viruses: + * - anything newly infected becomes an inanimate virus (change their node) + * and the virus respawns somewhere + * - if the newly infected thing is the adult, the game ends + */ +static void update_viruses(play_t *play, game_t *game) +{ + virus_search_t search = { .game = game }; + + assert(play); + assert(game); + + if (randf() > .95f && !stage_get_active(game->tv->entity.node)) { + /* sometimes turn on the TV at a random location, we + * get stuck to it */ + play_ticks_reset(play, GAME_TV_TIMER); + game->tv->entity.position.x = randf(); + game->tv->entity.position.y = randf(); + entity_update_x(game, &game->tv->entity); + stage_set_active(game->tv->entity.node, 1); + } + + for (int i = 0; i < NELEMS(game->viruses); i++) { + virus_t *virus = game->viruses[i]; + + /* are they off-screen? */ + if (virus->entity.position.y > 1.2f) { + + if (stage_get_active(virus->entity.node)) { + /* active and off-screen gets randomize and inactivated */ + reset_virus(virus); + } else { + /* inactive and off-screen gets activated and moved to the + * top */ + stage_set_active(virus->entity.node, 1); + virus->entity.position.y = -1.2f; + } + } + + virus->entity.position.y += GAME_VIRUS_SPEED; + entity_update_x(game, &virus->entity); + + if (stage_get_active(virus->entity.node)) { + search.virus = virus; + + /* search ix2 for collisions */ + ix2_search_by_aabb(game->ix2, NULL, NULL, &virus->entity.aabb_x, virus_search, &search); + } + } +} + + +static ix2_search_status_t adult_search(void *cb_context, ix2_object_t *ix2_object, v2f_t *ix2_object_position, bb2f_t *ix2_object_aabb, void *object) +{ + game_t *game = cb_context; + entity_t *entity = object; + + switch (entity->any.type) { + case ENTITY_TYPE_BABY: + if (!game->adult->holding) { + sfx_play(sfx.baby_held); + game->adult->holding = entity; + } + + /* we should probably keep looking because there could be a virus too, + * but fuck it, these types of bugs are fun in silly games. + */ + return IX2_SEARCH_STOP_HIT; + + case ENTITY_TYPE_ADULT: + /* ignore ourselves */ + return IX2_SEARCH_MORE_MISS; + + case ENTITY_TYPE_VIRUS: + if (!stage_get_active(entity->any.node)) + return IX2_SEARCH_MORE_MISS; + + /* convert adult into inanimate adult (of the adultes array) */ + stage_free(game->adult->entity.node); + game->adult->entity.node = virus_node_new(&(stage_conf_t){ .parent = game->viruses_node, .name = "adult-virus", .active = 1, .alpha = 1.f }, &game->adult->entity.model_x); + sfx_play(sfx.adult_infected); + + if (game->adult->holding) { + stage_free(game->adult->holding->any.node); + game->adult->holding->any.node = virus_node_new(&(stage_conf_t){ .parent = game->viruses_node, .name = "baby-virus", .active = 1, .alpha = 1.f }, &game->adult->holding->any.model_x); + sfx_play(sfx.baby_infected); + } + game->state = GAME_STATE_OVER; + return IX2_SEARCH_STOP_HIT; + + case ENTITY_TYPE_TV: + if (!stage_get_active(entity->any.node)) + return IX2_SEARCH_MORE_MISS; + + game->adult->frozen = 1; + return IX2_SEARCH_STOP_HIT; + + default: + assert(0); + } + + /* XXX: I'm not really caring about the return value for now */ +} + + +static void game_move_adult(game_t *game, v2f_t *dir) +{ + assert(game); + assert(dir); + + if (game->adult->frozen) + return; + + game->adult->entity.position.x += dir->x; + game->adult->entity.position.y += dir->y; + + /* prevent the player from going too far off the reservation */ + if (game->adult->entity.position.x > 1.1f) + game->adult->entity.position.x = 1.1f; + + if (game->adult->entity.position.x < -1.1f) + game->adult->entity.position.x = -1.1f; + + if (game->adult->entity.position.y > 1.1f) + game->adult->entity.position.y = 1.1f; + + if (game->adult->entity.position.y < -1.1f) + game->adult->entity.position.y = -1.1f; + + entity_update_x(game, &game->adult->entity); + + if (game->adult->holding) { + game->adult->holding->any.position = game->adult->entity.position; + entity_update_x(game, &game->adult->holding->any); + + if (game->adult->entity.position.x > 1.05f || + game->adult->entity.position.x < -1.05f || + game->adult->entity.position.y > 1.05f || + game->adult->entity.position.y < -1.05f) { + + /* rescued baby */ + sfx_play(sfx.baby_rescued); + + game->adult->holding->any.position.x = randf(); + game->adult->holding->any.position.y = randf(); + entity_update_x(game, &game->adult->holding->any); + + game->adult->holding = NULL; + game->adult->rescues++; + } + } + + /* search ix2 for collisions */ + ix2_search_by_aabb(game->ix2, NULL, NULL, &game->adult->entity.aabb_x, adult_search, game); +} + + +static void reset_game(game_t *game) +{ + ix2_reset(game->ix2); + stage_free(game->game_node); + + if (game->pad) /* XXX FIXME: this is a stupidty in libpad */ + pad_free(game->pad); + + game->game_node = stage_new(&(stage_conf_t){ .parent = game->stage, .name = "game", .active = 1, .alpha = 1.f }, NULL, NULL); + + game->babies_node = stage_new(&(stage_conf_t){ .parent = game->game_node, .name = "babies", .layer = 4, .alpha = 1.f }, NULL, NULL); + game->viruses_node = stage_new(&(stage_conf_t){ .parent = game->game_node, .name = "viruses", .layer = 5, .alpha = 1.f }, NULL, NULL); + game->score_node = stage_new(&(stage_conf_t){ .parent = game->game_node, .name = "score", .layer = 7, .alpha = 1 }, NULL, NULL); + + game->pad = pad_new(sizeof(entity_t) * 32, PAD_FLAGS_ZERO); + + game->tv = tv_new(game, game->game_node); + game->adult = adult_new(game, game->game_node); + for (int i = 0; i < NELEMS(game->viruses); i++) + game->viruses[i] = virus_new(game, game->viruses_node); + + for (int i = 0; i < GAME_NUM_BABIES; i++) + (void) baby_new(game, game->babies_node); + + stage_set_active(game->adult->entity.node, 1); + stage_set_active(game->babies_node, 1); + stage_set_active(game->viruses_node, 1); + + game->state = GAME_STATE_PLAYING; +} + + +static void show_score(game_t *game) +{ + unsigned score = game->adult->rescues * 420; + + for (unsigned i = 1000000000, pos = 0; i > 0; score %= i, i /= 10, pos++) { + unsigned v = score / i; + + digit_node_new(&(stage_conf_t){ .parent = game->score_node, .name = "score-digit", .active = 1, .alpha = 1.f }, v, &game->score_digits_x[pos]); + } + + stage_set_active(game->score_node, 1); +} + + +static void * game_init(play_t *play, int argc, char *argv[]) +{ + sars_t *sars = play_context(play, SARS_CONTEXT_SARS); + game_t *game; + + assert(sars); + + game = calloc(1, sizeof(game_t)); + fatal_if(!game, "Unable to allocate game_t"); + + game->stage = sars->stage; + game->plasma_node = plasma_node_new(&(stage_conf_t){ .parent = sars->stage, .name = "plasma", .alpha = 1 }); + + game->ix2 = ix2_new(NULL, 4, 4, 1 /* increase for nested searches */); + + /* setup transformation matrices for the score digits, this is really fast and nasty hack because + * I am completely delerious and ready to fall asleep. + */ + for (int i = 0; i < NELEMS(game->score_digits_x); i++) { + game->score_digits_x[i] = m4f_identity(); + game->score_digits_x[i] = m4f_translate(&game->score_digits_x[i], &(v3f_t){ 1.f / NELEMS(game->score_digits_x) * i - .5f, 0.f, 0.f }); + game->score_digits_x[i] = m4f_scale(&game->score_digits_x[i], &GAME_DIGITS_SCALE); + } + + sfx_init(); + + return game; +} + + +static void game_enter(play_t *play, void *context) +{ + game_t *game = context; + + assert(game); + + play_music_set(play, PLAY_MUSIC_FLAG_LOOP|PLAY_MUSIC_FLAG_IDEMPOTENT, "assets/game.ogg"); + play_ticks_reset(play, GAME_VIRUS_TIMER); + stage_set_active(game->plasma_node, 1); + reset_game(game); +} + + +static void game_update(play_t *play, void *context) +{ + sars_t *sars = play_context(play, SARS_CONTEXT_SARS); + game_t *game = context; + + assert(game); + assert(sars); + + switch (game->state) { + case GAME_STATE_PLAYING: { + if (play_ticks_elapsed(play, GAME_VIRUS_TIMER, GAME_VIRUS_DELAY_MS)) + update_viruses(play, game); + + if (play_ticks_elapsed(play, GAME_KBD_TIMER, GAME_KBD_DELAY_MS)) { + const Uint8 *key_state = SDL_GetKeyboardState(NULL); + v2f_t dir = {}, *move = NULL; + + if (key_state[SDL_SCANCODE_LEFT] || key_state[SDL_SCANCODE_A]) { + dir.x += -GAME_ADULT_SPEED; + move = &dir; + } + + if (key_state[SDL_SCANCODE_RIGHT] || key_state[SDL_SCANCODE_D]) { + dir.x += GAME_ADULT_SPEED; + move = &dir; + } + + if (key_state[SDL_SCANCODE_UP] || key_state[SDL_SCANCODE_W]) { + dir.y += GAME_ADULT_SPEED; + move = &dir; + } + + if (key_state[SDL_SCANCODE_DOWN] || key_state[SDL_SCANCODE_S]) { + dir.y += -GAME_ADULT_SPEED; + move = &dir; + } + + if (move) + game_move_adult(game, move); + } + + if (play_ticks_elapsed(play, GAME_TV_TIMER, GAME_TV_DELAY_MS)) { + stage_set_active(game->tv->entity.node, 0); + game->adult->frozen = 0; + } + + break; + } + + case GAME_STATE_OVER: + show_score(game); + play_ticks_reset(play, GAME_OVER_TIMER); + game->state = GAME_STATE_OVER_DELAY; + break; + + case GAME_STATE_OVER_DELAY: + if (!play_ticks_elapsed(play, GAME_OVER_TIMER, GAME_OVER_DELAY_MS)) + break; + + /* maybe throw something on-screen? */ + game->state = GAME_STATE_OVER_WAITING; + break; + + case GAME_STATE_OVER_WAITING: + /* just do nothing and wait for a keypress of some kind */ + break; + + default: + assert(0); + } + + /* just always dirty the stage in the game context */ + stage_dirty(sars->stage); +} + + +static void game_dispatch(play_t *play, void *context, SDL_Event *event) +{ + game_t *game = context; + + /* global handlers */ + sars_dispatch(play, context, event); + + /* anything more to do here? */ + switch (event->type) { + case SDL_KEYDOWN: + if (event->key.keysym.sym == SDLK_ESCAPE) + exit(0); + + if (game->state == GAME_STATE_OVER_WAITING) { + reset_game(game); + break; + } + + break; + + default: + break; + } +} + + +const play_ops_t game_ops = { + .init = game_init, +// .shutdown = game_shutdown, + .update = game_update, + .render = sars_render, + .dispatch = game_dispatch, + .enter = game_enter, +}; |