changeset 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 c458441ff472
children 7d5032755b7d
files examples/example-battle/main.c src/libmlk-core/core/event.h src/libmlk-rpg/CMakeLists.txt src/libmlk-rpg/rpg/battle-bar-default.c src/libmlk-rpg/rpg/battle-bar-default.h src/libmlk-rpg/rpg/battle-bar.c src/libmlk-rpg/rpg/battle-bar.h src/libmlk-rpg/rpg/battle-state-attacking.h src/libmlk-rpg/rpg/battle-state-menu.c src/libmlk-rpg/rpg/battle-state-selection.c src/libmlk-rpg/rpg/battle-state-sub.c src/libmlk-rpg/rpg/battle.c src/libmlk-rpg/rpg/battle.h
diffstat 13 files changed, 858 insertions(+), 566 deletions(-) [+]
line wrap: on
line diff
--- a/examples/example-battle/main.c	Sun Feb 13 11:34:19 2022 +0100
+++ b/examples/example-battle/main.c	Tue Feb 15 14:45:11 2022 +0100
@@ -41,6 +41,7 @@
 #include <ui/ui.h>
 
 #include <rpg/character.h>
+#include <rpg/battle-bar-default.h>
 #include <rpg/battle.h>
 #include <rpg/rpg.h>
 #include <rpg/spell.h>
@@ -179,6 +180,7 @@
 
 	bt->background = &registry_images[REGISTRY_IMAGE_BATTLE_BACKGROUND];
 
+	battle_bar_default(bt);
 	battle_start(bt);
 
 	fight_state.data = bt;
--- a/src/libmlk-core/core/event.h	Sun Feb 13 11:34:19 2022 +0100
+++ b/src/libmlk-core/core/event.h	Tue Feb 15 14:45:11 2022 +0100
@@ -30,6 +30,7 @@
 	EVENT_KEYUP,
 	EVENT_MOUSE,
 	EVENT_QUIT,
