Mirai's Miscellaneous Misadventures

M17 / core / game.c

// copyright 2022 zamfofex
// license: AGPLv3 or later

#include <mimimi/engines.h>
#include <mimimi/chapters.h>
#include <mimimi/models.h>
#include <mimimi/appearances.h>
#include <mimimi/fonts.h>

#include "appearances.h"

// types

struct mimimi_sprite
{
	struct mimimi_sprite *next;
	
	struct mimimi_game *game;
	
	struct mimimi_sprite_appearance *appearance;
	
	int x, y;
	int width, height;
	int dx, dy;
	
	char airborne;
	
	char moving_direction;
	char facing_direction;
	
	int knocked_time;
	int immunity_time;
	int pristinity;
	
	void (*behave)(struct mimimi_sprite *sprite, void *data);
	
	unsigned char animation_time;
	
	void *data;
};

struct mimimi_game
{
	struct mimimi_engine *engine;
	struct mimimi_chapter *chapter;
	
	struct mimimi_sprite *sprite;
	
	unsigned int left_history:16;
	unsigned int right_history:16;
	
	struct mimimi_sprite *camera;
	
	unsigned int ground_width;
	unsigned int ground_height;
	unsigned char *ground;
	
	struct mimimi_image *background;
	struct mimimi_image *overlay;
};

// constants

static struct mimimi_sprite mimimi_last_sprite;
static struct mimimi_image mimimi_empty_image_value;

// exported constants

int mimimi_game_size = sizeof (struct mimimi_game);
int mimimi_sprite_size = sizeof (struct mimimi_sprite);
int mimimi_width = 512;
int mimimi_height = 256;
struct mimimi_image *mimimi_empty_image = &mimimi_empty_image_value;

// functions

static void mimimi_stamp(struct mimimi_game *game, int x, int y, struct mimimi_image *image)
{
	x -= game->camera->x;
	x += mimimi_width * 4;
	x -= image->width * 4;
	
	y -= game->camera->y;
	y += mimimi_height * 4;
	y -= image->height * 8;
	
	(*game->engine->stamp)(game->engine->data, x / 8, y / 8, image);
}

static void mimimi_stamp_image(struct mimimi_image *target, int x0, int y0, struct mimimi_image *source)
{
	for (int x1 = 0 ; x1 < source->width ; x1++)
	for (int y1 = 0 ; y1 < source->height ; y1++)
	{
		int x = x0 + x1;
		int y = y0 + y1;
		
		if (x < 0) continue;
		if (y < 0) continue;
		
		if (x >= target->width) continue;
		if (y >= target->height) continue;
		
		unsigned char color = source->colors[x1 + y1 * source->width];
		if (color == 0) continue;
		
		target->colors[x + y * target->width] = color;
	}
}

static void mimimi_wall_physics(struct mimimi_sprite *sprite)
{
	struct mimimi_game *game = sprite->game;
	
	int x0 = sprite->x;
	int x1 = sprite->width / 2;
	int y0 = sprite->y;
	int y1 = sprite->height;
	
	int top = (y0 - y1) / 128;
	int top2 = top - 1;
	int bottom = (y0 - 127) / 128;
	int bottom2 = bottom - 1;
	int left = (x0 - x1) / 128;
	int right = (x0 + x1) / 128;
	
	if (
		game->ground[left + bottom * game->ground_width] != 0 &&
		game->ground[left + bottom2 * game->ground_width] != 0 ||
		game->ground[left + top * game->ground_width] != 0 &&
		game->ground[left + top2 * game->ground_width]
	)
	{
		if (sprite->dx < 0) sprite->dx = 0;
		sprite->x = (left + 1) * 128 + x1;
	}
	if (
		game->ground[right + bottom * game->ground_width] != 0 &&
		game->ground[right + bottom2 * game->ground_width] != 0 ||
		game->ground[right + top * game->ground_width] != 0 &&
		game->ground[right + top2 * game->ground_width]
	)
	{
		if (sprite->dx > 0) sprite->dx = 0;
		sprite->x = right * 128 - x1 - 1;
	}
}

static void mimimi_step_physics(struct mimimi_sprite *sprite)
{
	struct mimimi_game *game = sprite->game;
	
	int x0 = sprite->x;
	int x1 = sprite->width / 2;
	int y0 = sprite->y;
	int y1 = sprite->height;
	
	int top = (y0 - y1) / 128;
	int bottom = (y0 - 127) / 128;
	int left = (x0 - x1) / 128;
	int right = (x0 + x1) / 128;
	
	if (game->ground[left + bottom * game->ground_width] != 0 && game->ground[left + top * game->ground_width] == 0)
		sprite->y = bottom * 128;
	if (game->ground[right + bottom * game->ground_width] != 0 && game->ground[right + top * game->ground_width] == 0)
		sprite->y = bottom * 128;
}

static void mimimi_ceiling_physics(struct mimimi_sprite *sprite)
{
	struct mimimi_game *game = sprite->game;
	
	int x0 = sprite->x;
	int x1 = sprite->width / 2;
	int y0 = sprite->y;
	int y1 = sprite->height;
	
	int top = (y0 - y1) / 128;
	int top2 = top - 1;
	int left = (x0 - x1) / 128;
	int right = (x0 + x1) / 128;
	
	if (sprite->dy > 0) return;
	
	if (game->ground[left + top * game->ground_width] != 0)
	if (game->ground[right + top * game->ground_width] != 0)
	if (game->ground[left + top2 * game->ground_width] != 0 || game->ground[right + top2 * game->ground_width] != 0)
	{
		sprite->dy = 0;
		sprite->y = (top + 1) * 128 + y1;
	}
}

static void mimimi_landing_physics(struct mimimi_sprite *sprite)
{
	struct mimimi_game *game = sprite->game;
	
	int x0 = sprite->x;
	int x1 = sprite->width / 2;
	int y0 = sprite->y;
	
	int bottom = y0 / 128;
	int left = (x0 - x1) / 128;
	int right = (x0 + x1) / 128;
	
	if (sprite->dy < 0) return;
	
	if (game->ground[left + bottom * game->ground_width] != 0 || game->ground[right + bottom * game->ground_width] != 0)
	{
		sprite->airborne = 0;
		sprite->dy = 0;
		sprite->y = bottom * 128;
	}
}

static void mimimi_fall_physics(struct mimimi_sprite *sprite)
{
	struct mimimi_game *game = sprite->game;
	
	int x0 = sprite->x;
	int x1 = sprite->width / 2;
	int y0 = sprite->y;
	
	int bottom = y0 / 128;
	int center = x0 / 128;
	int left = (x0 - x1) / 128;
	int right = (x0 + x1) / 128;
	
	if (game->ground[left + bottom * game->ground_width] == 0 && game->ground[right + bottom * game->ground_width] == 0)
	{
		if (game->ground[center + (bottom + 1) * game->ground_width] == 0)
			// fall
			sprite->airborne = 1;
		else
			// step down
			sprite->y = (bottom + 1) * 128;
	}
}

static void mimimi_physics_dynamics(struct mimimi_sprite *sprite)
{
	sprite->x += sprite->dx;
	sprite->y += sprite->dy;
}

static void mimimi_ground_physics(struct mimimi_sprite *sprite)
{
	sprite->dx *= 7;
	sprite->dx /= 8;
	sprite->dy = 0;
	
	mimimi_physics_dynamics(sprite);
	
	mimimi_step_physics(sprite);
	mimimi_wall_physics(sprite);
	mimimi_fall_physics(sprite);
}

static void mimimi_airborne_physics(struct mimimi_sprite *sprite, struct mimimi_physics_data *data)
{
	sprite->dx *= 15;
	sprite->dx /= 16;
	sprite->dy += data->gravity;
	
	mimimi_physics_dynamics(sprite);
	
	mimimi_ceiling_physics(sprite);
	mimimi_wall_physics(sprite);
	mimimi_landing_physics(sprite);
}

