diff libmlk-rpg/mlk/rpg/battle-bar-default.c @ 434:4e78f045e8c0

rpg: cleanup hierarchy
author David Demelier <markand@malikania.fr>
date Sat, 15 Oct 2022 21:24:17 +0200
parents src/libmlk-rpg/rpg/battle-bar-default.c@862b15c3a3ae
children 25a56ca53ac2
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-rpg/mlk/rpg/battle-bar-default.c	Sat Oct 15 21:24:17 2022 +0200
@@ -0,0 +1,693 @@
+/*
+ * battle-bar-default.c -- default battle status bar and menu implementation
+ *
+ * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/event.h>
+#include <mlk/core/font.h>
+#include <mlk/core/sprite.h>
+#include <mlk/core/trace.h>
+#include <mlk/core/util.h>
+#include <mlk/core/window.h>
+
+#include <mlk/ui/align.h>
+#include <mlk/ui/theme.h>
+
+#include "battle-bar-default.h"
+#include "battle-bar.h"
+#include "battle-state-item.h"
+#include "battle-state-selection.h"
+#include "battle.h"
+#include "character.h"
+#include "inventory.h"
+#include "item.h"
+#include "spell.h"
+
+#define THEME(bar) ((bar)->theme ? (bar)->theme : theme_default())
+
+struct geo {
+	int x, y;
+	unsigned int w, h;
+};
+
+static inline void
+dimensions(struct geo geo[2], const struct battle_bar_default *bar)
+{
+	/* 0 == main menu */
+	geo[0].w = bar->w * 0.2;
+	geo[0].h = bar->h;
+	geo[0].x = bar->x + (bar->w / 2) - (geo[0].w / 2);
+	geo[0].y = window.h - bar->h;
+
+	/* 1 == status frame */
+	geo[1].x = geo[0].x + geo[0].w;
+	geo[1].y = geo[0].y;
+	geo[1].w = (bar->w - geo[0].w) / 2;
+	geo[1].h = bar->h;
+}
+
+/*
+ * The following validate_* functions are called when the user has validated a
+ * selection depending on the current menu (e.g. Magic, Items).
+ *
+ * They change the battle state to the appropriate one.
+ */
+
+static void
+validate_attack(struct battle_bar_default *bar, struct battle *bt, const struct selection *sel)
+{
+	(void)bar;
+
+	struct character *target;
+
+	if (sel->index_side == 0)
+		target = bt->enemies[sel->index_character]->ch;
+	else
+		target = bt->team[sel->index_character]->ch;
+
+	battle_attack(bt, battle_current(bt)->ch, target);
+}
+
+static void
+validate_magic(struct battle_bar_default *bar, struct battle *bt, const struct selection *sel)
+{
+	struct character *source = battle_current(bt)->ch;
+	const struct spell *spell = source->spells[bar->grid.selected];
+
+	battle_cast(bt, source, spell, sel);
+}
+
+static void
+validate_item(struct battle_bar_default *bar, struct battle *bt, const struct selection *sel)
+{
+	struct inventory_slot *slot;
+	struct battle_entity *source, *target;
+
+	if (bar->grid.selected >= INVENTORY_ITEM_MAX)
+		return;
+	if (!(slot = &bt->inventory->items[bar->grid.selected]))
+		return;
+
+	source = battle_current(bt);
+	target = sel->index_side == 0
+		? bt->enemies[sel->index_character]
+		: bt->team[sel->index_character];
+
+	/* TODO: battle_use? */
+	battle_state_item(bt, source, target, slot);
+}
+
+/*
+ * The following functions are used to switch to the battle selection state
+ * using the appropriate selector algorithm. For example, an item can only be
+ * used on a unique target while a spell can have multiple choices.
+ */
+
+static void
+switch_selection_attack(struct battle_bar_default *bar, struct battle *bt)
+{
+	struct selection sel = {
+		.allowed_kinds = SELECTION_KIND_ONE,
+		.allowed_sides = SELECTION_SIDE_ENEMY,
+		.index_side = 0
+	};
+
+	/* Disable handling anymore. */
+	bar->state = BATTLE_BAR_DEFAULT_STATE_NONE;
+
+	selection_first(&sel, bt);
+	battle_state_selection(bt, &sel);
+}
+
+static void
+switch_selection_spell(struct battle_bar_default *bar, struct battle *bt)
+{
+	const struct character *ch = battle_current(bt)->ch;
+	const struct spell *sp = ch->spells[bar->grid.selected];
+	struct selection sel = {0};
+
+	if (bar->grid.selected > CHARACTER_SPELL_MAX)
+		return;
+	if (!(sp = ch->spells[bar->grid.selected]) || sp->mp > (unsigned int)ch->mp)
+		return;
+
+	/* Use the spell selection algorithm to fill default values. */
+	spell_select(sp, bt, &sel);
+	battle_state_selection(bt, &sel);
+
+	/* A cursor should be present. */
+	if (!sprite_ok(BATTLE_THEME(bt)->sprites[THEME_SPRITE_CURSOR]))
+		tracef("battle: no cursor sprite in theme");
+}
+
+static void
+switch_selection_item(struct battle *bt)
+{
+	const struct selection slt = {
+		.allowed_kinds = SELECTION_KIND_ONE,
+		.allowed_sides = SELECTION_SIDE_TEAM | SELECTION_SIDE_ENEMY,
+		.index_side = 1,
+		.index_character = battle_index(bt)
+	};
+
+	battle_state_selection(bt, &slt);
+}
+
+/*
+ * The following functions actually draw the bar and their components depending
+ * on the current selected menu.
+ */
+
+static void
+draw_help(const struct battle_bar_default *bar, const char *what)
+{
+	struct label label = {0};
+	unsigned int lw = 0, lh = 0;
+
+	label.flags = LABEL_FLAGS_SHADOW;
+	label.text = what;
+	label_query(&label, &lw, &lh);
+	label.x = bar->grid.x + (bar->grid.w / 2) - (lw / 2);
+	label.y = bar->grid.y - lh - THEME(bar)->padding;
+	label_draw(&label);
+}
+
+static void
+draw_spell_help(const struct battle_bar_default *bar, const struct battle *bt)
+{
+	const struct character *ch = battle_current(bt)->ch;
+	const struct spell *sp;
+
+	if (bar->grid.selected >= CHARACTER_SPELL_MAX)
+		return;
+	if (!(sp = ch->spells[bar->grid.selected]))
+		return;
+
+	draw_help(bar, sp->description);
+}
+
+static void
+draw_item_help(const struct battle_bar_default *bar, const struct battle *bt)
+{
+	const struct inventory_slot *slot;
+
+	if (bar->grid.selected >= INVENTORY_ITEM_MAX)
+		return;
+
+	slot = &bt->inventory->items[bar->grid.selected];
+
+	if (!slot->item)
+		return;
+
+	draw_help(bar, slot->item->description);
+}
+
+static void
+draw_status_character_stats(const struct battle_bar_default *bar,
+                            const struct character *ch,
+                            int x,
+                            int y,
+                            unsigned int h)
+{
+	const struct theme *theme = THEME(bar);
+	struct label label;
+	unsigned int spacing, lw, lh;
+	char line[64];
+
+	/* Compute spacing between elements. */
+	spacing = h - (font_height(theme->fonts[THEME_FONT_INTERFACE]) * 3);
+	spacing /= 4;
+
+	/* Reuse the same label. */
+	label.theme = theme;
+	label.text = line;
+	label.flags = LABEL_FLAGS_SHADOW;
+
+	/* Name. */
+	snprintf(line, sizeof (line), "%s", ch->name);
+	label_query(&label, &lw, &lh);
+	label.x = x + theme->padding;
+	label.y = y + spacing;
+	label_draw(&label);
+
+	/* HP. */
+	snprintf(line, sizeof (line), "%d/%u", ch->hp, ch->hpmax);
+	label_query(&label, &lw, &lh);
+	label.x = x + theme->padding;
+	label.y = label.y + lh + spacing;
+	label_draw(&label);
+
+	/* MP. */
+	snprintf(line, sizeof (line), "%d/%u", ch->mp, ch->mpmax);
+	label_query(&label, &lw, &lh);
+	label.x = x + theme->padding;
+	label.y = label.y + lh + spacing;
+	label_draw(&label);
+
+	/* Status. */
+	/* TODO: list all status. */
+}
+
+static void
+draw_status_character(const struct battle_bar_default *bar,
+                      const struct battle *bt,
+                      const struct character *ch,
+                      const struct geo *geo,
+                      unsigned int index)
+{
+	int x, y;
+	unsigned int w, h;
+
+	/* Compute bounding box for rendering. */
+	w = geo->w / bt->teamsz;
+	h = geo->h;
+	x = geo->x + (index * w);
+	y = geo->y;
+
+	draw_status_character_stats(bar, ch, x, y, h);
+}
+
+static void
+draw_status_characters(const struct battle_bar_default *bar,
+                       const struct battle *bt,
+                       const struct geo *geo)
+{
+	const struct battle_entity *et;
+	unsigned int index = 0;
+
+	BATTLE_TEAM_FOREACH(bt, et) {
+		if (character_ok(et->ch))
+			draw_status_character(bar, bt, et->ch, geo, index);
+
+		++index;
+	}
+}
+
+static void
+draw_status(const struct battle_bar_default *bar, const struct battle *bt, const struct geo *geo)
+{
+	frame_draw(&(const struct frame) {
+		.x = geo->x,
+		.y = geo->y,
+		.w = geo->w,
+		.h = geo->h
+	});
+
+	draw_status_characters(bar, bt, geo);
+}
+
+static void
+draw_menu(const struct battle_bar_default *bar, const struct geo *geo)
+{
+	struct {
+		unsigned int w, h;
+		enum align align;
+		struct label label;
+	} buttons[] = {
+		{
+			.align = ALIGN_TOP,
+			.label = {
+				.text = "Attack",
+				.flags = LABEL_FLAGS_SHADOW
+			}
+		},
+		{
+			.align = ALIGN_RIGHT,
+			.label = {
+				.text = "Magic",
+				.flags = LABEL_FLAGS_SHADOW
+			}
+		},
+		{
+			.align = ALIGN_BOTTOM,
+			.label = {
+				.text = "Objects",
+				.flags = LABEL_FLAGS_SHADOW
+			}
+		},
+		{
+			.align = ALIGN_LEFT,
+			.label = {
+				.text = "Special",
+				.flags = LABEL_FLAGS_SHADOW
+			}
+		}
+	};
+
+	const struct theme *theme = THEME(bar);
+	int bx, by;
+	unsigned int bw, bh;
+
+	/* Compute bounding box with margins removed. */
+	bx = geo->x + theme->padding;
+	by = geo->y + theme->padding;
+	bw = geo->w - theme->padding * 2;
+	bh = geo->h - theme->padding * 2;
+
+	/* Draw menu frame. */
+	frame_draw(&(const struct frame) {
+		.x = geo->x,
+		.y = geo->y,
+		.w = geo->w,
+		.h = geo->h
+	});
+
+	for (size_t i = 0; i < UTIL_SIZE(buttons); ++i) {
+		buttons[i].label.theme = theme;
+
+		label_query(&buttons[i].label, &buttons[i].w, &buttons[i].h);
+
+		/* Change theme if it's selected. */
+		if ((size_t)bar->menu == i)
+			buttons[i].label.flags |=  LABEL_FLAGS_SELECTED;
+		else
+			buttons[i].label.flags &= ~LABEL_FLAGS_SELECTED;
+
+		align(buttons[i].align,
+		    &buttons[i].label.x, &buttons[i].label.y, buttons[i].w, buttons[i].h,
+		    bx, by, bw, bh);
+		label_draw(&buttons[i].label);
+	}
+}
+
+/*
+ * This function is called only in the first level of the bar menu: selecting
+ * one of the Attack, Magic, Item and Special items.
+ */
+static void
+handle_keydown_menu(struct battle_bar_default *bar, struct battle *bt, const union event *ev)
+{
+	(void)bt;
+
+	switch (ev->key.key) {
+	case KEY_UP:
+		bar->menu = BATTLE_BAR_DEFAULT_MENU_ATTACK;
+		break;
+	case KEY_RIGHT:
+		bar->menu = BATTLE_BAR_DEFAULT_MENU_MAGIC;
+		break;
+	case KEY_DOWN:
+		bar->menu = BATTLE_BAR_DEFAULT_MENU_ITEM;
+		break;
+	case KEY_LEFT:
+		bar->menu = BATTLE_BAR_DEFAULT_MENU_SPECIAL;
+		break;
+	case KEY_ENTER:
+		/*
+		 * At this step, attack does not require opening the sub menu so
+		 * we change selection state immediately if needed.
+		 */
+		switch (bar->menu) {
+		case BATTLE_BAR_DEFAULT_MENU_ATTACK:
+			switch_selection_attack(bar, bt);
+			break;
+		case BATTLE_BAR_DEFAULT_MENU_ITEM:
+			battle_bar_default_open_item(bar, bt);
+			break;
+		case BATTLE_BAR_DEFAULT_MENU_MAGIC:
+			battle_bar_default_open_magic(bar, bt, battle_current(bt)->ch);
+			break;
+		default:
+			break;
+		}
+		break;
+	default:
+		break;
+	}
+}
+
+/*
+ * This function is called when we're selecting a submenu entry from Items
+ * and Magic.
+ */
+static void
+handle_keydown_grid(struct battle_bar_default *bar, struct battle *bt, const union event *ev)
+{
+	/* Go back to main menu if I press escape. */
+	if (ev->key.key == KEY_ESCAPE)
+		bar->state = BATTLE_BAR_DEFAULT_STATE_MENU;
+	else if (gridmenu_handle(&bar->grid, ev)) {
+		switch (bar->menu) {
+		case BATTLE_BAR_DEFAULT_MENU_MAGIC:
+			switch_selection_spell(bar, bt);
+			break;
+		case BATTLE_BAR_DEFAULT_MENU_ITEM:
+			switch_selection_item(bt);
+			break;
+		default:
+			break;
+		}
+	}
+}
+
+static void
+handle_keydown(struct battle_bar_default *bar, struct battle *bt, const union event *ev)
+{
+	assert(ev->type == EVENT_KEYDOWN);
+
+	static void (*handlers[])(struct battle_bar_default *, struct battle *, const union event *) = {
+		[BATTLE_BAR_DEFAULT_STATE_MENU] = handle_keydown_menu,
+		[BATTLE_BAR_DEFAULT_STATE_GRID] = handle_keydown_grid
+	};
+
+	if (handlers[bar->state])
+		handlers[bar->state](bar, bt, ev);
+}
+
+#if 0
+
+static void
+handle_clickdown(struct battle_bar_default *bar, struct battle *bt, const union event *ev)
+{
+	assert(ev->type == EVENT_CLICKDOWN);
+
+	(void)bar;
+	(void)bt;
+
+	switch (bar->state) {
+	case BATTLE_BAR_DEFAULT_STATE_MENU:
+		/* We are selecting a main menu entry. */
+		/* TODO: implement click here too. */
+		break;
+	case BATTLE_BAR_DEFAULT_STATE_SUB:
+		/* We are in the sub menu (objects/spells). */
+		if (bar->sub_grid.state == GRIDMENU_STATE_ACTIVATED)
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+#endif
+
+static void
+self_start(struct battle_bar *bar, struct battle *bt)
+{
+	(void)bt;
+
+	battle_bar_default_start(bar->data);
+}
+
+static void
+self_select(struct battle_bar *bar, struct battle *bt, const struct selection *sel)
+{
+	battle_bar_default_select(bar->data, bt, sel);
+}
+
+static void
+self_handle(struct battle_bar *bar, struct battle *bt, const union event *ev)
+{
+	battle_bar_default_handle(bar->data, bt, ev);
+}
+
+static void
+self_draw(const struct battle_bar *bar, const struct battle *bt)
+{
+	battle_bar_default_draw(bar->data, bt);
+}
+
+void
+battle_bar_default_init(struct battle_bar_default *bar)
+{
+	assert(bar);
+
+	struct geo geo[2];
+
+	memset(bar, 0, sizeof (*bar));
+
+	bar->w = window.w;
+	bar->h = window.h * 0.12;
+	bar->x = 0;
+	bar->y = window.h - bar->h;
+
+	dimensions(geo, bar);
+
+	gridmenu_init(&bar->grid, 2, 2, NULL, 0);
+	gridmenu_resize(&bar->grid, bar->x, geo[0].y, geo[1].w, bar->h);
+	bar->grid.theme = bar->theme;
+}
+
+void
+battle_bar_default_open_magic(struct battle_bar_default *bar, const struct battle *bt, struct character *ch)
+{
+	assert(bar);
+	assert(bt);
+	assert(ch);
+
+	(void)bt;
+
+	bar->items = alloc_rearray0(bar->items, bar->itemsz,
+	    CHARACTER_SPELL_MAX, sizeof (*bar->items));
+	bar->itemsz = CHARACTER_SPELL_MAX;
+	bar->state = BATTLE_BAR_DEFAULT_STATE_GRID;
+
+	for (size_t i = 0; i < CHARACTER_SPELL_MAX; ++i)
+		if (ch->spells[i])
+			bar->items[i] = ch->spells[i]->name;
+
+	bar->grid.items = bar->items;
+	bar->grid.itemsz = bar->itemsz;
+}
+
+void
+battle_bar_default_open_item(struct battle_bar_default *bar, const struct battle *bt)
+{
+	assert(bar);
+	assert(bt);
+
+	/* TODO: not implemented yet. */
+	(void)bar;
+	(void)bt;
+#if 0
+	for (size_t i = 0; i < INVENTORY_ITEM_MAX; ++i) {
+		if (bt->inventory->items[i].item) {
+			snprintf(bar->sub_items[i], sizeof (bar->sub_items[i]), "%-16s %u",
+			    bt->inventory->items[i].item->name, bt->inventory->items[i].amount);
+			bar->sub_grid.menu[i] = bar->sub_items[i];
+		}
+	}
+
+	bar->state = BATTLE_BAR_DEFAULT_STATE_GRID;
+#endif
+}
+
+void
+battle_bar_default_start(struct battle_bar_default *bar)
+{
+	assert(bar);
+
+	bar->menu = BATTLE_BAR_DEFAULT_MENU_ATTACK;
+	bar->state = BATTLE_BAR_DEFAULT_STATE_MENU;
+}
+
+/*
+ * Apply the battle selection for the current menu item. This function is called
+ * from the battle-state-selection state when the user validated the selection.
+ */
+void
+battle_bar_default_select(struct battle_bar_default *bar, struct battle *bt, const struct selection *sel)
+{
+	assert(bar);
+	assert(bt);
+	assert(sel);
+
+	static void (*validate[])(struct battle_bar_default *, struct battle *, const struct selection *) = {
+		[BATTLE_BAR_DEFAULT_MENU_ATTACK]        = validate_attack,
+		[BATTLE_BAR_DEFAULT_MENU_ITEM]          = validate_item,
+		[BATTLE_BAR_DEFAULT_MENU_MAGIC]         = validate_magic,
+		[BATTLE_BAR_DEFAULT_MENU_SPECIAL]       = NULL
+	};
+
+	if (validate[bar->menu])
+		validate[bar->menu](bar, bt, sel);
+}
+
+void
+battle_bar_default_handle(struct battle_bar_default *bar, struct battle *bt, const union event *ev)
+{
+	assert(bar);
+	assert(bt);
+	assert(ev);
+
+	static void (*handlers[])(struct battle_bar_default *, struct battle *, const union event *) = {
+		[EVENT_KEYDOWN] = handle_keydown,
+		[EVENT_NUM] = NULL
+	};
+
+	if (handlers[ev->type])
+		handlers[ev->type](bar, bt, ev);
+}
+
+void
+battle_bar_default_draw(const struct battle_bar_default *bar, const struct battle *bt)
+{
+	assert(bar);
+	assert(bt);
+
+	struct geo geo[2];
+
+	dimensions(geo, bar);
+	draw_menu(bar, &geo[0]);
+	draw_status(bar, bt, &geo[1]);
+
+	if (bar->state == BATTLE_BAR_DEFAULT_STATE_GRID) {
+		switch (bar->menu) {
+		case BATTLE_BAR_DEFAULT_MENU_MAGIC:
+			draw_spell_help(bar, bt);
+			break;
+		case BATTLE_BAR_DEFAULT_MENU_ITEM:
+			draw_item_help(bar, bt);
+			break;
+		default:
+			break;
+		}
+	}
+
+	/* Sub menu is only shown if state is set to it. */
+	if (bar->state == BATTLE_BAR_DEFAULT_STATE_GRID)
+		gridmenu_draw(&bar->grid);
+}
+
+void
+battle_bar_default_finish(struct battle_bar_default *bar)
+{
+	assert(bar);
+
+	free(bar->items);
+	memset(bar, 0, sizeof (*bar));
+}
+
+void
+battle_bar_default(struct battle_bar_default *self, struct battle_bar *bar)
+{
+	assert(self);
+	assert(bar);
+
+	memset(bar, 0, sizeof (*bar));
+
+	bar->data = self;
+	bar->start = self_start;
+	bar->select = self_select;
+	bar->handle = self_handle;
+	bar->draw = self_draw;
+}