+	EVENT_NUM
 };
 
 struct event_key {
--- a/src/libmlk-rpg/CMakeLists.txt	Sun Feb 13 11:34:19 2022 +0100
+++ b/src/libmlk-rpg/CMakeLists.txt	Tue Feb 15 14:45:11 2022 +0100
@@ -20,6 +20,8 @@
 
 set(
 	SOURCES
+	${libmlk-rpg_SOURCE_DIR}/rpg/battle-bar-default.c
+	${libmlk-rpg_SOURCE_DIR}/rpg/battle-bar-default.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-bar.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-bar.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-entity-state-attacking.c
@@ -56,8 +58,6 @@
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-opening.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-selection.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-selection.h
-	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-sub.c
-	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-sub.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-victory.c
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state-victory.h
 	${libmlk-rpg_SOURCE_DIR}/rpg/battle-state.c
--- /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;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libmlk-rpg/rpg/battle-bar-default.h	Tue Feb 15 14:45:11 2022 +0100
@@ -0,0 +1,95 @@
+/*
+ * battle-bar-default.h -- 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.
+ */
+
+#ifndef MLK_RPG_BATTLE_BAR_DEFAULT_H
+#define MLK_RPG_BATTLE_BAR_DEFAULT_H
+
+#include <core/core.h>
+
+#include <ui/frame.h>
+#include <ui/gridmenu.h>
+
+struct battle;
+struct character;
+struct selection;
+
+union event;
+
+enum battle_bar_default_menu {
+	BATTLE_BAR_DEFAULT_MENU_ATTACK,
+	BATTLE_BAR_DEFAULT_MENU_MAGIC,
+	BATTLE_BAR_DEFAULT_MENU_ITEM,
+	BATTLE_BAR_DEFAULT_MENU_SPECIAL
+};
+
+enum battle_bar_default_state {
+	BATTLE_BAR_DEFAULT_STATE_MENU,
+	BATTLE_BAR_DEFAULT_STATE_GRID
+};
+
+struct battle_bar_default {
+	int x;
+	int y;
+	unsigned int w;
+	unsigned int h;
+	enum battle_bar_default_state state;
+
+	/* Right status frame. */
+	struct frame status_frame;
+
+	/* Main menu selection. */
+	struct frame menu_frame;
+	enum battle_bar_default_menu menu;
+
+	/* Sub menu selection (spells/objects). */
+	char sub_items[GRIDMENU_ENTRY_MAX][128];
+	struct gridmenu sub_grid;
+};
+
+CORE_BEGIN_DECLS
+
+void
+battle_bar_default_positionate(struct battle_bar_default *, const struct battle *);
+
+void
+battle_bar_default_open_magic(struct battle_bar_default *, const struct battle *, struct character *);
+
+void
+battle_bar_default_open_item(struct battle_bar_default *, const struct battle *);
+
+void
+battle_bar_default_start(struct battle_bar_default *);
+
+void
+battle_bar_default_select(struct battle_bar_default *, struct battle *, const struct selection *);
+
+void
+battle_bar_default_handle(struct battle_bar_default *bar, struct battle *bt, const union event *);
+
+void
+battle_bar_default_draw(const struct battle_bar_default *, const struct battle *);
+
+void
+battle_bar_default_finish(struct battle_bar_default *);
+
+void
+battle_bar_default(struct battle *);
+
+CORE_END_DECLS
+
+#endif /* !MLK_RPG_BATTLE_BAR_DEFAULT_H */
--- a/src/libmlk-rpg/rpg/battle-bar.c	Sun Feb 13 11:34:19 2022 +0100
+++ b/src/libmlk-rpg/rpg/battle-bar.c	Tue Feb 15 14:45:11 2022 +0100
@@ -1,5 +1,5 @@
 /*
- * battle-bar.h -- battle status bar and menu
+ * battle-bar.c -- abstract battle bar
  *
  * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
  *
@@ -17,349 +17,50 @@
  */
 
 #include <assert.h>
-#include <stdio.h>
-#include <string.h>
 
-#include <core/event.h>
-#include <core/font.h>
-#include <core/window.h>
-#include <core/util.h>
-
-#include <ui/align.h>
-#include <ui/theme.h>
-
-#include "battle.h"
 #include "battle-bar.h"
-#include "character.h"
-#include "inventory.h"
-#include "item.h"
-#include "spell.h"
-#include "rpg_p.h"
-
-static void
-init_gridmenu(struct battle_bar *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
-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 *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 *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 *bar, const struct battle *bt)
-{
-	frame_draw(&bar->status_frame);
-	draw_status_characters(bar, bt);
-}
-
-static void
-draw_menu(const struct battle_bar *bar, const struct battle *bt)
+void
+battle_bar_start(struct battle_bar *bar, 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_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];
+	assert(bar);
+	assert(bt);
 
-		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);
-	}
-}
-
-static void
-draw_sub(const struct battle_bar *bar)
-{
-	gridmenu_draw(&bar->sub_grid);
-}
-
-static int
-handle_keydown(struct battle_bar *bar, const union event *ev)
-{
-	assert(ev->type == EVENT_KEYDOWN);
-
-	switch (bar->state) {
-	case BATTLE_BAR_STATE_MENU:
-		/* We are selecting a main menu entry. */
-		switch (ev->key.key) {
-		case KEY_UP:
-			bar->menu = BATTLE_BAR_MENU_ATTACK;
-			break;
-		case KEY_RIGHT:
-			bar->menu = BATTLE_BAR_MENU_MAGIC;
-			break;
-		case KEY_DOWN:
-			bar->menu = BATTLE_BAR_MENU_OBJECTS;
-			break;
-		case KEY_LEFT:
-			bar->menu = BATTLE_BAR_MENU_SPECIAL;
-			break;
-		case KEY_ENTER:
-			return 1;
-		default:
-			break;
-		}
-		break;
-	case BATTLE_BAR_STATE_SUB:
-		/* We are in the sub menu (objects/spells). */
-		gridmenu_handle(&bar->sub_grid, ev);
-		return bar->sub_grid.state == GRIDMENU_STATE_ACTIVATED;
-	default:
-		break;
-	}
-
-	return 0;
-}
-
-static int
-handle_clickdown(struct battle_bar *bar, const union event *ev)
-{
-	assert(ev->type == EVENT_CLICKDOWN);
-
-	switch (bar->state) {
-	case BATTLE_BAR_STATE_MENU:
-		/* We are selecting a main menu entry. */
-		/* TODO: implement click here too. */
-		break;
-	case BATTLE_BAR_STATE_SUB:
-		/* We are in the sub menu (objects/spells). */
-		gridmenu_handle(&bar->sub_grid, ev);
-		return bar->sub_grid.state == GRIDMENU_STATE_ACTIVATED;
-	default:
-		break;
-	}
-
-	return 0;
+	if (bar->start)
+		bar->start(bar, bt);
 }
 
 void
