Mirai's Miscellaneous Misadventures

M12 / game.c

#include "game.h"
#include "math.c"

// types

struct mirai_animation
{
	int count;
	struct mirai_image *images;
};

struct mirai_direction_animation_set
{
	int count;
	struct mirai_animation *standing;
	struct mirai_animation knocked;
};

struct mirai_animation_set
{
	struct mirai_direction_animation_set left, right;
};

struct mirai_sprite_type
{
	struct mirai_animation_set animations;
	int width;
	int height;
};

struct mirai_sprite
{
	struct mirai_sprite_type *type;
	
	int x, y;
	// speed
	int dx, dy;
	// acceleration
	int ax;
	
	char airborne;
	char moving_direction;
	char last_moving_direction;
	
	int attack_y;
	int attack_width;
	int attack_height;
	int attack_time;
	
	void (*behave)(struct mirai_game *game, struct mirai_sprite *sprite);
	
	unsigned char animation_time;
	
	unsigned char knocked_time;
};

struct mirai_rotating_image_row
{
	int size;
	unsigned char count;
	unsigned char colors[64];
};

struct mirai_rotating_image
{
	int count;
	struct mirai_rotating_image_row rows[];
};


// constants

#include "assets.c"

static int mirai_gravity = 4;

static unsigned char mirai_ground_colors[16 * 16];

static unsigned char mirai_colors[80686];

static struct mirai_image mirai_images[145] =
{
	{16, 16, mirai_ground_colors},
};

static struct mirai_animation mirai_animations[9];

static struct mirai_image *mirai_ground_image = mirai_images;

static struct mirai_sprite_type mirai_mirai = {{}, 80, 250};

static struct mirai_image *mirai_glyphs[26 * 2 + 10];


// state

struct mirai_game
{
	struct mirai_engine engine;
	struct mirai_sprite sprites[256];
	unsigned char sprite_count;
	
	unsigned int left_history:16;
	unsigned int right_history:16;
	
	struct mirai_sprite *camera_sprite;
	int camera_x;
	int camera_y;
};


// functions

static void mirai_stamp(struct mirai_game *game, int x, int y, struct mirai_image *image)
{
	x -= game->camera_x;
	x += mirai_width * 4;
	y -= game->camera_y;
	y += mirai_height * 4;
	
	x -= image->width * 4;
	y -= image->height * 8;
	
	(*game->engine.stamp)(game->engine.data, x / 8, y / 8, image);
}

static int mirai_text(struct mirai_game *game, int x, int y, char *text)
{
	for (int i = 0 ; *text != '\0' ; i++)
	{
		char ch = *text++;
		
		if (ch == ' ')
		{
			x += 4;
			continue;
		}
		
		struct mirai_image *image;
		
		if (ch >= '0' && ch <= '9')
		{
			image = mirai_glyphs[ch - '0' + 0];
		}
		else if (ch >= 'A' && ch <= 'Z')
		{
			image = mirai_glyphs[ch - 'A' + 10];
		}
		else if (ch >= 'a' && ch <= 'z')
		{
			image = mirai_glyphs[ch - 'a' + 36];
		}
		else
		{
			continue;
		}
		
		(*game->engine.stamp)(game->engine.data, x - 1, y - image->height + 5, image);
		x += image->width - 1;
	}
	
	return x;
}

static void mirai_wall_physics(struct mirai_sprite *sprite)
{
	int x0 = sprite->x;
	int x1 = sprite->type->width / 2;
	int y0 = sprite->y;
	int y1 = sprite->type->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 (
		mirai_ground[bottom][left] != 0 && mirai_ground[bottom2][left] != 0 ||
		mirai_ground[top][left] != 0 && mirai_ground[top2][left]
	)
	{
		if (sprite->dx < 0) sprite->dx = 0;
		sprite->x = (left + 1) * 128 + x1;
	}
	if (
		mirai_ground[bottom][right] != 0 && mirai_ground[bottom2][right] != 0 ||
		mirai_ground[top][right] != 0 && mirai_ground[top2][right]
	)
	{
		if (sprite->dx > 0) sprite->dx = 0;
		sprite->x = right * 128 - x1 - 1;
	}
}

static void mirai_step_physics(struct mirai_sprite *sprite)
{
	int x0 = sprite->x;
	int x1 = sprite->type->width / 2;
	int y0 = sprite->y;
	int y1 = sprite->type->height;
	
	int top = (y0 - y1) / 128;
	int bottom = (y0 - 127) / 128;
	int left = (x0 - x1) / 128;
	int right = (x0 + x1) / 128;
	
	if (mirai_ground[bottom][left] != 0 && mirai_ground[top][left] == 0)
		sprite->y = bottom * 128;
	if (mirai_ground[bottom][right] != 0 && mirai_ground[top][right] == 0)
		sprite->y = bottom * 128;
}

