gitea-master
John Montagu, the 4th Earl of Sandvich 2026-04-13 16:15:26 -07:00
parent f313527b20
commit 80bd8b0a52
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
5 changed files with 342 additions and 48 deletions

View File

@ -53,6 +53,11 @@ enum class MatchMode : std::uint8_t {
Client = 2,
};
enum class StartupField : std::uint8_t {
HostIp = 0,
Port = 1,
};
struct UnitState {
std::uint32_t unit_id = 0;
PlayerId owner = PlayerId::None;
@ -133,9 +138,14 @@ struct LocalViewState {
std::string host_ip = "127.0.0.1";
std::string port = "9000";
std::string connection_message = "Not connected";
StartupField startup_field = StartupField::HostIp;
int selected_area = -1;
std::optional<std::uint32_t> selected_unit;
std::optional<std::uint32_t> selected_combat_unit;
bool selecting_move_destination = false;
int move_from_area = -1;
int move_target_area = -1;
std::optional<std::uint32_t> selected_card;
int pending_combat_spend = 0;
};

View File

@ -79,15 +79,21 @@ void sync_registry_from_game_state(AppContext &ctx) {
static_cast<float>(board_y + row * cell_size));
auto &render = ctx.registry.add_component<RenderComponent>(area_entity);
render.draw = [controller = area.controller](raylib::Vector2 screen_pos, bool,
render.draw = [controller = area.controller](raylib::Vector2 screen_pos, bool selected,
bool hl) {
DrawRectangle(static_cast<int>(screen_pos.x), static_cast<int>(screen_pos.y),
cell_size - 2, cell_size - 2, area_fill_color(controller));
if (selected) {
DrawRectangleLinesEx(Rectangle{screen_pos.x, screen_pos.y,
static_cast<float>(cell_size - 2),
static_cast<float>(cell_size - 2)},
2.0f, Color{32, 37, 47, 255});
}
if (hl) {
DrawRectangleLinesEx(Rectangle{screen_pos.x, screen_pos.y,
static_cast<float>(cell_size - 2),
static_cast<float>(cell_size - 2)},
3.0f, Color{32, 37, 47, 255});
5.0f, Color{234, 157, 52, 255});
}
};
@ -154,6 +160,7 @@ int main() {
raylib::Window window(700, 460, "Island Dominion");
window.SetState(FLAG_WINDOW_RESIZABLE);
window.SetTargetFPS(60);
SetExitKey(KEY_NULL);
AppContext ctx;
initialize_game_state(ctx.game_state);

View File

@ -5,8 +5,11 @@
#include "raylib.h"
#include <algorithm>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
namespace {
bool point_in_rect(float px, float py, float x, float y, float w, float h) {
@ -44,6 +47,198 @@ std::uint16_t parse_port_or_default(const std::string &text) {
}
return static_cast<std::uint16_t>(value);
}
void select_area_and_local_unit(AppContext &ctx, int area_index) {
if (area_index < 0 || area_index >= area_count) {
return;
}
ctx.local.selected_area = area_index;
ctx.local.selected_unit.reset();
ctx.local.selected_combat_unit.reset();
const AreaState &area = ctx.game_state.areas[area_index];
for (const UnitState &unit : area.units) {
if (unit.owner == ctx.local.local_player) {
ctx.local.selected_unit = unit.unit_id;
break;
}
}
ctx.local.selecting_move_destination = false;
ctx.local.move_from_area = -1;
ctx.local.move_target_area = -1;
}
void move_selected_area(AppContext &ctx, int d_row, int d_col) {
int current =
ctx.local.selecting_move_destination ? ctx.local.move_target_area : ctx.local.selected_area;
if (current < 0 || current >= area_count) {
current = 0;
}
int row = current / board_size;
int col = current % board_size;
row = std::clamp(row + d_row, 0, board_size - 1);
col = std::clamp(col + d_col, 0, board_size - 1);
const int next = area_index_from_row_col(row, col);
if (ctx.local.selecting_move_destination) {
ctx.local.move_target_area = next;
} else {
select_area_and_local_unit(ctx, next);
}
}
void cycle_selected_local_unit(AppContext &ctx) {
std::vector<std::pair<int, std::uint32_t>> owned_units;
owned_units.reserve(16);
for (int area_index = 0; area_index < area_count; ++area_index) {
const AreaState &area = ctx.game_state.areas[area_index];
for (const UnitState &unit : area.units) {
if (unit.owner == ctx.local.local_player) {
owned_units.emplace_back(area_index, unit.unit_id);
}
}
}
if (owned_units.empty()) {
ctx.local.selected_unit.reset();
return;
}
std::size_t index = 0;
if (ctx.local.selected_unit.has_value()) {
for (std::size_t i = 0; i < owned_units.size(); ++i) {
if (owned_units[i].second == *ctx.local.selected_unit) {
index = (i + 1) % owned_units.size();
break;
}
}
}
ctx.local.selected_area = owned_units[index].first;
ctx.local.selected_unit = owned_units[index].second;
ctx.local.selected_combat_unit.reset();
ctx.local.selecting_move_destination = false;
ctx.local.move_from_area = -1;
ctx.local.move_target_area = -1;
}
void begin_move_destination_selection(AppContext &ctx) {
if (ctx.local.selected_area < 0 || !ctx.local.selected_unit.has_value()) {
return;
}
ctx.local.selecting_move_destination = true;
ctx.local.move_from_area = ctx.local.selected_area;
ctx.local.move_target_area = ctx.local.selected_area;
}
void cancel_move_destination_selection(AppContext &ctx) {
ctx.local.selecting_move_destination = false;
ctx.local.move_from_area = -1;
ctx.local.move_target_area = -1;
}
void confirm_move_destination_selection(AppContext &ctx) {
if (!ctx.local.selecting_move_destination || !ctx.local.selected_unit.has_value()) {
return;
}
if (ctx.local.move_from_area < 0 || ctx.local.move_target_area < 0) {
return;
}
if (!are_adjacent(ctx.local.move_from_area, ctx.local.move_target_area)) {
return;
}
PlayerAction action;
action.type = ActionType::MoveUnit;
action.unit_id = *ctx.local.selected_unit;
action.from_area = ctx.local.move_from_area;
action.to_area = ctx.local.move_target_area;
ctx.pending_local_actions.push(action);
if (ctx.local.mode == MatchMode::Client) {
ctx.network.send(MessageType::ActionRequest, serialize_action_request(action));
}
ctx.local.selected_area = ctx.local.move_target_area;
cancel_move_destination_selection(ctx);
}
std::vector<std::uint32_t> local_units_in_combat_area(const AppContext &ctx) {
std::vector<std::uint32_t> ids;
const int area_index = ctx.game_state.combat.area_index;
if (area_index < 0 || area_index >= area_count) {
return ids;
}
const AreaState &area = ctx.game_state.areas[area_index];
for (const UnitState &unit : area.units) {
if (unit.owner == ctx.local.local_player) {
ids.push_back(unit.unit_id);
}
}
return ids;
}
void cycle_combat_unit(AppContext &ctx) {
std::vector<std::uint32_t> ids = local_units_in_combat_area(ctx);
if (ids.empty()) {
ctx.local.selected_combat_unit.reset();
return;
}
if (!ctx.local.selected_combat_unit.has_value()) {
ctx.local.selected_combat_unit = ids.front();
return;
}
for (std::size_t i = 0; i < ids.size(); ++i) {
if (ids[i] == *ctx.local.selected_combat_unit) {
ctx.local.selected_combat_unit = ids[(i + 1) % ids.size()];
return;
}
}
ctx.local.selected_combat_unit = ids.front();
}
void update_card_selection_for_combat(AppContext &ctx) {
const PlayerState &player = player_for(ctx.game_state, ctx.local.local_player);
if (player.hand.empty()) {
ctx.local.selected_card.reset();
return;
}
if (!ctx.local.selected_card.has_value()) {
ctx.local.selected_card = player.hand.front().card_id;
return;
}
std::size_t selected_index = player.hand.size();
for (std::size_t i = 0; i < player.hand.size(); ++i) {
if (player.hand[i].card_id == *ctx.local.selected_card) {
selected_index = i;
break;
}
}
if (IsKeyPressed(KEY_C)) {
if (selected_index == player.hand.size()) {
ctx.local.selected_card = player.hand.front().card_id;
} else {
ctx.local.selected_card =
player.hand[(selected_index + 1) % player.hand.size()].card_id;
}
}
if (IsKeyPressed(KEY_X)) {
ctx.local.selected_card.reset();
}
}
} // namespace
void input_system(AppContext &ctx) {
@ -117,12 +312,19 @@ void input_system(AppContext &ctx) {
return;
}
if (ctx.local.local_player != ctx.game_state.active_player &&
if (!ctx.game_state.combat.active && ctx.local.local_player != ctx.game_state.active_player &&
ctx.local.mode == MatchMode::Client) {
return;
}
if (ctx.game_state.combat.active) {
std::vector<std::uint32_t> combat_units = local_units_in_combat_area(ctx);
if (!combat_units.empty() && !ctx.local.selected_combat_unit.has_value()) {
ctx.local.selected_combat_unit = combat_units.front();
}
update_card_selection_for_combat(ctx);
if (IsKeyPressed(KEY_ONE)) {
ctx.local.pending_combat_spend = 1;
}
@ -135,27 +337,30 @@ void input_system(AppContext &ctx) {
if (IsKeyPressed(KEY_ZERO)) {
ctx.local.pending_combat_spend = 0;
}
if (IsKeyPressed(KEY_TAB)) {
cycle_combat_unit(ctx);
}
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
if (IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_KP_ENTER)) {
int area_index = ctx.game_state.combat.area_index;
if (area_index >= 0) {
const AreaState &area = ctx.game_state.areas[area_index];
for (const UnitState &unit : area.units) {
if (unit.owner == ctx.local.local_player) {
CombatChoice choice;
choice.unit_id = unit.unit_id;
choice.resources_spent = ctx.local.pending_combat_spend;
if (ctx.local.selected_card.has_value()) {
choice.card_id = ctx.local.selected_card;
}
choice.submitted = true;
if (!ctx.local.selected_combat_unit.has_value() && !combat_units.empty()) {
ctx.local.selected_combat_unit = combat_units.front();
}
ctx.pending_local_combat_choice = choice;
if (ctx.local.mode == MatchMode::Client) {
ctx.network.send(MessageType::CombatChoice,
serialize_combat_choice(choice));
}
break;
if (ctx.local.selected_combat_unit.has_value()) {
CombatChoice choice;
choice.unit_id = *ctx.local.selected_combat_unit;
choice.resources_spent = ctx.local.pending_combat_spend;
if (ctx.local.selected_card.has_value()) {
choice.card_id = ctx.local.selected_card;
}
choice.submitted = true;
ctx.pending_local_combat_choice = choice;
if (ctx.local.mode == MatchMode::Client) {
ctx.network.send(MessageType::CombatChoice,
serialize_combat_choice(choice));
}
}
}
@ -163,6 +368,24 @@ void input_system(AppContext &ctx) {
return;
}
ctx.local.selected_combat_unit.reset();
if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_L)) {
move_selected_area(ctx, 0, 1);
}
if (IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_H)) {
move_selected_area(ctx, 0, -1);
}
if (IsKeyPressed(KEY_DOWN) || IsKeyPressed(KEY_J)) {
move_selected_area(ctx, 1, 0);
}
if (IsKeyPressed(KEY_UP) || IsKeyPressed(KEY_K)) {
move_selected_area(ctx, -1, 0);
}
if (IsKeyPressed(KEY_TAB)) {
cycle_selected_local_unit(ctx);
}
int cell_size = 100;
int board_px = board_size * cell_size;
int board_x = (GetScreenWidth() - board_px) / 2;
@ -171,35 +394,27 @@ void input_system(AppContext &ctx) {
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
int area_idx = grid_cell_at_mouse(GetMouseX(), GetMouseY(), board_x, board_y, cell_size);
if (area_idx >= 0) {
ctx.local.selected_area = area_idx;
const AreaState &area = ctx.game_state.areas[area_idx];
ctx.local.selected_unit.reset();
for (const UnitState &unit : area.units) {
if (unit.owner == ctx.local.local_player) {
ctx.local.selected_unit = unit.unit_id;
break;
}
if (ctx.local.selecting_move_destination) {
ctx.local.move_target_area = area_idx;
} else {
select_area_and_local_unit(ctx, area_idx);
}
}
}
if (ctx.local.selected_area >= 0 && ctx.local.selected_unit.has_value()) {
if (IsKeyPressed(KEY_M)) {
int from = ctx.local.selected_area;
int to = from + 1;
if (to < area_count && are_adjacent(from, to)) {
PlayerAction action;
action.type = ActionType::MoveUnit;
action.unit_id = *ctx.local.selected_unit;
action.from_area = from;
action.to_area = to;
ctx.pending_local_actions.push(action);
if (ctx.local.mode == MatchMode::Client) {
ctx.network.send(MessageType::ActionRequest, serialize_action_request(action));
}
}
if (ctx.local.selected_area >= 0 && ctx.local.selected_unit.has_value() &&
IsKeyPressed(KEY_M)) {
begin_move_destination_selection(ctx);
}
if (ctx.local.selecting_move_destination) {
if (IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_KP_ENTER)) {
confirm_move_destination_selection(ctx);
}
if (IsKeyPressed(KEY_ESCAPE)) {
cancel_move_destination_selection(ctx);
}
return;
}
if (ctx.local.selected_area >= 0 && IsKeyPressed(KEY_B)) {

View File

@ -34,7 +34,8 @@ void render_system(AppContext &ctx) {
if (ctx.registry.has_component<AreaComponent>(e)) {
const auto *area = ctx.registry.get_component<AreaComponent>(e);
selected = area && area->area_index == ctx.local.selected_area;
highlighted = selected;
highlighted = area && ctx.local.selecting_move_destination &&
area->area_index == ctx.local.move_target_area;
}
if (ctx.registry.has_component<UnitComponent>(e)) {

View File

@ -15,6 +15,16 @@ const char *player_label(PlayerId p) {
}
return "None";
}
const char *tier_label(UnitTier tier) {
if (tier == UnitTier::Light) {
return "Light";
}
if (tier == UnitTier::Normal) {
return "Normal";
}
return "Heavy";
}
} // namespace
void ui_system(AppContext &ctx) {
@ -73,20 +83,71 @@ void ui_system(AppContext &ctx) {
ctx.game_state.actions_remaining),
GetScreenWidth() - 350, 14, 20, DARKBLUE);
DrawText("Keys: click area, B build unit, F build fortress, M move right, P pass", 20,
DrawText("Keys: arrows/hjkl move, Tab cycle units, M start move, Enter confirm, B/F/P", 20,
GetScreenHeight() - 30, 20, DARKGRAY);
if (ctx.local.selecting_move_destination) {
DrawText(TextFormat("Move preview: %d -> %d (Enter confirm, Esc cancel)",
ctx.local.move_from_area, ctx.local.move_target_area),
20, GetScreenHeight() - 56, 20, DARKBLUE);
}
if (ctx.game_state.combat.active) {
DrawRectangle(140, 140, GetScreenWidth() - 280, GetScreenHeight() - 280,
Color{25, 28, 34, 230});
DrawText("Combat", 170, 170, 32, RAYWHITE);
DrawText("Choose spend with 0-3, click to submit first valid unit.", 170, 212, 20,
DrawText("Tab: unit, C: next card, X: no card, 0-3: spend, Enter: submit", 170, 212, 20,
RAYWHITE);
DrawText(TextFormat("Pending spend: %d", ctx.local.pending_combat_spend), 170, 242, 22,
GOLD);
const int area_index = ctx.game_state.combat.area_index;
int y = 276;
if (area_index >= 0 && area_index < area_count) {
const AreaState &area = ctx.game_state.areas[area_index];
DrawText("Your units in combat area:", 170, y, 20, SKYBLUE);
y += 26;
bool any_local = false;
for (const UnitState &unit : area.units) {
if (unit.owner != ctx.local.local_player) {
continue;
}
any_local = true;
const bool selected = ctx.local.selected_combat_unit.has_value() &&
*ctx.local.selected_combat_unit == unit.unit_id;
DrawText(TextFormat("%s Unit #%u%s", tier_label(unit.tier), unit.unit_id,
selected ? " <selected>" : ""),
190, y, 20, selected ? YELLOW : RAYWHITE);
y += 22;
}
if (!any_local) {
DrawText("(no local units available)", 190, y, 20, LIGHTGRAY);
y += 22;
}
}
const PlayerState &self = player_for(ctx.game_state, ctx.local.local_player);
y += 10;
DrawText("Card selection:", 170, y, 20, SKYBLUE);
y += 24;
if (self.hand.empty()) {
DrawText("(no cards in hand)", 190, y, 20, LIGHTGRAY);
} else if (!ctx.local.selected_card.has_value()) {
DrawText("None selected", 190, y, 20, RAYWHITE);
} else {
int chosen_bonus = 0;
for (const CardState &card : self.hand) {
if (card.card_id == *ctx.local.selected_card) {
chosen_bonus = card.bonus;
break;
}
}
DrawText(TextFormat("Card #%u (+%d)", *ctx.local.selected_card, chosen_bonus), 190, y,
20, RAYWHITE);
}
if (!ctx.game_state.combat.last_result.empty()) {
DrawText(ctx.game_state.combat.last_result.c_str(), 170, 276, 22, SKYBLUE);
DrawText(ctx.game_state.combat.last_result.c_str(), 170, y + 30, 22, SKYBLUE);
}
}
}