initial menu/window system

This commit is contained in:
Zack Buhman 2023-08-06 22:56:56 +00:00
parent 3fffdab78d
commit bb4f236b5b
27 changed files with 921 additions and 397 deletions

View File

@ -1,6 +1,6 @@
CFLAGS = -Isaturn CFLAGS = -Isaturn
OPT ?= -O3 OPT ?= -Og
LIB = ./saturn LIB = ./saturn
GEN_PYTHON_SOURCE = $(wildcard tools/source/*.py) $(wildcard tools/generate/*.py) GEN_PYTHON_SOURCE = $(wildcard tools/source/*.py) $(wildcard tools/generate/*.py)
@ -35,11 +35,12 @@ SRC += input.cpp
SRC += vram.cpp SRC += vram.cpp
SRC += font.cpp SRC += font.cpp
SRC += graphic.cpp SRC += graphic.cpp
SRC += menu.cpp
SRC += number.cpp SRC += number.cpp
SRC += pokemon_instance.cpp SRC += pokemon_instance.cpp
SRC += ailment.cpp SRC += ailment.cpp
SRC += menu/stats.cpp SRC += party.cpp
SRC += window/window.cpp
SRC += window/menu.cpp
DEP = $(patsubst %.cpp,%.cpp.d,$(SRC)) DEP = $(patsubst %.cpp,%.cpp.d,$(SRC))

6
battle.hpp Normal file
View File

@ -0,0 +1,6 @@
enum battle_action {
fight,
swap_pokemon,
item,
run,
};

5
cell_offset.hpp Normal file
View File

@ -0,0 +1,5 @@
struct cell_offset
{
static constexpr int32_t x = (320 - 160) / (2 * 8);
static constexpr int32_t y = (240 - 144) / (2 * 8);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -4,7 +4,7 @@
#include "coordinates.hpp" #include "coordinates.hpp"
#include "start_size.hpp" #include "start_size.hpp"
#include "render_map.hpp" // for cell_offset #include "cell_offset.hpp" // for cell_offset
#include "vdp2.h" #include "vdp2.h"
#include "pokemon_instance.hpp" #include "pokemon_instance.hpp"

View File

@ -57,4 +57,5 @@ struct event {
static inline bool cursor_up() { return input_flopped(input.up ) >= 1; } static inline bool cursor_up() { return input_flopped(input.up ) >= 1; }
static inline bool cursor_down() { return input_flopped(input.down ) >= 1; } static inline bool cursor_down() { return input_flopped(input.down ) >= 1; }
static inline bool button_a() { return input_flopped(input.a ) == 1; } static inline bool button_a() { return input_flopped(input.a ) == 1; }
static inline bool button_b() { return input_flopped(input.b ) == 1; }
}; };

324
main.cpp
View File

@ -14,24 +14,17 @@
#include "vram.hpp" #include "vram.hpp"
#include "font.hpp" #include "font.hpp"
#include "gen/maps.hpp" #include "gen/tilesets.hpp"
#include "gen/sprites.hpp" #include "gen/sprites.hpp"
#include "map_objects.hpp"
#include "coordinates.hpp"
#include "render_map.hpp"
#include "actor.hpp"
#include "ledge_tiles.hpp"
#include "graphic.hpp"
#include "menu.hpp"
#include "pokemon.hpp" #include "pokemon.hpp"
#include "pokemon_instance.hpp" #include "gen/maps.hpp"
#include "actor.hpp"
#include "player.hpp"
#include "menu/stats.hpp" #include "cell_offset.hpp"
static int32_t pokemon_raw_index = 0; #include "window/window.hpp"
#include "window/window_stack.hpp"
struct draw_t { struct draw_t {
struct { struct {
@ -46,10 +39,8 @@ struct draw_t {
}; };
struct state_t { struct state_t {
enum map_t::map map;
enum map_t::map last_map;
draw_t draw; draw_t draw;
actor_t player; player_state_t player[1];
}; };
static state_t state = { map_t::pallet_town, map_t::last_map, 0 }; static state_t state = { map_t::pallet_town, map_t::last_map, 0 };
@ -203,6 +194,7 @@ void render_sprites(const offset_t& offset)
{ {
uint32_t ix = 2; uint32_t ix = 2;
/*
const uint32_t animation_frame = ((state.player.frame & 0b1000) != 0) * 3; const uint32_t animation_frame = ((state.player.frame & 0b1000) != 0) * 3;
render_sprite(ix, render_sprite(ix,
spritesheet_t::red, spritesheet_t::red,
@ -212,11 +204,6 @@ void render_sprites(const offset_t& offset)
state.player.y_offset()); state.player.y_offset());
ix++; ix++;
render_pokemon(ix, static_cast<enum pokemon_t::pokemon>(pokemon_raw_index),
{0, 0},
true); // invert horizontally
ix++;
const object_t& obj = map_objects[state.map]; const object_t& obj = map_objects[state.map];
for (uint32_t i = 0; i < obj.object_length; i++) { for (uint32_t i = 0; i < obj.object_length; i++) {
const object_event_t& event = obj.object_events[i]; const object_event_t& event = obj.object_events[i];
@ -229,6 +216,7 @@ void render_sprites(const offset_t& offset)
-4); -4);
ix++; ix++;
} }
*/
constexpr uint16_t top_x = 80 - 1; constexpr uint16_t top_x = 80 - 1;
constexpr uint16_t top_y = 48 - 1; constexpr uint16_t top_y = 48 - 1;
@ -262,249 +250,39 @@ void render_sprites(const offset_t& offset)
void render_map() void render_map()
{ {
const map_t& map = maps[state.map]; //const map_t& map = maps[state.map];
const uint32_t base_pattern = state.draw.base_pattern.tilesets[map.tileset]; //const uint32_t base_pattern = state.draw.base_pattern.tilesets[map.tileset];
vdp2.reg.PNCN0 = PNCN0__N0PNB__1WORD | PNCN0__N0CNSM | PNCN0__N0SCN((base_pattern >> 10) & 0x1f); //vdp2.reg.PNCN0 = PNCN0__N0PNB__1WORD | PNCN0__N0CNSM | PNCN0__N0SCN((base_pattern >> 10) & 0x1f);
for (int32_t y = (0 - 1); y < (9 + 2); y++) { for (int32_t y = (0 - 1); y < (9 + 2); y++) {
for (int32_t x = (0 - 2); x < (10 + 2); x++) { for (int32_t x = (0 - 2); x < (10 + 2); x++) {
/*
render_screen(base_pattern, render_screen(base_pattern,
map, map,
state.player.world, state.player.world,
{x, y} {x, y}
); );
} */
}
vdp2.reg.BGON = BGON__N0ON | BGON__N0TPON | BGON__N1ON;
}
constexpr inline world_t direction_offset(const world_t& world, const enum actor_t::direction dir)
{
switch (dir) {
default: [[fallthrough]];
case actor_t::up: return {world.x , world.y - 1};
case actor_t::down: return {world.x , world.y + 1};
case actor_t::left: return {world.x - 1, world.y };
case actor_t::right: return {world.x + 1, world.y };
}
}
constexpr inline uint8_t get_tile_ix(const map_t& map, const world_t& coord)
{
// keep this synchronized with render_screen()
const uint8_t block_ix = get_block(map, coord.to_block());
const tileset_t& tileset = tilesets[map.tileset];
const uint8_t * block_start = &(tileset.blockset.start)[block_ix * 4 * 4];
const int32_t quadrant_x = 2 * (coord.x & 1);
const int32_t quadrant_y = 2 * (coord.y & 1);
const int32_t block_row = 4 * (quadrant_y + 1);
const uint8_t tile_ix = block_start[block_row + quadrant_x];
return tile_ix;
}
constexpr inline enum actor_t::collision collision(const map_t& map,
const world_t& world,
enum actor_t::direction direction)
{
const world_t coord = direction_offset(world, direction);
uint8_t collision_tile_ix = get_tile_ix(map, coord);
const tileset_t& tileset = tilesets[map.tileset];
for (uint32_t i = 0; i < tileset.collision.size; i++) {
if (tileset.collision.start[i] == collision_tile_ix)
return actor_t::passable;
}
// check ledge_tile pairs
uint8_t actor_tile_ix = get_tile_ix(map, world);
for (uint32_t i = 0; i < ledge_tiles_length; i++) {
const ledge_tile_t& lt = ledge_tiles[i];
if (direction == lt.direction
&& actor_tile_ix == lt.actor
&& collision_tile_ix == lt.collision) {
return actor_t::jump;
}
}
return actor_t::impassable;
}
constexpr inline void collision_move(const map_t& map, actor_t::direction dir)
{
const enum actor_t::collision c = collision(map, state.player.world, dir);
state.player.move(c, dir);
}
void change_maps()
{
const map_t& map = maps[state.map];
#define _has(_dir_) (map.connections[map_t::connection_t::_dir_].map != map_t::unconnected)
#define _get(_dir_) (maps[map.connections[map_t::connection_t::_dir_].map])
#define _offset(_dir_) (map.connections[map_t::connection_t::_dir_].offset)
const block_t block = state.player.world.to_block();
if (block.y < 0) {
// north
if (_has(north)) {
const map_t& north_map = _get(north);
state.player.world.y = ((north_map.height + block.y) << 1) | (state.player.world.y & 1);
state.player.world.x = ((block.x - _offset(north)) << 1) | (state.player.world.x & 1);
state.map = map.connections[map_t::connection_t::north].map;
}
} else if (block.y >= static_cast<int32_t>(map.height)) {
// south
if (_has(south)) {
state.player.world.y = ((block.y - map.height) << 1) | (state.player.world.y & 1);
state.player.world.x = ((block.x - _offset(south)) << 1) | (state.player.world.x & 1);
state.map = map.connections[map_t::connection_t::south].map;
}
} else if (block.x < 0) {
// west
if (_has(west)) {
const map_t& west_map = _get(west);
state.player.world.x = ((west_map.width + block.x) << 1) | (state.player.world.x & 1);
state.player.world.y = ((block.y - _offset(west)) << 1) | (state.player.world.y & 1);
state.map = map.connections[map_t::connection_t::west].map;
}
} else if (block.x >= static_cast<int32_t>(map.width)) {
// east
if (_has(east)) {
state.player.world.x = ((block.x - map.width) << 1) | (state.player.world.x & 1);
state.player.world.y = ((block.y - _offset(east)) << 1) | (state.player.world.y & 1);
state.map = map.connections[map_t::connection_t::east].map;
}
}
#undef _offset
#undef _get
#undef _has
}
constexpr inline bool is_outside(const enum map_t::map& map_id)
{
const map_t& map = maps[map_id];
switch (map.tileset) {
case tileset_t::overworld: [[fallthrough]];
case tileset_t::plateau:
return true;
default:
return false;
}
}
struct collision_direction_t {
enum actor_t::collision collision;
enum actor_t::direction direction;
};
constexpr inline collision_direction_t find_direction(const map_t& map, const world_t& world, const enum actor_t::direction facing)
{
// first, try `facing`, as this is typically correct in non- edge-cases
const enum actor_t::collision c_facing = collision(map, world, facing);
if (c_facing != actor_t::impassable)
return {c_facing, facing};
// otherwise try all other directions
// (this checks facing a second time for no reason)
constexpr enum actor_t::direction dirs[] = {actor_t::up, actor_t::down, actor_t::left, actor_t::right};
for (const enum actor_t::direction& dir : dirs) {
const enum actor_t::collision c = collision(map, world, dir);
if (c != actor_t::impassable)
return {c, dir};
}
return {actor_t::impassable, facing};
}
void update_warp()
{
if (state.player.moving) return;
world_t& coord = state.player.world;
for (uint32_t j = 0; j < map_objects[state.map].warp_length; j++) {
const warp_event_t& warp = map_objects[state.map].warp_events[j];
if (coord.x == warp.position.x && coord.y == warp.position.y) {
if (warp.destination.map == map_t::last_map) {
if (state.last_map == map_t::last_map) {
// use last_map as a sentinel value for "the player hasn't
// warped yet". This should never happen in normal gameplay.
continue;
}
state.map = state.last_map;
} else {
// Only change last_map if the current map is an "outside"
// map. Warps that use last_map are designed to be used this
// way.
if (is_outside(state.map)) state.last_map = state.map;
state.map = warp.destination.map;
}
// warp_index starts at 1
const warp_event_t& dest = map_objects[state.map].warp_events[warp.destination.warp_index - 1];
coord.x = dest.position.x;
coord.y = dest.position.y;
// force the player to move off of the warp
const collision_direction_t c_d = find_direction(maps[state.map], state.player.world, state.player.facing);
state.player.move(c_d.collision, c_d.direction);
// must return: because map state.map changed, the rest of this
// loop is invalid
return;
} }
} }
} }
void check_sign()
{
const world_t coord = direction_offset(state.player.world, state.player.facing);
const map_t& map = maps[state.map];
const object_t& obj = map_objects[state.map];
for (uint32_t i = 0; i < obj.bg_length; i++) {
const bg_event_t& event = obj.bg_events[i];
const bool position_match = event.position.x == coord.x && event.position.y == coord.y;
if (position_match && event.sign_id != 0xff) {
const start_size_t& text = map.text_pointers[event.sign_id];
}
}
}
static uint32_t stats_page = 1;
static uint8_t level = 70;
void update() void update()
{ {
state.player.tick(); //state.player.tick();
change_maps();
update_warp();
/* enum window_t::input_event window_input_event;
if (event::cursor_left() ) collision_move(maps[state.map], actor_t::left);
else if (event::cursor_right()) collision_move(maps[state.map], actor_t::right);
else if (event::cursor_up() ) collision_move(maps[state.map], actor_t::up);
else if (event::cursor_down() ) collision_move(maps[state.map], actor_t::down);
else if (event::button_a() ) check_sign();
*/
if (event::cursor_left() ) pokemon_raw_index = (pokemon_raw_index - 1); if (event::cursor_left() ) window_input_event = window_t::input_left;
else if (event::cursor_right()) pokemon_raw_index = (pokemon_raw_index + 1); else if (event::cursor_right()) window_input_event = window_t::input_right;
else if (event::cursor_up() ) level++; else if (event::cursor_up() ) window_input_event = window_t::input_up;
else if (event::cursor_down() ) level--; else if (event::cursor_down() ) window_input_event = window_t::input_down;
else if (event::button_a() ) stats_page = !stats_page; else if (event::button_a() ) window_input_event = window_t::input_a;
else if (event::button_b() ) window_input_event = window_t::input_b;
if (pokemon_raw_index < 0) pokemon_raw_index = pokemon_t::count - 1; else return;
else if (pokemon_raw_index >= pokemon_t::count) pokemon_raw_index = 0;
state.player[0].window_stack.update(window_input_event,
state.player[0].trainer);
} }
static uint32_t frame = 0; static uint32_t frame = 0;
@ -513,7 +291,7 @@ void render()
{ {
frame++; frame++;
const offset_t offset = state.player.offset(); const offset_t offset = state.player[0].actor.offset();
render_sprites(offset); render_sprites(offset);
@ -524,33 +302,11 @@ void render()
vdp2.reg.SCYIN0 = offset.y; vdp2.reg.SCYIN0 = offset.y;
vdp2.reg.SCYDN0 = 0; vdp2.reg.SCYDN0 = 0;
render_map(); //render_map();
state.player[0].window_stack.draw(state.draw.base_pattern.font,
state.player[0].trainer);
draw_menu(state.draw.base_pattern.font, fight_menu); vdp2.reg.BGON = BGON__N0ON | BGON__N0TPON | BGON__N1ON;
cursor_t cursor = { 1, 1 };
draw_menu_cursor(state.draw.base_pattern.font, fight_menu, cursor);
static pokemon_instance_t pokemon_instance;
pokemon_instance.species = static_cast<enum pokemon_t::pokemon>(pokemon_raw_index);
pokemon_instance.stat_experience = {0, 0, 0, 0, 0};
pokemon_instance.determinant_values.dvs = 0b1110'0101'1000'0110;
pokemon_instance.level = level;
pokemon_instance.determine_stats();
pokemon_instance.learn_all_moves();
if (frame % 4 == 0)
pokemon_instance.current_hit_points++;
if (pokemon_instance.current_hit_points > pokemon_instance.stat_values.hit_points)
pokemon_instance.current_hit_points = 0;
switch (stats_page) {
default:
case 0:
draw_stats1(state.draw.base_pattern.font, pokemon_instance);
break;
case 1:
draw_stats2(state.draw.base_pattern.font, pokemon_instance);
break;
}
} }
extern "C" extern "C"
@ -580,9 +336,9 @@ void v_blank_in_int()
smpc.reg.IREG[0].val = INTBACK__IREG0__STATUS_DISABLE; smpc.reg.IREG[0].val = INTBACK__IREG0__STATUS_DISABLE;
smpc.reg.IREG[1].val = ( INTBACK__IREG1__PERIPHERAL_DATA_ENABLE smpc.reg.IREG[1].val = ( INTBACK__IREG1__PERIPHERAL_DATA_ENABLE
| INTBACK__IREG1__PORT2_15BYTE | INTBACK__IREG1__PORT2_15BYTE
| INTBACK__IREG1__PORT1_15BYTE | INTBACK__IREG1__PORT1_15BYTE
); );
smpc.reg.IREG[2].val = INTBACK__IREG2__MAGIC; smpc.reg.IREG[2].val = INTBACK__IREG2__MAGIC;
smpc.reg.COMREG = COMREG__INTBACK; smpc.reg.COMREG = COMREG__INTBACK;
@ -700,12 +456,13 @@ void init_vdp2()
void main() void main()
{ {
state.map = map_t::pallet_town; //state.map = map_t::pallet_town;
//state.map = map_t::pewter_gym; //state.map = map_t::pewter_gym;
//state.map = map_t::viridian_forest; //state.map = map_t::viridian_forest;
//state.map = map_t::route_2; //state.map = map_t::route_2;
state.player.world.x = 6; //state.player.world.x = 6;
state.player.world.y = 6; //state.player.world.y = 6;
default_party(state.player[0].trainer.party);
load_vram(); load_vram();
@ -741,4 +498,9 @@ void main()
scu.reg.IST = 0; scu.reg.IST = 0;
scu.reg.IMS = ~(IMS__SMPC | IMS__V_BLANK_IN); scu.reg.IMS = ~(IMS__SMPC | IMS__V_BLANK_IN);
window_stack_t& window_stack = state.player[0].window_stack;
window_stack.ix = 0;
window_stack.stack[0].type = window_descriptor_t::fight;
window_stack.stack[0].window = { 0 };
} }

View File

@ -1,73 +0,0 @@
#include "graphic.hpp"
#include "menu.hpp"
#include "control.hpp"
#define POKE "\x94"
#define TRAINER "\x92"
#define PKMN "\x96"
#define S(v) reinterpret_cast<const uint8_t *>(v)
const menu_t start_menu = {
.top_left = {10, 0},
.bottom_right = {19, 15},
.width = 1,
.height = 7,
.h_advance = 0,
.v_advance = 2,
.items = (menu_item_t[]){
{ S(POKE "DEX") },
{ S(POKE "MON") },
{ S("ITEM" ) },
{ S(TRAINER ) },
{ S("SAVE" ) },
{ S("OPTION" ) },
{ S("EXIT" ) },
},
};
const menu_t fight_menu = {
.top_left = {8, 12},
.bottom_right = {19, 17},
.width = 2,
.height = 2,
.h_advance = 6,
.v_advance = 2,
.items = (menu_item_t[]) {
{ S("FIGHT") }, { S(PKMN ) },
{ S("ITEM" ) }, { S("RUN") },
},
};
#undef S
void draw_menu(const uint32_t base_pattern,
const menu_t& menu)
{
draw_box_border(base_pattern,
menu.top_left, menu.bottom_right);
draw_box_background(base_pattern,
menu.top_left, menu.bottom_right);
for (uint32_t y = 0; y < menu.height; y++) {
for (uint32_t x = 0; x < menu.width; x++) {
const menu_item_t& item = menu.items[menu.width * y + x];
const uint32_t cell_x = menu.top_left.x + 2 + (menu.h_advance * x);
const uint32_t cell_y = menu.top_left.y + menu.v_advance + (menu.v_advance * y);
draw_text(base_pattern, item.label, cell_x, cell_y);
}
}
}
void draw_menu_cursor(const uint32_t base_pattern,
const menu_t& menu,
const cursor_t& cursor)
{
const uint32_t cell_x = menu.top_left.x + 1 + (menu.h_advance * cursor.x);
const uint32_t cell_y = menu.top_left.y + menu.v_advance + (menu.v_advance * cursor.y);
put_char(base_pattern, cell_x, cell_y, interactive::l_arrow_solid);
}

View File

@ -1,31 +0,0 @@
#include "coordinates.hpp"
struct menu_item_t {
const uint8_t * label;
};
struct menu_t {
screen_cell_t top_left; // in cells
screen_cell_t bottom_right; // in cells
uint8_t width; // in menu items
uint8_t height; // in menu items
uint8_t h_advance; // in cells
uint8_t v_advance; // in cells
const menu_item_t * items;
};
struct cursor_t {
uint16_t x; // in menu items
uint16_t y; // in menu items
};
extern const menu_t start_menu;
extern const menu_t fight_menu;
void draw_menu(const uint32_t base_pattern,
const menu_t& menu);
void draw_menu_cursor(const uint32_t base_pattern,
const menu_t& menu,
const cursor_t& cursor);

205
overworld/overworld.cpp Normal file
View File

@ -0,0 +1,205 @@
void overworld_update()
{
change_maps();
update_warp();
}
constexpr inline world_t direction_offset(const world_t& world, const enum actor_t::direction dir)
{
switch (dir) {
default: [[fallthrough]];
case actor_t::up: return {world.x , world.y - 1};
case actor_t::down: return {world.x , world.y + 1};
case actor_t::left: return {world.x - 1, world.y };
case actor_t::right: return {world.x + 1, world.y };
}
}
constexpr inline uint8_t get_tile_ix(const map_t& map, const world_t& coord)
{
// keep this synchronized with render_screen()
const uint8_t block_ix = get_block(map, coord.to_block());
const tileset_t& tileset = tilesets[map.tileset];
const uint8_t * block_start = &(tileset.blockset.start)[block_ix * 4 * 4];
const int32_t quadrant_x = 2 * (coord.x & 1);
const int32_t quadrant_y = 2 * (coord.y & 1);
const int32_t block_row = 4 * (quadrant_y + 1);
const uint8_t tile_ix = block_start[block_row + quadrant_x];
return tile_ix;
}
constexpr inline enum actor_t::collision collision(const map_t& map,
const world_t& world,
enum actor_t::direction direction)
{
const world_t coord = direction_offset(world, direction);
uint8_t collision_tile_ix = get_tile_ix(map, coord);
const tileset_t& tileset = tilesets[map.tileset];
for (uint32_t i = 0; i < tileset.collision.size; i++) {
if (tileset.collision.start[i] == collision_tile_ix)
return actor_t::passable;
}
// check ledge_tile pairs
uint8_t actor_tile_ix = get_tile_ix(map, world);
for (uint32_t i = 0; i < ledge_tiles_length; i++) {
const ledge_tile_t& lt = ledge_tiles[i];
if (direction == lt.direction
&& actor_tile_ix == lt.actor
&& collision_tile_ix == lt.collision) {
return actor_t::jump;
}
}
return actor_t::impassable;
}
constexpr inline void collision_move(const map_t& map, actor_t::direction dir)
{
const enum actor_t::collision c = collision(map, state.player.world, dir);
state.player.move(c, dir);
}
void change_maps()
{
const map_t& map = maps[state.map];
#define _has(_dir_) (map.connections[map_t::connection_t::_dir_].map != map_t::unconnected)
#define _get(_dir_) (maps[map.connections[map_t::connection_t::_dir_].map])
#define _offset(_dir_) (map.connections[map_t::connection_t::_dir_].offset)
const block_t block = state.player.world.to_block();
if (block.y < 0) {
// north
if (_has(north)) {
const map_t& north_map = _get(north);
state.player.world.y = ((north_map.height + block.y) << 1) | (state.player.world.y & 1);
state.player.world.x = ((block.x - _offset(north)) << 1) | (state.player.world.x & 1);
state.map = map.connections[map_t::connection_t::north].map;
}
} else if (block.y >= static_cast<int32_t>(map.height)) {
// south
if (_has(south)) {
state.player.world.y = ((block.y - map.height) << 1) | (state.player.world.y & 1);
state.player.world.x = ((block.x - _offset(south)) << 1) | (state.player.world.x & 1);
state.map = map.connections[map_t::connection_t::south].map;
}
} else if (block.x < 0) {
// west
if (_has(west)) {
const map_t& west_map = _get(west);
state.player.world.x = ((west_map.width + block.x) << 1) | (state.player.world.x & 1);
state.player.world.y = ((block.y - _offset(west)) << 1) | (state.player.world.y & 1);
state.map = map.connections[map_t::connection_t::west].map;
}
} else if (block.x >= static_cast<int32_t>(map.width)) {
// east
if (_has(east)) {
state.player.world.x = ((block.x - map.width) << 1) | (state.player.world.x & 1);
state.player.world.y = ((block.y - _offset(east)) << 1) | (state.player.world.y & 1);
state.map = map.connections[map_t::connection_t::east].map;
}
}
#undef _offset
#undef _get
#undef _has
}
struct collision_direction_t {
enum actor_t::collision collision;
enum actor_t::direction direction;
};
void check_sign()
{
const world_t coord = direction_offset(state.player.world, state.player.facing);
const map_t& map = maps[state.map];
const object_t& obj = map_objects[state.map];
for (uint32_t i = 0; i < obj.bg_length; i++) {
const bg_event_t& event = obj.bg_events[i];
const bool position_match = event.position.x == coord.x && event.position.y == coord.y;
if (position_match && event.sign_id != 0xff) {
const start_size_t& text = map.text_pointers[event.sign_id];
}
}
}
void update_warp()
{
if (state.player.moving) return;
world_t& coord = state.player.world;
for (uint32_t j = 0; j < map_objects[state.map].warp_length; j++) {
const warp_event_t& warp = map_objects[state.map].warp_events[j];
if (coord.x == warp.position.x && coord.y == warp.position.y) {
if (warp.destination.map == map_t::last_map) {
if (state.last_map == map_t::last_map) {
// use last_map as a sentinel value for "the player hasn't
// warped yet". This should never happen in normal gameplay.
continue;
}
state.map = state.last_map;
} else {
// Only change last_map if the current map is an "outside"
// map. Warps that use last_map are designed to be used this
// way.
if (is_outside(state.map)) state.last_map = state.map;
state.map = warp.destination.map;
}
// warp_index starts at 1
const warp_event_t& dest = map_objects[state.map].warp_events[warp.destination.warp_index - 1];
coord.x = dest.position.x;
coord.y = dest.position.y;
// force the player to move off of the warp
const collision_direction_t c_d = find_direction(maps[state.map], state.player.world, state.player.facing);
state.player.move(c_d.collision, c_d.direction);
// must return: because map state.map changed, the rest of this
// loop is invalid
return;
}
}
}
constexpr inline collision_direction_t find_direction(const map_t& map, const world_t& world, const enum actor_t::direction facing)
{
// first, try `facing`, as this is typically correct in non- edge-cases
const enum actor_t::collision c_facing = collision(map, world, facing);
if (c_facing != actor_t::impassable)
return {c_facing, facing};
// otherwise try all other directions
// (this checks facing a second time for no reason)
constexpr enum actor_t::direction dirs[] = {actor_t::up, actor_t::down, actor_t::left, actor_t::right};
for (const enum actor_t::direction& dir : dirs) {
const enum actor_t::collision c = collision(map, world, dir);
if (c != actor_t::impassable)
return {c, dir};
}
return {actor_t::impassable, facing};
}
constexpr inline bool is_outside(const enum map_t::map& map_id)
{
const map_t& map = maps[map_id];
switch (map.tileset) {
case tileset_t::overworld: [[fallthrough]];
case tileset_t::plateau:
return true;
default:
return false;
}
}

View File

@ -59,12 +59,6 @@ constexpr inline uint8_t get_block(const map_t& map, block_t block)
#undef _has #undef _has
} }
struct cell_offset
{
static constexpr int32_t x = (320 - 160) / (2 * 8);
static constexpr int32_t y = (240 - 144) / (2 * 8);
};
static inline void render_screen(const uint32_t base_pattern, static inline void render_screen(const uint32_t base_pattern,
const map_t& map, const map_t& map,
const world_t& origin, const world_t& origin,

9
party.cpp Normal file
View File

@ -0,0 +1,9 @@
#include "party.hpp"
#include "pokemon.hpp"
void default_party(party_t& party)
{
party.size = 1;
party.pokemon_instances[0].init(pokemon_t::pikachu, 60);
}

20
party.hpp Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include "pokemon_instance.hpp"
struct party_t {
static constexpr uint8_t max_size = 6;
pokemon_instance_t pokemon_instances[max_size];
uint8_t active_ix;
uint8_t size;
const pokemon_instance_t& active_pokemon() const
{
return pokemon_instances[active_ix];
}
};
void default_party(party_t& party);

14
player.hpp Normal file
View File

@ -0,0 +1,14 @@
#pragma once
#include "gen/maps.hpp"
#include "actor.hpp"
#include "trainer.hpp"
#include "window/window_stack.hpp"
struct player_state_t {
enum map_t::map map;
enum map_t::map last_map;
window_stack_t window_stack;
actor_t actor;
trainer_state_t trainer;
};

View File

@ -97,3 +97,19 @@ void pokemon_instance_t::learn_all_moves()
break; break;
} }
} }
void
pokemon_instance_t::init(const enum pokemon_t::pokemon species,
const uint8_t level)
{
this->species = species;
this->level = level;
stat_experience = {0, 0, 0, 0, 0};
determinant_values.dvs = 0b1110'0101'1000'0110;
determine_stats();
learn_all_moves();
current_hit_points = stat_values.hit_points;
total_experience = 0; // fixme exp
ailment = ailment_t::ok;
}