static void mirai_ceiling_physics(struct mirai_sprite *sprite)
{
	int x0 = sprite->x;
	int x1 = sprite->type->width / 2;
	int y0 = sprite->y;
	int y1 = sprite->type->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 (mirai_ground[top][left] != 0)
	if (mirai_ground[top][right] != 0)
	if (mirai_ground[top2][left] != 0 || mirai_ground[top2][right] != 0)
	{
		sprite->dy = 0;
		sprite->y = (top + 1) * 128 + y1;
	}
}

static void mirai_landing_physics(struct mirai_sprite *sprite)
{
	int x0 = sprite->x;
	int x1 = sprite->type->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 (mirai_ground[bottom][left] != 0 || mirai_ground[bottom][right] != 0)
	{
		sprite->airborne = 0;
		sprite->dy = 0;
		sprite->y = bottom * 128;
	}
}

static void mirai_fall_physics(struct mirai_sprite *sprite)
{
	int x0 = sprite->x;
	int x1 = sprite->type->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 (mirai_ground[bottom][left] == 0 && mirai_ground[bottom][right] == 0)
	{
		if (mirai_ground[bottom + 1][center] == 0)
			// fall
			sprite->airborne = 1;
		else
			// step down
			sprite->y = (bottom + 1) * 128;
	}
}

static void mirai_physics_dynamics(struct mirai_sprite *sprite)
{
	sprite->dx += sprite->ax;
	sprite->x += sprite->dx;
	sprite->y += sprite->dy;
}

static void mirai_ground_physics(struct mirai_sprite *sprite)
{
	sprite->dx *= 7;
	sprite->dx /= 8;
	sprite->dy = 0;
	
	mirai_physics_dynamics(sprite);
	
	mirai_step_physics(sprite);
	mirai_wall_physics(sprite);
	mirai_fall_physics(sprite);
}

static void mirai_airborne_physics(struct mirai_sprite *sprite)
{
	sprite->dx *= 15;
	sprite->dx /= 16;
	sprite->dy += mirai_gravity;
	
	mirai_physics_dynamics(sprite);
	
	mirai_ceiling_physics(sprite);
	mirai_wall_physics(sprite);
	mirai_landing_physics(sprite);
}

static void mirai_jump(struct mirai_sprite *sprite)
{
	if (sprite->airborne) return;
	sprite->airborne = 1;
	sprite->dy = -64;
}

static void mirai_damage(struct mirai_game *game, struct mirai_sprite *sprite)
{
	/*
	struct mirai_image area = {sprite->attack_width / 16, sprite->attack_height / 8};
	int x = sprite->x + sprite->attack_width / 4;
	int y = (sprite->y + sprite->attack_y);
	
	if (sprite->moving_direction == 1) x -= sprite->attack_width / 2;
	
	mirai_stamp(game, x, y, &area);
	*/
}

static void mirai_attack(struct mirai_game *game, struct mirai_sprite *sprite)
{
	sprite->attack_y = -16;
	sprite->attack_width = 512;
	sprite->attack_height = 256;
	mirai_damage(game, sprite);
}

static void mirai_rapid_attack(struct mirai_game *game, struct mirai_sprite *sprite)
{
	sprite->attack_y = -128;
	sprite->attack_width = sprite->attack_time * 16 + 64;
	sprite->attack_height = 64;
	if (sprite->attack_time != 0x7F) sprite->attack_time++;
	mirai_damage(game, sprite);
}

static unsigned char mirai_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;
}

static void mirai_controls(struct mirai_game *game, struct mirai_sprite *sprite)
{
	unsigned char left_oscillations = mirai_count_oscillations(game->left_history);
	unsigned char right_oscillations = mirai_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)
			mirai_jump(sprite);
		if ((game->right_history&1) != 0)
		{
			if (right_oscillations == 0)
				mirai_rapid_attack(game, sprite);
			else
				sprite->attack_time = 0;
		}
		if ((game->right_history&1) != 0 && (game->right_history&2) == 0)
			mirai_attack(game, sprite);
		break;
	case 2:
		if ((game->right_history&1) == 0)
			sprite->moving_direction = 0;
		else if (right_oscillations > 1)
			mirai_jump(sprite);
		if ((game->left_history&1) != 0)
		{
			if (left_oscillations == 0)
				mirai_rapid_attack(game, sprite);
			else
				sprite->attack_time = 0;
		}
		if ((game->left_history&1) != 0 && (game->left_history&2) == 0)
			mirai_attack(game, sprite);
		break;
	}
}

