diff src/libmlk-rpg/rpg/battle-bar-default.c @ 385:3f13dc6c0e37

rpg: separate battle and the bar, closes #2522
author David Demelier <markand@malikania.fr>
date Tue, 15 Feb 2022 14:45:11 +0100
parents
children 7d5032755b7d
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libmlk-rpg/rpg/battle-bar-default.c	Tue Feb 15 14:45:11 2022 +0100
@@ -0,0 +1,698 @@
+/*
+ * 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 <core/alloc.h>
+#include <core/event.h>
+#include <core/font.h>
+#include <core/sprite.h>
+#include <core/trace.h>
+#include <core/util.h>
+#include <core/window.h>
+
+#include <ui/align.h>
+#include <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 "rpg_p.h"
+#include "spell.h"
+
+struct self {
+	struct battle_bar_default data;
+	struct battle_bar bar;
+};
+
+/*
+ * 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, bt->order_cur->ch, target);
+}
+
+static void
+validate_magic(struct battle_bar_default *bar, struct battle *bt, const struct selection *sel)
+{
+	struct character *source = bt->order_cur->ch;
+	const struct spell *spell = source->spells[bar->sub_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->sub_grid.selected >= INVENTORY_ITEM_MAX)
+		return;
+	if (!(slot = &bt->inventory->items[bar->sub_grid.selected]))
+		return;
+
+	source = bt->order_cur;
+	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 *bt)
+{
+	struct selection sel = {
+		.allowed_kinds = SELECTION_KIND_ONE,
+		.allowed_sides = SELECTION_SIDE_ENEMY,
+		.index_side = 0
+	};
+
+	/* Just make sure */
+
+	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 = bt->order_cur->ch;
+	const struct spell *sp = ch->spells[bar->sub_grid.selected];
+	struct selection sel = {0};
+
+	/* Don't forget to reset the gridmenu state. */
+	gridmenu_reset(&bar->sub_grid);
+
+	if (bar->sub_grid.selected > CHARACTER_SPELL_MAX)
+		return;
+	if (!(sp = ch->spells[bar->sub_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 = bt->order_curindex
+	};
+
+	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 struct battle *bt, 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->sub_grid.x + (bar->sub_grid.w / 2) - (lw / 2);
+	label.y = bar->sub_grid.y - lh - BATTLE_THEME(bt)->padding;
+	label_draw(&label);
+}
+
+static void
+draw_spell_help(const struct battle_bar_default *bar, const struct battle *bt)
+{
+	const struct character *ch = bt->order_cur->ch;
+	const struct spell *sp;
+
+	if (bar->sub_grid.selected >= CHARACTER_SPELL_MAX)
+		return;
+	if (!(sp = ch->spells[bar->sub_grid.selected]))
+		return;
+
+	draw_help(bar, bt, sp->description);
+}
+
+static void
+draw_item_help(const struct battle_bar_default *bar, const struct battle *bt)
+{
+	const struct inventory_slot *slot;
+
+	if (bar->sub_grid.selected >= INVENTORY_ITEM_MAX)
+		return;
+
+	slot = &bt->inventory->items[bar->sub_grid.selected];
+
+	if (!slot->item)
+		return;
+
+	draw_help(bar, bt, slot->item->description);
+}
+
+static void
+draw_status_character_stats(const struct battle *bt,
+                            const struct character *ch,
+                            int x,
+                            int y,
+                            unsigned int w,
+                            unsigned int h)
+{
+	struct theme *theme = BATTLE_THEME(bt);
+	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;
+
+	/* HP. */
+	snprintf(line, sizeof (line), "%d/%u", ch->hp, ch->hpmax);
+	label_query(&label, &lw, &lh);
+	label.x = x + w - lw - theme->padding;
+	label.y = y + spacing;
+	label_draw(&label);
+
+	/* MP. */
+	snprintf(line, sizeof (line), "%d/%u", ch->mp, ch->mpmax);
+	label_query(&label, &lw, &lh);
+	label.x = x + w - lw - 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,
+                      unsigned int index)
+{
+	int x, y;
+	unsigned int w, h;
+
+	/* Compute bounding box for rendering. */
+	w = bar->status_frame.w / BATTLE_TEAM_MAX;
+	h = bar->status_frame.h;
+	x = bar->status_frame.x + (index * w);
+	y = bar->status_frame.y;
+
+	draw_status_character_stats(bt, ch, x, y, w, h);
+}
+
+static void
+draw_status_characters(const struct battle_bar_default *bar, const struct battle *bt)
+{
+	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, index);
+
+		++index;
+	}
+}
+
+static void
+draw_status(const struct battle_bar_default *bar, const struct battle *bt)
+{
+	frame_draw(&bar->status_frame);
+	draw_status_characters(bar, bt);
+}
+
+static void
+draw_menu(const struct battle_bar_default *bar, const struct battle *bt)
+{
+	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
+			}
+		}
+	};
+
+	struct theme theme;
+	int bx, by;
+	unsigned int bw, bh;
+
+	/* Copy theme according to menu selection. */
+	theme_shallow(&theme, bt->theme);
+
+	/* Compute bounding box with margins removed. */
+	bx = bar->menu_frame.x + theme.padding;
+	by = bar->menu_frame.y + theme.padding;
+	bw = bar->menu_frame.w - theme.padding * 2;
+	bh = bar->menu_frame.h - theme.padding * 2;
+
+	/* Draw menu frame. */
+	frame_draw(&bar->menu_frame);
+
+	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 /*&& bar->state != BATTLE_BAR_DEFAULT_STATE_NONE*/)
+			theme.colors[THEME_COLOR_NORMAL] = BATTLE_THEME(bt)->colors[THEME_COLOR_SELECTED];
+		else
+			theme.colors[THEME_COLOR_NORMAL] = BATTLE_THEME(bt)->colors[THEME_COLOR_NORMAL];
+
+		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(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, bt->order_cur->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) {
+		gridmenu_reset(&bar->sub_grid);
+		bar->state = BATTLE_BAR_DEFAULT_STATE_MENU;
+		return;
+	}
+		
+	gridmenu_handle(&bar->sub_grid, ev);
+
+	if (bar->sub_grid.state == GRIDMENU_STATE_ACTIVATED) {
+		gridmenu_reset(&bar->sub_grid);
+	
+		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
+	};
+
+	handlers[bar->state](bar, bt, ev);
+}
+
+#if 0
+
+static void
+handle_clickdown(struct battle_bar_default *bar, struct battle *bt, const union event *ev)
+{
+	(void)bar;
+	(void)bt;
+	(void)ev;
+	assert(ev->type == EVENT_CLICKDOWN);
+
+	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). */
+		gridmenu_handle(&bar->sub_grid, ev);
+
+		if (bar->sub_grid.state == GRIDMENU_STATE_ACTIVATED)
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+#endif
+
+static void
+init_gridmenu(struct battle_bar_default *bar, const struct battle *bt)
+{
+	bar->sub_grid.x = bar->x;
+	bar->sub_grid.y = bar->menu_frame.y;
+	bar->sub_grid.w = bar->status_frame.w;
+	bar->sub_grid.h = bar->h;
+	bar->sub_grid.theme = bt->theme;
+	bar->sub_grid.nrows = 3;
+	bar->sub_grid.ncols = 4;
+
+	memset(bar->sub_grid.menu, 0, sizeof (bar->sub_grid.menu));
+}
+
+static void
+start(struct battle_bar *bar, struct battle *bt)
+{
+	(void)bt;
+
+	battle_bar_default_start(bar->data);
+}
+
+static void
+select(struct battle_bar *bar, struct battle *bt, const struct selection *sel)
+{
+	battle_bar_default_select(bar->data, bt, sel);
+}
+
+static void
+handle(struct battle_bar *bar, struct battle *bt, const union event *ev)
+{
+	battle_bar_default_handle(bar->data, bt, ev);
+}
+
+static void
+draw(const struct battle_bar *bar, const struct battle *bt)
+{
+	battle_bar_default_draw(bar->data, bt);
+}
+
+static void
+finish(struct battle_bar *bar, struct battle *bt)
+{
+	(void)bt;
+
+	battle_bar_default_finish(bar->data);
+	free(bar->data);
+}
+
+void
+battle_bar_default_positionate(struct battle_bar_default *bar, const struct battle *bt)
+{
+	assert(bar);
+	assert(bt);
+
+	bar->w = window.w;
+	bar->h = window.h * 0.12;
+	bar->x = 0;
+	bar->y = window.h - bar->h;
+
+	/* Menu in the middle of the bar (take 20%). */
+	bar->menu_frame.w = bar->w * 0.2;
+	bar->menu_frame.h = bar->h;
+	bar->menu_frame.x = bar->x + (bar->w / 2) - (bar->menu_frame.w / 2);
+	bar->menu_frame.y = window.h - bar->h;
+	bar->menu_frame.theme = bt->theme;
+
+	/* Status bar on the right. */
+	bar->status_frame.x = bar->menu_frame.x + bar->menu_frame.w;
+	bar->status_frame.y = bar->menu_frame.y;
+	bar->status_frame.w = (bar->w - bar->menu_frame.w) / 2;
+	bar->status_frame.h = bar->h;
+	bar->status_frame.theme = bt->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);
+
+	init_gridmenu(bar, bt);
+
+	for (size_t i = 0; i < CHARACTER_SPELL_MAX; ++i)
+		if (ch->spells[i])
+			bar->sub_grid.menu[i] = ch->spells[i]->name;
+
+	gridmenu_repaint(&bar->sub_grid);
+
+	bar->state = BATTLE_BAR_DEFAULT_STATE_GRID;
+}
+
+void
+battle_bar_default_open_item(struct battle_bar_default *bar, const struct battle *bt)
+{
+	asssert(bar);
+	assert(bt);
+
+	init_gridmenu(bar, bt);
+
+	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];
+		}
+	}
+
+	gridmenu_repaint(&bar->sub_grid);
+
+	bar->state = BATTLE_BAR_DEFAULT_STATE_GRID;
+}
+
+void
+battle_bar_default_start(struct battle_bar_default *bar)
+{
+	assert(bar);
+
+	gridmenu_reset(&bar->sub_grid);
+
+	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);
+
+	draw_status(bar, bt);
+	draw_menu(bar, bt);
+
+	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->sub_grid);
+}
+
+void
+battle_bar_default_finish(struct battle_bar_default *bar)
+{
+	assert(bar);
+
+	gridmenu_finish(&bar->sub_grid);
+
+	memset(bar, 0, sizeof (*bar));
+}
+
+void
+battle_bar_default(struct battle *bt)
+{
+	assert(bt);
+
+	struct self *self;
+
+	self = alloc_new0(sizeof (*self));
+	self->bar.data = self;
+	self->bar.start = start;
+	self->bar.select = select;
+	self->bar.handle = handle;
+	self->bar.draw = draw;
+	self->bar.finish = finish;
+
+	battle_bar_default_positionate(&self->data, bt);
+
+	bt->bar = &self->bar;
+}