View File

@ -70,6 +70,8 @@ struct move_instance_t {
}; };
struct pokemon_instance_t { struct pokemon_instance_t {
static constexpr uint8_t maximum_moves = 4;
uint8_t nickname[12]; uint8_t nickname[12];
enum pokemon_t::pokemon species; enum pokemon_t::pokemon species;
uint8_t level; uint8_t level;
@ -77,7 +79,7 @@ struct pokemon_instance_t {
determinant_values_t determinant_values; determinant_values_t determinant_values;
stat_values_t stat_values; stat_values_t stat_values;
stat_experience_t stat_experience; stat_experience_t stat_experience;
move_instance_t move_instances[4]; move_instance_t move_instances[maximum_moves];
uint16_t current_hit_points; uint16_t current_hit_points;
enum ailment_t::ailment ailment; enum ailment_t::ailment ailment;
@ -86,8 +88,19 @@ struct pokemon_instance_t {
// - id number // - id number
// - original trainer // - original trainer
inline constexpr uint8_t moves_count() const
{
for (int32_t i = maximum_moves; i > 0; i--) {
if (move_instances[i - 1].type != move_t::no_move)
return i;
}
return 0;
}
void determine_stats(); void determine_stats();
void learn_move(enum move_t::move move, int32_t index); void learn_move(enum move_t::move move, int32_t index);
void learn_all_moves(); void learn_all_moves();
void init(const enum pokemon_t::pokemon species,
const uint8_t level);
}; };

31
stats.cpp Normal file
View File

@ -0,0 +1,31 @@
draw_menu(state.draw.base_pattern.font, fight_menu);
cursor_t cursor = { 1, 1 };
draw_menu_cursor(state.draw.base_pattern.font, fight_menu, cursor);
static pokemon_instance_t pokemon_instance;
if (frame % 4 == 0)
pokemon_instance.current_hit_points++;
if (pokemon_instance.current_hit_points > pokemon_instance.stat_values.hit_points)
pokemon_instance.current_hit_points = 0;
switch (stats_page) {
default:
case 0:
draw_stats1(state.draw.base_pattern.font, pokemon_instance);
break;
case 1:
draw_stats2(state.draw.base_pattern.font, pokemon_instance);
break;
}
if (event::cursor_left() ) pokemon_raw_index = (pokemon_raw_index - 1);
else if (event::cursor_right()) pokemon_raw_index = (pokemon_raw_index + 1);
else if (event::cursor_up() ) level++;
else if (event::cursor_down() ) level--;
else if (event::button_a() ) stats_page = !stats_page;
if (pokemon_raw_index < 0) pokemon_raw_index = pokemon_t::count - 1;
else if (pokemon_raw_index >= pokemon_t::count) pokemon_raw_index = 0;
}

View File

@ -84,7 +84,7 @@ def move(constant_index, constant_name):
".animation = 0,", ".animation = 0,",
".effect = 0,", ".effect = 0,",
f".power = {_move.power},", f".power = {_move.power},",
".type = 0,", f".type = type_t::{_move.type.removesuffix('_TYPE').lower()},",
f".accuracy = {_move.accuracy},", f".accuracy = {_move.accuracy},",
f".pp = {_move.pp},", f".pp = {_move.pp},",
"}," "},"
@ -104,6 +104,7 @@ def includes_source():
yield '#include <cstdint>' yield '#include <cstdint>'
yield '' yield ''
yield '#include "moves.hpp"' yield '#include "moves.hpp"'
yield '#include "types.hpp"'
yield '' yield ''
def generate_source(): def generate_source():

8
trainer.hpp Normal file
View File

@ -0,0 +1,8 @@
#pragma once
#include "party.hpp"
struct trainer_state_t {
party_t party;
// items
};

306
window/menu.cpp Normal file
View File

@ -0,0 +1,306 @@
#include "menu.hpp"
#include "window.hpp"
#include "../graphic.hpp"
#include "../control.hpp"
#include "../trainer.hpp"
#include "../font.hpp"
#include "../number.hpp"
#include "../gen/pokemon/moves.hpp"
#include "../gen/pokemon/types.hpp"
#define POKE "\x94"
#define TRAINER "\x92"
#define PKMN "\x96"
#define S(v) reinterpret_cast<const uint8_t *>(v)
static void draw_cursor(const uint32_t base_pattern,
const menu_t& menu,
const cursor_t& cursor)
{
const uint32_t cell_x = menu.top_left.x + 1 + (menu.h_advance * cursor.x);
const uint32_t cell_y = menu.top_left.y + menu.v_advance + (menu.v_advance * cursor.y);
put_char(base_pattern, cell_x, cell_y, interactive::l_arrow_solid);
}
static void draw_menu(const uint32_t base_pattern,
const menu_t& menu,
const trainer_state_t& trainer_state)
{
draw_box_border(base_pattern,
menu.top_left, menu.bottom_right);
draw_box_background(base_pattern,
menu.top_left, menu.bottom_right);
for (uint32_t y = 0; y < menu.height; y++) {
for (uint32_t x = 0; x < menu.width; x++) {
const uint32_t cell_x = menu.top_left.x + 2 + (menu.h_advance * x);
const uint32_t cell_y = menu.top_left.y + menu.v_advance + (menu.v_advance * y);
const uint32_t menu_entry_index = menu.width * y + x;
const menu_entry_t& entry = menu.entry;
if (menu.entry_type == menu_t::labels) {
draw_text(base_pattern,
entry.labels[menu_entry_index],
cell_x, cell_y);
} else {
draw_text(base_pattern,
entry.label_func(menu_entry_index, trainer_state),
cell_x, cell_y);
}
}
}
}
void
window__menu_t::draw(const uint32_t base_pattern,
const union window_descriptor_t::data& data,
const window_t& window,
const trainer_state_t& trainer_state)
{
const menu_t& menu = *data.menu;
draw_menu(base_pattern, menu, trainer_state);
draw_cursor(base_pattern, menu, window.cursor);
}
static void
_draw_move_types(const uint32_t base_pattern,
const uint8_t index,
const trainer_state_t& trainer_state)
{
// this is fully custom, not worth making a data model for this
constexpr screen_cell_t top_left = {0, 8};
constexpr screen_cell_t bottom_right = {10, 12};
draw_box_border(base_pattern, top_left, bottom_right);
draw_box_background(base_pattern,
top_left, bottom_right);
const pokemon_instance_t& pokemon_instance = trainer_state.party.active_pokemon();
const move_instance_t& move_instance = pokemon_instance.move_instances[index];
const move_t& move = moves[move_instance.type];
draw_text(base_pattern,
S("TYPE/"),
top_left.x + 1, top_left.y + 1);
draw_text(base_pattern,
types[move.type].name,
top_left.x + 2, top_left.y + 2);
draw_number_right_align(base_pattern,
{top_left.x + 5, top_left.y + 3},
move_instance.pp,
2,
ascii_to_font(' '));
put_char(base_pattern,
top_left.x + 7, top_left.y + 3,
ascii_to_font('/'));
draw_number_right_align(base_pattern,
{top_left.x + 8, top_left.y + 3},
move.pp,
2,
ascii_to_font(' '));
}
void
window__menu_t::draw_fight_moves(const uint32_t base_pattern,
const union window_descriptor_t::data& data,
const window_t& window,
const trainer_state_t& trainer_state)
{
window__menu_t::draw(base_pattern,
data,
window,
trainer_state);
const menu_t& menu = *data.menu;
const cursor_t& cursor = window.cursor;
const uint32_t menu_entry_index = menu.width * cursor.y + cursor.x;
_draw_move_types(base_pattern,
menu_entry_index,
trainer_state);
}
static inline constexpr void
_move_cursor(const uint8_t width,
const uint8_t height,
const enum window_t::input_event event,
cursor_t& cursor)
{
switch (event) {
case window_t::input_left:
if ((--cursor.x) < 0) cursor.x = width - 1;
break;
case window_t::input_right:
if ((++cursor.x) >= width) cursor.x = 0;
break;
case window_t::input_up:
if ((--cursor.y) < 0) cursor.y = height - 1;
break;
case window_t::input_down:
if ((++cursor.y) >= height) cursor.y = 0;
break;
default: break;
}
}
window_descriptor_t::result
window__menu_t::update(const union window_descriptor_t::data& data,
window_t& window,
trainer_state_t& trainer_state,
const enum window_t::input_event event)
{
const menu_t& menu = *data.menu;
switch (event) {
case window_t::input_left: [[fallthrough]];
case window_t::input_right: [[fallthrough]];
case window_t::input_up: [[fallthrough]];
case window_t::input_down:
_move_cursor(menu.width, menu.height, event, window.cursor);
break;
case window_t::input_a: break;
case window_t::input_b: break;
default: break;
}
return { window_t::no_op };
}
window_descriptor_t::result
window__menu_t::update_fight(const union window_descriptor_t::data& data,
window_t& window,
trainer_state_t& trainer_state,
const enum window_t::input_event event)
{
const menu_t& menu = *data.menu;
switch (event) {
case window_t::input_a:
{
const cursor_t& cursor = window.cursor;
const uint32_t menu_entry_index = menu.width * cursor.y + cursor.x;
switch (menu_entry_index) {
case 0: // FIGHT
return { window_t::spawn, window_descriptor_t::fight_moves };
break;
case 1: // PKMN
break;
case 2: // ITEM
break;
case 3: // RUN
break;
default:
break;
}
}
break;
case window_t::input_b:
break;
default:
_move_cursor(menu.width, menu.width, event, window.cursor);
break;
}
return { window_t::no_op };
}
static uint8_t const * const
_get_move_string(const uint32_t index,
const trainer_state_t& trainer_state)
{
const pokemon_instance_t& pokemon_instance = trainer_state.party.active_pokemon();
if (index >= pokemon_instance_t::maximum_moves ||
pokemon_instance.move_instances[index].type == move_t::no_move)
return S("-");
else {
return moves[pokemon_instance.move_instances[index].type].name;
}
}
window_descriptor_t::result
window__menu_t::update_fight_moves(const union window_descriptor_t::data& data,
window_t& window,
trainer_state_t& trainer_state,
const enum window_t::input_event event)
{
const menu_t& menu = *data.menu;
const uint8_t max_height = trainer_state.party.active_pokemon().moves_count();
switch (event) {
case window_t::input_a:
break;
case window_t::input_b:
return { window_t::dismiss };
break;
default:
_move_cursor(menu.width, max_height, event, window.cursor);
break;
}
return { window_t::no_op };
}
const menu_t menu_fight_moves = {
.top_left = {4, 12},
.bottom_right = {19, 17},
.width = 1,
.height = 4,
.h_advance = 0,
.v_advance = 1,
.entry_type = menu_t::func,
.entry = {
{ .label_func = _get_move_string }
}
};
const menu_t menu_start = {
.top_left = {10, 0},
.bottom_right = {19, 15},
.width = 1,
.height = 7,
.h_advance = 0,
.v_advance = 2,
.entry_type = menu_t::labels,
.entry {
.labels = (menu_label_t[]){
{ S(POKE "DEX") },
{ S(POKE "MON") },
{ S("ITEM" ) },
{ S(TRAINER ) },
{ S("SAVE" ) },
{ S("OPTION" ) },
{ S("EXIT" ) },
},
}
};
const menu_t menu_fight = {
.top_left = {8, 12},
.bottom_right = {19, 17},
.width = 2,
.height = 2,
.h_advance = 6,
.v_advance = 2,
.entry_type = menu_t::labels,
.entry = {
.labels = (menu_label_t[]) {
{ S("FIGHT") }, { S(PKMN ) },
{ S("ITEM" ) }, { S("RUN") },
}
},
};
#undef S

35
window/menu.hpp Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include "../coordinates.hpp"
#include "../trainer.hpp"
typedef uint8_t const * const menu_label_t;
struct menu_entry_t {
union {
menu_label_t * labels;
menu_label_t (* const label_func)(const uint32_t index,
const trainer_state_t& trainer);
};
};
struct menu_t {
enum entry_type {
labels,
func,
};
const screen_cell_t top_left; // in cells
const screen_cell_t bottom_right; // in cells
const uint8_t width; // in menu items
const uint8_t height; // in menu items
const uint8_t h_advance; // in cells
const uint8_t v_advance; // in cells
const enum entry_type entry_type;
const menu_entry_t entry;
};
extern const menu_t menu_start;
extern const menu_t menu_fight;
extern const menu_t menu_fight_moves;

21
window/window.cpp Normal file
View File

@ -0,0 +1,21 @@
#include "window.hpp"
#include "menu.hpp"
const window_descriptor_t window_descriptors[] = {
[window_descriptor_t::start] = {
.data = { .menu = &menu_start },
.draw = window__menu_t::draw,
.update = window__menu_t::update,
},
[window_descriptor_t::fight] = {
.data = { .menu = &menu_fight },
.draw = window__menu_t::draw,
.update = window__menu_t::update_fight,
},
[window_descriptor_t::fight_moves] = {
.data = { .menu = &menu_fight_moves },
.draw = window__menu_t::draw_fight_moves,
.update = window__menu_t::update_fight_moves,
},
};

113
window/window.hpp Normal file
View File

@ -0,0 +1,113 @@
#pragma once
#include <cstdint>
#include "menu.hpp"
#include "../trainer.hpp"
struct cursor_t {
int16_t x; // in menu items
int16_t y; // in menu items
};
struct window_t {
enum input_event {
input_up,
input_down,
input_left,
input_right,
input_a,
input_b,
};
enum window_event {
no_op,
spawn,
dismiss,
};
cursor_t cursor;
};
struct window_descriptor_t {
enum type {
start,
fight,
fight_moves,
};
union data {
menu_t const * const menu;
};
union data data;
struct result {
enum window_t::window_event window_event;
union {
uint8_t no_op;
enum type type;
};
result(enum window_t::window_event window_event,
enum type type)
: window_event(window_event), type(type)
{}
result(enum window_t::window_event window_event)
: window_event(window_event), no_op(0)
{}
};
void
(* const draw)(const uint32_t base_pattern,
const union data& data,
const window_t& window,
const trainer_state_t& trainer_state);
result
(* const update)(const union data& data,
window_t& window,
trainer_state_t& trainer_state,
const enum window_t::input_event event
);
};
extern const window_descriptor_t window_descriptors[];
// window types
struct window__menu_t {
static void
draw(const uint32_t base_pattern,
const union window_descriptor_t::data& data,
const window_t& window,
const trainer_state_t& trainer_state);
static void
draw_fight_moves(const uint32_t base_pattern,
const union window_descriptor_t::data& data,
const window_t& window,
const trainer_state_t& trainer_state);
static window_descriptor_t::result
update(const union window_descriptor_t::data& data,
window_t& window,
trainer_state_t& trainer_state,
const enum window_t::input_event event
);
static window_descriptor_t::result
update_fight(const union window_descriptor_t::data& data,
window_t& window,
trainer_state_t& trainer_state,
const enum window_t::input_event event
);
static window_descriptor_t::result
update_fight_moves(const union window_descriptor_t::data& data,
window_t& window,
trainer_state_t& trainer_state,
const enum window_t::input_event event
);
};

67
window/window_stack.hpp Normal file
View File

@ -0,0 +1,67 @@
#pragma once
#include <cstdint>
#include "window.hpp"
#include "../trainer.hpp"
struct window_stack_t {
struct {
enum window_descriptor_t::type type;
window_t window;
} stack[16];
int8_t ix;
constexpr inline void
draw(const uint32_t base_pattern,
trainer_state_t& trainer_state);
constexpr inline void
update(const enum window_t::input_event event,
trainer_state_t& trainer_state);
};
constexpr inline void
window_stack_t::draw(const uint32_t base_pattern,
trainer_state_t& trainer_state)
{
if (ix < 0) return;
const window_descriptor_t& descriptor = \
window_descriptors[stack[ix].type];
descriptor.draw(base_pattern,
descriptor.data,
stack[ix].window,
trainer_state);
}
constexpr inline void
window_stack_t::update(const enum window_t::input_event event,
trainer_state_t& trainer_state)
{
if (ix < 0) return;
const window_descriptor_t& descriptor = \
window_descriptors[stack[ix].type];
const window_descriptor_t::result& result = \
descriptor.update(descriptor.data,
stack[ix].window,
trainer_state,
event);
switch (result.window_event) {
case window_t::spawn:
if (ix >= 15) return;
ix++;
stack[ix].type = result.type;
stack[ix].window.cursor = { 0 };
break;
case window_t::dismiss:
if (ix < 0) return;
ix--;
default:
break;
}
}