Skip to content

Instantly share code, notes, and snippets.

@colinbellino
Created December 17, 2024 12:36
Show Gist options
  • Save colinbellino/b3a269c32341a39c824a4c97bc1ae858 to your computer and use it in GitHub Desktop.
Save colinbellino/b3a269c32341a39c824a4c97bc1ae858 to your computer and use it in GitHub Desktop.
The game code for https://colinbellino.itch.io/monstrum-prison (engine/renderer and platform code not included)
#import "Basic";
#import "Random";
#import "Math";
#import "Sort";
#import "Flat_Pool";
#import "String";
#import "Tagged_Union"(DEBUG);
#import,dir "../engine"(
DEBUG = DEBUG,
MAX_SPRITES = 256,
MAX_LAYERS = 4,
GRID_SIZE = GRID_SIZE,
ENABLE_GAME_VIEW = true,
ENABLE_3D = false,
EMBED_ASSETS = !DEBUG,
ASSETS_PATH = ASSETS_PATH
);
#if SUPER_DEBUG {
WINDOW_SIZE :: Vector2s.{ 1920, 1080 };
} else {
WINDOW_SIZE :: Vector2s.{ 1280, 720 };
}
UI_RESOLUTION :: Vector2s.{ 1920/4, 1080/4 };
#if SUPER_DEBUG {
VOLUME_MAIN :: 0.1;
VOLUME_MUSIC :: 0.0;
} else {
VOLUME_MAIN :: 0.5;
VOLUME_MUSIC :: 0.5;
}
FONT_SIZE : s32 : 8;
VOLUME_SFX :: 0.5;
VOLUME_FOOTSTEP :: 0.5;
GRID_SIZE :: cast(s32) 32;
GRID_SIZE_V2 :: Vector2.{ xx GRID_SIZE, xx GRID_SIZE };
GRID_SIZE_V2S :: Vector2s.{ GRID_SIZE, GRID_SIZE };
PLAYER_CONTROLLER :: 0;
PLAYER_INDEX :: 0;
OFFSCREEN_POSITION :: Vector2s.{ 999, 999 };
COLOR_RED2 :: #run(hex_string_to_color("#8e3438ff"));
COLOR_RED3 :: #run(hex_string_to_color("#af4146ff"));
COLOR_GREEN2 :: #run(hex_string_to_color("#325546ff"));
COLOR_GREEN3 :: #run(hex_string_to_color("#376c55ff"));
MAX_HEALTH :: 99;
GAME_VIEW_CENTER :: Vector2s.{ 151, 135 };
PARTY_ICON_SCALE :: 1;
HUD_PADDING :: 4;
WALLS_OFFSET :: Vector2.[
.{ +0.0, +0.0 }, // .NORTH
.{ +0.9, +0.0 }, // .EAST
.{ +0.0, +0.9 }, // .SOUTH
.{ +0.0, +0.0 }, // .WEST
];
PARTY_HEADER_Y :: 40;
PARTY_MEMBER_SIZE :: Vector2s.{ 88, 28 };
TILE_TEXTURES_DATA :: Tile_Texture.[
.{ west_pos = .{ 88, 32 }, east_pos = .{ 64, 32 }, side_size = .{ 8, 32 }, front_pos = .{ 64, 80 }, front_size = .{ 16, 16 } },
.{ west_pos = .{ 88, 32 }, east_pos = .{ 64, 32 }, side_size = .{ 8, 32 }, front_pos = .{ 64, 80 }, front_size = .{ 16, 16 } },
.{ west_pos = .{ 272, 32 }, east_pos = .{ 240, 32 }, side_size = .{ 16, 64 }, front_pos = .{ 240, 96 }, front_size = .{ 32, 32 } },
.{ west_pos = .{ 112, 160 }, east_pos = .{ 64, 160 }, side_size = .{ 16, 96 }, front_pos = .{ 64, 256 }, front_size = .{ 64, 64 } },
];
using Sounds :: enum u32 {
SFX_ERROR;
SFX_MONSTER_DEATH;
SFX_CONFIRM;
SFX_LAND;
SFX_DASH1;
SFX_HIT1;
SFX_HIT2;
SFX_FOUNTAIN_HEAL;
SFX_CREATURE_DRAGON;
SFX_CREATURE_CENTIPEDE;
SFX_CREATURE_MOUSE;
SFX_CREATURE_FROG;
SFX_CREATURE_BUNNY;
SFX_SPIKE_TRAP;
SFX_KEY_BUTTON;
SFX_COLLAR;
SFX_ATTACK_READY;
SFX_STAIRS;
SFX_FOOTSTEP1;
SFX_FOOTSTEP2;
SFX_FOOTSTEP3;
SFX_MUSIC1;
SFX_MUSIC1_INTRO;
SFX_NONE;
SFX_COUNT;
}
using Images :: enum u32 {
IMG_PLACEHOLDER;
IMG_SPRITESHEET;
IMG_TILESET1;
IMG_TILESET1_DOOR_C;
IMG_TILESET1_DOOR_O;
IMG_CREATURE1;
IMG_CREATURE1_PART1;
IMG_CREATURE1_PART1_O;
IMG_CREATURE1_PART2;
IMG_CREATURE1_PART2_O;
IMG_CREATURE1_PART3;
IMG_CREATURE1_PART3_O;
IMG_CREATURE2;
IMG_CREATURE2_PART1;
IMG_CREATURE2_PART1_O;
IMG_CREATURE2_PART2;
IMG_CREATURE2_PART2_O;
IMG_CREATURE2_PART3;
IMG_CREATURE2_PART3_O;
IMG_CREATURE3;
IMG_CREATURE3_PART1;
IMG_CREATURE3_PART1_O;
IMG_CREATURE3_PART2;
IMG_CREATURE3_PART2_O;
IMG_CREATURE3_PART3;
IMG_CREATURE3_PART3_O;
IMG_CREATURE4;
IMG_CREATURE4_PART1;
IMG_CREATURE4_PART1_O;
IMG_CREATURE4_PART2;
IMG_CREATURE4_PART2_O;
IMG_CREATURE4_PART3;
IMG_CREATURE4_PART3_O;
IMG_CREATURE5;
IMG_CREATURE5_PART1;
IMG_CREATURE5_PART1_O;
IMG_CREATURE5_PART2;
IMG_CREATURE5_PART2_O;
IMG_CREATURE5_PART3;
IMG_CREATURE5_PART3_O;
IMG_TRAP_SPIKE_C;
IMG_TRAP_SPIKE_O;
IMG_FOUNTAIN_HEAL_F;
IMG_FOUNTAIN_HEAL_E;
IMG_KEY1;
IMG_COLLAR;
IMG_CREATURE_ICONS;
IMG_UI_LAYOUT;
IMG_GAME_OVER;
IMG_TITLE;
IMG_STAIRS;
IMG_STARTER1;
IMG_STARTER2;
IMG_STARTER3;
IMG_STARTER4;
IMG_STARTER5;
IMG_ATTACK1;
IMG_ATTACK2;
IMG_ATTACK3;
IMG_NONE;
IMG_COUNT;
}
using Fonts :: enum u32 {
FONT_BABYBLOCKS;
FONT_COUNT;
}
CREATURE_TILE_POSITIONS :: Vector2s.[.{ 0, 16 }, .{ 16, 16 }, .{ 48, 16 }];
CREATURE_TILE_SIZES :: Vector2s.[.{ 16, 16 }, .{ 32, 32 }, .{ 64, 64 }];
Damage_Type :: enum {
TAIL;
CLAW;
FANG;
}
ATTACK_OFFSETS :: Vector2s.[
.{ -27, -35 },
.{ -28, -25 },
.{ -23, -22 },
];
Creature :: struct {
id: Creature_Id;
name: Fixed_Size_String(16);
image: Images;
encounter_sfx: Sounds;
color: Color;
health: u8;
parts: Fixed_Size_Array(Creature_Part, MAX_PART_SIZE);
attack_type: Damage_Type;
attack_cooldown: s64;
attack_damage: u8;
attack_sfx: Sounds;
required_for_capture: int; // index int parts[]
}
Creature_Part :: struct {
position: Vector2s;
size: Vector2s;
image: Images;
health: u8;
z_index: s32;
priority: s32;
weakness: Damage_Type;
type: Creature_Part_Type;
}
Creature_Part_Type :: enum { TAIL; BODY; LEGS; HEAD; }
// :creatures
CREATURES :: Creature.[
.{ id = 0, name = #run(string_to_fixed_string("DUMMY")), },
.{
id = 1, name = #run(string_to_fixed_string("LIZZY")), image = IMG_CREATURE1, encounter_sfx = SFX_CREATURE_DRAGON, color = #run(hex_string_to_color("#233a31ff")), health = 45,
attack_type = .TAIL, attack_cooldown = 8000, attack_damage = 8, attack_sfx = SFX_LAND, required_for_capture = 0,
parts.data[0] = .{ type = .HEAD, health = 16, z_index = 2, priority = 2, weakness = .FANG, image = IMG_CREATURE1_PART1, position = .{ 0, -18 }, size = .{ 32, 32 } }, // head
parts.data[1] = .{ type = .BODY, health = 20, z_index = 1, priority = 0, weakness = .TAIL, image = IMG_CREATURE1_PART2, position = .{ 0, 8 }, size = .{ 64, 48 } }, // body
parts.data[2] = .{ type = .TAIL, health = 8, z_index = 0, priority = 1, weakness = .CLAW, image = IMG_CREATURE1_PART3, position = .{ -16, 0 }, size = .{ 36, 48 } }, // tail
parts.count = 3,
},
.{
id = 2, name = #run(string_to_fixed_string("CENTI")), image = IMG_CREATURE2, encounter_sfx = SFX_CREATURE_CENTIPEDE, color = #run(hex_string_to_color("#45191bff")), health = 50,
attack_type = .CLAW, attack_cooldown = 5000, attack_damage = 5, attack_sfx = SFX_DASH1, required_for_capture = 1,
parts.data[0] = .{ type = .HEAD, health = 16, z_index = 1, priority = 2, weakness = .FANG, image = IMG_CREATURE2_PART1, position = .{ 4, -10 }, size = .{ 36, 32 } }, // head
parts.data[1] = .{ type = .LEGS, health = 8, z_index = 2, priority = 1, weakness = .TAIL, image = IMG_CREATURE2_PART2, position = .{ 0, 7 }, size = .{ 64, 36 } }, // legs
parts.data[2] = .{ type = .BODY, health = 20, z_index = 0, priority = 0, weakness = .CLAW, image = IMG_CREATURE2_PART3, position = .{ 0, 6 }, size = .{ 44, 36 } }, // body
parts.count = 3,
},
.{
id = 3, name = #run(string_to_fixed_string("MOWSE")), image = IMG_CREATURE3, encounter_sfx = SFX_CREATURE_MOUSE, color = #run(hex_string_to_color("#33394aff")), health = 43,
attack_type = .FANG, attack_cooldown = 3000, attack_damage = 5, attack_sfx = SFX_HIT2, required_for_capture = 1,
parts.data[0] = .{ type = .HEAD, health = 16, z_index = 2, priority = 2, weakness = .FANG, image = IMG_CREATURE3_PART1, position = .{ 0, 14 }, size = .{ 32, 32 } }, // head
parts.data[1] = .{ type = .BODY, health = 8, z_index = 1, priority = 0, weakness = .TAIL, image = IMG_CREATURE3_PART2, position = .{ 0, 0 }, size = .{ 64, 64 } }, // body
parts.data[2] = .{ type = .TAIL, health = 20, z_index = 0, priority = 1, weakness = .CLAW, image = IMG_CREATURE3_PART3, position = .{ 0, -18 }, size = .{ 48, 22 } }, // tail
parts.count = 3,
},
.{
id = 4, name = #run(string_to_fixed_string("FROGGY")), image = IMG_CREATURE4, encounter_sfx = SFX_CREATURE_FROG, color = #run(hex_string_to_color("#163f7cff")), health = 50,
attack_type = .TAIL, attack_cooldown = 3000, attack_damage = 4, attack_sfx = SFX_HIT2, required_for_capture = 2,
parts.data[0] = .{ type = .HEAD, health = 8, z_index = 2, priority = 2, weakness = .FANG, image = IMG_CREATURE4_PART1, position = .{ -6, -16 }, size = .{ 32, 32 } }, // head
parts.data[1] = .{ type = .BODY, health = 22, z_index = 1, priority = 0, weakness = .TAIL, image = IMG_CREATURE4_PART2, position = .{ 0, 0 }, size = .{ 64, 48 } }, // body
parts.data[2] = .{ type = .TAIL, health = 22, z_index = 0, priority = 1, weakness = .CLAW, image = IMG_CREATURE4_PART3, position = .{ 18, 8 }, size = .{ 28, 22 } }, // tail
parts.count = 3,
},
.{
id = 5, name = #run(string_to_fixed_string("BUN-BUN")), image = IMG_CREATURE5, encounter_sfx = SFX_CREATURE_BUNNY, color = #run(hex_string_to_color("#394f96ff")), health = 56,
attack_type = .CLAW, attack_cooldown = 3000, attack_damage = 4, attack_sfx = SFX_HIT2, required_for_capture = 0,
parts.data[0] = .{ type = .HEAD, health = 16, z_index = 2, priority = 1, weakness = .FANG, image = IMG_CREATURE5_PART1, position = .{ -1, -14 }, size = .{ 24, 32 } }, // head
parts.data[1] = .{ type = .LEGS, health = 16, z_index = 1, priority = 2, weakness = .TAIL, image = IMG_CREATURE5_PART2, position = .{ -8, 4 }, size = .{ 36, 36 } }, // arms
parts.data[2] = .{ type = .BODY, health = 16, z_index = 0, priority = 0, weakness = .CLAW, image = IMG_CREATURE5_PART3, position = .{ 0, 14 }, size = .{ 48, 42 } }, // body
parts.count = 3,
},
];
Pickup :: struct {
id: Pickup_Id;
image: Images;
encounter_sfx: Sounds;
color: Color;
effect: Pickup_Effect;
}
Pickup_Effect :: enum {
NONE;
DAMAGE;
HEAL;
DOOR;
KEY;
COLLAR;
STAIRS;
}
PICKUPS :: Pickup.[
.{ id = 0 },
.{ id = 1, image = IMG_TRAP_SPIKE_C, encounter_sfx = SFX_SPIKE_TRAP, color = #run(hex_string_to_color("#736246ff")), effect = .DAMAGE },
.{ id = 2, image = IMG_FOUNTAIN_HEAL_F, encounter_sfx = SFX_FOUNTAIN_HEAL, color = #run(hex_string_to_color("#542121ff")), effect = .HEAL },
.{ id = 3, image = IMG_NONE, encounter_sfx = SFX_NONE, color = #run(hex_string_to_color("#72723eff")), effect = .DOOR },
.{ id = 4, image = IMG_KEY1, encounter_sfx = SFX_KEY_BUTTON, color = #run(hex_string_to_color("#5b664aff")), effect = .KEY },
.{ id = 5, image = IMG_COLLAR, encounter_sfx = SFX_COLLAR, color = #run(hex_string_to_color("#31c7ecff")), effect = .COLLAR },
.{ id = 6, image = IMG_TRAP_SPIKE_O, encounter_sfx = SFX_SPIKE_TRAP, color = #run(hex_string_to_color("#736246ff")), effect = .DAMAGE },
.{ id = 7, image = IMG_FOUNTAIN_HEAL_E, encounter_sfx = SFX_FOUNTAIN_HEAL, color = #run(hex_string_to_color("#542121ff")), effect = .NONE },
.{ id = 8, image = IMG_STAIRS, encounter_sfx = SFX_STAIRS, color = #run(hex_string_to_color("#31c7ecff")), effect = .STAIRS },
];
#run { for PICKUPS { assert(it.id == it_index); } }
Tile_Texture :: struct {
west_pos: Vector2s;
east_pos: Vector2s;
side_size: Vector2s;
front_pos: Vector2s;
front_size: Vector2s;
}
TILE_TEXTURES :: Images.[
IMG_PLACEHOLDER,
IMG_TILESET1,
IMG_TILESET1_DOOR_C,
IMG_TILESET1_DOOR_O,
];
VIEW_WIDTH :: 5;
VIEW_HEIGHT :: 4;
VIEW_SIZE :: Vector2s.{ VIEW_WIDTH, VIEW_HEIGHT };
Level_Layers :: enum u8 {
ENTITIES :: 0;
GRID :: 1;
}
Level_Grid_Value :: enum u8 {
EMPTY :: 0;
WALL_1 :: 1;
WALL_2 :: 2;
WALL_3 :: 3;
}
Cell_Instance :: struct {
id: Cell_Id;
dir: Direction_Cardinal;
position: Vector2s;
encounter: Encounter;
opened: bool;
}
Cell_Id :: u8;
Cell :: struct {
floor: u8;
walls: [4]Tile_Id;
}
Level :: struct {
size: Vector2s;
player_spawn_position: Vector2s;
player_spawn_direction: Direction_Cardinal;
cells: []Cell_Instance;
hidden_cells: []Cell_Debug_State;
}
Encounter_Type :: enum {
NONE;
CREATURE;
PICKUP;
STARTER;
}
Encounter :: struct {
type: Encounter_Type;
creature_id: Creature_Id;
pickup_id: Pickup_Id;
target_cell_index: int;
}
Tile_Id :: enum u8 {
TILE_EMPTY;
TILE_BRICK;
TILE_METAL;
TILE_BUBBLE;
};
Cell_Debug_State :: struct {
hidden: bool;
walls_hidden: [4]bool;
}
CELLS :: Cell.[
/* 00 */.{ walls = .[ 0, 0, 0, 0 ] },
/* 01 */.{ walls = .[ 1, 1, 1, 1 ], floor = 1 },
/* 02 */.{ walls = .[ 2, 2, 2, 2 ], floor = 1 },
/* 03 */.{ walls = .[ 3, 3, 3, 3 ], floor = 1 },
];
DIRECTIONS_CARDINAL :: Vector2s.[
.{ +0, -1 }, // .NORTH
.{ +1, +0 }, // .EAST
.{ +0, +1 }, // .SOUTH
.{ -1, +0 }, // .WEST
];
Direction_Cardinal :: enum { NORTH; EAST; SOUTH; WEST; }
Effect :: struct {
using transform: Transform;
using sprite: Sprite;
layer_id: s32;
}
Sprite :: struct {
active: bool;
size: Vector2s;
rect_index: s32;
texture_position: Vector2s;
texture_size: Vector2s;
texture_padding: s32;
z_index: s32;
color: Color; @UI(Color)
palette: s32; // FIXME: delete this
}
Creature_Id :: u32;
Creature_Part_Id :: u32;
Pickup_Id :: u32;
Party_Member :: struct {
creature_id: Creature_Id;
creature_part_id: Creature_Part_Id;
health: Stat;
}
Stat :: struct { current: u8; max: u8; }
World_Mode :: enum {
EXPLORATION;
BATTLE;
}
MAX_PARTY_SIZE :: 4;
MAX_PART_SIZE :: 4;
// :battle
Battle :: struct {
started: bool;
cell_index: int; // index into game.level.cells
foe_creature_id: Creature_Id;
foe_attack_timer: Timer;
foe_parts_health: Fixed_Size_Array(Stat, MAX_PART_SIZE);
party_attack_timers: Fixed_Size_Array(Timer, MAX_PARTY_SIZE);
party_flash_timers: Fixed_Size_Array(Timer, MAX_PARTY_SIZE);
party_attack_sound_played: Fixed_Size_Array(bool, MAX_PARTY_SIZE);
party_member_current: int; // index into game.party
foe_part_highlighted: int; // index into creature.parts
foe_part_flashing: int; // index into creature.parts
foe_part_flashing_timer: Timer;
}
Log_Type :: enum { NORMAL; CRITICAL; }
Log :: struct {
text: Fixed_Size_String(64);
type: Log_Type;
}
// :game
Game :: struct {
engine: *Engine;
ctx: Context;
// gamelevel
arena: *Arena_Allocator;
battle_arena: *Arena_Allocator;
mode: Mode(Game_Mode);
mouse_position_grid: Vector2s;
mouse_position_world: Vector2;
skip_title: bool = SUPER_DEBUG;
player_inputs: [4]Player_Input;
clear_color: Color;
fade_color: Color;
random: Random_State;
seed: u64 = 2; // This number was fairly chosen with a 1D20 roll...
effects: Fixed_Size_Array(Effect, 128);
force_fov_update: bool;
levels: Fixed_Size_Array(Fixed_Size_String(16), 32);
party: Fixed_Size_Array(Party_Member, MAX_PARTY_SIZE);
collar: bool;
current_level: int;
logs: Fixed_Size_Array(Log, 9);
using world: struct {
world_mode: Mode(World_Mode);
level: Level;
field_of_view: [VIEW_WIDTH*VIEW_HEIGHT]Cell_Instance;
player_position: Vector2s;
player_direction: Direction_Cardinal;
battle: Battle;
};
floating_messages: Fixed_Size_Array(Floating_Message, 32);
ui: struct {
scale: float;
hovered: bool;
hud_position: Vector2;
hud_size: Vector2;
party_size: Vector2;
party_position: Vector2;
minimap_size: Vector2;
minimap_position: Vector2;
inventory_size: Vector2;
inventory_position: Vector2;
inventory_collar_pressed: bool;
foe_size: Vector2;
foe_position: Vector2;
logs_size: Vector2;
logs_position: Vector2;
settings: bool;
};
cursor: SystemCursor;
// debug stuff
debug_ui_debug: bool;
// assets
sounds: [SFX_COUNT]*Asset_Sound;
images: [IMG_COUNT]*Asset_Image;
fonts: [FONT_COUNT]*Asset_Font;
ldtk: *LDTK_Root;
}
Game_Mode :: enum { INIT; TITLE; WORLD; GAME_OVER; }
Player_Input :: struct {
move: Input_Repeater = .{ threshold = 200, rate = 150 };
aim: Input_Repeater = .{ threshold = 200, rate = 150 };
shoulder_left: Key_State;
shoulder_right: Key_State;
confirm: Key_State;
cancel: Key_State;
actions: [12]Key_State;
}
game: *Game;
#program_export
update :: () -> bool {
context.allocator = panic_allocator;
context._Tracy.ZoneScoped("frame");
if engine == null {
engine = engine_init();
engine.game_version = BUILD_VERSION;
arena := make_arena("game_main");
game = New(Game,, arena.allocator);
game.arena = arena;
// game.arena.logging = true;
game.engine = engine;
game.battle_arena = make_arena("game_battle");
game_context := context;
game_context.logger = engine.logger.procedure;
game_context.logger_data = engine.logger;
game_context.allocator = game.arena.allocator;
game_context.assertion_failed = engine_support_assertion_failed;
game.ctx = game_context;
push_context,defer_pop game.ctx;
log("BUILD_VERSION: %", engine.game_version);
log("DEBUG: %", DEBUG);
window_open("Monstrum Prison", WINDOW_SIZE);
// :assets
game.images[IMG_PLACEHOLDER] = image_load(#run load_asset_path("images/placeholder.aseprite"));
game.images[IMG_SPRITESHEET] = image_load(#run load_asset_path("images/spritesheet.aseprite"));
game.images[IMG_TILESET1] = image_load(#run load_asset_path("images/7dfps_tileset1.aseprite"));
game.images[IMG_TILESET1_DOOR_C] = image_load(#run load_asset_path("images/7dfps_tileset1_door_closed.aseprite"));
game.images[IMG_TILESET1_DOOR_O] = image_load(#run load_asset_path("images/7dfps_tileset1_door_opened.aseprite"));
game.images[IMG_CREATURE1] = image_load(#run load_asset_path("images/mon_01.aseprite"));
game.images[IMG_CREATURE1_PART1] = image_load(#run load_asset_path("images/mon_03_part_01.aseprite"));
game.images[IMG_CREATURE1_PART1_O] = image_load(#run load_asset_path("images/mon_03_part_01_outline.aseprite"));
game.images[IMG_CREATURE1_PART2] = image_load(#run load_asset_path("images/mon_03_part_02.aseprite"));
game.images[IMG_CREATURE1_PART2_O] = image_load(#run load_asset_path("images/mon_03_part_02_outline.aseprite"));
game.images[IMG_CREATURE1_PART3] = image_load(#run load_asset_path("images/mon_03_part_03.aseprite"));
game.images[IMG_CREATURE1_PART3_O] = image_load(#run load_asset_path("images/mon_03_part_03_outline.aseprite"));
game.images[IMG_CREATURE2] = image_load(#run load_asset_path("images/mon_02.aseprite"));
game.images[IMG_CREATURE2_PART1] = image_load(#run load_asset_path("images/mon_02_part_01.aseprite"));
game.images[IMG_CREATURE2_PART1_O] = image_load(#run load_asset_path("images/mon_02_part_01_outline.aseprite"));
game.images[IMG_CREATURE2_PART2] = image_load(#run load_asset_path("images/mon_02_part_02.aseprite"));
game.images[IMG_CREATURE2_PART2_O] = image_load(#run load_asset_path("images/mon_02_part_02_outline.aseprite"));
game.images[IMG_CREATURE2_PART3] = image_load(#run load_asset_path("images/mon_02_part_03.aseprite"));
game.images[IMG_CREATURE2_PART3_O] = image_load(#run load_asset_path("images/mon_02_part_03_outline.aseprite"));
game.images[IMG_CREATURE3] = image_load(#run load_asset_path("images/mon_03.aseprite"));
game.images[IMG_CREATURE3_PART1] = image_load(#run load_asset_path("images/mon_01_part_01.aseprite"));
game.images[IMG_CREATURE3_PART1_O] = image_load(#run load_asset_path("images/mon_01_part_01_outline.aseprite"));
game.images[IMG_CREATURE3_PART2] = image_load(#run load_asset_path("images/mon_01_part_02.aseprite"));
game.images[IMG_CREATURE3_PART2_O] = image_load(#run load_asset_path("images/mon_01_part_02_outline.aseprite"));
game.images[IMG_CREATURE3_PART3] = image_load(#run load_asset_path("images/mon_01_part_03.aseprite"));
game.images[IMG_CREATURE3_PART3_O] = image_load(#run load_asset_path("images/mon_01_part_03_outline.aseprite"));
game.images[IMG_CREATURE4] = image_load(#run load_asset_path("images/mon_04.aseprite"));
game.images[IMG_CREATURE4_PART1] = image_load(#run load_asset_path("images/mon_04_part_01.aseprite"));
game.images[IMG_CREATURE4_PART1_O] = image_load(#run load_asset_path("images/mon_04_part_01_outline.aseprite"));
game.images[IMG_CREATURE4_PART2] = image_load(#run load_asset_path("images/mon_04_part_02.aseprite"));
game.images[IMG_CREATURE4_PART2_O] = image_load(#run load_asset_path("images/mon_04_part_02_outline.aseprite"));
game.images[IMG_CREATURE4_PART3] = image_load(#run load_asset_path("images/mon_04_part_03.aseprite"));
game.images[IMG_CREATURE4_PART3_O] = image_load(#run load_asset_path("images/mon_04_part_03_outline.aseprite"));
game.images[IMG_CREATURE5] = image_load(#run load_asset_path("images/mon_05.aseprite"));
game.images[IMG_CREATURE5_PART1] = image_load(#run load_asset_path("images/mon_05_part_01.aseprite"));
game.images[IMG_CREATURE5_PART1_O] = image_load(#run load_asset_path("images/mon_05_part_01_outline.aseprite"));
game.images[IMG_CREATURE5_PART2] = image_load(#run load_asset_path("images/mon_05_part_02.aseprite"));
game.images[IMG_CREATURE5_PART2_O] = image_load(#run load_asset_path("images/mon_05_part_02_outline.aseprite"));
game.images[IMG_CREATURE5_PART3] = image_load(#run load_asset_path("images/mon_05_part_03.aseprite"));
game.images[IMG_CREATURE5_PART3_O] = image_load(#run load_asset_path("images/mon_05_part_03_outline.aseprite"));
game.images[IMG_TRAP_SPIKE_C] = image_load(#run load_asset_path("images/trap_closed.aseprite"));
game.images[IMG_TRAP_SPIKE_O] = image_load(#run load_asset_path("images/trap.aseprite"));
game.images[IMG_FOUNTAIN_HEAL_F] = image_load(#run load_asset_path("images/fountain_health.aseprite"));
game.images[IMG_FOUNTAIN_HEAL_E] = image_load(#run load_asset_path("images/fountain_empty.aseprite"));
game.images[IMG_KEY1] = image_load(#run load_asset_path("images/key1.aseprite"));
game.images[IMG_COLLAR] = image_load(#run load_asset_path("images/collar_collectable.aseprite"));
game.images[IMG_CREATURE_ICONS] = image_load(#run load_asset_path("images/spr_icon_monsters.aseprite"));
game.images[IMG_UI_LAYOUT] = image_load(#run load_asset_path("images/7dfps_ui-elements_project_iteration2.aseprite"));
game.images[IMG_GAME_OVER] = image_load(#run load_asset_path("images/game_over_screen.aseprite"));
game.images[IMG_TITLE] = image_load(#run load_asset_path("images/logo_title_bg.aseprite"));
game.images[IMG_STAIRS] = image_load(#run load_asset_path("images/stairs.aseprite"));
game.images[IMG_STARTER1] = image_load(#run load_asset_path("images/starter_01.aseprite"));
game.images[IMG_STARTER2] = image_load(#run load_asset_path("images/starter_02.aseprite"));
game.images[IMG_STARTER3] = image_load(#run load_asset_path("images/starter_03.aseprite"));
game.images[IMG_STARTER4] = image_load(#run load_asset_path("images/starter_04.aseprite"));
game.images[IMG_STARTER5] = image_load(#run load_asset_path("images/starter_05.aseprite"));
game.images[IMG_ATTACK1] = image_load(#run load_asset_path("images/attack_tail.png"), is_aseprite = false);
game.images[IMG_ATTACK2] = image_load(#run load_asset_path("images/attack_claw.png"), is_aseprite = false);
game.images[IMG_ATTACK3] = image_load(#run load_asset_path("images/attack_fangs.png"), is_aseprite = false);
game.images[IMG_NONE] = image_load(#run load_asset_path("images/empty.png"), is_aseprite = false);
game.sounds[SFX_ERROR] = audio_load(#run load_asset_path("audio/sounds/invalid.wav"), "invalid", .GENERAL_SFX);
game.sounds[SFX_MONSTER_DEATH] = audio_load(#run load_asset_path("audio/sounds/death_monster.wav"), "death_monster", .GENERAL_SFX);
game.sounds[SFX_CONFIRM] = audio_load(#run load_asset_path("audio/sounds/confirm.wav"), "confirm", .GENERAL_SFX);
game.sounds[SFX_LAND] = audio_load(#run load_asset_path("audio/sounds/land.wav"), "land", .GENERAL_SFX);
game.sounds[SFX_DASH1] = audio_load(#run load_asset_path("audio/sounds/dash1.wav"), "dash1", .GENERAL_SFX);
game.sounds[SFX_HIT1] = audio_load(#run load_asset_path("audio/sounds/hit.wav"), "hit1", .GENERAL_SFX);
game.sounds[SFX_HIT2] = audio_load(#run load_asset_path("audio/sounds/hit2.wav"), "hit2", .GENERAL_SFX);
game.sounds[SFX_FOUNTAIN_HEAL] = audio_load(#run load_asset_path("audio/sounds/fountain.wav"), "fountain", .GENERAL_SFX);
game.sounds[SFX_CREATURE_DRAGON] = audio_load(#run load_asset_path("audio/sounds/creature03_drag.wav"), "creature03_drag", .GENERAL_SFX);
game.sounds[SFX_CREATURE_CENTIPEDE] = audio_load(#run load_asset_path("audio/sounds/creature04_cent.wav"), "creature04_cent", .GENERAL_SFX);
game.sounds[SFX_CREATURE_MOUSE] = audio_load(#run load_asset_path("audio/sounds/creature01_mouse.wav"), "creature01_mouse", .GENERAL_SFX);
game.sounds[SFX_CREATURE_FROG] = audio_load(#run load_asset_path("audio/sounds/creature02_frog.wav"), "creature02_frog", .GENERAL_SFX);
game.sounds[SFX_CREATURE_BUNNY] = audio_load(#run load_asset_path("audio/sounds/creature05_rabb.wav"), "creature05_rabb", .GENERAL_SFX);
game.sounds[SFX_SPIKE_TRAP] = audio_load(#run load_asset_path("audio/sounds/spike_trap.wav"), "spike_trap", .GENERAL_SFX);
game.sounds[SFX_KEY_BUTTON] = audio_load(#run load_asset_path("audio/sounds/door.wav"), "door", .GENERAL_SFX);
game.sounds[SFX_COLLAR] = audio_load(#run load_asset_path("audio/sounds/leash.wav"), "leash", .GENERAL_SFX);
game.sounds[SFX_ATTACK_READY] = audio_load(#run load_asset_path("audio/sounds/ATB.wav"), "leash", .GENERAL_SFX);
game.sounds[SFX_STAIRS] = audio_load(#run load_asset_path("audio/sounds/ALttP_Stairs_Sound_3.wav"), "stairs", .GENERAL_SFX);
game.sounds[SFX_FOOTSTEP1] = audio_load(#run load_asset_path("audio/sounds/footsteps01.wav"), "footsteps01", .FOOTSTEPS);
game.sounds[SFX_FOOTSTEP2] = audio_load(#run load_asset_path("audio/sounds/footsteps02.wav"), "footsteps02", .FOOTSTEPS);
game.sounds[SFX_FOOTSTEP3] = audio_load(#run load_asset_path("audio/sounds/footsteps03.wav"), "footsteps03", .FOOTSTEPS);
game.sounds[SFX_MUSIC1] = audio_load(#run load_asset_path("audio/musics/dungeoncrawl1.ogg"), "music_level1", .MUSIC);
game.sounds[SFX_MUSIC1_INTRO] = audio_load(#run load_asset_path("audio/musics/dungeoncrawl1_intro.ogg"), "music_level1_intro", .MUSIC);
game.sounds[SFX_NONE] = audio_load(#run load_asset_path("audio/sounds/empty.wav"), "empty", .GENERAL_SFX);
game.fonts[FONT_BABYBLOCKS] = font_load (#run load_asset_path("fonts/ChevyRay - Babyblocks.ttf"), "babyblocks");
game.ldtk = ldtk_load (#run load_asset_path("levels.ldtk"), game.arena.allocator,, logger = noop_logger);
#if DEBUG {
for game.images { assert(it != null, tprint("Image not loaded: %", cast(Images) it_index)); }
for game.sounds { assert(it != null, tprint("Sound not loaded: %", cast(Sounds) it_index)); }
for game.fonts { assert(it != null, tprint("Font not loaded: %", cast(Fonts) it_index)); }
}
renderer_init(engine.window);
load_cursor(#run(assets_path("images/cursor_crosshair.png")), .CROSSHAIR, 16, 15);
load_cursor(#run(assets_path("images/cursor_arrow.png")), .ARROW, 0, 0);
load_cursor(#run(assets_path("images/cursor_hand.png")), .HAND, 13, 0);
audio_init();
audio_set_master_volume (VOLUME_MAIN);
audio_set_music_volume (VOLUME_MUSIC);
audio_set_sound_volume (VOLUME_SFX);
audio_set_footstep_volume(VOLUME_FOOTSTEP);
update_vsync();
}
push_context,defer_pop game.ctx;
engine_frame_start();
defer engine_frame_end();
game.ui.hovered = false;
ratio := cast(float) engine.window_size.x / engine.window_size.y;
ui_ratio := cast(float) UI_RESOLUTION.x / UI_RESOLUTION.y;
if ratio > ui_ratio {
game.ui.scale = cast(float) engine.window_size.y / UI_RESOLUTION.y;
} else {
game.ui.scale = cast(float) engine.window_size.x / UI_RESOLUTION.x;
}
push_pop_text_font(xx FONT_BABYBLOCKS);
push_pop_text_size(FONT_SIZE);
// FIXME: use the proper pixel art font!
game.mouse_position_world = window_to_world_position(engine.inputs.mouse_position);
game.mouse_position_grid = world_to_grid_position(game.mouse_position_world, .{ GRID_SIZE, GRID_SIZE });
engine.clear_pass_action.colors[0].clear_value = xx,force game.clear_color;
if engine.inputs.keys[SDL_SCANCODE_F1].pressed {
game.ui.settings = !game.ui.settings;
}
#if DEBUG {
// Debug inputs
if engine.inputs.keys[SDL_SCANCODE_LCTRL].down {
// :restart
if engine.inputs.keys[SDL_SCANCODE_R].pressed {
clear_console(engine.logger);
log_content("Game restarted.");
game.party = .{};
game.collar = false;
mode_transition(*game.mode, .WORLD);
}
}
if engine.inputs.keys[SDL_SCANCODE_F12].pressed {
for :for_ptr game.party {
it.health.current = it.health.max;
}
battle_log("[CHEAT] Party fully healed.");
}
if engine.inputs.keys[SDL_SCANCODE_LSHIFT].down {
camera_move: Vector2;
camera_zoom: float;
if engine.inputs.keys[SDL_SCANCODE_A].down {
camera_move.x -= 1;
} else if engine.inputs.keys[SDL_SCANCODE_D].down {
camera_move.x += 1;
}
if engine.inputs.keys[SDL_SCANCODE_W].down {
camera_move.y -= 1;
} else if engine.inputs.keys[SDL_SCANCODE_S].down {
camera_move.y += 1;
}
if engine.inputs.mouse_wheel.y > 0 {
camera_zoom = 1;
} else if engine.inputs.mouse_wheel.y < 0 {
camera_zoom = -1;
}
if camera_move != .{} {
engine.camera_main.position += xx (camera_move * engine.frame_stat.delta_time / 4);
}
if camera_zoom != 0 {
engine.camera_main.zoom = clamp(engine.camera_main.zoom + (camera_zoom * engine.frame_stat.delta_time / 100), 0.2, 6);
}
}
}
// :player inputs
player_inputs := *game.player_inputs[PLAYER_INDEX];
if engine.inputs.keys[SDL_SCANCODE_LSHIFT].down == false && ImGui.GetIO().WantCaptureMouse == false {
player_inputs_move: Vector2;
player_inputs_aim: Vector2;
if engine.inputs.controller_was_used {
// if engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_LEFTX] < -CONTROLLER_DEADZONE {
// player_inputs_move.x = -(cast(float) engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_LEFTX] / S16_MIN);
// } else if engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_LEFTX] > CONTROLLER_DEADZONE {
// player_inputs_move.x = +(cast(float) engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_LEFTX] / S16_MAX);
// }
// if engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_LEFTY] < -CONTROLLER_DEADZONE {
// player_inputs_move.y = -(cast(float) engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_LEFTY] / S16_MIN);
// } else if engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_LEFTY] > CONTROLLER_DEADZONE {
// player_inputs_move.y = +(cast(float) engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_LEFTY] / S16_MAX);
// }
if engine.inputs.controllers[PLAYER_CONTROLLER].buttons[SDL_CONTROLLER_BUTTON_DPAD_LEFT].down {
player_inputs_move.x = -1;
} else if engine.inputs.controllers[PLAYER_CONTROLLER].buttons[SDL_CONTROLLER_BUTTON_DPAD_RIGHT].down {
player_inputs_move.x = +1;
}
if engine.inputs.controllers[PLAYER_CONTROLLER].buttons[SDL_CONTROLLER_BUTTON_DPAD_UP].down {
player_inputs_move.y = -1;
} else if engine.inputs.controllers[PLAYER_CONTROLLER].buttons[SDL_CONTROLLER_BUTTON_DPAD_DOWN].down {
player_inputs_move.y = +1;
}
if engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_RIGHTX] < -CONTROLLER_DEADZONE {
player_inputs_aim.x = -(cast(float) engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_RIGHTX] / S16_MIN);
} else if engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_RIGHTX] > CONTROLLER_DEADZONE {
player_inputs_aim.x = +(cast(float) engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_RIGHTX] / S16_MAX);
}
if engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_RIGHTY] < -CONTROLLER_DEADZONE {
player_inputs_aim.y = -(cast(float) engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_RIGHTY] / S16_MIN);
} else if engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_RIGHTY] > CONTROLLER_DEADZONE {
player_inputs_aim.y = +(cast(float) engine.inputs.controllers[PLAYER_CONTROLLER].axes[SDL_CONTROLLER_AXIS_RIGHTY] / S16_MAX);
}
player_inputs.confirm = engine.inputs.controllers[PLAYER_CONTROLLER].buttons[SDL_CONTROLLER_BUTTON_A];
player_inputs.cancel = engine.inputs.controllers[PLAYER_CONTROLLER].buttons[SDL_CONTROLLER_BUTTON_B];
player_inputs.shoulder_left = engine.inputs.controllers[PLAYER_CONTROLLER].buttons[SDL_CONTROLLER_BUTTON_LEFTSHOULDER];
player_inputs.shoulder_right = engine.inputs.controllers[PLAYER_CONTROLLER].buttons[SDL_CONTROLLER_BUTTON_RIGHTSHOULDER];
} else {
if engine.inputs.keys[SDL_SCANCODE_LEFT].down {
player_inputs_aim.x -= 1;
} else if engine.inputs.keys[SDL_SCANCODE_RIGHT].down {
player_inputs_aim.x += 1;
}
if engine.inputs.keys[SDL_SCANCODE_UP].down {
player_inputs_aim.y -= 1;
} else if engine.inputs.keys[SDL_SCANCODE_DOWN].down {
player_inputs_aim.y += 1;
}
if engine.inputs.keys[SDL_SCANCODE_A].down {
player_inputs_move.x -= 1;
} else if engine.inputs.keys[SDL_SCANCODE_D].down {
player_inputs_move.x += 1;
}
if engine.inputs.keys[SDL_SCANCODE_W].down {
player_inputs_move.y -= 1;
} else if engine.inputs.keys[SDL_SCANCODE_S].down {
player_inputs_move.y += 1;
}
player_inputs.confirm = engine.inputs.keys[SDL_SCANCODE_SPACE];
player_inputs.cancel = engine.inputs.keys[SDL_SCANCODE_ESCAPE];
player_inputs.shoulder_left = engine.inputs.keys[SDL_SCANCODE_Q];
player_inputs.shoulder_right = engine.inputs.keys[SDL_SCANCODE_E];
player_inputs.actions[0] = engine.inputs.keys[SDL_SCANCODE_1];
player_inputs.actions[1] = engine.inputs.keys[SDL_SCANCODE_2];
player_inputs.actions[2] = engine.inputs.keys[SDL_SCANCODE_3];
player_inputs.actions[3] = engine.inputs.keys[SDL_SCANCODE_4];
player_inputs.actions[4] = engine.inputs.keys[SDL_SCANCODE_5];
player_inputs.actions[5] = engine.inputs.keys[SDL_SCANCODE_6];
player_inputs.actions[6] = engine.inputs.keys[SDL_SCANCODE_7];
player_inputs.actions[7] = engine.inputs.keys[SDL_SCANCODE_8];
player_inputs.actions[8] = engine.inputs.keys[SDL_SCANCODE_9];
player_inputs.actions[9] = engine.inputs.keys[SDL_SCANCODE_0];
player_inputs.actions[10] = engine.inputs.keys[SDL_SCANCODE_MINUS];
player_inputs.actions[11] = engine.inputs.keys[SDL_SCANCODE_EQUALS];
}
process_repeater(*player_inputs.move, normalize(player_inputs_move));
process_repeater(*player_inputs.aim, normalize(player_inputs_aim));
}
// :camera main
engine.camera_main.zoom = game.ui.scale * 3;
engine.camera_main.position.x = 88 / 3;
// :camera ui
engine.camera_ui.zoom = game.ui.scale;
hud_offset: Vector2;
if ratio > ui_ratio {
ideal := engine.window_size.y * ui_ratio;
diff := engine.window_size.x - ideal;
hud_offset = .{ engine.window_size.x - ideal, 0 };
} else {
ideal := engine.window_size.x / ui_ratio;
diff := engine.window_size.y - ideal;
hud_offset = .{ 0, engine.window_size.y - ideal };
}
engine.camera_ui.position.x = cast(float) +engine.window_size.x / 2 - cast(float)hud_offset.x/2;
engine.camera_ui.position.y = cast(float) -engine.window_size.y / 2 + cast(float)hud_offset.y/2;
game.ui.hud_size = Vector2.{ 192, 270 };
game.ui.hud_position = hud_offset/2/game.ui.scale;
previous_player_position := game.player_position;
previous_player_direction := game.player_direction;
game.cursor = SystemCursor.ARROW;
defer set_cursor(game.cursor);
GAME_VIEW_SIZE : s32 : 80;
game_size := GAME_VIEW_SIZE * 3 * game.ui.scale;
if game.ui.settings && ImGui.Begin("Settings (F1)", null, .NoResize | .NoMove | .NoCollapse | .NoSavedSettings | .NoBringToFrontOnFocus) {
// ImGui.SetWindowSize(.{ cast(float) engine.window_size.x * pixel_density, console_height }, .Always);
ImGui.SetWindowPos(.{ 40, 40 }, .Always);
ImGui.Text("Controls:");
ImGui.Indent();
ImGui.Text("WASD: move");
ImGui.Text("QE: turn");
ImGui.Text("1-4 / wheel: change creature");
ImGui.Text("Mouse: aim (during battles)");
ImGui.Text("F1: toggle this menu");
ImGui.Unindent();
ImGui.Text("Video:");
ImGui.Indent();
if ImGui.RadioButton("Windowed", engine.window_fullscreen_mode == .WINDOWED) {
set_fullscreen_mode(.WINDOWED);
}
if ImGui.RadioButton("Fullscreen", engine.window_fullscreen_mode == .FULLSCREEN) {
set_fullscreen_mode(.FULLSCREEN);
}
ImGui.Text("Resolution:");
if ImGui.Button("960x540 ") { set_window_size(.{ 960, 540 }); }
if ImGui.Button("1280x720 ") { set_window_size(.{ 1280, 720 }); }
if ImGui.Button("1920x1080") { set_window_size(.{ 1920, 1080 }); }
if ImGui.Button("2560x1440") { set_window_size(.{ 2560, 1440 }); }
if ImGui.Button("3840x2160") { set_window_size(.{ 3840, 2160 }); }
ImGui.Unindent();
ImGui.Text("Audio:");
ImGui.Indent();
{
ImGui.SetNextItemWidth(100);
volume := engine.audio.volume_main;
if ImGui.SliderFloat("Main", *volume, 0, 1) {
audio_set_master_volume(volume);
}
}
{
ImGui.SetNextItemWidth(100);
volume := Sound_Player.mix_levels[Sound_Category.MUSIC];
if ImGui.SliderFloat("Music", *volume, 0, 1) {
audio_set_music_volume(volume);
}
}
{
ImGui.SetNextItemWidth(100);
volume := Sound_Player.mix_levels[Sound_Category.GENERAL_SFX];
if ImGui.SliderFloat("Sounds", *volume, 0, 1) {
audio_set_sound_volume(volume);
}
}
{
ImGui.SetNextItemWidth(100);
volume := Sound_Player.mix_levels[Sound_Category.FOOTSTEPS];
if ImGui.SliderFloat("Footsteps", *volume, 0, 1) {
audio_set_footstep_volume(volume);
}
}
ImGui.Unindent();
ImGui.End();
}
// :render letterbox
ui_push_rect(.{ -cast(s32)game.ui.hud_position.x, 0 }, .{ cast(s32)game.ui.hud_position.x, engine.window_size.y}, COLOR_BLACK, layer_id = 3, z_index = 71);
ui_push_rect(.{ UI_RESOLUTION.x, 0 }, .{ cast(s32)game.ui.hud_position.x, engine.window_size.y}, COLOR_BLACK, layer_id = 3, z_index = 71);
// :render hud
ui_push_sprite(.{ 0, 0 }, UI_RESOLUTION, .{ 0, 0 }, UI_RESOLUTION, rect_index = IMG_UI_LAYOUT, layer_id = 2);
// :render party
{
game.ui.party_size = Vector2.{ 92, 128 };
game.ui.party_position = .{ 293, 21 };
// ui_push_rect(make_vector2s(game.ui.party_position), make_vector2s(game.ui.party_size), .{ 1, 1, 1, 0.1 }, z_index = 71);
position := game.ui.party_position;
party_member_selected := game.battle.party_member_current;
member_position := make_vector2s(position);
for party_member, party_index : game.party {
creature := CREATURES[party_member.creature_id];
creature_part := creature.parts[party_member.creature_part_id];
icon_size := Vector2s.{ PARTY_MEMBER_SIZE.y, PARTY_MEMBER_SIZE.y };
is_selected_member := game.battle.party_member_current == party_index;
ui_push_sprite(member_position + .{ 7, 5 }, CREATURE_TILE_SIZES[0] * PARTY_ICON_SCALE, .{ CREATURE_TILE_SIZES[0].x * cast(s32)party_member.creature_id, 0 }, CREATURE_TILE_SIZES[0], z_index = 83, rect_index = IMG_CREATURE_ICONS);
if party_member.health.current <= 0 {
ui_push_sprite(member_position + .{ 7, 5 }, CREATURE_TILE_SIZES[0] * PARTY_ICON_SCALE, .{ 0, 64 }, CREATURE_TILE_SIZES[0], z_index = 84, rect_index = IMG_SPRITESHEET);
}
if is_selected_member {
ui_push_sprite(member_position - .{ 1, 1 }, .{ 96, 32 }, .{ 16, 64 }, .{ 96, 32 }, z_index = 81, rect_index = IMG_SPRITESHEET);
}
x := member_position.x + icon_size.x + 2;
y := member_position.y + 7;
ui_push_text(to_temp_string(creature.name), .{ x, y });
ui_push_sprite(.{ x + 47, y - 3 }, .{ 8, 8 }, .{ 32 + 16 * cast(s32)creature.attack_type, 48 }, .{ 8, 8 }, z_index = 82, rect_index = IMG_SPRITESHEET);
y += 6;
progress_h : s32 = 4;
progress_w := PARTY_MEMBER_SIZE.x - icon_size.x - 6;
{
// ui_push_text("hp", .{ x + 48, y - 1 });
progress := cast(float) party_member.health.current / party_member.health.max;
w := cast(s32)(progress_w * progress); if w % 2 != 0 { w += 1; }; // quick hack to make sure we have only pair number
ui_push_rect(.{ x, y }, .{ progress_w, progress_h }, color = #run(hex_string_to_color("#4d402dff")), z_index = 82);
ui_push_rect(.{ x, y }, .{ w, progress_h }, color = COLOR_RED2, z_index = 83);
y += 6;
}
{
// ui_push_text("cd", .{ x + 48, y - 1 });
color := lerp(COLOR_GREEN2, COLOR_GREEN3, timer_progress(game.battle.party_flash_timers[party_index]));
progress := timer_progress(game.battle.party_attack_timers[party_index]);
w := cast(s32)(progress_w * progress); if w % 2 != 0 { w += 1; }; // quick hack to make sure we have only pair number
ui_push_rect(.{ x, y }, .{ progress_w, progress_h }, color = #run(hex_string_to_color("#4d402dff")), z_index = 82);
ui_push_rect(.{ x, y }, .{ w, progress_h }, color = color, z_index = 83);
y += 6;
}
{
button_position := member_position;
button_size := Vector2s.{ PARTY_MEMBER_SIZE.x, icon_size.y };
pressed, hovered := ui_button(button_position, button_size);
if pressed {
party_member_selected = party_index;
}
}
member_position.y += PARTY_MEMBER_SIZE.y + 2;
}
for action_index : 0..3 {
if player_inputs.actions[action_index].pressed {
party_member_selected = action_index;
break;
}
}
if engine.inputs.mouse_wheel.y != 0 {
if engine.inputs.mouse_wheel.y > 0 {
party_member_selected -= 1;
if party_member_selected < 0 {
party_member_selected = game.party.count-1;
}
} else {
party_member_selected += 1;
if party_member_selected > game.party.count-1 {
party_member_selected = 0;
}
}
}
if party_member_selected != game.battle.party_member_current {
game.battle.party_member_current = party_member_selected;
// play_sound(SFX_CONFIRM, false);
}
}
// :render minimap
{
game.ui.minimap_size = Vector2.{ 57, 57 };
game.ui.minimap_position = Vector2.{ 406, 21 };
player_rotation_radian := vector2_to_radians(to_vector2(DIRECTIONS_CARDINAL[xx game.player_direction]));
cell_size := Vector2s.{ 10, 10 };
// ui_push_rect(make_vector2s(game.ui.minimap_position), make_vector2s(game.ui.minimap_size), .{ 1, 1, 1, 0.5 }, z_index = 71, layer_id = 3);
view_distance := Vector2s.{ 2, 2 };
spacing := Vector2s.{ 1, 1 };
if game.level.cells.count {
for y : game.player_position.y-view_distance.x .. game.player_position.y+view_distance.x {
for x : game.player_position.x-view_distance.y .. game.player_position.x+view_distance.y {
cell_position := Vector2s.{ x, y };
cell_relative_position := cell_position - game.player_position + view_distance;
if !is_in_bounds(cell_position, game.level.size) {
cell := CELLS[0];
ui_push_sprite(
window_position = make_vector2s(game.ui.minimap_position) + cell_relative_position * (cell_size + spacing),
size = cell_size,
texture_position = .{ 48, 32 },
texture_size = cell_size,
rect_index = xx IMG_SPRITESHEET,
color = COLOR_WHITE,
z_index = 101,
layer_id = 2,
);
continue;
}
cell_index := grid_position_to_index(cell_position, game.level.size.x);
cell_instance := game.level.cells[cell_index];
cell := CELLS[cell_instance.id];
ui_push_sprite(
window_position = make_vector2s(game.ui.minimap_position) + cell_relative_position * (cell_size + spacing),
size = cell_size,
texture_position = .{ 32, 32 },
texture_size = cell_size,
rect_index = xx IMG_SPRITESHEET,
color = COLOR_WHITE,
z_index = 101,
layer_id = 2,
);
if cell_instance.opened {
continue;
}
for wall, wall_index : cell.walls {
if wall == 0 { continue; }
wall_position := WALLS_OFFSET[wall_index];
wall_scale := ifx(wall_index % 2 == 0) then Vector2.{ 1, 0.2 } else Vector2.{ 0.2, 1 };
wall_color := COLOR_GREY;
if wall == 2 { wall_color = COLOR_RED; }
if wall == 3 { wall_color = COLOR_BLUE; }
ui_push_sprite(
window_position = make_vector2s(game.ui.minimap_position) + cell_relative_position * (cell_size + spacing),
size = cell_size,
texture_position = .{ 48, 32 },
texture_size = cell_size,
rect_index = xx IMG_SPRITESHEET,
color = COLOR_WHITE,
z_index = 102,
layer_id = 2,
);
}
// encounter
if cell_instance.encounter.type {
color: Color;
if #complete cell_instance.encounter.type == {
case .NONE; { }
case .CREATURE; { color = CREATURES[cell_instance.encounter.creature_id].color; }
case .STARTER; { color = CREATURES[cell_instance.encounter.creature_id].color; }
case .PICKUP; { color = PICKUPS[cell_instance.encounter.pickup_id].color; }
}
ui_push_sprite(
window_position = make_vector2s(game.ui.minimap_position) + cell_relative_position * (cell_size + spacing),
size = cell_size,
texture_position = .{ 80, 32 },
texture_size = cell_size,
rect_index = xx IMG_SPRITESHEET,
color = color,
z_index = 110,
layer_id = 2,
);
}
}
}
}
// player
ui_push_sprite(
window_position = make_vector2s(game.ui.minimap_position) + (cell_size + spacing) * view_distance,
size = cell_size,
rotation = player_rotation_radian,
texture_position = .{ 64, 32 },
texture_size = cell_size,
rect_index = xx IMG_SPRITESHEET,
color = COLOR_WHITE,
z_index = 111,
layer_id = 2,
);
#if false {
fov_position := Vector2s.{ HUD_PADDING*2, HUD_PADDING*2 };
for cell_instance, cell_index : game.field_of_view {
cell := CELLS[cell_instance.id];
cell_view_position := grid_index_to_position(cell_index, VIEW_SIZE);
// floor
ui_push_sprite(
window_position = fov_position + cell_view_position * GRID_SIZE_V2S,
size = GRID_SIZE_V2S,
texture_position = .{ 0, 0 },
texture_size = GRID_SIZE_V2S,
rect_index = xx IMG_SPRITESHEET,
color = .{ 0.2, 0.2, 0.2, 0.8},
z_index = 100,
layer_id = 2,
);
for wall, wall_index : cell.walls {
if wall == 0 { continue; }
wall_position := WALLS_OFFSET[wall_index];
wall_scale := ifx(wall_index % 2 == 0) then Vector2.{ 1, 0.2 } else Vector2.{ 0.2, 1 };
wall_color := COLOR_GREY;
if wall == 2 { wall_color = COLOR_RED; }
if wall == 3 { wall_color = COLOR_BLUE; }
ui_push_sprite(
window_position = fov_position + make_vector2s((to_vector2(cell_view_position) + wall_position) * GRID_SIZE_V2),
size = make_vector2s(GRID_SIZE_V2 * wall_scale),
texture_position = .{ 0, 0 },
texture_size = GRID_SIZE_V2S,
rect_index = xx IMG_SPRITESHEET,
color = wall_color,
z_index = 101,
layer_id = 2,
);
}
}
}
}
// :render inventory
{
game.ui.inventory_size = Vector2.{ 50, 32 };
game.ui.inventory_position = Vector2.{ 407, 104 };
game.ui.inventory_collar_pressed = false;
if game.collar {
icon_position := make_vector2s(game.ui.inventory_position) + .{ 10, 1 };
icon_size := Vector2s.{ 32, 32 };
is_in_battle := game.battle.foe_creature_id;
foe_creature := CREATURES[game.battle.foe_creature_id];
required_part_broken := game.battle.foe_parts_health[foe_creature.required_for_capture].current == 0;
if is_in_battle && required_part_broken {
ui_push_sprite(icon_position, icon_size, .{ 32, 96 }, .{ 32, 32 }, z_index = 100);
push_pop_text_color(#run(hex_string_to_color("#ec3157ff")));
ui_push_text("CAPTURE", .{ 418, 98 }, layer_id = 2);
} else {
ui_push_sprite(icon_position, icon_size, .{ 0, 96 }, .{ 32, 32 }, z_index = 100);
}
pressed, hovered := ui_button(icon_position, icon_size);
if pressed {
if !is_in_battle {
battle_log("No creature to use this on.");
} else if !required_part_broken {
battle_log(tprint("% is not weak enough!", to_temp_string(foe_creature.name)));
} else {
game.ui.inventory_collar_pressed = pressed;
}
}
}
}
// :render foe
for render_foe : 0..0 {
game.ui.foe_size = Vector2.{ 55, 44 };
game.ui.foe_position = Vector2.{ 297, 151 };
// ui_push_rect(make_vector2s(game.ui.foe_position), make_vector2s(game.ui.foe_size), .{ 1, 1, 1, 0.1 }, z_index = 71);
if game.battle.foe_creature_id == 0 {
break render_foe;
}
foe_creature := CREATURES[game.battle.foe_creature_id];
base_x := cast(s32)game.ui.foe_position.x + HUD_PADDING;
base_y := cast(s32)game.ui.foe_position.y + HUD_PADDING;
ui_push_sprite(.{ base_x, base_y - 1 }, CREATURE_TILE_SIZES[0] * PARTY_ICON_SCALE, .{ CREATURE_TILE_SIZES[0].x * cast(s32) game.battle.foe_creature_id, 0 }, CREATURE_TILE_SIZES[0], z_index = 81, rect_index = IMG_CREATURE_ICONS);
x: s32 = base_x + CREATURE_TILE_SIZES[0].x * PARTY_ICON_SCALE + HUD_PADDING;
y: s32 = base_y + 3;
ui_push_text(to_temp_string(foe_creature.name), .{ x, y });
x += 34;
ui_push_sprite(.{ x, y-4 }, .{ 8, 8 }, .{ 32 + 16 * cast(s32)foe_creature.attack_type, 48 }, .{ 8, 8 }, z_index = 84, rect_index = IMG_SPRITESHEET);
x += 12;
progress_h: s32 = 4;
{
y -= 1;
progress_w: s32 = 82;
progress := timer_progress(game.battle.foe_attack_timer);
w := cast(s32)(progress_w * progress); if w % 2 != 0 { w += 1; }; // quick hack to make sure we have only pair number
// w = clamp(w, 0, progress_w);
ui_push_rect(.{ x, y }, .{ progress_w, progress_h }, color = #run(hex_string_to_color("#05060eff")), z_index = 81);
ui_push_rect(.{ x, y }, .{ w, progress_h }, color = COLOR_GREEN2, z_index = 82);
y += 9;
x -= 34 + 12;
}
for health : game.battle.foe_parts_health {
progress_w: s32 = 41;
progress := cast(float)health.current / health.max;
w := cast(s32)(progress_w * progress); // quick hack to make sure we have only pair number
ui_push_rect(.{ x, y }, .{ progress_w, progress_h }, color = #run(hex_string_to_color("#05060eff")), z_index = 81);
ui_push_rect(.{ x, y }, .{ w, progress_h }, color = ifx(game.battle.foe_part_highlighted == it_index) then COLOR_RED3 else COLOR_RED2, z_index = 82);
x += progress_w + 3;
}
}
// :render logs
{
game.ui.logs_size = Vector2.{ 163, 60 };
game.ui.logs_position = Vector2.{ 290, 183 };
x := cast(s32)game.ui.logs_position.x + HUD_PADDING;
y := cast(s32)game.ui.logs_position.y + HUD_PADDING;
for log_index : 0 .. game.logs.count-1 {
color := COLOR_WHITE;
if game.logs[log_index].type == .CRITICAL { color = #run(hex_string_to_color("#90834dff")); }
push_pop_text_color(color);
ui_push_text(to_temp_string(game.logs[log_index].text), .{ x, y });
y += cast(s32) (cast(float) FONT_SIZE * 1.2);
}
// ui_push_rect(make_vector2s(game.ui.logs_position), make_vector2s(game.ui.logs_size), .{ 1, 1, 1, 0.1 }, z_index = 71);
}
defer mode_transition_current_to_next(*game.mode);
if #complete game.mode.current == {
case .INIT; {
if mode_enter(*game.mode) {
game.party = .{};
game.collar = false;
game.fade_color = COLOR_BLACK;
game.clear_color = COLOR_BLACK;
game.levels = .{};
for level : game.ldtk.levels {
fixed_array_add(*game.levels, string_to_fixed_string(level.identifier));
}
game.current_level = level_name_to_index("THE_BEGINNING");
#if SUPER_DEBUG {
game.current_level = level_name_to_index("Test_Encounters");
}
}
if mode_running(game.mode) {
timer_done := engine.now >= game.mode.entered_at + milliseconds_to_apollo(50);
if timer_done {
if game.skip_title {
mode_transition(*game.mode, .WORLD);
} else {
mode_transition(*game.mode, .TITLE);
}
}
}
}
// :title
case .TITLE; {
if mode_enter(*game.mode) {
fade_out(duration = ifx(game.skip_title) then cast(u64)0);
game.party = .{};
game.battle = .{};
game.collar = false;
game.logs = .{};
game.level = .{};
game.player_direction = .NORTH;
}
if mode_running(game.mode) {
level_selected: bool;
push_text_size(8*3);
pop_text_size();
ui_push_text("F1 = Settings / Help", .{ 37, 248 }, layer_id = 0);
ui_push_text(BUILD_VERSION, .{ 242, 248 }, layer_id = 0);
ui_push_text("made by Colin Bellino, Beowulfus Universum, Squirrelsquid", .{ 56, 112 }, layer_id = 0);
push_sprite(.{
position = .{ 0, 0 },
scale = .{ 80, 80 },
texture_size = .{ 80, 80 },
z_index = 10,
layer_id = 0,
rect_index = xx IMG_TITLE,
color = COLOR_WHITE,
});
{ // Start button
button_size := Vector2s.{ 8*9, 14 };
button_position := Vector2s.{ 120, 155 };
pressed, hovered := ui_push_rect_button(button_position, " START", button_size, .{ 0, 10 }, layer_id = 0);
if pressed {
level_selected = true;
}
}
{ // Quit button
button_size := Vector2s.{ 8*9, 14 };
button_position := Vector2s.{ 120, 175 };
if ui_push_rect_button(button_position, " QUIT", button_size, .{ 0, 10 }, layer_id = 0) {
engine.inputs.quit_requested = true;
}
}
if player_inputs.confirm.pressed {
level_selected = true;
}
if level_selected {
fade_in((target: *Effect, step_progress: float, user_data: *void) {
battle_log("You wake up in a cell...");
battle_log("What creature will you save?");
mode_transition(*game.mode, .WORLD);
});
}
}
}
// :world
case .WORLD; {
if mode_enter(*game.mode) {
game.world = .{};
// random_seed(*game.random, game.seed);
random_seed(*game.random, cast(u64) to_milliseconds(current_time_consensus()));
for :for_ptr party_member : game.party {
party_member.health.current = party_member.health.max;
}
game.effects = .{};
#if DEBUG {
game.ldtk = ldtk_load(#run load_asset_path("levels.ldtk"), game.arena.allocator,, logger = noop_logger);
game.levels = .{};
for level : game.ldtk.levels {
fixed_array_add(*game.levels, string_to_fixed_string(level.identifier));
}
if game.current_level == level_name_to_index("Test_Encounters") {
fixed_array_add(*game.party, Party_Member.{ creature_id = 1, health = .{ CREATURES[1].health, CREATURES[1].health } });
fixed_array_add(*game.party, Party_Member.{ creature_id = 2, health = .{ CREATURES[2].health, CREATURES[2].health } });
fixed_array_add(*game.party, Party_Member.{ creature_id = 3, health = .{ CREATURES[3].health, CREATURES[3].health } });
}
}
assert(game.levels.count > 0);
assert(game.current_level < game.levels.count);
load_level(game.ldtk, *game.level, to_temp_string(game.levels[game.current_level]));
game.player_position = game.level.player_spawn_position;
game.player_direction = game.level.player_spawn_direction;
calculate_field_of_view(*game.field_of_view, game.level, game.player_position, game.player_direction);
if game.sounds[SFX_MUSIC1_INTRO].stream == null && game.sounds[SFX_MUSIC1].stream == null {
play_music(SFX_MUSIC1_INTRO, repeating = false);
}
fade_out((target: *Effect, step_progress: float, user_data: *void) {
});
}
if mode_running(game.mode) {
if is_game_over() {
battle_log("Your party was wiped...");
mode_transition(*game.mode, .GAME_OVER);
}
if audio_stream_progress(game.sounds[SFX_MUSIC1_INTRO]) >= 1 && game.sounds[SFX_MUSIC1].stream == null {
audio_stop(game.sounds[SFX_MUSIC1_INTRO]);
play_music(SFX_MUSIC1);
}
{
defer mode_transition_current_to_next(*game.world_mode);
if #complete game.world_mode.current == {
case .EXPLORATION; {
if mode_enter(*game.world_mode) {
game.force_fov_update = true;
}
if mode_running(*game.world_mode) {
if player_inputs.move.value != .{} {
player_move: Vector2s;
if player_inputs.move.value.y < 0 {
player_move = DIRECTIONS_CARDINAL[game.player_direction];
}
if player_inputs.move.value.y > 0 {
player_move = DIRECTIONS_CARDINAL[(game.player_direction+2)%4];
}
if player_inputs.move.value.x > 0 {
dir := game.player_direction + 1;
if dir > 3 { dir = 0; }
player_move = DIRECTIONS_CARDINAL[dir];
}
if player_inputs.move.value.x < 0 {
dir := game.player_direction - 1;
if dir < 0 { dir = 3; }
player_move = DIRECTIONS_CARDINAL[dir];
}
if player_move != .{} {
destination := game.player_position + player_move;
valid_destination := true;
valid_destination &= is_in_bounds(destination, game.level.size);
cell_index := grid_position_to_index(destination, game.level.size.x);
cell := *game.level.cells[cell_index];
valid_destination &= cell.id == 0 || cell.opened;
has_encounter := cell.encounter.type;
if has_encounter {
direction := destination - game.player_position;
turn_direction: Direction_Cardinal;
for DIRECTIONS_CARDINAL {
if it == direction {
turn_direction = cast(Direction_Cardinal) it_index;
break;
}
}
if #complete cell.encounter.type == {
case .NONE; {}
case .CREATURE; {
if game.party.count == 0 {
battle_log("Too dangerous to approach by yourself!");
valid_destination = false;
} else {
creature := CREATURES[cell.encounter.creature_id];
game.player_direction = turn_direction;
game.battle = .{};
game.battle.cell_index = cell_index;
game.battle.foe_creature_id = cell.encounter.creature_id;
game.battle.party_attack_timers.count = game.party.count;
for part : creature.parts {
fixed_array_add(*game.battle.foe_parts_health, .{ part.health, part.health });
}
battle_log(tprint("% approaches!", to_temp_string(creature.name)));
play_sound(creature.encounter_sfx);
mode_transition(*game.world_mode, .BATTLE);
valid_destination = false;
}
}
// :pickup
case .PICKUP; {
pickup := PICKUPS[cell.encounter.pickup_id];
if #complete pickup.effect == {
case .NONE; {}
case .DAMAGE; {
play_sound(PICKUPS[cell.encounter.pickup_id].encounter_sfx);
battle_log("Your party triggered a trap!", .CRITICAL);
for :for_ptr party_member, party_member_index : game.party {
damage_party_member(party_member_index, 5, .TAIL);
}
cell.encounter.pickup_id = 6;
valid_destination &= true;
}
case .HEAL; {
play_sound(PICKUPS[cell.encounter.pickup_id].encounter_sfx);
battle_log("Your party was healed.");
for :for_ptr party_member : game.party {
heal_party_member(party_member, MAX_HEALTH);
}
cell.encounter.pickup_id = 7;
valid_destination = false;
}
case .DOOR; {
play_sound(SFX_ERROR);
battle_log("The door is locked...");
valid_destination = false;
}
case .KEY; {
play_sound(PICKUPS[cell.encounter.pickup_id].encounter_sfx);
battle_log("Somewhere, a door opened...");
game.level.cells[cell.encounter.target_cell_index].id = 3; // 1 = wall, 2 = door closed, 3 = door opened
game.level.cells[cell.encounter.target_cell_index].opened = true;
game.level.cells[cell.encounter.target_cell_index].encounter = .{};
cell.encounter = .{};
}
case .COLLAR; {
if game.collar {
play_sound(SFX_ERROR);
battle_log("You already have a collar...");
valid_destination = false;
} else {
play_sound(PICKUPS[cell.encounter.pickup_id].encounter_sfx);
battle_log("You pickup a collar...");
cell.encounter = .{};
game.collar = true;
valid_destination = false;
}
}
case .STAIRS; {
current_world_depth := game.ldtk.levels[game.current_level].worldDepth;
next_levels: [..]int;
for level, level_index : game.ldtk.levels {
if level.worldDepth == current_world_depth + 1 {
array_add(*next_levels, level_index);
}
}
if next_levels.count == 0 {
battle_log("Your party escaped the dungeon! \\o/");
mode_transition(*game.mode, .TITLE);
} else {
play_sound(PICKUPS[cell.encounter.pickup_id].encounter_sfx);
battle_log("Your party moves on to the next floor...");
next_level := next_levels[cast(int) random_get_within_range(*game.random, 0, xx next_levels.count)];
game.current_level = next_level;
fade_in((target: *Effect, step_progress: float, user_data: *void) {
mode_transition(*game.mode, .WORLD);
});
}
}
}
}
// :starter effect
case .STARTER; {
creature := CREATURES[cell.encounter.creature_id];
target_cell_index := cell.encounter.target_cell_index;
// FIXME: get the stats from the CREATURES data
fixed_array_add(*game.party, Party_Member.{ creature_id = cell.encounter.creature_id, health = .{ creature.health, creature.health } });
for *cell : game.level.cells {
if cell.encounter.type == .STARTER {
cell.encounter = .{};
}
}
play_sound(creature.encounter_sfx);
battle_log(tprint("% joined your party.", to_temp_string(creature.name)));
// copy pasted from .KEY
// play_sound(PICKUPS[cell.encounter.pickup_id].encounter_sfx);
battle_log("Somewhere, a door opened...");
game.level.cells[target_cell_index].id += 1; // n = wall, n+1 = door closed, n+2 = door opened
game.level.cells[target_cell_index].opened = true;
game.level.cells[target_cell_index].encounter = .{};
cell.encounter = .{};
valid_destination = false;
}
}
game.force_fov_update = true;
}
if valid_destination {
game.player_position = destination;
audio_play_sound(.[game.sounds[SFX_FOOTSTEP1], game.sounds[SFX_FOOTSTEP2], game.sounds[SFX_FOOTSTEP3]], true);
}
}
}
if player_inputs.shoulder_left.pressed {
game.player_direction -= 1;
if game.player_direction < 0 { game.player_direction = 3; }
}
if player_inputs.shoulder_right.pressed {
game.player_direction += 1;
if game.player_direction > 3 { game.player_direction = 0; }
}
game.force_fov_update |= previous_player_position != game.player_position || previous_player_direction != game.player_direction;
if game.force_fov_update {
calculate_field_of_view(*game.field_of_view, game.level, game.player_position, game.player_direction);
game.force_fov_update = false;
}
}
}
case .BATTLE; {
party_member := game.party[game.battle.party_member_current];
party_creature := CREATURES[party_member.creature_id];
foe_creature := CREATURES[game.battle.foe_creature_id];
if mode_enter(*game.world_mode) {
game.battle.started = true;
timer_start_now(*game.battle.foe_attack_timer, foe_creature.attack_cooldown);
for party_member, party_index : game.party {
timer_start_now(*game.battle.party_attack_timers[party_index], CREATURES[party_member.creature_id].attack_cooldown);
}
}
// :battle running
if mode_running(*game.world_mode) {
fire_pressed: bool;
quit_pressed: bool;
capture_pressed: bool;
cursor_window_position := engine.inputs.mouse_position;
cursor_position := window_to_world_position(engine.inputs.mouse_position);
if engine.inputs.mouse_keys[Mouse_Button.LEFT].released && !ui_is_hovering_anything() {
fire_pressed = true;
}
if game.ui.inventory_collar_pressed {
capture_pressed = true;
}
#if DEBUG {
if player_inputs.cancel.down {
quit_pressed = true;
}
}
foe_attack_progress := timer_progress(game.battle.foe_attack_timer);
if foe_attack_progress == 1 {
target_party_index := cast(int) random_get_within_range(*game.random, 0, xx game.party.count);
// We just can just retry a random target next frame if this one is dead... It's not great but this is a game jam
if game.party.data[target_party_index].health.current > 0 {
damage_party_member(target_party_index, foe_creature.attack_damage, foe_creature.attack_type);
play_sound(foe_creature.attack_sfx, false);
timer_start_now(*game.battle.foe_attack_timer, foe_creature.attack_cooldown);
}
}
foe_part_id := -1;
for part, part_index : foe_creature.parts {
part_position := to_vector2(part.position);
part_size := to_vector2(part.size);
// #if DEBUG {
// PART_COLORS :: Color.[
// .{ 1, 0, 0, 0.2 },
// .{ 0, 1, 0, 0.2 },
// .{ 0, 0, 1, 0.2 },
// .{ 0, 1, 1, 0.2 },
// ];
// push_sprite(.{
// position = part_position,
// scale = part_size,
// texture_position = .{ 0, 0 },
// texture_size = GRID_SIZE_V2S,
// rect_index = xx IMG_SPRITESHEET,
// color = PART_COLORS[part_index],
// layer_id = 3,
// z_index = 999,
// });
// }
collides := aabb_collides(make_vector2s(cursor_position), .{ 1, 1 }, make_vector2s(part_position - part_size/2), make_vector2s(part_size));
if collides {
if foe_part_id > -1 && foe_creature.parts[foe_part_id].priority > foe_creature.parts[part_index].priority {
continue;
}
if game.battle.foe_parts_health[part_index].current == 0 {
continue;
}
foe_part_id = part_index;
}
}
game.battle.foe_part_highlighted = foe_part_id;
if game.battle.foe_part_highlighted > -1 {
ui_push_text(tprint("TARGET: %", foe_creature.parts[foe_part_id].type), .{ 226, 248 }, layer_id = 0);
}
attack_ready := -1;
for attack_timer, party_index : game.battle.party_attack_timers {
if timer_progress(attack_timer) == 1 && !game.battle.party_attack_sound_played[party_index] {
game.battle.party_attack_sound_played[party_index] = true;
attack_ready = party_index;
}
}
if attack_ready > -1 {
play_sound(SFX_ATTACK_READY);
timer_start_now(*game.battle.party_flash_timers[attack_ready], 200);
}
if fire_pressed {
attack_timer := *game.battle.party_attack_timers[game.battle.party_member_current];
if timer_progress(attack_timer) == 1 && foe_part_id > -1 {
timer_start_now(*game.battle.foe_part_flashing_timer, 200);
for_creature_part := foe_creature.parts[foe_part_id];
game.battle.foe_part_flashing = foe_part_id;
game.battle.party_attack_sound_played[game.battle.party_member_current] = false;
damage_foe_part(party_creature.id, game.battle.foe_creature_id, foe_part_id, party_creature.attack_damage, party_creature.attack_type, to_vector2(GAME_VIEW_CENTER + make_vector2s(cursor_position * engine.camera_ui.zoom)));
play_sound(party_creature.attack_sfx, false);
broken_parts: int;
for health : game.battle.foe_parts_health {
if health.current <= 0 {
broken_parts += 1;
}
}
if broken_parts == game.battle.foe_parts_health.count {
log_error("End of battle.");
game.level.cells[game.battle.cell_index].encounter = .{};
play_sound(foe_creature.encounter_sfx, true);
mode_transition(*game.world_mode, .EXPLORATION);
}
timer_start_now(attack_timer, party_creature.attack_cooldown);
} else {
timer_start_now(*game.battle.party_flash_timers[game.battle.party_member_current], 200);
play_sound(SFX_ERROR);
battle_log(tprint("% is not ready yet!", to_temp_string(party_creature.name)));
}
}
if capture_pressed {
if game.party.count == game.party.data.count {
battle_log("Your party is full!", .CRITICAL);
play_sound(SFX_ERROR, false);
} else {
foe_creature := CREATURES[game.battle.foe_creature_id];
game.collar = false;
play_sound(foe_creature.encounter_sfx);
battle_log(tprint("You tamed %.", to_temp_string(foe_creature.name)));
health_current := 0;
health_max := 0;
for health : game.battle.foe_parts_health {
health_current += health.current;
health_max += health.max;
}
health_progress := cast(float) health_current / health_max;
fixed_array_add(*game.party, Party_Member.{ creature_id = game.battle.foe_creature_id, health = .{ cast(u8) (health_progress * foe_creature.health), foe_creature.health } });
mode_transition(*game.world_mode, .EXPLORATION);
cell := *game.level.cells[game.battle.cell_index];
cell.encounter = .{};
}
}
if quit_pressed {
battle_log("[CHEAT] You fled from the monster D:");
mode_transition(*game.world_mode, .EXPLORATION);
}
}
if mode_exit(*game.world_mode) {
new_party: Fixed_Size_Array(Party_Member, MAX_PARTY_SIZE);
for party_member, party_index : game.party {
if party_member.health.current > 0 {
fixed_array_add(*new_party, party_member);
}
}
game.party = new_party;
game.battle = .{};
arena_reset(game.battle_arena);
}
}
}
}
// :render level name
ui_push_text(to_temp_string(game.levels[game.current_level]), .{ 37, 248 }, layer_id = 0);
// :render game view
{
// background
push_sprite(.{
position = .{ 0, 0 },
scale = .{ 80, 80 },
texture_position = .{ 336+8, 176+8 },
texture_size = .{ 80, 80 },
// scale = .{ 96, 96 },
// texture_position = .{ 160, 176 },
// texture_size = .{ 96, 96 },
rect_index = xx IMG_TILESET1, // TODO: get this from tile data
layer_id = 0,
color = COLOR_WHITE,
z_index = 0,
});
for y : 0..VIEW_SIZE.y-1 {
for x : 0..VIEW_SIZE.x-1 {
cell_index := grid_position_to_index(.{ x, y }, VIEW_SIZE.x);
cell_view_position := Vector2s.{ x-VIEW_SIZE.x/2, y };
cell_instance := game.field_of_view[cell_index];
cell := CELLS[cell_instance.id];
if game.level.hidden_cells[cell_index].hidden { continue; }
// ImGui.Text("cell_view_position: %", cell_view_position);
for wall_id, wall_index : cell.walls {
if game.level.hidden_cells[cell_index].walls_hidden[wall_index] { continue; }
position: Vector2;
texture_position := Vector2s.{ 0, 0 };
texture_size := Vector2s.{ 1, 1 };
z_index := y*3;
texture_data := TILE_TEXTURES_DATA[y];
wall_offset_side: float;
// TODO: hmmmm this is ugly but good enough for now i guess
if cell_view_position.x == 0 {
// if y == 1 { wall_offset_side = cast(float) (texture_data.front_size.x - texture_data.side_size.x/2); }
// if y == 2 { wall_offset_side = 27.5; }
// if y == 3 { wall_offset_side = 42.5; }
} else {
if y == 1 { wall_offset_side = 4; }
if y == 2 { wall_offset_side = 8; }
if y == 3 { wall_offset_side = 24; }
}
is_current_cell := x == VIEW_WIDTH/2 && y == VIEW_HEIGHT-1;
// :render walls
wall_direction := cast(Direction_Cardinal) wall_index;
if #complete wall_direction == {
case .NORTH; {
if cell_view_position.y == 0 { continue; }
if is_current_cell { continue; }
position.x = cast(float) (cell_view_position.x * texture_data.front_size.x);
texture_position += texture_data.front_pos;
texture_size = texture_data.front_size;
z_index -= 1;
}
case .SOUTH; {
if y >= VIEW_SIZE.y-1 { continue; }
wall_i := ifx(cell_view_position.y == 0) then y else y+1;
position.x = cast(float) (cell_view_position.x * TILE_TEXTURES_DATA[wall_i].front_size.x);
texture_position = TILE_TEXTURES_DATA[wall_i].front_pos;
texture_size = TILE_TEXTURES_DATA[wall_i].front_size;
z_index += 1;
// TODO: is there a reason we are doing this in .SOUTH??
// :render encounter
if cell_instance.encounter.type {
texture_position: Vector2s;
texture_size: Vector2s;
creature := CREATURES[cell_instance.encounter.creature_id];
image: Images;
if #complete cell_instance.encounter.type == {
case .NONE; { }
case .STARTER; {
texture_position = CREATURE_TILE_POSITIONS[y];
texture_size = CREATURE_TILE_SIZES[y];
image = cast(Images) IMG_STARTER1 + cast(Images) cell_instance.encounter.creature_id-1;
};
case .CREATURE; {
texture_position = CREATURE_TILE_POSITIONS[y];
texture_size = CREATURE_TILE_SIZES[y];
image = creature.image;
}
case .PICKUP; {
texture_position = CREATURE_TILE_POSITIONS[y];
texture_size = CREATURE_TILE_SIZES[y];
image = PICKUPS[cell_instance.encounter.pickup_id].image;
}
}
if image != IMG_NONE {
// :render parts
render_individual_parts := x == VIEW_WIDTH/2 && y == VIEW_HEIGHT/2 && cell_instance.encounter.type == .CREATURE;
render_starter_name := x == VIEW_WIDTH/2 && y == VIEW_HEIGHT/2 && cell_instance.encounter.type == .STARTER;
if render_individual_parts { // render each parts (for battle)
part_image_size :: Vector2s.{ 96, 96 };
part_image_offset :: Vector2s.{ 0, 0 };
// Yes the code is ugly, stolen from https://easings.net/#easeInOutElastic
ease_in_out_elastic :: (x: float) -> float {
c5 := (2 * PI) / 4.5;
return ifx(x == 0)
then 0
else ifx(x == 1)
then 1
else ifx(x < 0.5)
then -(pow(2, 20 * x - 10) * sin((20 * x - 11.125) * c5)) / 2
else (pow(2, -20 * x + 10) * sin((20 * x - 11.125) * c5)) / 2 + 1;
}
for part, part_index : creature.parts {
flash_progress := timer_progress(game.battle.foe_part_flashing_timer);
is_part_flashing := part_index == game.battle.foe_part_flashing && flash_progress < 1;
is_part_dead := game.battle.foe_parts_health[part_index].current == 0;
animation_offset := Vector2.{ 0, 0 };
if is_part_flashing { animation_offset.x = ease_in_out_elastic(flash_progress); }
color := COLOR_WHITE;
if is_part_flashing { color = COLOR_RED3; }
if is_part_dead { color = COLOR_GREY; }
push_sprite(.{
position = position + to_vector2(part_image_offset) + animation_offset,
scale = to_vector2(part_image_size),
texture_position = .{ 0, 0 },
texture_size = part_image_size,
rect_index = xx part.image,
layer_id = 0,
color = color,
z_index = 10 + z_index + part.z_index*3,
});
is_part_highlighted := game.battle.started && part_index == game.battle.foe_part_highlighted;
if is_part_highlighted && !is_part_dead {
push_sprite(.{
position = position + to_vector2(part_image_offset) + animation_offset,
scale = to_vector2(part_image_size),
texture_position = .{ 0, 0 },
texture_size = part_image_size,
rect_index = xx (part.image+1),
layer_id = 0,
color = .{ 1, 1, 1, 0.5 },
z_index = 10 + z_index + part.z_index*3 + 1,
});
}
}
} else {
push_sprite(.{
position = position,
scale = to_vector2(texture_size),
texture_position = texture_position,
texture_size = texture_size,
rect_index = xx image,
layer_id = 0,
color = .{ 1, 1, 1, 1 },
z_index = 10 + z_index,
});
}
if render_starter_name {
ui_push_text(to_temp_string(CREATURES[cell_instance.encounter.creature_id].name), .{ 140, 80 });
}
}
}
}
case .WEST; {
if cell_view_position.y == 0 { continue; }
if cell_view_position.x < 0 { continue; }
if is_current_cell { continue; }
position.x = cast(float) (cell_view_position.x * texture_data.front_size.x);
offset := 0.0;
if cell_view_position.x == 0 {
offset -= wall_offset_side;
} else {
offset += wall_offset_side;
if abs(cell_view_position.x) == 2 { offset -= 32; }
}
position.x += offset * -1;
texture_position += texture_data.west_pos;
texture_size = texture_data.side_size;
}
case .EAST; {
if cell_view_position.y == 0 { continue; }
if cell_view_position.x > 0 { continue; }
if is_current_cell { continue; }
position.x = cast(float) (cell_view_position.x * texture_data.front_size.x);
offset := 0.0;
if cell_view_position.x == 0 {
offset -= wall_offset_side;
} else {
offset += wall_offset_side;
if abs(cell_view_position.x) == 2 { offset -= 32; }
}
position.x += offset;
texture_position += texture_data.east_pos;
texture_size = texture_data.side_size;
}
}
// ImGui.Text("- wall: % tpos: % tsize: % pos: %", wall_direction, texture_position, texture_size, position);
if wall_id > 0 {
push_sprite(.{
position = position,
scale = to_vector2(texture_size),
texture_position = texture_position,
texture_size = texture_size,
rect_index = xx TILE_TEXTURES[wall_id],
layer_id = 0,
color = .{ 1, 1, 1, 1 },
z_index = 10 + z_index,
});
}
}
}
}
}
// :render effects
for effect : game.effects {
context._Tracy.ZoneScoped("render_effects");
if effect.active == false { continue; }
ui_push_sprite(
window_position = make_vector2s(effect.position),
size = effect.size,
texture_position = effect.texture_position,
texture_size = effect.texture_size,
rect_index = xx effect.rect_index,
layer_id = effect.layer_id,
color = effect.color,
z_index = effect.z_index,
);
}
}
if mode_exit(*game.mode) {
audio_stop(game.sounds[SFX_MUSIC1]);
}
}
// :game over
case .GAME_OVER; {
if mode_enter(*game.mode) {
audio_stop(game.sounds[SFX_MUSIC1]);
}
if mode_running(game.mode) {
push_sprite(.{
position = .{ 0, 0 },
scale = .{ 80, 80 },
texture_size = .{ 80, 80 },
z_index = 999,
layer_id = 1,
rect_index = xx IMG_GAME_OVER,
color = COLOR_WHITE,
});
if player_inputs.confirm.pressed || engine.inputs.mouse_keys[Mouse_Button.LEFT].pressed {
fade_in((target: *Effect, step_progress: float, user_data: *void) {
mode_transition(*game.mode, .TITLE);
});
}
}
}
}
animation_update();
// if ImGui.Begin("Help") {
// ImGui.Text("Controls:");
// ImGui.Indent();
// ImGui.Text("WASD: move");
// ImGui.Text("QE: turn");
// ImGui.Text("~: open the console/logs");
// ImGui.Text("F1: open the debug menu");
// ImGui.Text("F2: open the animation menu");
// ImGui.Text("CTRL + R: restart the game (reload the levels as well)");
// ImGui.Unindent();
// }
// ImGui.End();
// :render screen transition
ui_push_rect(.{ 0, 0 }, UI_RESOLUTION*2, color = game.fade_color, z_index = MAX_Z_INDEX-1, layer_id = 1);
// :render floating messages
for :for_ptr message : game.floating_messages {
if message.timer.start == .{} { continue; }
if message.timer.start > engine.now { continue; }
push_pop_text_color(message.color);
push_pop_text_size(message.size);
ui_push_text(to_temp_string(message.text), make_vector2s(message.window_position));
progress := timer_progress(message.timer);
if progress == 1 {
message.timer = .{};
}
}
engine_ui_main_menu_push_window("Game Debug", *game.debug_ui_debug, game_ui_window_debug);
engine_ui_main_menu_push_window("Animation", *engine.debug_ui_animation, animation_ui_window);
return engine.inputs.quit_requested;
}
#program_export
unload :: () -> *Game {
push_context,defer_pop game.ctx;
engine_unload();
return game;
}
#program_export
reload :: (memory: *Game) {
assert(memory != null);
game = cast(*Game) memory;
push_context,defer_pop game.ctx;
engine_reload(game.engine);
}
play_sound :: (sound_id: Sounds, perturb := false, loc := #caller_location) {
audio_play_sound(game.sounds[sound_id], perturb, loc);
}
play_music :: (sound: Sounds, repeating := true, loc := #caller_location) {
audio_play_music(game.sounds[sound], repeating, loc);
}
// :game_ui debug
game_ui_window_debug :: () {
if ImGui.Begin("Game", *game.debug_ui_debug) {
defer ImGui.End();
if ImGui.CollapsingHeader("Common", .DefaultOpen) {
imgui_inspect("game", game);
imgui_inspect("engine", engine);
if ImGui.Button("Title") {
game.skip_title = false;
mode_transition(*game.mode, .TITLE);
}
for level, level_index : game.levels {
ImGui.SameLine();
if level_index && level_index % 5 == 0 { ImGui.NewLine(); }
if ImGui.Button(to_temp_c_string(level)) {
game.current_level = level_index;
mode_transition(*game.mode, .WORLD);
}
}
}
if ImGui.CollapsingHeader("Memory", .DefaultOpen) {
imgui_arena_progress(engine.arena);
imgui_arena_progress(game.arena);
}
if ImGui.CollapsingHeader("Camera: Main", .DefaultOpen) {
ImGui.InputFloat2("position###camera_camera_main_position", xx *engine.camera_main.position);
ImGui.InputFloat("zoom###camera_main_zoom", *engine.camera_main.zoom);
ImGui.InputFloat("rotation###camera_main_rotation", *engine.camera_main.rotation);
imgui_inspect("details", *engine.camera_main);
}
if ImGui.CollapsingHeader("Camera: UI", .DefaultOpen) {
ImGui.InputFloat2("position###camera_camera_ui_position", xx *engine.camera_ui.position);
ImGui.InputFloat("zoom###camera_ui_zoom", *engine.camera_ui.zoom);
ImGui.InputFloat("rotation###camera_ui_rotation", *engine.camera_ui.rotation);
imgui_inspect("details", *engine.camera_ui);
}
if ImGui.CollapsingHeader("Mouse", .DefaultOpen) {
ImGui.Text("position_window: %", engine.inputs.mouse_position);
ImGui.Text("position_world: %", game.mouse_position_world);
ImGui.Text("position_grid: %", game.mouse_position_grid);
}
if ImGui.CollapsingHeader("Inputs") {
imgui_inspect("quit_requested", *engine.inputs.quit_requested);
imgui_inspect("window_resized", *engine.inputs.window_resized);
imgui_inspect("window_is_focused", *engine.inputs.window_is_focused);
imgui_inspect("keyboard_was_used", *engine.inputs.keyboard_was_used);
imgui_inspect("keys", *engine.inputs.keys);
imgui_inspect("mouse_was_used", *engine.inputs.mouse_was_used);
imgui_inspect("mouse_keys", *engine.inputs.mouse_keys);
imgui_inspect("mouse_position", *engine.inputs.mouse_position);
imgui_inspect("mouse_wheel", *engine.inputs.mouse_wheel);
imgui_inspect("mouse_moved", *engine.inputs.mouse_moved);
imgui_inspect("controller_was_used", *engine.inputs.controller_was_used);
imgui_inspect("controllers", *engine.inputs.controllers);
imgui_inspect("player_inputs", *game.player_inputs);
}
if ImGui.CollapsingHeader("Audio", .DefaultOpen) {
ImGui.Text("Musics:");
if ImGui.BeginTable("musics_table", 3, .BordersInnerH | .SizingStretchProp) {
defer ImGui.EndTable();
for sound_asset, sound_id : game.sounds {
if sound_asset == null || sound_asset.category != .MUSIC { continue; }
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.Text(tprint("%", cast(Sounds) sound_id));
ImGui.TableNextColumn();
if sound_asset.stream {
ImGui.ProgressBar(audio_stream_progress(sound_asset), .{ 200, 18 });
// imgui_inspect("stream", sound_asset.stream, true);
} else {
ImGui.Dummy(.{ 200, 0 });
}
ImGui.TableNextColumn();
if sound_asset.stream {
if ImGui.Button("STOP") { audio_stop(game.sounds[sound_id]); }
} else {
if ImGui.Button("START") { audio_play_music(game.sounds[sound_id]); }
}
}
}
ImGui.Text("Sounds:");
if ImGui.BeginTable("sounds_table", 2, .BordersInnerH | .SizingStretchProp) {
defer ImGui.EndTable();
for sound_asset, sound_id : game.sounds {
if sound_asset == null || sound_asset.category == .MUSIC { continue; }
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.Text(tprint("%", cast(Sounds) sound_id));
ImGui.TableNextColumn();
if ImGui.Button("PLAY") {
audio_play_sound(game.sounds[sound_id]);
}
}
}
}
if ImGui.CollapsingHeader("Level", .DefaultOpen) {
if ImGui.Button("show_all") {
for game.level.hidden_cells { game.level.hidden_cells[it_index] = .{}; }
}
for layer : 0..VIEW_HEIGHT-1 {
ImGui.SameLine();
if ImGui.Button(tprint_c("show_layer_%", layer)) {
for cell, cell_index : game.level.hidden_cells {
p := grid_index_to_position(cell_index, VIEW_SIZE);
if p.y == layer {
game.level.hidden_cells[cell_index].hidden = false;
}
}
}
}
if ImGui.Button("hide_all") {
for cell, cell_index : game.level.hidden_cells {
game.level.hidden_cells[cell_index].hidden = true;
for wall, wall_index : game.level.hidden_cells[cell_index].walls_hidden {
// game.level.hidden_cells[cell_index].walls_visible[wall_index] = true;
}
}
}
for layer : 0..VIEW_HEIGHT-1 {
ImGui.SameLine();
if ImGui.Button(tprint_c("hide_layer_%", layer)) {
for cell, cell_index : game.level.hidden_cells {
p := grid_index_to_position(cell_index, VIEW_SIZE);
if p.y == layer {
game.level.hidden_cells[cell_index].hidden = true;
}
}
}
}
if game.level.cells.count {
for cell_instance, view_index : game.field_of_view {
cell := CELLS[cell_instance.id];
cell_state := *game.level.hidden_cells[view_index];
view_position := grid_index_to_position(view_index, VIEW_SIZE);
ImGui.PushID(cast(s32) view_index);
defer ImGui.PopID();
text_color := ImGui.GetStyleColorVec4(.Text).*;
color := text_color;
if cell.walls[0] == 0 && cell.walls[1] == 0 && cell.walls[2] == 0 && cell.walls[3] == 0 { color = ImGui.GetStyleColorVec4(.PlotLines).*; }
ImGui.PushStyleColor(.Text, color);
ImGui.Text("%", view_position);
ImGui.SameLine();
ImGui.Text("| HIDE:");
ImGui.SameLine();
ImGui.Checkbox("CELL", *cell_state.hidden);
for cell_state.walls_hidden {
ImGui.SameLine();
ImGui.Checkbox(tprint_c("% %", cast(Direction_Cardinal) it_index, cast(int) cell.walls[it_index]), *cell_state.walls_hidden[it_index]);
}
ImGui.PopStyleColor(1);
}
}
}
}
}
grid_to_world_position_center :: (grid_position: Vector2s) -> Vector2 {
return grid_to_world_position_center(grid_position, .{ GRID_SIZE, GRID_SIZE });
}
// https://en.wikipedia.org/wiki/Midpoint_circle_algorithm
// https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
plot_circle :: (center: Vector2s, radius: s32) -> []Vector2s {
result: [..]Vector2s;
x: s32;
y: s32 = radius;
d: s32 = 3 - 2 * radius;
array_add(*result, center + .{ +x, +y });
array_add(*result, center + .{ -x, +y });
array_add(*result, center + .{ +x, -y });
array_add(*result, center + .{ -x, -y });
array_add(*result, center + .{ +y, +x });
array_add(*result, center + .{ -y, +x });
array_add(*result, center + .{ +y, -x });
array_add(*result, center + .{ -y, -x });
while y >= x {
if d > 0 {
y -= 1;
d = d + 4 * (x - y) + 10;
} else {
d = d + 4 * x + 6;
}
x += 1;
array_add(*result, center + .{ +x, +y });
array_add(*result, center + .{ -x, +y });
array_add(*result, center + .{ +x, -y });
array_add(*result, center + .{ -x, -y });
array_add(*result, center + .{ +y, +x });
array_add(*result, center + .{ -y, +x });
array_add(*result, center + .{ +y, -x });
array_add(*result, center + .{ -y, -x });
}
return result;
}
manhathan_distance :: (a: Vector2s, b: Vector2s) -> u32 {
return cast(u32) (abs(a.x - b.x) + abs(a.y - b.y));
}
make_effect :: (ui_position := Vector2.{ 0, 0 }, texture_position := Vector2s.{ 0, 0 }, scale := Vector2.{ 1, 1 }, z_index: s32 = 80) -> Effect {
return .{
transform = .{
position = ui_position,
scale = scale,
},
sprite = .{
active = true,
size = GRID_SIZE_V2S,
texture_size = GRID_SIZE_V2S,
texture_position = texture_position,
z_index = z_index,
color = COLOR_WHITE,
},
};
}
// FIXME: enable pan_camera again when we go back to larger levels
pan_camera :: (camera: *Camera_Orthographic, world_position: Vector2, animation_id: Animation_Id) {
// final_animation_id := animation_id;
// if final_animation_id == 0 {
// final_animation_id = animation_make("pan_camera", active = true, self_destroy = true);
// }
// assert(final_animation_id != 0);
// pan_curve := animation_make_curve(final_animation_id, *camera.position, "camera_world_position");
// move_distance := distance(camera.position, world_position) / GRID_SIZE;
// animation_make_frame(pan_curve, camera.position, 0);
// animation_make_frame(pan_curve, world_position, cast(u64) (ANIMATION_DURATION_CAMERA_PAN * move_distance));
}
Done_Callback :: #type (target: *Effect, step_progress: float, user_data: *void);
fade_in :: (callback: Done_Callback = null, user_data: *void = null, color: Color = COLOR_BLACK, duration: u64 = 1000) -> Animation_Id {
return _fade(callback, user_data, "fade_in", COLOR_TRANSPARENT, color, duration);
}
fade_out :: (callback: Done_Callback = null, user_data: *void = null, color: Color = COLOR_BLACK, duration: u64 = 1000) -> Animation_Id {
return _fade(callback, user_data, "fade_out", game.fade_color, COLOR_TRANSPARENT, duration);
}
_fade :: (callback: Done_Callback, user_data: *void, name: string, color_start: Color, color_end: Color, duration: u64 = 1000) -> Animation_Id {
animation_id := animation_make(name, active = true, self_destroy = true);
assert(animation_id != 0);
color_curve := animation_make_curve(animation_id, *game.fade_color, "color");
animation_make_frame(color_curve, color_start, 0);
animation_make_frame(color_curve, color_end, duration);
event_curve := animation_make_curve(animation_id, *game, "events");
animation_make_frame(event_curve, Frame_Event.{ xx callback, user_data }, duration);
return animation_id;
}
ui_push_rect :: (window_position: Vector2s, size: Vector2s = GRID_SIZE_V2S, color : Color = COLOR_WHITE, z_index: s32 = 80, layer_id: s32 = 2, loc := #caller_location) {
push_sprite(.{
position = (to_vector2(window_position) + to_vector2(size/2)) * -1,
scale = to_vector2(size) * -1,
z_index = z_index,
layer_id = layer_id,
rect_index = xx IMG_SPRITESHEET,
color = color,
camera = .UI,
}, loc);
}
ui_push_sprite :: (window_position: Vector2s, size: Vector2s, texture_position: Vector2s, texture_size: Vector2s, rect_index : Images = IMG_SPRITESHEET, z_index : s32 = 80, layer_id: s32 = 2, color := COLOR_WHITE, rotation : float = 0, loc := #caller_location) {
push_sprite(.{
position = (to_vector2(window_position) + to_vector2(size/2)) * -1,
scale = to_vector2(size) * -1,
texture_position = texture_position,
texture_size = texture_size,
rect_index = xx rect_index,
rotation = rotation,
z_index = z_index,
layer_id = layer_id,
color = color,
camera = .UI,
}, loc);
}
ui_push_text :: (text: string, window_position: Vector2s, layer_id: s32 = 2) {
position := Vector2s.{
cast(s32) ((cast(float) window_position.x )),
cast(s32) ((cast(float) window_position.y + engine.text_current_size[engine.text_current_color.count] / 2)),
} + make_vector2s(game.ui.hud_position);
push_text_window(text, position, layer_id);
}
ui_button :: (position: Vector2s, size: Vector2s) -> (pressed: bool, hovered: bool) {
mouse_position := engine.inputs.mouse_position;
scaled_position := make_vector2s((game.ui.hud_position + to_vector2(position)) * game.ui.scale);
scaled_size := Vector2s.{ cast(s32) (size.x * game.ui.scale), cast(s32) (size.y * game.ui.scale) };
hovered := aabb_collides(mouse_position, .{ 1, 1 }, scaled_position, scaled_size);
if hovered {
game.ui.hovered = true;
// game.cursor = .HAND;
}
pressed := hovered && engine.inputs.mouse_keys[Mouse_Button.LEFT].pressed && !ImGui.GetIO().WantCaptureMouse;
return pressed, hovered;
}
ui_push_rect_button :: (button_position: Vector2s, label: string, button_size: Vector2s = .{ 96, 48 }, offset : Vector2s = .{ 28, 30 }, layer_id : s32 = 1) -> (pressed: bool, hovered: bool) {
pressed, hovered := ui_button(button_position, button_size);
button_color := color_alpha(COLOR_WHITE, 0.15);
if hovered { button_color = color_alpha(COLOR_WHITE, 0.3); }
// if pressed { play_sound(SFX_CONFIRM, false); }
ui_push_rect(button_position, button_size, color = button_color, layer_id = layer_id);
push_text_window(label, button_position + offset + make_vector2s(game.ui.hud_position), layer_id = layer_id);
return pressed, hovered;
}
print_cell_instances :: (cells: []Cell_Instance, width: s32) -> string {
new_context := context;
new_context.allocator = temp;
new_context.print_style.default_format_int.minimum_digits = 2;
push_context,defer_pop new_context;
b: String_Builder;
for cell_instance, cell_index : cells {
if cell_index % width == 0 { append(*b, #char "\n"); }
append(*b, tprint("% ", cell_instance.id));
}
return builder_to_string(*b);
}
print_cells :: (cells: []Cell_Id, width: s32) -> string {
new_context := context;
new_context.allocator = temp;
new_context.print_style.default_format_int.minimum_digits = 2;
push_context,defer_pop new_context;
b: String_Builder;
for cell_id, cell_index : cells {
if cell_index % width == 0 { append(*b, #char "\n"); }
append(*b, tprint("% ", cell_id));
}
return builder_to_string(*b);
}
print_level :: (level: *Level, width: s32) -> string {
new_context := context;
new_context.allocator = temp;
new_context.print_style.default_format_int.minimum_digits = 2;
push_context,defer_pop new_context;
b: String_Builder;
for cell, cell_index : level.cells {
if cell_index % width == 0 { append(*b, #char "\n"); }
append(*b, tprint("% ", cell.id));
}
return builder_to_string(*b);
}
rotate_2d_array :: (arr: *[]Cell_Instance, width: s32, diff: s32) {
s := width-1;
for 0 .. diff-1 {
arr_temp := array_copy(arr.*,, temp);
for #v2 < y : 0..s {
for x : 0..s {
dest := grid_position_to_index(.{ cast(s32) x, cast(s32) (s-y) }, width);
src := grid_position_to_index(.{ cast(s32) y, cast(s32) x }, width);
arr_temp[dest] = arr.*[src];
}
}
arr.* = arr_temp;
}
}
calculate_field_of_view :: (field_of_view: *[VIEW_WIDTH*VIEW_HEIGHT]Cell_Instance, level: Level, player_position: Vector2s, player_direction: Direction_Cardinal) {
diff := cast(s32) (player_direction - Direction_Cardinal.NORTH);
// log("% -> % (diff: %)", Direction_Cardinal.NORTH, player_direction, diff);
forward := DIRECTIONS_CARDINAL[player_direction];
right := DIRECTIONS_CARDINAL[(player_direction+1) % 4];
// log("forward: %, right: %", forward, right);
view_index := 0;
for #v2 < y : 0..VIEW_SIZE.y-1 {
for x : -VIEW_SIZE.x/2..VIEW_SIZE.x/2 {
defer view_index += 1;
level_position := player_position + forward*cast(s32)y + right*cast(s32)x;
// log("view_pos: %,% view_index: % level_pos: %", x, y, view_index, level_position);
// log("player_pos: % forward: % right: %", player_position, y, x);
// position := Vector2s.{ x + player_position.x-1, cast(s32)y + player_position.y-2 };
if !is_in_bounds(level_position, level.size) {
field_of_view.*[view_index] = .{};
continue;
}
level_index := grid_position_to_index(level_position, level.size.x);
field_of_view.*[view_index] = level.cells[level_index];
}
}
// log("\nfield_of_view: \n");
// log(print_cells(field_of_view.*, VIEW_SIZE.x));
}
load_level :: (root: *LDTK_Root, level: *Level, level_identifier: string) {
level_indexes: [..]int;
level_indexes.allocator = temp;
level_index := -1;
for level : root.levels {
if level.identifier == level_identifier {
level_index = it_index;
break;
}
}
load_level(root, level, level_index);
}
// :load level
load_level :: (root: *LDTK_Root, level: *Level, level_index: int) {
assert(level_index < root.levels.count, tprint("Level out of bounds: % / %", level_index, root.levels.count));
ldtk_level := *root.levels[level_index];
{
layer := root.defs.layers[Level_Layers.GRID];
layer_instance := ldtk_level.layerInstances[Level_Layers.GRID];
scale := GRID_SIZE / layer.gridSize; // In case the grid size in the tilemap is different from the one we use in the game/renderer
level.size.x = ldtk_level.pxWid / layer.gridSize;
level.size.y = ldtk_level.pxHei / layer.gridSize;
level.cells = NewArray(level.size.x * level.size.y, Cell_Instance);
level.hidden_cells = NewArray(VIEW_WIDTH * VIEW_HEIGHT, Cell_Debug_State);
for grid_value, local_grid_index : layer_instance.intGridCsv {
local_position := grid_index_to_position(local_grid_index, level.size);
level.cells[local_grid_index].id = xx grid_value;
level.cells[local_grid_index].position = local_position;
// level.cells[grid_index].flags = int_grid_csv_to_flags(grid_value);
}
}
{
player_spawn_found := false;
layer_instance := ldtk_level.layerInstances[Level_Layers.ENTITIES];
entity_layer_index := -1;
for layer, i : root.defs.layers {
if layer.uid == layer_instance.layerDefUid {
entity_layer_index = i;
break;
}
}
assert(entity_layer_index > -1, tprint("Can't find layer with uid: %", layer_instance.layerDefUid));
entity_layer := root.defs.layers[entity_layer_index];
for entity_instance : layer_instance.entityInstances {
local_position := to_vector2s(entity_instance.px) / entity_layer.gridSize;
local_grid_index := grid_position_to_index(local_position, level.size.x);
cell := *level.cells[local_grid_index];
entity: LDTK_Entity_Def;
for e : root.defs.entities {
if e.uid == entity_instance.defUid {
entity = e;
break;
}
}
assert(entity.uid != 0, "Couldn't find entity.");
if entity.identifier == {
case "Player_Spawn"; {
assert(!player_spawn_found, tprint("Can't have multiple player spawn per level: %, position: %.", ldtk_level.identifier, local_position));
player_spawn_found = true;
for field_instance, field_index : entity_instance.fieldInstances {
if field_instance.__identifier == {
case "direction"; {
direction := Direction_Cardinal.NORTH;
if field_instance.__value.str == "EAST" direction = .EAST;
if field_instance.__value.str == "SOUTH" direction = .SOUTH;
if field_instance.__value.str == "WEST" direction = .WEST;
level.player_spawn_direction = direction;
level.player_spawn_position = local_position;
}
}
}
}
case "Battle"; {
cell.encounter.type = .CREATURE;
for field_instance, field_index : entity_instance.fieldInstances {
if field_instance.__identifier == {
case "creature_id"; {
creature_id: u32;
if field_instance.__value.str == "NONE" creature_id = 0;
if field_instance.__value.str == "DRAGON" creature_id = 1;
if field_instance.__value.str == "CENTIPEDE" creature_id = 2;
if field_instance.__value.str == "MOUSE" creature_id = 3;
if field_instance.__value.str == "FROG" creature_id = 4;
if field_instance.__value.str == "BUNNY" creature_id = 5;
cell.encounter.creature_id = creature_id;
}
}
}
}
case "Starter"; {
cell.encounter.type = .STARTER;
for field_instance, field_index : entity_instance.fieldInstances {
if field_instance.__identifier == {
case "creature_id"; {
creature_id: u32;
if field_instance.__value.str == "NONE" creature_id = 0;
if field_instance.__value.str == "DRAGON" creature_id = 1;
if field_instance.__value.str == "CENTIPEDE" creature_id = 2;
if field_instance.__value.str == "MOUSE" creature_id = 3;
if field_instance.__value.str == "FROG" creature_id = 4;
if field_instance.__value.str == "BUNNY" creature_id = 5;
cell.encounter.creature_id = creature_id;
}
case "target"; {
if field_instance.__value.object == null {
if cell.encounter.type == .STARTER {
assert(false, tprint("Missing target at level: %, position: %.", ldtk_level.identifier, local_position));
}
continue;
}
#import "Hash_Table";
#import "jaison";
value, success := table_find(field_instance.__value.object, "entityIid");
assert(success);
target_entity_iid := value.str;
target_cell_index := -1;
for other : layer_instance.entityInstances {
if other.iid == target_entity_iid {
local_position := to_vector2s(other.px) / entity_layer.gridSize;
local_grid_index := grid_position_to_index(local_position, level.size.x);
target_cell_index = local_grid_index;
break;
}
}
assert(target_cell_index > -1, tprint("Invalid target at level: %, position: %.", ldtk_level.identifier, local_position));
cell.encounter.target_cell_index = target_cell_index;
}
}
}
}
case "Trap"; {
cell.encounter.type = .PICKUP;
cell.encounter.pickup_id = 1;
}
case "Fountain"; {
cell.encounter.type = .PICKUP;
cell.encounter.pickup_id = 2;
}
case "Door"; {
cell.encounter.type = .PICKUP;
cell.encounter.pickup_id = 3;
}
case "Key"; {
cell.encounter.type = .PICKUP;
cell.encounter.pickup_id = 4;
for field_instance, field_index : entity_instance.fieldInstances {
if field_instance.__identifier == {
case "target"; {
if field_instance.__value.object == null {
if cell.encounter.type == .STARTER {
assert(false, tprint("Missing target at level: %, position: %.", ldtk_level.identifier, local_position));
}
continue;
}
#import "Hash_Table";
#import "jaison";
value, success := table_find(field_instance.__value.object, "entityIid");
assert(success);
target_entity_iid := value.str;
target_cell_index := -1;
for other : layer_instance.entityInstances {
if other.iid == target_entity_iid {
local_position := to_vector2s(other.px) / entity_layer.gridSize;
local_grid_index := grid_position_to_index(local_position, level.size.x);
target_cell_index = local_grid_index;
break;
}
}
assert(target_cell_index > -1, tprint("Invalid target at level: %, position: %.", ldtk_level.identifier, local_position));
cell.encounter.target_cell_index = target_cell_index;
}
}
}
}
case "Collar"; {
cell.encounter.type = .PICKUP;
cell.encounter.pickup_id = 5;
}
case "Stairs"; {
cell.encounter.type = .PICKUP;
cell.encounter.pickup_id = 8;
}
case; {
assert(false, tprint("Invalid entity type at level: %, position: %.", ldtk_level.identifier, local_position));
}
}
}
assert(player_spawn_found, "Missing player spawn in level.");
}
}
Floating_Message :: struct {
text: Fixed_Size_String(32);
window_position: Vector2;
timer: Timer;
size: s32;
color: Color; @UI(Color);
}
ui_push_floating_message :: (text: string, window_position: Vector2, duration := 800, font_size: s32 = FONT_SIZE, color: Color = COLOR_WHITE, delay := 0, animate_y := -6, loc := #caller_location) -> *Floating_Message {
message: *Floating_Message;
// TODO: That's a quick and dirty estimation, we'll want to do the real calculation later down the line
text_size := Vector2.{ cast(float) text.count * 4.8, cast(float) font_size * 4 };
message_data := Floating_Message.{
text = string_to_fixed_string(text, 32),
window_position = window_position - text_size/2,
timer = timer_make(duration, cast(s64) (cast(float)delay / engine.time_scale)),
size = font_size,
color = color,
};
// Quick and dirty way to reuse floating messages, might need to do a proper pool later down the line.
for game.floating_messages {
if it.timer.start == .{} {
message = *game.floating_messages[it_index];
message.* = .{};
break;
}
}
if message == null {
message = fixed_array_add(*game.floating_messages, .{}, loc = loc);
}
message.* = message_data;
message_animation_id, message_animation := animation_make("message", self_destroy = true);
window_position_curve := animation_make_curve(message_animation_id, *message.window_position.y, "message.y");
animation_make_frame(window_position_curve, message.window_position.y, cast(u64) delay);
animation_make_frame(window_position_curve, message.window_position.y + animate_y, cast(u64) delay + cast(u64) message.timer.duration);
return message;
}
ui_is_hovering_anything :: () -> bool {
return ImGui.GetIO().WantCaptureMouse || game.ui.hovered;
}
damage_party_member :: (party_member_index: int, amount: u8, damage_type: Damage_Type) {
party_member := *game.party[party_member_index];
party_creature := CREATURES[party_member.creature_id];
party_member.health.current = cast(u8) clamp(cast(s32)party_member.health.current - amount, 0, party_member.health.max);
// TODO: animate health bar
message_position := game.ui.party_position + (.{ 0, cast(float) PARTY_MEMBER_SIZE.y } * cast(float) party_member_index) + Vector2.{ 16, 30 };
// ui_push_floating_message(tprint("%", amount), message_position, duration = 1000, animate_y = 0);
battle_log(tprint("% received % damage.", to_temp_string(party_creature.name), amount));
{
animation_id := animation_make("attack", self_destroy = true);
effect := fixed_array_add(*game.effects, make_effect());
effect.size = .{ 81, 80 };
effect.position = game.ui.party_position + .{ 0, cast(float)PARTY_MEMBER_SIZE.y } * cast(float)party_member_index + to_vector2(ATTACK_OFFSETS[damage_type]);
effect.texture_size = .{ 81, 80 };
if #complete damage_type == {
case .TAIL; { effect.rect_index = xx IMG_ATTACK1; }
case .CLAW; { effect.rect_index = xx IMG_ATTACK2; }
case .FANG; { effect.rect_index = xx IMG_ATTACK3; }
}
effect.layer_id = 2;
effect.z_index = 999;
effect_curve := animation_make_curve(animation_id, effect, "flipbook");
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*0, 0 }), 100*0);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*1, 0 }), 100*1);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*2, 0 }), 100*2);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*3, 0 }), 100*3);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*4, 0 }), 100*4);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*5, 0 }), 100*5);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*6, 0 }), 100*6);
}
}
damage_foe_part :: (party_creature_id: Creature_Id, foe_creature_id: Creature_Id, part_id: int, amount: u8, damage_type: Damage_Type, position: Vector2) {
party_creature := CREATURES[party_creature_id];
foe_creature := CREATURES[foe_creature_id];
health := *game.battle.foe_parts_health[part_id];
multiplier : float = 1.0;
if party_creature.attack_type == party_creature.parts[part_id].weakness { multiplier = 1.5; }
damage := cast(u8) (cast(float)amount * multiplier);
health.current = cast(u8) max(cast(s32)health.current - damage, 0);
battle_log(tprint("%1 hit %2 for %3%4.", to_temp_string(party_creature.name), to_temp_string(foe_creature.name), damage, ifx(multiplier == 1.0) then "" else " (weakness)"), ifx(multiplier == 1.0) then .NORMAL else .CRITICAL);
{
animation_id := animation_make("foe_attack", self_destroy = true);
effect := fixed_array_add(*game.effects, make_effect());
effect.size = .{ 81*3, 80*3 };
effect.texture_size = .{ 81, 80 };
effect.position = position - to_vector2(effect.size/2);
if #complete damage_type == {
case .TAIL; { effect.rect_index = xx IMG_ATTACK1; }
case .CLAW; { effect.rect_index = xx IMG_ATTACK2; }
case .FANG; { effect.rect_index = xx IMG_ATTACK3; }
}
effect.layer_id = 2;
effect.z_index = 999;
effect_curve := animation_make_curve(animation_id, effect, "flipbook");
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*0, 0 }), 100*0);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*1, 0 }), 100*1);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*2, 0 }), 100*2);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*3, 0 }), 100*3);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*4, 0 }), 100*4);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*5, 0 }), 100*5);
animation_make_frame(effect_curve, make_sprite_animation_event(.{ 81*6, 0 }), 100*6);
}
}
heal_party_member :: (party_member: *Party_Member, amount: u8) {
party_member.health.current = cast(u8) clamp(party_member.health.current + amount, 0, party_member.health.max);
}
level_name_to_index :: (name: string) -> int {
for game.levels {
if to_temp_string(it) == name {
return it_index;
}
}
assert(false, "Couldn't find level!");
return -1;
}
is_game_over :: () -> bool {
if game.party.count == 0 { return false; }
party_member_dead: int;
for party_member : game.party {
if party_member.health.current <= 0 {
party_member_dead += 1;
}
}
return party_member_dead == game.party.count;
}
battle_log :: (text: string, type := Log_Type.NORMAL) {
if game.logs.count == game.logs.data.count-1 {
for 0 .. game.logs.count-2 {
game.logs[it] = game.logs[it+1];
}
game.logs.count -= 1;
}
fixed_array_add(*game.logs, Log.{ string_to_fixed_string(text, 64), type });
}
make_sprite_animation_event :: (texture_position: Vector2s) -> Frame_Event {
animate_sprite :: (target: *Effect, step_progress: float, user_data: *Vector2s) {
target.sprite.texture_position = user_data;
}
push_arena(game.battle_arena);
user_data := New(Vector2s);
user_data.* = texture_position;
return Frame_Event.{ xx animate_sprite, user_data };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment