diff --git a/basic1.cpp b/basic1.cpp index 7501e71..73b8c2c 100644 --- a/basic1.cpp +++ b/basic1.cpp @@ -306,8 +306,8 @@ int main() // Initialize renderer and GUI system LowLevelRenderer renderer(bit_buffer, V_WIDTH, V_HEIGHT); - renderer.set_font(&font_5x5_obj); - LowLevelGUI gui = LowLevelGUI(&renderer, font_BMplain_obj); + renderer.set_font(&font_homespun_obj); + LowLevelGUI gui = LowLevelGUI(&renderer, font_homespun_obj); // Initialize touch screen using abstraction FIRST (before InputManager needs it) touch = LowLevelTouch::create((TouchType)TOUCH_TYPE_SELECTED, V_WIDTH, V_HEIGHT, @@ -353,7 +353,9 @@ int main() launcher.register_game("Monopoly", "Classic property trading game", [](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* { - return new MonopolyGame(w, h, r, g, im); + // For Feather TFT (480x320), reduce width to 430 to make room for sidebar buttons + uint16_t game_w = (w == 480) ? 430 : w; + return new MonopolyGame(game_w, h, r, g, im); }); launcher.register_game("Demo Game", "Simple test game", diff --git a/emulator/Makefile b/emulator/Makefile index 37e4686..d38cb80 100644 --- a/emulator/Makefile +++ b/emulator/Makefile @@ -8,11 +8,11 @@ CXXFLAGS = -std=c++17 -Wall # Paths for Homebrew on macOS (Silicon/M1/M2/M3) ifeq ($(OS), Darwin) SFML_DIR = $(shell brew --prefix sfml@2) - INCLUDES = -I. -I$(SFML_DIR)/include -I../display -I../fonts -I../games + INCLUDES = -I. -I.. -I$(SFML_DIR)/include -I../display -I../fonts -I../games LIBS = -L$(SFML_DIR)/lib -lsfml-graphics -lsfml-window -lsfml-system else # Standard Linux paths - INCLUDES = -I. -I../display -I../fonts -I../games + INCLUDES = -I. -I.. -I../display -I../fonts -I../games LIBS = -lsfml-graphics -lsfml-window -lsfml-system endif diff --git a/emulator/basic1_emulator b/emulator/basic1_emulator index 3258335..8bcdc5a 100755 Binary files a/emulator/basic1_emulator and b/emulator/basic1_emulator differ diff --git a/emulator/input_manager.h b/emulator/input_manager.h index ee397fd..7e9fa2d 100644 --- a/emulator/input_manager.h +++ b/emulator/input_manager.h @@ -1,6 +1,56 @@ -#pragma once +#ifndef INPUT_MANAGER_H +#define INPUT_MANAGER_H + +#include +#include "input_event.h" + // Minimal stub for emulator build class InputManager { public: bool has_buttons() const { return false; } + bool has_touch() const { return false; } + + void get_virtual_button_regions(int* a_rect, int* b_rect) const { + for (int i = 0; i < 4; i++) { + a_rect[i] = v_button_a[i]; + b_rect[i] = v_button_b[i]; + } + } + void set_virtual_button_regions(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) { + v_button_a[0] = ax; v_button_a[1] = ay; v_button_a[2] = aw; v_button_a[3] = ah; + v_button_b[0] = bx; v_button_b[1] = by; v_button_b[2] = bw; v_button_b[3] = bh; + v_buttons_active = true; + } + void clear_virtual_button_regions() { + v_buttons_active = false; + for (int i = 0; i < 4; i++) { + v_button_a[i] = 0; + v_button_b[i] = 0; + } + } + + bool check_virtual_buttons(int16_t x, int16_t y, InputType& out_type) const { + if (!v_buttons_active) return false; + + if (x >= v_button_a[0] && x <= v_button_a[0] + v_button_a[2] && + y >= v_button_a[1] && y <= v_button_a[1] + v_button_a[3]) { + out_type = INPUT_BUTTON_0; + return true; + } + + if (x >= v_button_b[0] && x <= v_button_b[0] + v_button_b[2] && + y >= v_button_b[1] && y <= v_button_b[1] + v_button_b[3]) { + out_type = INPUT_BUTTON_1; + return true; + } + + return false; + } + +private: + int v_button_a[4] = {0, 0, 0, 0}; + int v_button_b[4] = {0, 0, 0, 0}; + bool v_buttons_active = false; }; + +#endif diff --git a/emulator/main.cpp b/emulator/main.cpp index b7ec687..f44873b 100644 --- a/emulator/main.cpp +++ b/emulator/main.cpp @@ -64,6 +64,12 @@ int main() { event.x = sfEvent.mouseButton.x; event.y = sfEvent.mouseButton.y; event.valid = true; + + // Check for virtual buttons + InputType virtual_type; + if (input_manager.check_virtual_buttons(event.x, event.y, virtual_type)) { + event.type = virtual_type; + } } else if (sfEvent.type == sf::Event::KeyPressed) { if (sfEvent.key.code == sf::Keyboard::Space) { event.type = INPUT_BUTTON_0; diff --git a/games/monopoly/DiceModalGame.h b/games/monopoly/DiceModalGame.h index e6e5424..ab1f6b4 100644 --- a/games/monopoly/DiceModalGame.h +++ b/games/monopoly/DiceModalGame.h @@ -104,10 +104,10 @@ public: char move_buf[64]; if (from_tile && to_tile) { snprintf(move_buf, sizeof(move_buf), "FROM: %s", from_tile->name); - renderer->draw_string_scaled(ix + 10, info_y, move_buf, 1); - info_y += 12; + renderer->draw_string_scaled(ix + 10, info_y, move_buf, 2); + info_y += 24; snprintf(move_buf, sizeof(move_buf), "TO: %s", to_tile->name); - renderer->draw_string_scaled(ix + 10, info_y, move_buf, 1); + renderer->draw_string_scaled(ix + 10, info_y, move_buf, 2); } // Button diff --git a/games/monopoly/ModalButtonHelper.h b/games/monopoly/ModalButtonHelper.h index 496618f..907bb25 100644 --- a/games/monopoly/ModalButtonHelper.h +++ b/games/monopoly/ModalButtonHelper.h @@ -11,23 +11,32 @@ public: static void set_monopoly_regions(InputManager* input_manager, uint16_t width, uint16_t height) { if (!input_manager) return; - // --- BUTTON CONFIGURATION --- - // Adjust these variables to move the virtual buttons easily - int btn_w = 60; - int btn_h = 60; - int margin_right = 135; // Positioned for the inner board area (W - 135 = 345) - int start_y = 80; // Vertical start position - int spacing = 30; // Gap between buttons - - int btn_x = width - margin_right; - int btn_a_y = start_y; - int btn_b_y = btn_a_y + btn_h + spacing; + if (width < 480) { + // Sidebar mode for 480x320 screen (Buttons at 400-480) + int sidebar_x = width; + int sidebar_w = 480 - width; + int btn_h = height / 2; + input_manager->set_virtual_button_regions( + sidebar_x, 0, sidebar_w, btn_h, // Button A (Top half) + sidebar_x, btn_h, sidebar_w, btn_h // Button B (Bottom half) + ); + } else { + // Inner dashboard mode (fallback) + int btn_w = 60; + int btn_h = 60; + int margin_right = 135; // Positioned for the inner board area (W - 135 = 345) + int start_y = 80; // Vertical start position + int spacing = 30; // Gap between buttons + + int btn_x = width - margin_right; + int btn_a_y = start_y; + int btn_b_y = btn_a_y + btn_h + spacing; - // Apply regions to input manager - input_manager->set_virtual_button_regions( - btn_x, btn_a_y, btn_w, btn_h, // Button A (Top) - btn_x, btn_b_y, btn_w, btn_h // Button B (Bottom) - ); + input_manager->set_virtual_button_regions( + btn_x, btn_a_y, btn_w, btn_h, + btn_x, btn_b_y, btn_w, btn_h + ); + } } /** @@ -42,17 +51,20 @@ public: // Save current color bool original_color = renderer->get_current_text_color(); - // Draw Button A (Top) + // Draw Button A renderer->draw_filled_rectangle(a[0], a[1], a[2], a[3], false, 0); // White back renderer->draw_rectangle(a[0], a[1], a[2], a[3], true, 2); // Black border renderer->set_text_color(true); - renderer->draw_string_scaled(a[0] + (a[2] - 12) / 2, a[1] + (a[3] - 16) / 2, "A", 2); + // Use larger font scaling if buttons are big (sidebar mode) + int scale_a = (a[3] > 100) ? 4 : 2; + renderer->draw_string_scaled(a[0] + (a[2] - 6 * scale_a) / 2, a[1] + (a[3] - 8 * scale_a) / 2, "A", scale_a); - // Draw Button B (Under A) + // Draw Button B renderer->draw_filled_rectangle(b[0], b[1], b[2], b[3], false, 0); // White back renderer->draw_rectangle(b[0], b[1], b[2], b[3], true, 2); // Black border renderer->set_text_color(true); - renderer->draw_string_scaled(b[0] + (b[2] - 12) / 2, b[1] + (b[3] - 16) / 2, "B", 2); + int scale_b = (b[3] > 100) ? 4 : 2; + renderer->draw_string_scaled(b[0] + (b[2] - 6 * scale_b) / 2, b[1] + (b[3] - 8 * scale_b) / 2, "B", scale_b); // Restore color renderer->set_text_color(original_color); diff --git a/games/monopoly/PaymentModalGame.h b/games/monopoly/PaymentModalGame.h new file mode 100644 index 0000000..fb69f3b --- /dev/null +++ b/games/monopoly/PaymentModalGame.h @@ -0,0 +1,159 @@ +// PaymentModalGame.h +#pragma once +#include "../../lib/game.h" +#include "../../display/low_level_render.h" +#include "../../display/low_level_gui.h" +#include "input_manager.h" +#include "player.h" +#include "MonopolyBoardRenderer.h" +#include "ModalButtonHelper.h" +#include +#include + +class PaymentModalGame : public Game { + Player* payer; + Player* recipient; // nullptr if Bank + int amount; + int options[3]; + bool option_visible[3]; + int correct_answer; + int selected_choice; + bool dismissed; + const char* reason; + bool show_error; + +public: + PaymentModalGame(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager, + Player* p, Player* r, int amt, const char* reas) + : Game(width, height, renderer, gui, input_manager), payer(p), recipient(r), amount(amt), reason(reas), + selected_choice(-1), dismissed(false), show_error(false) { + + correct_answer = payer->balance - amount; + + for (int i = 0; i < 3; i++) option_visible[i] = true; + + // Generate two fake answers + int fake1 = correct_answer + (rand() % 4 + 1) * 10; + int fake2 = correct_answer - (rand() % 4 + 1) * 10; + if (fake2 < 0 && correct_answer > 10) fake2 = correct_answer - 5; + if (fake2 == correct_answer || fake2 == fake1) fake2 = fake1 + 10; + + int rand_pos = rand() % 3; + if (rand_pos == 0) { + options[0] = correct_answer; + options[1] = fake1; + options[2] = fake2; + } else if (rand_pos == 1) { + options[0] = fake1; + options[1] = correct_answer; + options[2] = fake2; + } else { + options[0] = fake1; + options[1] = fake2; + options[2] = correct_answer; + } + } + + void init() override { + dismissed = false; + selected_choice = -1; + show_error = false; + for (int i = 0; i < 3; i++) option_visible[i] = true; + ModalButtonHelper::set_monopoly_regions(input_manager, width, height); + } + + Type get_type() const override { return Type::MONOPOLY_PAYMENT; } + + bool update(const InputEvent& event) override { + if (event.type == INPUT_BUTTON_0) { // Select + // Find next visible option + do { + selected_choice = (selected_choice + 1) % 3; + } while (!option_visible[selected_choice]); + + show_error = false; + return true; + } + if (event.type == INPUT_BUTTON_1) { // Execute + if (selected_choice == -1) return false; + if (!option_visible[selected_choice]) return false; + + if (options[selected_choice] == correct_answer) { + payer->balance -= amount; + if (recipient) recipient->balance += amount; + dismissed = true; + } else { + option_visible[selected_choice] = false; + show_error = true; + // De-select the hidden option + selected_choice = -1; + } + return true; + } + return false; + } + + void draw() override { + renderer->clear_buffer(); + + // Draw board background if possible (simplified here) + int win_w = width - 2 * (width / 8); + int win_h = height - 2 * (height / 8); + int win_x = (width - win_w) / 2; + int win_y = (height - win_h) / 2; + + renderer->draw_filled_rectangle(win_x, win_y, win_w, win_h, false, 0); + renderer->draw_rectangle(win_x, win_y, win_w, win_h, true, 2); + renderer->draw_rectangle(win_x + 3, win_y + 3, win_w - 6, win_h - 6, true, 1); + + // Header + renderer->draw_filled_rectangle(win_x + 4, win_y + 4, win_w - 8, 35, true, 1); + renderer->set_text_color(false); + renderer->draw_string_scaled(win_x + (win_w - 12 * 10) / 2, win_y + 10, "PAYMENT DUE", 2); + renderer->set_text_color(true); + + char buf[128]; + int content_y = win_y + 50; + + snprintf(buf, sizeof(buf), "Paying: $%d from $%d", amount, payer->balance); + renderer->draw_string_scaled(win_x + 20, content_y, buf, 2); + content_y += 25; + + snprintf(buf, sizeof(buf), "To: %s", recipient ? recipient->name : "The Bank"); + renderer->draw_string_scaled(win_x + 20, content_y, buf, 2); + content_y += 25; + + snprintf(buf, sizeof(buf), "Reason: %s", reason); + renderer->draw_string_scaled(win_x + 20, content_y, buf, 1); + content_y += 30; + + renderer->draw_line(win_x + 10, content_y, win_x + win_w - 10, content_y, true); + content_y += 15; + + renderer->draw_string_scaled(win_x + 20, content_y, "What is your new balance?", 1); + content_y += 25; + + // Options + for (int i = 0; i < 3; i++) { + if (!option_visible[i]) { + content_y += 25; + continue; + } + snprintf(buf, sizeof(buf), "%s $%d", (selected_choice == i ? ">" : " "), options[i]); + if (selected_choice == i) renderer->draw_filled_rectangle(win_x + 15, content_y - 2, 120, 22, true, 1); + if (selected_choice == i) renderer->set_text_color(false); + renderer->draw_string_scaled(win_x + 20, content_y, buf, 2); + renderer->set_text_color(true); + content_y += 25; + } + + if (show_error) { + renderer->set_text_color(true); + renderer->draw_string_scaled(win_x + 20, content_y + 10, "TRY AGAIN!", 2); + } + + ModalButtonHelper::draw_virtual_buttons(renderer, input_manager); + } + + bool is_dismissed() const { return dismissed; } +}; diff --git a/games/monopoly/monopoly_game.cpp b/games/monopoly/monopoly_game.cpp index db291e4..f720dfa 100644 --- a/games/monopoly/monopoly_game.cpp +++ b/games/monopoly/monopoly_game.cpp @@ -23,6 +23,7 @@ extern "C" { #include "ChanceModalGame.h" #include "CommunityChestModalGame.h" #include "TurnModalGame.h" +#include "PaymentModalGame.h" #include "ModalButtonHelper.h" #include "sprites.h" #include "MonopolyBoardRenderer.h" @@ -55,6 +56,7 @@ void MonopolyGame::init() { srand(time(NULL)); shuffle_chance_deck(); shuffle_community_deck(); + //renderer->set_font(&font_tama_mini02_obj); if (active_modal) { delete active_modal; active_modal = nullptr; } active_modal = new TurnModalGame(width, height, renderer, gui, input_manager, &players[current_player_idx]); // TODO: Reset all board state, property ownership, etc. @@ -113,6 +115,16 @@ bool MonopolyGame::update(const InputEvent& event) { ChanceModalGame* chance_modal = (active_modal->get_type() == Game::Type::MONOPOLY_CHANCE) ? static_cast(active_modal) : nullptr; CommunityChestModalGame* community_modal = (active_modal->get_type() == Game::Type::MONOPOLY_COMMUNITY_CHEST) ? static_cast(active_modal) : nullptr; TurnModalGame* turn_modal = (active_modal->get_type() == Game::Type::MONOPOLY_TURN) ? static_cast(active_modal) : nullptr; + PaymentModalGame* pay_modal = (active_modal->get_type() == Game::Type::MONOPOLY_PAYMENT) ? static_cast(active_modal) : nullptr; + + if (pay_modal && pay_modal->is_dismissed()) { + delete active_modal; + active_modal = nullptr; + selected_action = -1; + needs_redraw = true; + ModalButtonHelper::set_monopoly_regions(input_manager, width, height); + return needs_redraw; + } if (dice_modal && dice_modal->is_dismissed()) { delete active_modal; @@ -165,7 +177,8 @@ bool MonopolyGame::update(const InputEvent& event) { p->balance += card->value; break; case CHANCE_SPEND: - p->balance -= card->value; + active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, card->value, "CHANCE CARD"); + if (active_modal) active_modal->init(); break; case CHANCE_ADVANCE: { int target = card->value; @@ -211,12 +224,12 @@ bool MonopolyGame::update(const InputEvent& event) { break; } - delete active_modal; - active_modal = nullptr; needs_redraw = true; ModalButtonHelper::set_monopoly_regions(input_manager, width, height); - if (position_changed) { + if (active_modal) { + active_modal->init(); + } else if (position_changed) { // If we moved, check if we landed on a property const BoardTile* landed = &MONOPOLY_BOARD[p->position]; if (landed->type == TILE_PROPERTY || landed->type == TILE_RAILROAD || landed->type == TILE_UTILITY) { @@ -250,7 +263,8 @@ bool MonopolyGame::update(const InputEvent& event) { p->balance += card->value; break; case COMMUNITY_SPEND: - p->balance -= card->value; + active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, card->value, "COMMUNITY CHEST"); + if (active_modal) active_modal->init(); break; case COMMUNITY_ADVANCE: p->position = card->value; @@ -277,12 +291,12 @@ bool MonopolyGame::update(const InputEvent& event) { break; } - delete active_modal; - active_modal = nullptr; needs_redraw = true; ModalButtonHelper::set_monopoly_regions(input_manager, width, height); - if (position_changed) { + if (active_modal) { + active_modal->init(); + } else if (position_changed) { const BoardTile* landed = &MONOPOLY_BOARD[p->position]; if (landed->type == TILE_PROPERTY || landed->type == TILE_RAILROAD || landed->type == TILE_UTILITY) { bool is_owned = false; @@ -304,28 +318,30 @@ bool MonopolyGame::update(const InputEvent& event) { } } } else if (prop_modal && prop_modal->is_dismissed()) { - if (prop_modal->wants_to_buy()) { + bool wants_buy = prop_modal->wants_to_buy(); + bool wants_rent = prop_modal->wants_to_pay_rent(); + int owner_id_from_modal = prop_modal->get_owner_id(); + + delete active_modal; + active_modal = nullptr; + + if (wants_buy) { const BoardTile* tile = &MONOPOLY_BOARD[p->position]; if (p->balance >= tile->cost) { - p->balance -= tile->cost; + active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, tile->cost, "BUY PROPERTY"); p->properties_owned[p->property_count++] = p->position; } - } else if (prop_modal->wants_to_pay_rent()) { + } else if (wants_rent) { const BoardTile* tile = &MONOPOLY_BOARD[p->position]; int rent = 0; if (tile->type == TILE_PROPERTY) { - // Logic for rent: If owner has all properties of group, rent is doubled (base only) - // For now, let's just use rent[0] as requested, but we should probably eventually - // check for houses. Let's stick to rent[0] to match the modal's display. rent = tile->rent[0]; } else if (tile->type == TILE_RAILROAD) { - // Utility logic for Railroads: 1:25, 2:50, 3:100, 4:200 - int owner_id = prop_modal->get_owner_id(); int rr_count = 0; - if (owner_id != -1) { - for (int i = 0; i < players[owner_id].property_count; ++i) { - if (MONOPOLY_BOARD[players[owner_id].properties_owned[i]].type == TILE_RAILROAD) { + if (owner_id_from_modal != -1) { + for (int i = 0; i < players[owner_id_from_modal].property_count; ++i) { + if (MONOPOLY_BOARD[players[owner_id_from_modal].properties_owned[i]].type == TILE_RAILROAD) { rr_count++; } } @@ -334,18 +350,14 @@ bool MonopolyGame::update(const InputEvent& event) { else if (rr_count == 2) rent = 50; else if (rr_count == 3) rent = 100; else if (rr_count == 4) rent = 200; - else rent = 25; // Fallback + else rent = 25; } else if (tile->type == TILE_UTILITY) { - // Utility: 4x dice if 1 owned, 10x if both. - // Since we don't have the dice roll here, let's use a fixed 40 for now - // or calculate it from last_dice1 + last_dice2. int total_dice = last_dice1 + last_dice2; - int owner_id = prop_modal->get_owner_id(); int utility_count = 0; - if (owner_id != -1) { - for (int i = 0; i < players[owner_id].property_count; ++i) { - if (MONOPOLY_BOARD[players[owner_id].properties_owned[i]].type == TILE_UTILITY) { + if (owner_id_from_modal != -1) { + for (int i = 0; i < players[owner_id_from_modal].property_count; ++i) { + if (MONOPOLY_BOARD[players[owner_id_from_modal].properties_owned[i]].type == TILE_UTILITY) { utility_count++; } } @@ -359,20 +371,17 @@ bool MonopolyGame::update(const InputEvent& event) { rent *= rent_multiplier; - int o_id = prop_modal->get_owner_id(); - if (o_id != -1 && (int)current_player_idx != o_id) { - p->balance -= rent; - players[o_id].balance += rent; + if (owner_id_from_modal != -1 && (int)current_player_idx != owner_id_from_modal) { + active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, &players[owner_id_from_modal], rent, "RENT"); } } // Reset multipliers rent_multiplier = 1; force_utility_10x = false; - delete active_modal; - active_modal = nullptr; needs_redraw = true; ModalButtonHelper::set_monopoly_regions(input_manager, width, height); + if (active_modal) active_modal->init(); } else if (board_modal && board_modal->is_dismissed()) { delete active_modal; active_modal = nullptr; @@ -400,7 +409,8 @@ bool MonopolyGame::update(const InputEvent& event) { if (selected_action == -1) return false; if (p->is_in_jail && !has_rolled && selected_action == 1) { - p->balance -= 50; + active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, 50, "JAIL BAIL"); + if (active_modal) active_modal->init(); p->is_in_jail = false; p->jail_turns = 0; selected_action = -1; @@ -423,7 +433,9 @@ roll_dice_logic: } else { p->jail_turns++; if (p->jail_turns >= 3) { - p->balance -= 50; p->is_in_jail = false; p->jail_turns = 0; + active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, 50, "JAIL BAIL (FORCED)"); + if (active_modal) active_modal->init(); + p->is_in_jail = false; p->jail_turns = 0; } else { has_rolled = true; last_dice1 = d1; last_dice2 = d2; @@ -468,7 +480,8 @@ roll_dice_logic: current_community_idx = (current_community_idx + 1) % COMMUNITY_DECK_SIZE; if (current_community_idx == 0) shuffle_community_deck(); } else if (lnd->type == TILE_TAX) { - p->balance -= lnd->cost; + active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, lnd->cost, "TAXES"); + if (active_modal) active_modal->init(); } needs_redraw = true; } else { diff --git a/lib/game.h b/lib/game.h index ee3f53f..53bc866 100644 --- a/lib/game.h +++ b/lib/game.h @@ -35,7 +35,8 @@ public: MONOPOLY_BOARD, MONOPOLY_CHANCE, MONOPOLY_COMMUNITY_CHEST, - MONOPOLY_TURN + MONOPOLY_TURN, + MONOPOLY_PAYMENT }; /**