static void mirai_spawn(struct mirai_game *game, int x, int y, struct mirai_sprite_type *type, void (*behave)(struct mirai_game *game, struct mirai_sprite *sprite))
{
	if (game->sprite_count == 0xFF) return;
	struct mirai_sprite *sprite = game->sprites + game->sprite_count;
	game->sprite_count++;
	sprite->type = type;
	sprite->x = x;
	sprite->y = y;
	sprite->dx = 0;
	sprite->dy = 0;
	sprite->ax = 0;
	sprite->airborne = 1;
	sprite->moving_direction = 0;
	sprite->last_moving_direction = 1;
	sprite->attack_time = 0;
	sprite->animation_time = 0;
	sprite->behave = behave;
}

static void mirai_image_rotation(struct mirai_image *image, struct mirai_rotating_image *rotating_image, unsigned char angle, int x0, int y0, int tilt, int tilt_origin, int tilt_threshold, char flip)
{
	if (flip != 0) angle = 0x100 - angle;
	
	int width = image->width;
	int height = image->height;
	
	for (int y = 0 ; y < rotating_image->count ; y++)
	for (int x = 0 ; x < rotating_image->rows[y].size ; x++)
	{
		int count = rotating_image->rows[y].count;
		
		int w = rotating_image->rows[y].size;
		int h = rotating_image->count;
		
		unsigned int a = angle;
		a *= count;
		a += 128;
		a /= 256;
		a += x;
		a %= count;
		
		if (flip != 0) a = count - a - 1;
		
		int yy = y;
		if (yy < tilt_threshold) yy = tilt_threshold;
		
		int x1 = x + x0 + (width - w) / 2 + tilt * (tilt_origin - yy) / 256;
		int y1 = y - y0 + height - h;
		
		unsigned char color = rotating_image->rows[y].colors[a];
		if (color == 0) continue;
		
		image->colors[x1 + y1 * width] = color;
	}
}

static void mirai_mirai_animation_frame(struct mirai_image *image, unsigned char **colors_pointer, int angle, int tilt, int arm_tilt)
{
	unsigned char *colors = *colors_pointer;
	
	image->colors = colors;
	image->width = 26;
	image->height = 36;
	
	(*colors_pointer) += image->width * image->height;
	
	for (int x = 0 ; x < image->width ; x++)
	for (int y = 0 ; y < image->height ; y++)
		image->colors[x + y * image->width] = 0;
	
	int x = mirai_sine[angle];
	x *= 5;
	x /= 128;
	int left_x = x + (mirai_mirai_left_arm.count + 10) * tilt / 256;
	int right_x = x - (mirai_mirai_right_arm.count + 10) * tilt / 256;
	
	int height = mirai_mirai_torso.count;
	
	mirai_image_rotation(image, &mirai_mirai_hair, angle, 0, 7, tilt, height, 11, 1);
	if ((angle + 64) % 256 < 128)
	{
		mirai_image_rotation(image, &mirai_mirai_left_arm, angle, left_x, 10, tilt - arm_tilt, 0, 0, 0);
		mirai_image_rotation(image, &mirai_mirai_torso, angle, 0, 0, tilt, height, 11, 0);
		mirai_image_rotation(image, &mirai_mirai_right_arm, angle, -right_x, 10, tilt + arm_tilt, 0, 0, 0);
	}
	else
	{
		mirai_image_rotation(image, &mirai_mirai_right_arm, angle, -right_x, 10, tilt + arm_tilt, 0, 0, 0);
		mirai_image_rotation(image, &mirai_mirai_torso, angle, 0, 0, tilt, height, 11, 0);
		mirai_image_rotation(image, &mirai_mirai_left_arm, angle, left_x, 10, tilt - arm_tilt, 0, 0, 0);
	}
	mirai_image_rotation(image, &mirai_mirai_hair, angle, 0, 7, tilt, height, 11, 0);
}

static void mirai_mirai_animation(struct mirai_animation *animation, struct mirai_image **images, unsigned char **colors, int angle, int count, int tilt, int arm_tilt)
{
	animation->count = count;
	animation->images = *images;
	
	for (int i = 0 ; i < count ; i++)
		mirai_mirai_animation_frame((*images)++, colors, angle, tilt, arm_tilt * mirai_sine[i * 255 / count] / 128);
}

static void mirai_mirai_animation_set(struct mirai_animation **animations, struct mirai_image **images, unsigned char **colors, int coefficient, int count, int count2, int tilt, int arm_tilt)
{
	for (int i = 0 ; i < count ; i++)
		mirai_mirai_animation((*animations)++, images, colors, 64 + (12 + 34 * i / count) * coefficient, count2, -tilt * i / count * coefficient, arm_tilt * i / count);
}

// adapted from https://rosettacode.org/wiki/Matrix_transposition#C
static void mirai_transpose_image(struct mirai_image *image)
{
	int width = image->width;
	int height = image->height;
	image->width = height;
	image->height = width;
	
	for (int start = 0 ; start < width * height ; start++)
	{
		int next = start;
		int i = 0;
		for (;;)
		{
			i++;
			next = (next % height) * width + next / height;
			if (next <= start) break;
		}
		
		if (i == 1) continue;
		if (next < start) continue;
		
		char tmp = image->colors[next = start];
		for (;;)
		{
			int i = (next % height) * width + next / height;
			image->colors[next] = (i == start) ? tmp : image->colors[i];
			next = i;
			if (next <= start) break;
		}
	}
}

static void mirai_flip_image(struct mirai_image *image)
{
	for (int x = 0 ; x < image->width / 2 ; x++)
	for (int y = 0 ; y < image->height ; y++)
	{
		char color = image->colors[image->width - x - 1 + y * image->width];
		image->colors[image->width - x - 1 + y * image->width] = image->colors[x + y * image->width];
		image->colors[x + y * image->width] = color;
	}
}

static void mirai_rotate_image_cw(struct mirai_image *image)
{
	mirai_transpose_image(image);
	mirai_flip_image(image);
}

static void mirai_rotate_image_ccw(struct mirai_image *image)
{
	mirai_flip_image(image);
	mirai_transpose_image(image);
}


// constant exports

int mirai_game_size = sizeof (struct mirai_game);
int mirai_width = 512;
int mirai_height = 256;


// function exports

void mirai_start(struct mirai_game *game, struct mirai_engine *engine)
{
	game->engine = *engine;
	
	game->left_history = 0;
	game->right_history = 0;
	game->sprite_count = 0;
	
	for (int y = 0 ; y < sizeof mirai_ground / sizeof *mirai_ground ; y++)
	for (int x = 0 ; x < sizeof *mirai_ground / sizeof **mirai_ground ; x++)
	{
		if (mirai_ground[y][x] == 2)
		{
			game->camera_x = x * 128;
			game->camera_y = y * 128 - 512;
			game->camera_sprite = game->sprites + game->sprite_count;
			mirai_spawn(game, x * 128, y * 128, &mirai_mirai, &mirai_controls);
		}
	}
	
	mirai_assets();
}

void mirai_step(struct mirai_game *game, struct mirai_keys keys)
{
	game->left_history <<= 1;
	game->left_history |= keys.left;
	game->right_history <<= 1;
	game->right_history |= keys.right;
	
	game->camera_x *= 3;
	game->camera_y *= 3;
	game->camera_x += game->camera_sprite->x;
	game->camera_y += game->camera_sprite->y - 512;
	game->camera_x /= 4;
	game->camera_y /= 4;
	
	for (int x0 = -16 ; x0 <= 16 ; x0++)
	for (int y0 = -8 ; y0 <= 8 ; y0++)
	{
		int x = x0 + game->camera_x / 128;
		int y = y0 + game->camera_y / 128;
		if (mirai_ground[y][x] != 0)
			mirai_stamp(game, x * 128 + 64, y * 128 + 128, mirai_ground_image);
	}
	
	for (int i = 0 ; i < game->sprite_count ; i++)
	{
		struct mirai_sprite *sprite = game->sprites + i;
		
		if (sprite->dy > 128)
			sprite->knocked_time = 64;
		else if (sprite->knocked_time != 0)
			sprite->knocked_time--;
		
		sprite->ax = 0;
		if (sprite->knocked_time == 0)
		{
			(*sprite->behave)(game, sprite);
			
			if (sprite->moving_direction != 0)
				sprite->last_moving_direction = sprite->moving_direction;
			
			if (sprite->airborne)
			{
				if (sprite->moving_direction == 1)
					sprite->ax = -3;
				if (sprite->moving_direction == 2)
					sprite->ax = 3;
			}
			else
			{
				if (sprite->moving_direction == 1)
					sprite->ax = -4;
				if (sprite->moving_direction == 2)
					sprite->ax = 4;
			}
		}
		
		if (sprite->airborne == 0)
			mirai_ground_physics(sprite);
		else
			mirai_airborne_physics(sprite);
		
		int dx;
		int dy = sprite->dy;
		
		struct mirai_direction_animation_set *animations;
		
		if (sprite->last_moving_direction == 1)
		{
			dx = -sprite->dx;
			animations = &sprite->type->animations.left;
		}
		if (sprite->last_moving_direction == 2)
		{
			dx = sprite->dx;
			animations = &sprite->type->animations.right;
		}
		
		struct mirai_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 * (animations->count - 1) / 32;
			sprite->animation_time += dx / 4;
		}
		else
		{
			animation = &animations->knocked;
			if (dy == 0) sprite->animation_time = 0;
			else sprite->animation_time += dy / 8;
		}
		
		struct mirai_image *image = animation->images + sprite->animation_time * (animation->count - 1) / 256;
		int y = 0;
		if (sprite->knocked_time != 0) y = image->height * 4 - 56;
		
		mirai_stamp(game, sprite->x, sprite->y + y, image);
	}
	
	mirai_text(game, 16, 16, "Mirai's Miscellaneous Misadventures M12 November 1 2021");
	mirai_text(game, 16, 32, "a Senran Kagura fangame by zamfofex");
}