-battle_bar_positionate(struct battle_bar *bar, const struct battle *bt)
+battle_bar_select(struct battle_bar *bar, struct battle *bt, const struct selection *sel)
 {
 	assert(bar);
+	assert(bt);
+	assert(sel);
 
-	/* 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;
+	if (bar->select)
+		bar->select(bar, bt, sel);
 
-	/* 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;
 }
 
-int
-battle_bar_handle(struct battle_bar *bar, const struct battle *bt, const union event *ev)
+void
+battle_bar_handle(struct battle_bar *bar, struct battle *bt, const union event *ev)
 {
-	/* Not needed yet. */
-	(void)bt;
-
 	assert(bar);
 	assert(bt);
 	assert(ev);
 
-	if (bar->state == BATTLE_BAR_STATE_NONE)
-		return 0;
-
-	switch (ev->type) {
-	case EVENT_KEYDOWN:
-		return handle_keydown(bar, ev);
-	case EVENT_CLICKDOWN:
-		return handle_clickdown(bar, ev);
-	default:
-		break;
-	}
-
-	return 0;
-}
-
-void
-battle_bar_reset(struct battle_bar *bar)
-{
-	gridmenu_reset(&bar->sub_grid);
-
-	bar->menu = BATTLE_BAR_MENU_ATTACK;
-	bar->state = BATTLE_BAR_STATE_NONE;
+	if (bar->handle)
+		bar->handle(bar, bt, ev);
 }
 
 void
-battle_bar_open_menu(struct battle_bar *bar)
+battle_bar_update(struct battle_bar *bar, struct battle *bt, unsigned int ticks)
 {
-	gridmenu_reset(&bar->sub_grid);
-
-	bar->state = BATTLE_BAR_STATE_MENU;
-	bar->menu = BATTLE_BAR_MENU_ATTACK;
-}
-
-void
-battle_bar_open_spells(struct battle_bar *bar, const struct battle *bt, struct character *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);
+	assert(bar);
+	assert(bt);
 
-	bar->state = BATTLE_BAR_STATE_SUB;
-}
-
-void
-battle_bar_open_items(struct battle_bar *bar, const struct battle *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_STATE_SUB;
+	if (bar->update)
+		bar->update(bar, bt, ticks);
 }
 
 void
@@ -368,20 +69,16 @@
 	assert(bar);
 	assert(bt);
 
-	draw_status(bar, bt);
-	draw_menu(bar, bt);
-
-	/* Sub menu is only shown if state is set to it. */
-	if (bar->state == BATTLE_BAR_STATE_SUB)
-		draw_sub(bar);
+	if (bar->draw)
+		bar->draw(bar, bt);
 }
 
 void
-battle_bar_finish(struct battle_bar *bar)
+battle_bar_finish(struct battle_bar *bar, struct battle *bt)
 {
 	assert(bar);
-
-	gridmenu_finish(&bar->sub_grid);
+	assert(bt);
 
-	memset(bar, 0, sizeof (*bar));
+	if (bar->finish)
+		bar->finish(bar, bt);
 }
--- a/src/libmlk-rpg/rpg/battle-bar.h	Sun Feb 13 11:34:19 2022 +0100
+++ b/src/libmlk-rpg/rpg/battle-bar.h	Tue Feb 15 14:45:11 2022 +0100
@@ -1,5 +1,5 @@
 /*
- * battle-bar.h -- battle status bar and menu
+ * battle-bar.h -- abstract battle bar
  *
  * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
  *
@@ -21,73 +21,40 @@
 
 #include <core/core.h>
 
-#include <ui/frame.h>
-#include <ui/gridmenu.h>
-
 struct battle;
-struct character;
+struct selection;
 
 union event;
 
-enum battle_bar_menu {
-	BATTLE_BAR_MENU_ATTACK  = 0,
-	BATTLE_BAR_MENU_MAGIC   = 1,
-	BATTLE_BAR_MENU_OBJECTS = 2,
-	BATTLE_BAR_MENU_SPECIAL = 3
-};
-
-enum battle_bar_state {
-	BATTLE_BAR_STATE_NONE,
-	BATTLE_BAR_STATE_MENU,
-	BATTLE_BAR_STATE_SUB
-};
-
 struct battle_bar {
-	int x;
-	int y;
-	unsigned int w;
-	unsigned int h;
-	enum battle_bar_state state;
-
-	/* Right status frame. */
-	struct frame status_frame;
-
-	/* Main menu selection. */
-	struct frame menu_frame;
-	enum battle_bar_menu menu;
-
-	/* Sub menu selection (spells/objects). */
-	char sub_items[GRIDMENU_ENTRY_MAX][128];
-	struct gridmenu sub_grid;
+	void *data;
+	void (*start)(struct battle_bar *, struct battle *);
+	void (*select)(struct battle_bar *, struct battle *, const struct selection *);
+	void (*handle)(struct battle_bar *, struct battle *, const union event *);
+	void (*update)(struct battle_bar *, struct battle *, unsigned int);
+	void (*draw)(const struct battle_bar *, const struct battle *);
+	void (*finish)(struct battle_bar *, struct battle *);
 };
 
 CORE_BEGIN_DECLS
 
 void
-battle_bar_positionate(struct battle_bar *bar, const struct battle *bt);
-
-int
-battle_bar_handle(struct battle_bar *bar,
-                  const struct battle *bt,
-                  const union event *ev);
+battle_bar_start(struct battle_bar *, struct battle *);
 
 void
-battle_bar_reset(struct battle_bar *bar);
+battle_bar_select(struct battle_bar *, struct battle *, const struct selection *);
 
 void
-battle_bar_open_menu(struct battle_bar *bar);
-
-void
-battle_bar_open_spells(struct battle_bar *bar, const struct battle *bt, struct character *ch);
+battle_bar_handle(struct battle_bar *, struct battle *, const union event *);
 
 void
-battle_bar_open_items(struct battle_bar *bar, const struct battle *bt);
+battle_bar_update(struct battle_bar *, struct battle *, unsigned int);
 
 void
-battle_bar_draw(const struct battle_bar *bar, const struct battle *bt);
+battle_bar_draw(const struct battle_bar *, const struct battle *);
 
 void
-battle_bar_finish(struct battle_bar *bar);
+battle_bar_finish(struct battle_bar *, struct battle *);
 
 CORE_END_DECLS
 
--- a/src/libmlk-rpg/rpg/battle-state-attacking.h	Sun Feb 13 11:34:19 2022 +0100
+++ b/src/libmlk-rpg/rpg/battle-state-attacking.h	Tue Feb 15 14:45:11 2022 +0100
@@ -43,7 +43,7 @@
 void
 battle_state_attacking_init(struct battle_state_attacking *,
                             struct battle_entity *,
-			    struct battle_entity *);
+                            struct battle_entity *);
 
 int
 battle_state_attacking_update(struct battle_state_attacking *, struct battle *);
--- a/src/libmlk-rpg/rpg/battle-state-menu.c	Sun Feb 13 11:34:19 2022 +0100
+++ b/src/libmlk-rpg/rpg/battle-state-menu.c	Tue Feb 15 14:45:11 2022 +0100
@@ -23,12 +23,10 @@
 
 #include "battle-bar.h"
 #include "battle-state-menu.h"
-#include "battle-state-selection.h"
-#include "battle-state-sub.h"
 #include "battle-state.h"
 #include "battle.h"
-#include "character.h"
-#include "spell.h"
+
+#if 0
 
 static void
 open_spells(struct battle *bt)
@@ -55,6 +53,8 @@
 	battle_state_sub(bt);
 }
 
+#endif
+
 static void
 handle(struct battle_state *st, struct battle *bt, const union event *ev)
 {
@@ -77,23 +77,7 @@
 	assert(bt);
 	assert(ev);
 
-	if (battle_bar_handle(&bt->bar, bt, ev)) {
-		switch (bt->bar.menu) {
-		case BATTLE_BAR_MENU_ATTACK:
-			open_attack(bt);
-			break;
-		case BATTLE_BAR_MENU_MAGIC:
-			open_spells(bt);
-			break;
-		case BATTLE_BAR_MENU_OBJECTS:
-			open_items(bt);
-			break;
-		case BATTLE_BAR_MENU_SPECIAL:
-			break;
-		default:
-			break;
-		}
-	}
+	battle_bar_handle(bt->bar, bt, ev);
 }
 
 void
@@ -108,6 +92,6 @@
 	state->handle = handle;
 	state->finish = finish;
 
-	battle_bar_open_menu(&bt->bar);
+	battle_bar_start(bt->bar, bt);
 	battle_switch(bt, state);
 }
--- a/src/libmlk-rpg/rpg/battle-state-selection.c	Sun Feb 13 11:34:19 2022 +0100
+++ b/src/libmlk-rpg/rpg/battle-state-selection.c	Tue Feb 15 14:45:11 2022 +0100
@@ -46,47 +46,6 @@
 };
 
 static void