static void mimimi_attack(struct mimimi_sprite *sprite)
{
	mimimi_damage(sprite, 64, -256, 512, 512);
}

static void mimimi_rapid_attack(struct mimimi_sprite *sprite)
{
	// todo
}

static unsigned char mimimi_count_oscillations(unsigned int history)
{
	unsigned char count = 0;
	unsigned char prev = history&1;
	for (int i = 0 ; i < 16 ; i++)
	{
		if ((history&1) != prev) count++;
		prev = history&1;
		history >>= 1;
	}
	return count;
}

// exported functions

int mimimi_text(struct mimimi_image *image, struct mimimi_image *glyphs, int x, int y, char *text)
{
	while (*text != 0)
	{
		char ch = *text++;
		
		if (ch == 0x20)
		{
			x += 4;
			continue;
		}
		
		if (ch <= 0x20) continue;
		if (ch >= 0x7F) continue;
		
		struct mimimi_image *glyph = glyphs + (ch - 0x20);
		
		mimimi_stamp_image(image, x - 1, y - glyph->height + 6, glyph);
		x += glyph->width - 1;
	}
	
	return x;
}

void mimimi_physics(struct mimimi_sprite *sprite, void *data)
{
	if (sprite->airborne == 0)
		mimimi_ground_physics(sprite);
	else
		mimimi_airborne_physics(sprite, data);
}

void mimimi_jump(struct mimimi_sprite *sprite)
{
	if (sprite->airborne) return;
	sprite->airborne = 1;
	sprite->dy = -64;
}

void mimimi_damage(struct mimimi_sprite *sprite, int damage, int attack_y, int attack_width, int attack_height)
{
	struct mimimi_game *game = sprite->game;
	
	int x1 = sprite->x - attack_width;
	int y1 = sprite->y + attack_y;
	
	if (sprite->facing_direction == 2) x1 += attack_width;
	
	int x2 = x1 + attack_width;
	int y2 = y1 + attack_height;
	
	for (struct mimimi_sprite *other = game->sprite ; other != &mimimi_last_sprite ; other = other->next)
	{
		if (other == sprite) continue;
		if (other->immunity_time != 0) continue;
		
		int x = other->x;
		int y = other->y;
		
		if (x > x1 && x < x2)
		if (y > y1 && y < y2)
		{
			other->airborne = 1;
			other->dy -= 32;
			if (other->x > sprite->x)
				other->dx += 32;
			else
				other->dx -= 32;
			
			if (other->pristinity < damage)
				other->knocked_time = 128;
			else
				other->pristinity -= damage;
		}
	}
}

void mimimi_controls(struct mimimi_sprite *sprite, void *data)
{
	struct mimimi_game *game = sprite->game;
	
	unsigned char left_oscillations = mimimi_count_oscillations(game->left_history);
	unsigned char right_oscillations = mimimi_count_oscillations(game->right_history);
	
	switch (sprite->moving_direction)
	{
	case 0:
		if ((game->left_history&1) != 0)
			sprite->moving_direction = 1;
		if ((game->right_history&1) != 0)
			sprite->moving_direction = 2;
		break;
	case 1:
		if ((game->left_history&1) == 0)
			sprite->moving_direction = 0;
		else if (left_oscillations > 1)
			mimimi_jump(sprite);
		if ((game->right_history&1) != 0)
		{
			if (right_oscillations == 0)
				mimimi_rapid_attack(sprite);
		}
		if ((game->right_history&1) != 0 && (game->right_history&2) == 0)
			mimimi_attack(sprite);
		break;
	case 2:
		if ((game->right_history&1) == 0)
			sprite->moving_direction = 0;
		else if (right_oscillations > 1)
			mimimi_jump(sprite);
		if ((game->left_history&1) != 0)
		{
			if (left_oscillations == 0)
				mimimi_rapid_attack(sprite);
		}
		if ((game->left_history&1) != 0 && (game->left_history&2) == 0)
			mimimi_attack(sprite);
		break;
	}
}