struct mirai_image *mirai_assets(void)
{
	static char done = 0;
	if (done != 0) return mirai_images;
	
	int color_count = sizeof mirai_colors / sizeof *mirai_colors;
	unsigned char *last_color = mirai_colors + color_count - 1;
	
	int image_count = sizeof mirai_images / sizeof *mirai_images;
	struct mirai_image *last_image = mirai_images + image_count - 1;
	
	int animation_count = sizeof mirai_animations / sizeof *mirai_animations;
	struct mirai_animation *last_animation = mirai_animations + animation_count - 1;
	
	last_image->width = 0;
	last_image->height = 0;
	
	for (int i = 0 ; i < 256 ; i++)
		mirai_ground_colors[i] = 0x3B;
	
	unsigned char *colors = mirai_colors;
	struct mirai_image *images = mirai_images + 1;
	struct mirai_animation *animations = mirai_animations;
	
	mirai_mirai.animations.left.standing = animations;
	mirai_mirai.animations.left.count = 4;
	mirai_mirai_animation_set(&animations, &images, &colors, 1, 4, 8, 64, 128);
	
	mirai_mirai.animations.right.standing = animations;
	mirai_mirai.animations.right.count = 4;
	mirai_mirai_animation_set(&animations, &images, &colors, -1, 4, 8, 64, 128);
	
	struct mirai_image *rotated_ccw = images;
	mirai_mirai_animation(&mirai_mirai.animations.left.knocked, &images, &colors, 64 - 16, 8, 0, 128);
	while (rotated_ccw != images) mirai_rotate_image_ccw(rotated_ccw++);
	
	struct mirai_image *rotated_cw = images;
	mirai_mirai_animation(&mirai_mirai.animations.right.knocked, &images, &colors, 64 + 16, 8, 0, 128);
	while (rotated_cw != images) mirai_rotate_image_cw(rotated_cw++);
	
	for (int i = 0 ; i < 10 ; i++)
	{
		mirai_glyphs[i] = images;
		images->colors = colors;
		images->height = 15;
		
		for (int x = 1 ; x < 12 ; x++)
		{
			int done = 1;
			for (int y = 0 ; y < images->height ; y++)
			{
				if (mirai_digits[i][y][x] != 0)
				{
					done = 0;
					break;
				}
			}
			if (done == 0) continue;
			images->width = x + 1;
			break;
		}
		
		for (int y = 0 ; y < images->height ; y++)
		for (int x = 0 ; x < images->width ; x++)
			*colors++ = mirai_digits[i][y][x] == 0 ? 0 : 108;
		
		images++;
	}
	
	for (int c = 0 ; c < 2 ; c++)
	for (int i = 0 ; i < 26 ; i++)
	{
		mirai_glyphs[i + 26 * c + 10] = images;
		images->colors = colors;
		images->height = 15;
		
		for (int x = 1 ; x < 12 ; x++)
		{
			int done = 1;
			for (int y = 0 ; y < images->height ; y++)
			{
				if (mirai_letters[i][y * 2 + c][x] != 0)
				{
					done = 0;
					break;
				}
			}
			if (done == 0) continue;
			images->width = x + 1;
			break;
		}
		
		for (int y = 0 ; y < images->height ; y++)
		for (int x = 0 ; x < images->width ; x++)
			*colors++ = mirai_letters[i][y * 2 + c][x] == 0 ? 0 : 108;
		
		images++;
	}
	
	if (colors != last_color) return 0;
	if (images != last_image - 1) return 0;
	if (animations != last_animation) return 0;
	
	done = 1;
	return mirai_images;
}