-use(const struct battle_state_selection *slt, struct battle *bt)
-{
-	struct inventory_slot *slot;
-	struct battle_entity *source, *target;
-
-	if (bt->bar.sub_grid.selected >= INVENTORY_ITEM_MAX)
-		return;
-	if (!(slot = &bt->inventory->items[bt->bar.sub_grid.selected]))
-		return;
-
-	source = bt->order_cur;
-	target = slt->select.index_side == 0
-		? &bt->enemies[slt->select.index_character]
-		: &bt->team[slt->select.index_character];
-
-	battle_state_item(bt, source, target, slot);
-}
-
-static void
-attack(struct battle_state_selection *slt, struct battle *bt)
-{
-	struct character *target;
-
-	if (slt->select.index_side == 0)
-		target = bt->enemies[slt->select.index_character].ch;
-	else
-		target = bt->team[slt->select.index_character].ch;
-
-	battle_attack(bt, bt->order_cur->ch, target);
-}
-
-static void
-cast(struct battle_state_selection *slt, struct battle *bt)
-{
-	struct character *source = bt->order_cur->ch;
-	const struct spell *spell = source->spells[bt->bar.sub_grid.selected];
-
-	battle_cast(bt, source, spell, &slt->select);
-}
-
-static void
 select_adj_in(struct battle_state_selection *slt, const struct battle_entity *entities, size_t entitiesz, int step)
 {
 	assert(slt->select.index_character != (unsigned int)-1);
@@ -126,30 +85,10 @@
 
 	switch (ev->key.key) {
 	case KEY_ESCAPE:
-		switch (bt->bar.menu) {
-		case BATTLE_BAR_MENU_MAGIC:
-		case BATTLE_BAR_MENU_OBJECTS:
-			battle_state_sub(bt);
-			break;
-		default:
-			battle_state_menu(bt);
-			break;
-		}
+		battle_state_menu(bt);
 		break;
 	case KEY_ENTER:
-		switch (bt->bar.menu) {
-		case BATTLE_BAR_MENU_ATTACK:
-			attack(stl, bt);
-			break;
-		case BATTLE_BAR_MENU_MAGIC:
-			cast(stl, bt);
-			break;
-		case BATTLE_BAR_MENU_OBJECTS:
-			use(stl, bt);
-			break;
-		default:
-			break;
-		}
+		battle_bar_select(bt->bar, bt, &stl->select);
 		break;
 	case KEY_LEFT:
 		if (stl->select.allowed_sides & SELECTION_SIDE_ENEMY)
--- a/src/libmlk-rpg/rpg/battle-state-sub.c	Sun Feb 13 11:34:19 2022 +0100
+++ b/src/libmlk-rpg/rpg/battle-state-sub.c	Tue Feb 15 14:45:11 2022 +0100
@@ -38,85 +38,6 @@
 #include "character.h"
 #include "spell.h"
 
-static void
-start_select_spell(struct battle *bt)
-{
-	const struct character *ch = bt->order_cur->ch;
-	const struct spell *sp = ch->spells[bt->bar.sub_grid.selected];
-	struct selection slt = {0};
-
-	/* Don't forget to reset the gridmenu state. */
-	gridmenu_reset(&bt->bar.sub_grid);
-
-	if (bt->bar.sub_grid.selected > CHARACTER_SPELL_MAX)
-		return;
-	if (!(sp = ch->spells[bt->bar.sub_grid.selected]) || sp->mp > (unsigned int)(ch->mp))
-		return;
-
-	spell_select(sp, bt, &slt);
-	battle_state_selection(bt, &slt);
-
-	/* A cursor should be present. */
-	if (!sprite_ok(BATTLE_THEME(bt)->sprites[THEME_SPRITE_CURSOR]))
-		tracef("battle: no cursor sprite in theme");
-}
-
-static void
-start_select_object(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);
-}
-
-static void
-draw_help(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 = bt->bar.sub_grid.x + (bt->bar.sub_grid.w / 2) - (lw / 2);
-	label.y = bt->bar.sub_grid.y - lh - BATTLE_THEME(bt)->padding;
-	label_draw(&label);
-}
-
-static void
-draw_spell_help(const struct battle *bt)
-{
-	const struct character *ch = bt->order_cur->ch;
-	const struct spell *sp;
-
-	if (bt->bar.sub_grid.selected >= CHARACTER_SPELL_MAX)
-		return;
-	if ((sp = ch->spells[bt->bar.sub_grid.selected]))
-		return;
-
-	draw_help(bt, sp->description);
-}
-
-static void
-draw_object_help(const struct battle *bt)
-{
-	const struct inventory_slot *slot;
-
-	if (bt->bar.sub_grid.selected >= INVENTORY_ITEM_MAX)
-		return;
-
-	slot = &bt->inventory->items[bt->bar.sub_grid.selected];
-
-	if (!slot->item)
-		return;
-
-	draw_help(bt, slot->item->description);
-}
 
 static void
 handle(struct battle_state *st, struct battle *bt, const union event *ev)
--- a/src/libmlk-rpg/rpg/battle.c	Sun Feb 13 11:34:19 2022 +0100
+++ b/src/libmlk-rpg/rpg/battle.c	Tue Feb 15 14:45:11 2022 +0100
@@ -36,14 +36,15 @@
 #include <ui/label.h>
 #include <ui/theme.h>
 
-#include "battle.h"
+#include "battle-bar.h"
 #include "battle-indicator.h"
-#include "battle-state.h"
 #include "battle-state-ai.h"
 #include "battle-state-attacking.h"
 #include "battle-state-check.h"
 #include "battle-state-menu.h"
 #include "battle-state-opening.h"
+#include "battle-state.h"
+#include "battle.h"
 #include "character.h"
 #include "inventory.h"
 #include "item.h"
@@ -190,18 +191,6 @@
 }
 
 static void