void mimimi_enemy_ai(struct mimimi_sprite *sprite, void *data)
{
	struct mimimi_sprite *target = data;
	
	int x = sprite->x - target->x;
	int y = sprite->y - target->y;
	
	int abs_x = x;
	int abs_y = y;
	
	if (abs_x < 0) abs_x = -abs_x;
	if (abs_y < 0) abs_y = -abs_y;
	
	sprite->moving_direction = 0;
	if (x > 256)
		sprite->moving_direction = 1;
	else if (x < -256)
		sprite->moving_direction = 2;
	else
		mimimi_attack( sprite);
	
	if (y > 512) mimimi_jump(sprite);
}

void mimimi_stationary_ai(struct mimimi_sprite *sprite, void *data)
{
	struct mimimi_sprite *target = data;
	
	int x = sprite->x - target->x;
	
	if (x > 0) sprite->facing_direction = 1;
	else sprite->facing_direction = 2;
}

void mimimi_spawn(struct mimimi_game *game, struct mimimi_sprite *sprite, int x, int y, int width, int height, struct mimimi_sprite_appearance *appearance, struct mimimi_behavior *behavior)
{
	sprite->next = game->sprite;
	game->sprite = sprite;
	
	sprite->game = game;
	sprite->appearance = appearance;
	sprite->x = x;
	sprite->y = y;
	sprite->width = width;
	sprite->height = height;
	sprite->dx = 0;
	sprite->dy = 0;
	sprite->airborne = 1;
	sprite->moving_direction = 0;
	sprite->facing_direction = 1;
	sprite->animation_time = 0;
	sprite->immunity_time = 0;
	sprite->pristinity = -1;
	sprite->behave = behavior->behave;
	sprite->data = behavior->data;
	
	(*game->chapter->occurrence)(game->chapter, mimimi_sprite_spawned, sprite);
}

void mimimi_camera_sprite(struct mimimi_sprite *sprite)
{
	sprite->game->camera = sprite;
}

void mimimi_fall(struct mimimi_sprite *sprite, void *data)
{
	struct mimimi_fall_data *fall_data = data;
	
	if (sprite->dy > fall_data->max_speed)
	if (sprite->knocked_time < fall_data->knocked_time)
		sprite->knocked_time = fall_data->knocked_time;
}

void mimimi_live(struct mimimi_sprite *sprite, void *data)
{
	struct mimimi_live_data *live_data = data;
	
	if (sprite->knocked_time != 0)
	{
		sprite->knocked_time--;
		sprite->immunity_time = live_data->recovery_immunity_time;
	}
	
	if (sprite->immunity_time != 0)
	{
		sprite->pristinity = live_data->max_pristinity;
		sprite->immunity_time--;
	}
	
	if (sprite->pristinity < live_data->max_pristinity)
		sprite->pristinity++;
}

void mimimi_walk(struct mimimi_sprite *sprite, void *data)
{
	struct mimimi_walk_data *walk_data = data;
	
	if (sprite->moving_direction != 0)
	{
		sprite->facing_direction = sprite->moving_direction;
		
		int ax;
		if (sprite->airborne)
			ax = walk_data->airborne_speed;
		else
			ax = walk_data->ground_speed;
		
		if (sprite->moving_direction == 1)
			sprite->dx -= ax;
		if (sprite->moving_direction == 2)
			sprite->dx += ax;
	}
}

void mimimi_camera(struct mimimi_sprite *sprite, void *data)
{
	struct mimimi_camera_data *camera_data = data;
	
	sprite->x *= 3;
	sprite->y *= 3;
	sprite->x += camera_data->target->x;
	sprite->y += camera_data->target->y - 512;
	sprite->x /= 4;
	sprite->y /= 4;
}

void mimimi_behave(struct mimimi_sprite *sprite, void *data)
{
	struct mimimi_behave_data *behave_data = data;
	mimimi_live(sprite, behave_data->live_data);
	mimimi_fall(sprite, behave_data->fall_data);
	if (sprite->knocked_time == 0)
	{
		(*behave_data->behavior->behave)(sprite, behave_data->behavior->data);
		mimimi_walk(sprite, behave_data->walk_data);
	}
	mimimi_physics(sprite, behave_data->physics_data);
}

void mimimi_physical_attributes(struct mimimi_sprite *sprite, struct mimimi_physical_attributes *attributes)
{
	attributes->x = sprite->x;
	attributes->y = sprite->y;
	attributes->width = sprite->width;
	attributes->height = sprite->height;
	attributes->dx = sprite->dx;
	attributes->dy = sprite->dy;
	attributes->airborne = sprite->airborne;
	attributes->direction = sprite->facing_direction;
}

void mimimi_living_attributes(struct mimimi_sprite *sprite, struct mimimi_living_attributes *attributes)
{
	attributes->knocked_time = sprite->knocked_time;
	attributes->immunity_time = sprite->immunity_time;
	attributes->pristinity = sprite->pristinity;
}

void mimimi_start(struct mimimi_game *game, struct mimimi_engine *engine, struct mimimi_chapter *chapter)
{
	game->engine = engine;
	game->chapter = chapter;
	
	game->left_history = 0;
	game->right_history = 0;
	game->sprite = &mimimi_last_sprite;
	game->camera = 0;
	
	game->background = mimimi_empty_image;
	
	(*chapter->start)(chapter, game);
}

void mimimi_ground(struct mimimi_game *game, unsigned char *ground, int width, int height)
{
	game->ground = ground;
	game->ground_width = width;
	game->ground_height = height;
}

void mimimi_background(struct mimimi_game *game, struct mimimi_image *background)
{
	game->background = background;
}

void mimimi_overlay(struct mimimi_game *game, struct mimimi_image *overlay)
{
	game->overlay = overlay;
}

void mimimi_step(struct mimimi_game *game, struct mimimi_keys keys)
{
	(game->chapter->step)(game->chapter);
	
	game->left_history <<= 1;
	game->left_history |= keys.left;
	game->right_history <<= 1;
	game->right_history |= keys.right;
	
	mimimi_stamp(game, game->background->width * 4, game->background->height * 8 - 7, game->background);
	
	for (struct mimimi_sprite *sprite = game->sprite ; sprite != &mimimi_last_sprite ; sprite = sprite->next)
	{
		(sprite->behave)(sprite, sprite->data);
		
		int dx;
		int dy = sprite->dy;
		
		struct mimimi_animation_set *animations;
		
		if (sprite->facing_direction == 1)
		{
			dx = -sprite->dx;
			animations = &sprite->appearance->left;
		}
		if (sprite->facing_direction == 2)
		{
			dx = sprite->dx;
			animations = &sprite->appearance->right;
		}
		
		struct mimimi_animation *animation;
		
		if (dx > 32) dx = 32;
		if (dx < 0) dx = 0;
		if (dy < 0) dy = 0;
		
		if (sprite->knocked_time == 0)
		{
			animation = animations->standing + dx * mimimi_animation_count / 32;
			sprite->animation_time += dx / 4;
		}
		else
		{
			animation = &animations->knocked;
			if (dy == 0) sprite->animation_time = 0;
			else sprite->animation_time += dy / 8;
		}
		
		struct mimimi_image *image = &animation->images[sprite->animation_time * mimimi_image_count / 256].image;
		int y = 0;
		if (sprite->knocked_time != 0) y = image->height * 4 - 32;
		
		mimimi_stamp(game, sprite->x, sprite->y + y, image);
	}
	
	(*game->engine->stamp)(game->engine->data, 0, 0, game->overlay);
}