-positionate_bar(struct battle *bt)
-{
-	/* Bar is located at bottom. */
-	bt->bar.w = window.w;
-	bt->bar.h = window.h * 0.12;
-	bt->bar.x = 0;
-	bt->bar.y = window.h - bt->bar.h;
-	
-	battle_bar_positionate(&bt->bar, bt);
-}
-
-static void
 draw_entities(const struct battle *bt, struct battle_entity *entities, size_t entitiesz)
 {
 	for (size_t i = 0; i < entitiesz; ++i) {
@@ -232,7 +221,6 @@
 			battle_entity_init(et);
 
 	positionate_team(bt);
-	positionate_bar(bt);
 	positionate_names(bt);
 
 	/* Start the state "opening" animation. */
@@ -333,8 +321,6 @@
 {
 	assert(bt);
 
-	battle_bar_reset(&bt->bar);
-
 	if (!bt->order_cur) {
 		battle_order(bt);
 		bt->order_cur = bt->order[bt->order_curindex = 0];
@@ -355,7 +341,7 @@
 
 	/* Change state depending on the kind of entity. */
 	if (is_team(bt, bt->order_cur->ch)) {
-		battle_bar_open_menu(&bt->bar);
+		battle_bar_start(bt->bar, bt);
 		battle_state_menu(bt);
 	} else
 		battle_state_ai(bt);
@@ -447,7 +433,7 @@
 	draw_entities(bt, bt->team, UTIL_SIZE(bt->team));
 	draw_entities(bt, bt->enemies, UTIL_SIZE(bt->enemies));
 
-	battle_bar_draw(&bt->bar, bt);
+	battle_bar_draw(bt->bar, bt);
 
 	action_stack_draw(&bt->actions[0]);
 	action_stack_draw(&bt->actions[1]);
@@ -470,5 +456,7 @@
 	action_stack_finish(&bt->actions[0]);
 	action_stack_finish(&bt->actions[1]);
 
+	battle_bar_finish(bt->bar, bt);
+
 	memset(bt, 0, sizeof (*bt));
 }
--- a/src/libmlk-rpg/rpg/battle.h	Sun Feb 13 11:34:19 2022 +0100
+++ b/src/libmlk-rpg/rpg/battle.h	Tue Feb 15 14:45:11 2022 +0100
@@ -28,7 +28,6 @@
 #include <ui/frame.h>
 #include <ui/gridmenu.h>
 
-#include "battle-bar.h"
 #include "battle-entity.h"
 #include "battle-state.h"
 #include "selection.h"
@@ -36,6 +35,7 @@
 
 union event;
 
+struct battle_bar;
 struct character;
 struct inventory;
 struct item;
@@ -76,7 +76,7 @@
 	struct drawable_stack effects;
 	struct action_stack actions[2];
 	struct inventory *inventory;
-	struct battle_bar bar;
+	struct battle_bar *bar;
 };
 
 CORE_BEGIN_DECLS