diff --git a/GNUmakefile b/GNUmakefile index 90c658f..c815479 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -6,7 +6,7 @@ MAKEFLAGS += -rR ARCH := x86_64 # Default user QEMU flags. These are appended to the QEMU command calls. -QEMUFLAGS := -m 2G -d int -D qemu.log +QEMUFLAGS := -smp 4 -m 2G -d int -D qemu.log override IMAGE_NAME := montauk-$(ARCH) diff --git a/kernel/src/ACPI/AcpiEvents.cpp b/kernel/src/ACPI/AcpiEvents.cpp index 65a55de..bd69f3c 100644 --- a/kernel/src/ACPI/AcpiEvents.cpp +++ b/kernel/src/ACPI/AcpiEvents.cpp @@ -79,8 +79,7 @@ namespace Hal { KernelLogStream(INFO, "ACPI") << "Sleep button pressed"; } - // Send EOI - LocalApic::SendEOI(); + // Note: EOI is sent by HalIrqDispatch after this handler returns. } // ============================================================================ @@ -88,7 +87,13 @@ namespace Hal { // Initialize // ============================================================================ - void Initialize(const FADT::ParsedFADT& fadt) { + void Initialize(ACPI::CommonSDTHeader* xsdt) { + FADT::ParsedFADT fadt{}; + if (!FADT::Parse(xsdt, fadt) || !fadt.Valid) { + KernelLogStream(ERROR, "ACPI") << "Failed to parse FADT - ACPI events unavailable"; + return; + } + g_pm1aEventBlock = fadt.PM1aEventBlock; g_pm1bEventBlock = fadt.PM1bEventBlock; g_pm1EventLength = fadt.PM1EventLength; diff --git a/kernel/src/ACPI/AcpiEvents.hpp b/kernel/src/ACPI/AcpiEvents.hpp index b3287d4..47eb2f5 100644 --- a/kernel/src/ACPI/AcpiEvents.hpp +++ b/kernel/src/ACPI/AcpiEvents.hpp @@ -13,8 +13,8 @@ namespace Hal { // Initialize ACPI event handling: route the SCI interrupt, // register the handler, and enable fixed events (power button). - // Must be called after APIC and FADT initialization. - void Initialize(const FADT::ParsedFADT& fadt); + // Must be called after APIC initialization. + void Initialize(ACPI::CommonSDTHeader* xsdt); // Re-enable ACPI events after S3 resume. void Reinitialize(); diff --git a/kernel/src/ACPI/AcpiShutdown.cpp b/kernel/src/ACPI/AcpiShutdown.cpp index 26706e8..1d6b714 100644 --- a/kernel/src/ACPI/AcpiShutdown.cpp +++ b/kernel/src/ACPI/AcpiShutdown.cpp @@ -69,8 +69,8 @@ namespace Hal { // Initialize S3 suspend support (reads FACS and \_S3_ from namespace) AcpiSleep::Initialize(xsdt); - // Initialize ACPI event handling (SCI interrupt, power button) - AcpiEvents::Initialize(fadt); + // Note: ACPI event handling (SCI, power button) is initialized + // from Main.cpp after the APIC subsystem is ready. } bool IsAvailable() { diff --git a/kernel/src/Main.cpp b/kernel/src/Main.cpp index 6d07e17..236895b 100644 --- a/kernel/src/Main.cpp +++ b/kernel/src/Main.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -144,6 +145,9 @@ extern "C" void kmain() { Hal::ApicInitialize(g_acpi.GetXSDT()); + // Initialize ACPI events (SCI, power button) after APIC is ready + Hal::AcpiEvents::Initialize(g_acpi.GetXSDT()); + Pci::Initialize(g_acpi.GetXSDT()); Drivers::ProbeEarly(); diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 31ff8ae..903a68d 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,7 +59,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth desktop login shell +CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth desktop login shell rpgdemo SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, apps go to bin/apps//. @@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt # Common shared assets (wallpapers, etc.) COMMONKEEP := $(BINDIR)/common/.keep -.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth login desktop shell icons fonts bearssl libc tls libjpeg install-apps +.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth login desktop shell rpgdemo icons fonts bearssl libc tls libjpeg install-apps -all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth doom login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) +all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth doom rpgdemo login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) # Build BearSSL static library (cross-compiled for freestanding x86_64). BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc) @@ -184,12 +184,16 @@ icons: fonts: ../scripts/copy_fonts.sh +# Build RPG demo game via its own Makefile (depends on libc). +rpgdemo: libc + $(MAKE) -C src/rpgdemo + # Build doom via its own Makefile. doom: $(MAKE) -C src/doom # Install app bundles (manifests, icons, data files) into bin/apps//. -install-apps: doom spreadsheet weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music bluetooth +install-apps: doom rpgdemo spreadsheet weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music bluetooth ../scripts/install_apps.sh # Copy man pages into bin/man/ so mkramdisk.sh picks them up. @@ -241,3 +245,4 @@ clean: $(MAKE) -C src/volume clean $(MAKE) -C src/music clean $(MAKE) -C src/bluetooth clean + $(MAKE) -C src/rpgdemo clean diff --git a/programs/gameengine/Cute_Fantasy_Free/Animals/Chicken/Chicken.png b/programs/gameengine/Cute_Fantasy_Free/Animals/Chicken/Chicken.png new file mode 100644 index 0000000..19163df Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Animals/Chicken/Chicken.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Animals/Cow/Cow.png b/programs/gameengine/Cute_Fantasy_Free/Animals/Cow/Cow.png new file mode 100644 index 0000000..ea62b9a Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Animals/Cow/Cow.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Animals/Pig/Pig.png b/programs/gameengine/Cute_Fantasy_Free/Animals/Pig/Pig.png new file mode 100644 index 0000000..5312368 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Animals/Pig/Pig.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Animals/Sheep/Sheep.png b/programs/gameengine/Cute_Fantasy_Free/Animals/Sheep/Sheep.png new file mode 100644 index 0000000..242c1a0 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Animals/Sheep/Sheep.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Enemies/Skeleton.png b/programs/gameengine/Cute_Fantasy_Free/Enemies/Skeleton.png new file mode 100644 index 0000000..0e45bc4 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Enemies/Skeleton.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Enemies/Slime_Green.png b/programs/gameengine/Cute_Fantasy_Free/Enemies/Slime_Green.png new file mode 100644 index 0000000..352ff0e Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Enemies/Slime_Green.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Bridge_Wood.png b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Bridge_Wood.png new file mode 100644 index 0000000..8d72abb Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Bridge_Wood.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Chest.png b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Chest.png new file mode 100644 index 0000000..a0fe2b0 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Chest.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Fences.png b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Fences.png new file mode 100644 index 0000000..72e7260 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Fences.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/House_1_Wood_Base_Blue.png b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/House_1_Wood_Base_Blue.png new file mode 100644 index 0000000..fa216ce Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/House_1_Wood_Base_Blue.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Oak_Tree.png b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Oak_Tree.png new file mode 100644 index 0000000..5dbb903 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Oak_Tree.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Oak_Tree_Small.png b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Oak_Tree_Small.png new file mode 100644 index 0000000..e1f77ef Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Oak_Tree_Small.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Outdoor_Decor_Free.png b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Outdoor_Decor_Free.png new file mode 100644 index 0000000..c4a8218 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Outdoor decoration/Outdoor_Decor_Free.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Player/Player.png b/programs/gameengine/Cute_Fantasy_Free/Player/Player.png new file mode 100644 index 0000000..56575c8 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Player/Player.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Player/Player_Actions.png b/programs/gameengine/Cute_Fantasy_Free/Player/Player_Actions.png new file mode 100644 index 0000000..13c5d71 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Player/Player_Actions.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Tiles/Beach_Tile.png b/programs/gameengine/Cute_Fantasy_Free/Tiles/Beach_Tile.png new file mode 100644 index 0000000..2a67052 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Tiles/Beach_Tile.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Tiles/Cliff_Tile.png b/programs/gameengine/Cute_Fantasy_Free/Tiles/Cliff_Tile.png new file mode 100644 index 0000000..e0404ce Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Tiles/Cliff_Tile.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Tiles/FarmLand_Tile.png b/programs/gameengine/Cute_Fantasy_Free/Tiles/FarmLand_Tile.png new file mode 100644 index 0000000..3c291c9 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Tiles/FarmLand_Tile.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Tiles/Grass_Middle.png b/programs/gameengine/Cute_Fantasy_Free/Tiles/Grass_Middle.png new file mode 100644 index 0000000..8625931 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Tiles/Grass_Middle.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Tiles/Path_Middle.png b/programs/gameengine/Cute_Fantasy_Free/Tiles/Path_Middle.png new file mode 100644 index 0000000..161b3d3 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Tiles/Path_Middle.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Tiles/Path_Tile.png b/programs/gameengine/Cute_Fantasy_Free/Tiles/Path_Tile.png new file mode 100644 index 0000000..bc20df5 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Tiles/Path_Tile.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Tiles/Water_Middle.png b/programs/gameengine/Cute_Fantasy_Free/Tiles/Water_Middle.png new file mode 100644 index 0000000..d824f6a Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Tiles/Water_Middle.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/Tiles/Water_Tile.png b/programs/gameengine/Cute_Fantasy_Free/Tiles/Water_Tile.png new file mode 100644 index 0000000..904eff2 Binary files /dev/null and b/programs/gameengine/Cute_Fantasy_Free/Tiles/Water_Tile.png differ diff --git a/programs/gameengine/Cute_Fantasy_Free/read_me.txt b/programs/gameengine/Cute_Fantasy_Free/read_me.txt new file mode 100644 index 0000000..6ea16bf --- /dev/null +++ b/programs/gameengine/Cute_Fantasy_Free/read_me.txt @@ -0,0 +1,10 @@ +Hello! Thank you for downloading the Cute Fantasy asset pack. + +This project will be getting updates over time. This version of the asset pack is not final and there will be few more additional sprites. + +License - Free Version + - You can use these assets in non-commercial projects. + - You can modify the assets. + - You can not redistribute or resale, even if modified + +If you like the asset pack leave a comment. It helps to support the asset pack and get more people to see it. Thanks! \ No newline at end of file diff --git a/programs/include/engine/audio.h b/programs/include/engine/audio.h new file mode 100644 index 0000000..b6646db --- /dev/null +++ b/programs/include/engine/audio.h @@ -0,0 +1,81 @@ +/* + * audio.h + * MontaukOS 2D Game Engine - Audio System + * Wraps MontaukOS audio syscalls for game sound playback + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once +#include +#include + +namespace engine { + +static constexpr int AUDIO_SAMPLE_RATE = 44100; +static constexpr int AUDIO_CHANNELS = 2; +static constexpr int AUDIO_BITS = 16; + +struct AudioEngine { + int handle = -1; + int volume = 80; + + bool init() { + handle = montauk::audio_open(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS, AUDIO_BITS); + if (handle < 0) return false; + montauk::audio_set_volume(handle, volume); + return true; + } + + void shutdown() { + if (handle >= 0) { + montauk::audio_close(handle); + handle = -1; + } + } + + void set_volume(int percent) { + volume = percent; + if (handle >= 0) + montauk::audio_set_volume(handle, volume); + } + + // Write raw PCM samples to the audio device. + // data: signed 16-bit stereo PCM samples + // size: number of bytes + bool write_pcm(const void* data, uint32_t size) { + if (handle < 0) return false; + return montauk::audio_write(handle, data, size) >= 0; + } + + // Generate and play a simple tone (square wave) for duration_ms. + // Useful for basic sound effects. Non-blocking (writes samples to buffer). + void play_tone(int freq_hz, int duration_ms, int tone_volume = 50) { + if (handle < 0 || freq_hz <= 0) return; + + int total_samples = (AUDIO_SAMPLE_RATE * duration_ms) / 1000; + int half_period = AUDIO_SAMPLE_RATE / (2 * freq_hz); + if (half_period <= 0) half_period = 1; + + int16_t amp = (int16_t)(327 * tone_volume / 100); // ~1% of max + static constexpr int CHUNK = 512; + int16_t buf[CHUNK * 2]; // stereo + + int written = 0; + while (written < total_samples) { + int n = total_samples - written; + if (n > CHUNK) n = CHUNK; + + for (int i = 0; i < n; i++) { + int sample_pos = written + i; + int16_t val = ((sample_pos / half_period) % 2 == 0) ? amp : -amp; + buf[i * 2] = val; + buf[i * 2 + 1] = val; + } + + montauk::audio_write(handle, buf, n * 4); + written += n; + } + } +}; + +} // namespace engine diff --git a/programs/include/engine/collision.h b/programs/include/engine/collision.h new file mode 100644 index 0000000..3ba3234 --- /dev/null +++ b/programs/include/engine/collision.h @@ -0,0 +1,48 @@ +/* + * collision.h + * MontaukOS 2D Game Engine - Collision Detection + * AABB overlap testing and tile-based collision response + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once +#include +#include "engine/tilemap.h" + +namespace engine { + +struct AABB { + float x, y, w, h; + + bool overlaps(const AABB& other) const { + return x < other.x + other.w && x + w > other.x && + y < other.y + other.h && y + h > other.y; + } + + bool contains_point(float px, float py) const { + return px >= x && px < x + w && py >= y && py < y + h; + } +}; + +// Try to move an entity by (dx, dy) on the tilemap. +// Uses separate X/Y axis resolution so the entity slides along walls. +// box: the entity's collision box in world pixel coordinates. +// Returns adjusted dx, dy after collision. +inline void resolve_tilemap_collision(const Tilemap& map, + float bx, float by, float bw, float bh, + float& dx, float& dy) { + // Try X movement + float new_x = bx + dx; + if (map.collides((int)new_x, (int)by, (int)bw, (int)bh)) { + dx = 0.0f; + new_x = bx; + } + + // Try Y movement + float new_y = by + dy; + if (map.collides((int)new_x, (int)new_y, (int)bw, (int)bh)) { + dy = 0.0f; + } +} + +} // namespace engine diff --git a/programs/include/engine/engine.h b/programs/include/engine/engine.h new file mode 100644 index 0000000..2af3a1e --- /dev/null +++ b/programs/include/engine/engine.h @@ -0,0 +1,200 @@ +/* + * engine.h + * MontaukOS 2D Game Engine - Core + * Window management, timing, pixel buffer helpers + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +} + +namespace engine { + +// ============================================================================ +// Core engine +// ============================================================================ + +struct Engine { + int win_id = -1; + uint32_t* pixels = nullptr; + int screen_w = 0; + int screen_h = 0; + + // Timing + uint64_t last_time_ms = 0; + float dt = 0.016f; // delta time in seconds + uint64_t frame_count = 0; + + // Font + gui::TrueTypeFont* font = nullptr; + + bool running = true; + + // ---- Lifecycle ---- + + bool init(const char* title, int w, int h) { + screen_w = w; + screen_h = h; + + // Load font + font = (gui::TrueTypeFont*)montauk::malloc(sizeof(gui::TrueTypeFont)); + if (font) { + montauk::memset(font, 0, sizeof(gui::TrueTypeFont)); + if (!font->init("0:/fonts/Roboto-Medium.ttf")) { + montauk::mfree(font); + font = nullptr; + } + } + + // Create window + Montauk::WinCreateResult wres; + if (montauk::win_create(title, w, h, &wres) < 0 || wres.id < 0) + return false; + + win_id = wres.id; + pixels = (uint32_t*)(uintptr_t)wres.pixelVa; + last_time_ms = montauk::get_milliseconds(); + return true; + } + + void update_timing() { + uint64_t now = montauk::get_milliseconds(); + uint64_t elapsed = now - last_time_ms; + if (elapsed == 0) elapsed = 1; + if (elapsed > 100) elapsed = 100; // cap to avoid spiral + dt = (float)elapsed / 1000.0f; + last_time_ms = now; + frame_count++; + } + + // Poll one window event. Returns true if an event was received. + bool poll(Montauk::WinEvent* ev) { + int r = montauk::win_poll(win_id, ev); + if (r < 0) { running = false; return false; } + if (r == 0) return false; + if (ev->type == 3) { running = false; return false; } // close + if (ev->type == 2) { // resize + screen_w = ev->resize.w; + screen_h = ev->resize.h; + pixels = (uint32_t*)(uintptr_t)montauk::win_resize( + win_id, screen_w, screen_h); + } + return true; + } + + void present() { + montauk::win_present(win_id); + } + + void shutdown() { + if (win_id >= 0) montauk::win_destroy(win_id); + win_id = -1; + } + + // ---- Drawing helpers ---- + + void clear(uint32_t color) { + int count = screen_w * screen_h; + for (int i = 0; i < count; i++) + pixels[i] = color; + } + + void fill_rect(int x, int y, int w, int h, uint32_t color) { + int x0 = x < 0 ? 0 : x; + int y0 = y < 0 ? 0 : y; + int x1 = x + w > screen_w ? screen_w : x + w; + int y1 = y + h > screen_h ? screen_h : y + h; + for (int row = y0; row < y1; row++) + for (int col = x0; col < x1; col++) + pixels[row * screen_w + col] = color; + } + + void fill_rect_alpha(int x, int y, int w, int h, uint32_t color) { + uint8_t sa = (color >> 24) & 0xFF; + uint8_t sr = (color >> 16) & 0xFF; + uint8_t sg = (color >> 8) & 0xFF; + uint8_t sb = color & 0xFF; + if (sa == 0) return; + int x0 = x < 0 ? 0 : x; + int y0 = y < 0 ? 0 : y; + int x1 = x + w > screen_w ? screen_w : x + w; + int y1 = y + h > screen_h ? screen_h : y + h; + for (int row = y0; row < y1; row++) { + for (int col = x0; col < x1; col++) { + uint32_t dst = pixels[row * screen_w + col]; + uint8_t dr = (dst >> 16) & 0xFF; + uint8_t dg = (dst >> 8) & 0xFF; + uint8_t db = dst & 0xFF; + uint32_t inv = 255 - sa; + uint32_t rr = (sa * sr + inv * dr + 128) / 255; + uint32_t gg = (sa * sg + inv * dg + 128) / 255; + uint32_t bb = (sa * sb + inv * db + 128) / 255; + pixels[row * screen_w + col] = + 0xFF000000 | (rr << 16) | (gg << 8) | bb; + } + } + } + + void draw_rect_outline(int x, int y, int w, int h, uint32_t color) { + fill_rect(x, y, w, 1, color); + fill_rect(x, y + h - 1, w, 1, color); + fill_rect(x, y, 1, h, color); + fill_rect(x + w - 1, y, 1, h, color); + } + + void draw_text(int x, int y, const char* text, gui::Color c, int size = 16) { + if (font) + font->draw_to_buffer(pixels, screen_w, screen_h, x, y, text, c, size); + } + + int text_width(const char* text, int size = 16) { + return font ? font->measure_text(text, size) : 0; + } + + int text_height(int size = 16) { + if (!font) return size; + auto* gc = font->get_cache(size); + if (!gc) return size; + return gc->ascent - gc->descent; + } +}; + +// ============================================================================ +// File loading helper +// ============================================================================ + +struct FileData { + uint8_t* data = nullptr; + uint64_t size = 0; + + bool load(const char* vfs_path) { + int fd = montauk::open(vfs_path); + if (fd < 0) return false; + size = montauk::getsize(fd); + if (size == 0 || size > 16 * 1024 * 1024) { + montauk::close(fd); + return false; + } + data = (uint8_t*)montauk::malloc(size); + if (!data) { montauk::close(fd); return false; } + montauk::read(fd, data, 0, size); + montauk::close(fd); + return true; + } + + void free() { + if (data) { montauk::mfree(data); data = nullptr; } + size = 0; + } +}; + +} // namespace engine diff --git a/programs/include/engine/input.h b/programs/include/engine/input.h new file mode 100644 index 0000000..da6c515 --- /dev/null +++ b/programs/include/engine/input.h @@ -0,0 +1,87 @@ +/* + * input.h + * MontaukOS 2D Game Engine - Input State + * Keyboard and mouse state tracking across frames + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once +#include +#include +#include + +namespace engine { + +// Scancode constants for common keys +namespace key { + static constexpr uint8_t ESC = 0x01; + static constexpr uint8_t W = 0x11; + static constexpr uint8_t A = 0x1E; + static constexpr uint8_t S = 0x1F; + static constexpr uint8_t D = 0x20; + static constexpr uint8_t E = 0x12; + static constexpr uint8_t SPACE = 0x39; + static constexpr uint8_t UP = 0x48; + static constexpr uint8_t DOWN = 0x50; + static constexpr uint8_t LEFT = 0x4B; + static constexpr uint8_t RIGHT = 0x4D; + static constexpr uint8_t ENTER = 0x1C; + static constexpr uint8_t TAB = 0x0F; +} + +struct InputState { + // Key states: true = currently held + bool keys[256]; + // Keys pressed this frame (edge-triggered) + bool keys_pressed[256]; + + // Mouse + int mouse_x = 0; + int mouse_y = 0; + int mouse_scroll = 0; + uint8_t mouse_buttons = 0; + uint8_t prev_mouse_buttons = 0; + + void init() { + montauk::memset(keys, 0, sizeof(keys)); + montauk::memset(keys_pressed, 0, sizeof(keys_pressed)); + } + + // Call at the start of each frame to clear per-frame state + void begin_frame() { + montauk::memset(keys_pressed, 0, sizeof(keys_pressed)); + mouse_scroll = 0; + prev_mouse_buttons = mouse_buttons; + } + + // Process a window event + void handle_event(const Montauk::WinEvent& ev) { + if (ev.type == 0) { // keyboard + // PS/2 scan code set 1: release codes have bit 7 set. + // Mask to base scancode so press and release map to the same index. + uint8_t sc = ev.key.scancode & 0x7F; + if (ev.key.pressed) { + if (!keys[sc]) + keys_pressed[sc] = true; + keys[sc] = true; + } else { + keys[sc] = false; + } + } else if (ev.type == 1) { // mouse + mouse_x = ev.mouse.x; + mouse_y = ev.mouse.y; + mouse_scroll = ev.mouse.scroll; + mouse_buttons = ev.mouse.buttons; + } + } + + bool key_held(uint8_t scancode) const { return keys[scancode]; } + bool key_just_pressed(uint8_t scancode) const { return keys_pressed[scancode]; } + + bool mouse_clicked() const { + return (mouse_buttons & 1) && !(prev_mouse_buttons & 1); + } + bool mouse_held() const { return mouse_buttons & 1; } +}; + +} // namespace engine diff --git a/programs/include/engine/sprite.h b/programs/include/engine/sprite.h new file mode 100644 index 0000000..3f38a5f --- /dev/null +++ b/programs/include/engine/sprite.h @@ -0,0 +1,264 @@ +/* + * sprite.h + * MontaukOS 2D Game Engine - Sprite and Animation System + * PNG spritesheet loading, frame extraction, alpha-blended rendering + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once +#include +#include +#include +#include "engine/engine.h" + +extern "C" { +#include +} + +namespace engine { + +// ============================================================================ +// Convert stb_image RGBA output to MontaukOS ARGB pixel format +// stb on little-endian x86: pixel bytes in memory are R,G,B,A +// When read as uint32_t: 0xAABBGGRR +// MontaukOS format: 0xAARRGGBB +// ============================================================================ + +inline void rgba_to_argb(uint32_t* data, int count) { + for (int i = 0; i < count; i++) { + uint32_t px = data[i]; + uint8_t r = px & 0xFF; + uint8_t g = (px >> 8) & 0xFF; + uint8_t b = (px >> 16) & 0xFF; + uint8_t a = (px >> 24) & 0xFF; + data[i] = ((uint32_t)a << 24) | ((uint32_t)r << 16) | + ((uint32_t)g << 8) | b; + } +} + +// ============================================================================ +// Spritesheet +// ============================================================================ + +struct Spritesheet { + uint32_t* pixels = nullptr; + int width = 0; + int height = 0; + int frame_w = 0; + int frame_h = 0; + int cols = 0; + int rows = 0; + + // Load a PNG spritesheet from VFS and split into frames of given size. + // If frame_w/frame_h are 0, treat the entire image as a single frame. + bool load(const char* vfs_path, int fw = 0, int fh = 0) { + FileData file; + if (!file.load(vfs_path)) return false; + + int w, h, channels; + uint32_t* img = (uint32_t*)stbi_load_from_memory( + file.data, (int)file.size, &w, &h, &channels, 4); + file.free(); + + if (!img) return false; + + // Convert RGBA to ARGB + rgba_to_argb(img, w * h); + + pixels = img; + width = w; + height = h; + frame_w = fw > 0 ? fw : w; + frame_h = fh > 0 ? fh : h; + cols = w / frame_w; + rows = h / frame_h; + return true; + } + + void unload() { + if (pixels) { + stbi_image_free(pixels); + pixels = nullptr; + } + } + + // Blit a single frame to the destination buffer with scaling and alpha. + // frame_col/frame_row select which frame from the spritesheet. + // dst_x/dst_y is the screen position. scale is the integer scale factor. + // flip_h mirrors the sprite horizontally. + void draw_frame(uint32_t* dst, int dst_w, int dst_h, + int frame_col, int frame_row, + int dst_x, int dst_y, int scale = 1, + bool flip_h = false) const { + if (!pixels) return; + if (frame_col < 0 || frame_col >= cols) return; + if (frame_row < 0 || frame_row >= rows) return; + + int src_ox = frame_col * frame_w; + int src_oy = frame_row * frame_h; + int out_w = frame_w * scale; + int out_h = frame_h * scale; + + for (int py = 0; py < out_h; py++) { + int dy = dst_y + py; + if (dy < 0 || dy >= dst_h) continue; + int sy = src_oy + py / scale; + + for (int px = 0; px < out_w; px++) { + int dx = dst_x + px; + if (dx < 0 || dx >= dst_w) continue; + + int sx_local = px / scale; + if (flip_h) sx_local = frame_w - 1 - sx_local; + int sx = src_ox + sx_local; + + uint32_t src_px = pixels[sy * width + sx]; + uint8_t sa = (src_px >> 24) & 0xFF; + if (sa == 0) continue; + + if (sa == 255) { + dst[dy * dst_w + dx] = src_px; + } else { + uint32_t d = dst[dy * dst_w + dx]; + uint8_t sr = (src_px >> 16) & 0xFF; + uint8_t sg = (src_px >> 8) & 0xFF; + uint8_t sb = src_px & 0xFF; + uint8_t dr = (d >> 16) & 0xFF; + uint8_t dg = (d >> 8) & 0xFF; + uint8_t db = d & 0xFF; + uint32_t inv = 255 - sa; + uint32_t rr = (sa * sr + inv * dr + 128) / 255; + uint32_t gg = (sa * sg + inv * dg + 128) / 255; + uint32_t bb = (sa * sb + inv * db + 128) / 255; + dst[dy * dst_w + dx] = + 0xFF000000 | (rr << 16) | (gg << 8) | bb; + } + } + } + } + + // Draw an arbitrary sub-rectangle of the spritesheet (not frame-aligned) + void draw_region(uint32_t* dst, int dst_w, int dst_h, + int src_x, int src_y, int src_w, int src_h, + int dst_x, int dst_y, int scale = 1) const { + if (!pixels) return; + + int out_w = src_w * scale; + int out_h = src_h * scale; + + for (int py = 0; py < out_h; py++) { + int dy = dst_y + py; + if (dy < 0 || dy >= dst_h) continue; + int sy = src_y + py / scale; + if (sy < 0 || sy >= height) continue; + + for (int px = 0; px < out_w; px++) { + int dx = dst_x + px; + if (dx < 0 || dx >= dst_w) continue; + int sx = src_x + px / scale; + if (sx < 0 || sx >= width) continue; + + uint32_t src_px = pixels[sy * width + sx]; + uint8_t sa = (src_px >> 24) & 0xFF; + if (sa == 0) continue; + + if (sa == 255) { + dst[dy * dst_w + dx] = src_px; + } else { + uint32_t d = dst[dy * dst_w + dx]; + uint8_t sr = (src_px >> 16) & 0xFF; + uint8_t sg = (src_px >> 8) & 0xFF; + uint8_t sb = src_px & 0xFF; + uint8_t dr = (d >> 16) & 0xFF; + uint8_t dg = (d >> 8) & 0xFF; + uint8_t db = d & 0xFF; + uint32_t inv = 255 - sa; + uint32_t rr = (sa * sr + inv * dr + 128) / 255; + uint32_t gg = (sa * sg + inv * dg + 128) / 255; + uint32_t bb = (sa * sb + inv * db + 128) / 255; + dst[dy * dst_w + dx] = + 0xFF000000 | (rr << 16) | (gg << 8) | bb; + } + } + } + } +}; + +// ============================================================================ +// Animation +// ============================================================================ + +struct Animation { + int row = 0; // spritesheet row for this animation + int start_col = 0; // first frame column + int num_frames = 1; // number of frames + float speed = 8.0f; // frames per second + + float timer = 0.0f; + int current = 0; + + void update(float dt) { + timer += dt * speed; + while (timer >= 1.0f) { + timer -= 1.0f; + current++; + if (current >= num_frames) current = 0; + } + } + + void reset() { + timer = 0.0f; + current = 0; + } + + int frame_col() const { return start_col + current; } + int frame_row() const { return row; } +}; + +// ============================================================================ +// Animated Sprite - combines a spritesheet with named animations +// ============================================================================ + +static constexpr int MAX_ANIMS = 16; + +struct AnimatedSprite { + Spritesheet* sheet = nullptr; + Animation anims[MAX_ANIMS]; + int anim_count = 0; + int current_anim = 0; + bool flip_h = false; + + int add_anim(int row, int start_col, int num_frames, float speed = 8.0f) { + if (anim_count >= MAX_ANIMS) return -1; + int idx = anim_count++; + anims[idx].row = row; + anims[idx].start_col = start_col; + anims[idx].num_frames = num_frames; + anims[idx].speed = speed; + return idx; + } + + void play(int anim_idx) { + if (anim_idx < 0 || anim_idx >= anim_count) return; + if (current_anim != anim_idx) { + current_anim = anim_idx; + anims[current_anim].reset(); + } + } + + void update(float dt) { + if (current_anim >= 0 && current_anim < anim_count) + anims[current_anim].update(dt); + } + + void draw(uint32_t* dst, int dst_w, int dst_h, + int x, int y, int scale = 1) const { + if (!sheet || current_anim < 0 || current_anim >= anim_count) return; + const Animation& a = anims[current_anim]; + sheet->draw_frame(dst, dst_w, dst_h, + a.frame_col(), a.frame_row(), + x, y, scale, flip_h); + } +}; + +} // namespace engine diff --git a/programs/include/engine/tilemap.h b/programs/include/engine/tilemap.h new file mode 100644 index 0000000..3df9a9f --- /dev/null +++ b/programs/include/engine/tilemap.h @@ -0,0 +1,134 @@ +/* + * tilemap.h + * MontaukOS 2D Game Engine - Tile Map + * Grid-based terrain with per-tile rendering and collision + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once +#include +#include +#include +#include "engine/sprite.h" + +namespace engine { + +static constexpr int MAX_TILE_TYPES = 16; + +struct TileType { + Spritesheet* sheet; // tile image (may be a single-tile spritesheet) + int src_x, src_y; // source position within the sheet + int src_w, src_h; // source size (typically tile_size x tile_size) + bool solid; // blocks movement +}; + +struct Tilemap { + int* data = nullptr; + int map_w = 0; + int map_h = 0; + int tile_size = 16; // native tile size in pixels + + TileType types[MAX_TILE_TYPES]; + int type_count = 0; + + bool alloc(int w, int h, int ts = 16) { + map_w = w; + map_h = h; + tile_size = ts; + data = (int*)montauk::malloc(w * h * sizeof(int)); + if (!data) return false; + montauk::memset(data, 0, w * h * sizeof(int)); + return true; + } + + void free_map() { + if (data) { montauk::mfree(data); data = nullptr; } + } + + // Register a tile type. Returns the tile ID. + int add_type(Spritesheet* sheet, int sx, int sy, int sw, int sh, bool solid) { + if (type_count >= MAX_TILE_TYPES) return -1; + int id = type_count++; + types[id].sheet = sheet; + types[id].src_x = sx; + types[id].src_y = sy; + types[id].src_w = sw; + types[id].src_h = sh; + types[id].solid = solid; + return id; + } + + void set(int x, int y, int tile_id) { + if (x >= 0 && x < map_w && y >= 0 && y < map_h) + data[y * map_w + x] = tile_id; + } + + int get(int x, int y) const { + if (x < 0 || x >= map_w || y < 0 || y >= map_h) return -1; + return data[y * map_w + x]; + } + + bool is_solid(int x, int y) const { + int id = get(x, y); + if (id < 0 || id >= type_count) return true; // out of bounds = solid + return types[id].solid; + } + + // Check if a world-pixel rectangle collides with any solid tile. + // World coordinates are in native (unscaled) pixels. + bool collides(int wx, int wy, int ww, int wh) const { + // Negative coordinates are always solid (out of bounds) + if (wx < 0 || wy < 0) return true; + int tx0 = wx / tile_size; + int ty0 = wy / tile_size; + int tx1 = (wx + ww - 1) / tile_size; + int ty1 = (wy + wh - 1) / tile_size; + + for (int ty = ty0; ty <= ty1; ty++) + for (int tx = tx0; tx <= tx1; tx++) + if (is_solid(tx, ty)) + return true; + return false; + } + + // Draw visible tiles to the pixel buffer. + // cam_x/cam_y: camera position in world pixels (native scale). + // scale: rendering scale factor. + void draw(uint32_t* dst, int dst_w, int dst_h, + int cam_x, int cam_y, int scale) const { + if (!data) return; + + int ts = tile_size * scale; + + // Determine visible tile range + int tx0 = cam_x / tile_size; + int ty0 = cam_y / tile_size; + int tx1 = tx0 + dst_w / ts + 2; + int ty1 = ty0 + dst_h / ts + 2; + + if (tx0 < 0) tx0 = 0; + if (ty0 < 0) ty0 = 0; + if (tx1 > map_w) tx1 = map_w; + if (ty1 > map_h) ty1 = map_h; + + for (int ty = ty0; ty < ty1; ty++) { + for (int tx = tx0; tx < tx1; tx++) { + int id = data[ty * map_w + tx]; + if (id < 0 || id >= type_count) continue; + + const TileType& tt = types[id]; + if (!tt.sheet) continue; + + int sx = tx * ts - cam_x * scale; + int sy = ty * ts - cam_y * scale; + + tt.sheet->draw_region(dst, dst_w, dst_h, + tt.src_x, tt.src_y, + tt.src_w, tt.src_h, + sx, sy, scale); + } + } + } +}; + +} // namespace engine diff --git a/programs/include/engine/ui.h b/programs/include/engine/ui.h new file mode 100644 index 0000000..98028f2 --- /dev/null +++ b/programs/include/engine/ui.h @@ -0,0 +1,114 @@ +/* + * ui.h + * MontaukOS 2D Game Engine - UI Rendering + * Health bars, text overlays, dialog boxes, menus + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once +#include +#include "engine/engine.h" + +extern "C" { +#include +} + +namespace engine { + +// ============================================================================ +// Health / stat bar +// ============================================================================ + +inline void draw_bar(Engine& eng, int x, int y, int w, int h, + int value, int max_value, + uint32_t fill_color, uint32_t bg_color, + uint32_t border_color) { + eng.fill_rect(x, y, w, h, bg_color); + if (max_value > 0 && value > 0) { + int fw = (value * (w - 2)) / max_value; + if (fw < 1) fw = 1; + eng.fill_rect(x + 1, y + 1, fw, h - 2, fill_color); + } + eng.draw_rect_outline(x, y, w, h, border_color); +} + +// ============================================================================ +// Text box / dialog +// ============================================================================ + +inline void draw_dialog(Engine& eng, int x, int y, int w, int h, + const char* text, gui::Color text_color, + int font_size = 16) { + // Background with border + eng.fill_rect(x, y, w, h, 0xF0FFFFFF); + eng.draw_rect_outline(x, y, w, h, 0xFF333333); + eng.draw_rect_outline(x + 1, y + 1, w - 2, h - 2, 0xFFCCCCCC); + + // Text (with word wrapping) + int tx = x + 8; + int ty = y + 6; + int max_w = w - 16; + int line_h = eng.text_height(font_size) + 2; + + // Simple word wrap + char line_buf[128]; + int li = 0; + const char* p = text; + while (*p) { + li = 0; + while (*p && *p != '\n') { + // Save state before adding next word + int word_start = li; + const char* word_p = p; + + while (*p && *p != ' ' && *p != '\n' && li < 126) { + line_buf[li++] = *p++; + } + line_buf[li] = '\0'; + + // Check width + if (eng.text_width(line_buf, font_size) > max_w && word_start > 0) { + // Line too long - rewind to before this word + li = word_start; + if (li > 0 && line_buf[li - 1] == ' ') li--; + line_buf[li] = '\0'; + p = word_p; // rewind pointer to retry this word on next line + break; + } + + if (*p == ' ') { line_buf[li++] = ' '; p++; } + } + line_buf[li] = '\0'; + if (*p == '\n') p++; + + if (ty + line_h > y + h - 4) break; + eng.draw_text(tx, ty, line_buf, text_color, font_size); + ty += line_h; + } +} + +// ============================================================================ +// Prompt bar (bottom of screen) +// ============================================================================ + +inline void draw_prompt(Engine& eng, const char* text, int font_size = 14) { + int bar_h = eng.text_height(font_size) + 8; + int y = eng.screen_h - bar_h; + eng.fill_rect_alpha(0, y, eng.screen_w, bar_h, 0xCC000000); + int tw = eng.text_width(text, font_size); + eng.draw_text((eng.screen_w - tw) / 2, y + 4, text, + gui::Color::from_rgb(0xFF, 0xFF, 0xFF), font_size); +} + +// ============================================================================ +// Simple HUD text +// ============================================================================ + +inline void draw_hud_text(Engine& eng, int x, int y, + const char* text, gui::Color c, int size = 14) { + // Draw shadow first for readability + eng.draw_text(x + 1, y + 1, text, gui::Color::from_rgba(0, 0, 0, 180), size); + eng.draw_text(x, y, text, c, size); +} + +} // namespace engine diff --git a/programs/src/rpgdemo/Makefile b/programs/src/rpgdemo/Makefile new file mode 100644 index 0000000..689ac01 --- /dev/null +++ b/programs/src/rpgdemo/Makefile @@ -0,0 +1,116 @@ +# Makefile for rpgdemo (Montauk Quest - 2D RPG demo) on MontaukOS +# Copyright (c) 2026 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +# ---- Toolchain ---- + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +# ---- Paths ---- + +PROG_INC := ../../include +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj +LIBDIR := ../../lib + +# Game asset source directory +ASSET_SRC := ../../gameengine/Cute_Fantasy_Free + +# ---- Compiler flags ---- + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-function \ + -Wno-missing-field-initializers \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -fno-tree-loop-distribute-patterns \ + -m64 \ + -march=x86-64 \ + -msse \ + -msse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I $(PROG_INC) \ + -I . \ + -isystem $(PROG_INC)/libc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +# ---- Linker flags ---- + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +# ---- Source files ---- + +SRCS := main.cpp stb_impl.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +# ---- Target ---- + +TARGET := $(BINDIR)/apps/rpgdemo/rpgdemo.elf +APP_DIR := $(BINDIR)/apps/rpgdemo + +.PHONY: all clean assets + +all: $(TARGET) assets + +$(TARGET): $(OBJS) $(LINK_LD) Makefile + mkdir -p $(APP_DIR) + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +# Copy game assets into the app bundle +assets: + mkdir -p $(APP_DIR) + cp $(ASSET_SRC)/Player/Player.png $(APP_DIR)/Player.png + cp $(ASSET_SRC)/Player/Player_Actions.png $(APP_DIR)/Player_Actions.png + cp $(ASSET_SRC)/Enemies/Skeleton.png $(APP_DIR)/Skeleton.png + cp $(ASSET_SRC)/Enemies/Slime_Green.png $(APP_DIR)/Slime_Green.png + cp $(ASSET_SRC)/Tiles/Grass_Middle.png $(APP_DIR)/Grass_Middle.png + cp $(ASSET_SRC)/Tiles/Path_Middle.png $(APP_DIR)/Path_Middle.png + cp $(ASSET_SRC)/Tiles/Water_Middle.png $(APP_DIR)/Water_Middle.png + cp $(ASSET_SRC)/Tiles/Cliff_Tile.png $(APP_DIR)/Cliff_Tile.png + cp "$(ASSET_SRC)/Outdoor decoration/Oak_Tree.png" $(APP_DIR)/Oak_Tree.png + cp "$(ASSET_SRC)/Outdoor decoration/Oak_Tree_Small.png" $(APP_DIR)/Oak_Tree_Small.png + cp "$(ASSET_SRC)/Outdoor decoration/House_1_Wood_Base_Blue.png" $(APP_DIR)/House.png + cp "$(ASSET_SRC)/Outdoor decoration/Chest.png" $(APP_DIR)/Chest.png + cp "$(ASSET_SRC)/Outdoor decoration/Fences.png" $(APP_DIR)/Fences.png + cp "$(ASSET_SRC)/Outdoor decoration/Bridge_Wood.png" $(APP_DIR)/Bridge_Wood.png + cp "$(ASSET_SRC)/Outdoor decoration/Outdoor_Decor_Free.png" $(APP_DIR)/Outdoor_Decor.png + cp $(ASSET_SRC)/Animals/Chicken/Chicken.png $(APP_DIR)/Chicken.png + cp $(ASSET_SRC)/Animals/Cow/Cow.png $(APP_DIR)/Cow.png + cp $(ASSET_SRC)/Animals/Pig/Pig.png $(APP_DIR)/Pig.png + cp $(ASSET_SRC)/Animals/Sheep/Sheep.png $(APP_DIR)/Sheep.png + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/rpgdemo/main.cpp b/programs/src/rpgdemo/main.cpp new file mode 100644 index 0000000..c244042 --- /dev/null +++ b/programs/src/rpgdemo/main.cpp @@ -0,0 +1,1079 @@ +/* + * main.cpp + * MontaukOS RPG Demo - "Montauk Quest" + * A 2D fantasy RPG demonstrating the MontaukOS game engine + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +using namespace engine; +using namespace gui; + +// ============================================================================ +// Constants +// ============================================================================ + +static constexpr int WIN_W = 800; +static constexpr int WIN_H = 600; +static constexpr int SCALE = 2; // pixel art scale +static constexpr int TILE_SIZE = 16; // native tile size +static constexpr int MAP_W = 50; // map width in tiles +static constexpr int MAP_H = 40; // map height in tiles +static constexpr float PLAYER_SPEED = 80.0f; // pixels/sec (native scale) +static constexpr float ENEMY_SPEED = 30.0f; +static constexpr int PLAYER_HP = 100; +static constexpr int MAX_ENEMIES = 8; +static constexpr int MAX_DECORATIONS = 96; +static constexpr int MAX_CHESTS = 6; + +// Player sprite layout (32x32 frames, 6 cols x 10 rows) +static constexpr int SPR_W = 32; +static constexpr int SPR_H = 32; + +// Directions +enum Dir { DIR_DOWN = 0, DIR_RIGHT = 1, DIR_UP = 2, DIR_LEFT = 3 }; + +// ============================================================================ +// Entity types +// ============================================================================ + +struct Player { + float x, y; // world position (native pixels) + int health; + int max_health; + int direction; + bool moving; + int score; + float hit_timer; // invincibility after taking damage + AnimatedSprite sprite; + int anim_walk[4]; // walk anim indices per direction + int anim_idle[4]; // idle anim indices per direction +}; + +struct Enemy { + float x, y; + int health; + int max_health; + int direction; + bool active; + float patrol_timer; + float patrol_duration; + AnimatedSprite sprite; + int anim_walk[4]; + int type; // 0 = skeleton, 1 = slime +}; + +struct Decoration { + float x, y; + Spritesheet* sheet; + bool solid; + float col_x, col_y, col_w, col_h; // collision box offset + int frame_col, frame_row; // -1 = draw full sheet +}; + +struct Chest { + float x, y; + bool opened; + bool collected; +}; + +// ============================================================================ +// Game state +// ============================================================================ + +static Engine g_engine; +static InputState g_input; +static AudioEngine g_audio; +static Tilemap g_map; + +// Spritesheets +static Spritesheet g_spr_player; +static Spritesheet g_spr_skeleton; +static Spritesheet g_spr_slime; +static Spritesheet g_spr_grass; +static Spritesheet g_spr_path; +static Spritesheet g_spr_water; +static Spritesheet g_spr_tree; +static Spritesheet g_spr_tree_small; +static Spritesheet g_spr_house; +static Spritesheet g_spr_chest; +static Spritesheet g_spr_fences; +static Spritesheet g_spr_gravestone; + +// Entities +static Player g_player; +static Enemy g_enemies[MAX_ENEMIES]; +static int g_enemy_count = 0; +static Decoration g_decor[MAX_DECORATIONS]; +static int g_decor_count = 0; +static Chest g_chests[MAX_CHESTS]; +static int g_chest_count = 0; + +// Camera +static float g_cam_x = 0; +static float g_cam_y = 0; + +// UI state +static char g_prompt[128] = {}; +static char g_dialog_text[256] = {}; +static float g_dialog_timer = 0; + +// ============================================================================ +// Asset loading +// ============================================================================ + +static bool load_assets() { + const char* base = "0:/apps/rpgdemo/"; + char path[128]; + + auto make_path = [&](const char* file) { + snprintf(path, sizeof(path), "%s%s", base, file); + }; + + // Player (32x32 frames, 6 columns x 10 rows) + make_path("Player.png"); + if (!g_spr_player.load(path, SPR_W, SPR_H)) return false; + + // Skeleton (same layout as player) + make_path("Skeleton.png"); + if (!g_spr_skeleton.load(path, SPR_W, SPR_H)) return false; + + // Slime (64x64 frames) + make_path("Slime_Green.png"); + g_spr_slime.load(path, 64, 64); // not fatal if missing + + // Tiles (single 16x16 images) + make_path("Grass_Middle.png"); + if (!g_spr_grass.load(path, TILE_SIZE, TILE_SIZE)) return false; + + make_path("Path_Middle.png"); + if (!g_spr_path.load(path, TILE_SIZE, TILE_SIZE)) return false; + + make_path("Water_Middle.png"); + if (!g_spr_water.load(path, TILE_SIZE, TILE_SIZE)) return false; + + // Decorations + make_path("Oak_Tree.png"); + g_spr_tree.load(path); + + make_path("Oak_Tree_Small.png"); + g_spr_tree_small.load(path, 32, 48); + + make_path("House.png"); + g_spr_house.load(path); + + make_path("Chest.png"); + g_spr_chest.load(path, TILE_SIZE, TILE_SIZE); + + make_path("Fences.png"); + g_spr_fences.load(path, TILE_SIZE, TILE_SIZE); + + // Procedural gravestone sprite (16x16) + { + static const uint8_t bmp[16 * 16] = { + 0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0, + 0,0,0,0,0,2,1,1,1,1,2,0,0,0,0,0, + 0,0,0,0,2,1,1,1,1,1,1,2,0,0,0,0, + 0,0,0,0,2,1,1,1,1,1,1,2,0,0,0,0, + 0,0,0,0,2,1,1,2,1,1,1,2,0,0,0,0, + 0,0,0,0,2,1,2,2,2,1,1,2,0,0,0,0, + 0,0,0,0,2,1,1,2,1,1,1,2,0,0,0,0, + 0,0,0,0,2,1,1,2,1,1,1,2,0,0,0,0, + 0,0,0,0,2,1,1,1,1,1,1,2,0,0,0,0, + 0,0,0,0,2,1,1,1,1,1,1,2,0,0,0,0, + 0,0,0,0,2,1,1,1,1,1,1,2,0,0,0,0, + 0,0,0,0,2,1,1,1,1,1,1,2,0,0,0,0, + 0,0,0,0,2,2,2,2,2,2,2,2,0,0,0,0, + 0,0,0,3,3,3,3,3,3,3,3,3,3,0,0,0, + 0,0,0,0,0,3,3,3,3,3,3,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + }; + static const uint32_t pal[4] = { + 0x00000000, 0xFF999999, 0xFF555555, 0xFF665544, + }; + g_spr_gravestone.width = 16; + g_spr_gravestone.height = 16; + g_spr_gravestone.frame_w = 16; + g_spr_gravestone.frame_h = 16; + g_spr_gravestone.cols = 1; + g_spr_gravestone.rows = 1; + g_spr_gravestone.pixels = (uint32_t*)montauk::malloc(16 * 16 * 4); + if (g_spr_gravestone.pixels) { + for (int i = 0; i < 16 * 16; i++) + g_spr_gravestone.pixels[i] = pal[bmp[i]]; + } + } + + return true; +} + +// ============================================================================ +// Entity initialization +// ============================================================================ + +enum TileId { TILE_GRASS = 0, TILE_PATH = 1, TILE_WATER = 2 }; + +static void setup_player_anims(AnimatedSprite& spr, Spritesheet* sheet) { + spr.sheet = sheet; + for (int d = 0; d < 4; d++) + spr.add_anim(d, 0, 6, 10.0f); + for (int d = 0; d < 4; d++) + spr.add_anim(d, 0, 1, 1.0f); +} + +static void init_player() { + g_player.x = 24 * TILE_SIZE; + g_player.y = 22 * TILE_SIZE; + g_player.health = PLAYER_HP; + g_player.max_health = PLAYER_HP; + g_player.direction = DIR_DOWN; + g_player.moving = false; + g_player.score = 0; + g_player.hit_timer = 0; + + setup_player_anims(g_player.sprite, &g_spr_player); + for (int d = 0; d < 4; d++) { + g_player.anim_walk[d] = d; + g_player.anim_idle[d] = 4 + d; + } + g_player.sprite.play(g_player.anim_idle[DIR_DOWN]); +} + +static void add_enemy(float x, float y, int type) { + if (g_enemy_count >= MAX_ENEMIES) return; + Enemy& e = g_enemies[g_enemy_count++]; + e.x = x; + e.y = y; + e.health = 30; + e.max_health = 30; + e.direction = DIR_DOWN; + e.active = true; + e.patrol_timer = 0; + e.patrol_duration = 2.0f; + e.type = type; + + if (type == 0) { + setup_player_anims(e.sprite, &g_spr_skeleton); + for (int d = 0; d < 4; d++) + e.anim_walk[d] = d; + } else { + e.sprite.sheet = &g_spr_slime; + e.sprite.add_anim(0, 0, 4, 6.0f); + for (int d = 0; d < 4; d++) + e.anim_walk[d] = 0; + } + e.sprite.play(0); +} + +static void add_decoration(float x, float y, Spritesheet* sheet, + bool solid, float cx, float cy, float cw, float ch, + int fcol = -1, int frow = -1) { + if (g_decor_count >= MAX_DECORATIONS) return; + Decoration& d = g_decor[g_decor_count++]; + d.x = x; + d.y = y; + d.sheet = sheet; + d.solid = solid; + d.col_x = cx; + d.col_y = cy; + d.col_w = cw; + d.col_h = ch; + d.frame_col = fcol; + d.frame_row = frow; +} + +static void add_chest(float x, float y) { + if (g_chest_count >= MAX_CHESTS) return; + Chest& c = g_chests[g_chest_count++]; + c.x = x; + c.y = y; + c.opened = false; + c.collected = false; +} + +// ============================================================================ +// World Generation +// ============================================================================ +// +// Uses an occupancy grid to prevent overlapping placements. Each tile cell +// stores bit flags indicating what occupies it. All placement functions +// validate against the grid before committing. +// +// Generation order (later phases respect earlier placements): +// 1. Water bodies -> OCC_WATER +// 2. Path network -> OCC_PATH +// 3. Houses -> OCC_VISUAL + OCC_SOLID (base) +// 4. Fences -> OCC_SOLID +// 5. Trees -> OCC_VISUAL + OCC_SOLID (trunk) +// 6. Gravestones -> OCC_VISUAL + OCC_SOLID +// 7. Chests -> OCC_SOLID +// 8. Enemies -> OCC_SPAWN +// +// ============================================================================ + +// ---- Occupancy grid ---- + +enum OccFlag : uint16_t { + OCC_WATER = 1 << 0, + OCC_PATH = 1 << 1, + OCC_SOLID = 1 << 2, + OCC_VISUAL = 1 << 3, + OCC_SPAWN = 1 << 4, +}; + +static uint16_t g_occ[MAP_W * MAP_H]; + +static uint16_t occ_at(int tx, int ty) { + if (tx < 0 || tx >= MAP_W || ty < 0 || ty >= MAP_H) return 0xFFFF; + return g_occ[ty * MAP_W + tx]; +} + +static bool occ_free(int tx, int ty, int tw, int th, uint16_t mask) { + for (int y = ty; y < ty + th; y++) + for (int x = tx; x < tx + tw; x++) + if (occ_at(x, y) & mask) return false; + return true; +} + +static void occ_mark(int tx, int ty, int tw, int th, uint16_t flags) { + for (int y = ty; y < ty + th; y++) + for (int x = tx; x < tx + tw; x++) { + if (x >= 0 && x < MAP_W && y >= 0 && y < MAP_H) + g_occ[y * MAP_W + x] |= flags; + } +} + +// ---- Terrain helpers ---- + +static void set_tiles(int x1, int y1, int x2, int y2, int tile, uint16_t occ) { + for (int y = y1; y <= y2; y++) + for (int x = x1; x <= x2; x++) { + if (x >= 0 && x < MAP_W && y >= 0 && y < MAP_H) { + g_map.set(x, y, tile); + occ_mark(x, y, 1, 1, occ); + } + } +} + +// ---- Placement functions ---- + +// Tree: 64x80px = 4w x 5h tiles +// Trunk collision at pixel (16,48) size (32,28) -> tile offset (1,3) size 2x2 +static bool place_tree(int tx, int ty) { + if (!occ_free(tx, ty, 4, 5, OCC_VISUAL | OCC_SOLID | OCC_WATER)) + return false; + if (!occ_free(tx + 1, ty + 3, 2, 2, OCC_PATH)) + return false; + add_decoration((float)tx * 16, (float)ty * 16, + &g_spr_tree, true, 16, 48, 32, 28); + occ_mark(tx, ty, 4, 5, OCC_VISUAL); + occ_mark(tx + 1, ty + 3, 2, 2, OCC_SOLID); + return true; +} + +// House: 96x128px = 6w x 8h tiles +// Base collision at pixel (8,80) size (80,44) -> tile offset (0,5) size ~6x3 +// Visual may overlap paths (roof overhang) but base must not. +static bool place_house(int tx, int ty) { + if (!occ_free(tx, ty, 6, 8, OCC_VISUAL | OCC_SOLID | OCC_WATER)) + return false; + if (!occ_free(tx, ty + 5, 6, 3, OCC_PATH)) + return false; + add_decoration((float)tx * 16, (float)ty * 16, + &g_spr_house, true, 8, 80, 80, 44); + occ_mark(tx, ty, 6, 8, OCC_VISUAL); + occ_mark(tx, ty + 5, 6, 3, OCC_SOLID); + return true; +} + +// Gravestone: 16x16px = 1x1 tile +static bool place_gravestone(int tx, int ty) { + if (!occ_free(tx, ty, 1, 1, OCC_SOLID | OCC_VISUAL | OCC_WATER)) + return false; + add_decoration((float)tx * 16, (float)ty * 16, + &g_spr_gravestone, true, 2, 2, 12, 14); + occ_mark(tx, ty, 1, 1, OCC_SOLID | OCC_VISUAL); + return true; +} + +// Chest: 1x1 tile, must be on walkable ground +static bool place_chest_at(int tx, int ty) { + if (!occ_free(tx, ty, 1, 1, OCC_SOLID | OCC_VISUAL | OCC_WATER)) + return false; + add_chest((float)tx * 16, (float)ty * 16); + occ_mark(tx, ty, 1, 1, OCC_SOLID); + return true; +} + +// Enemy: 2x2 tile footprint, needs walkable ground +static bool place_enemy_at(int tx, int ty, int type) { + if (!occ_free(tx, ty, 2, 2, OCC_SOLID | OCC_WATER)) + return false; + add_enemy((float)tx * 16, (float)ty * 16, type); + occ_mark(tx, ty, 2, 2, OCC_SPAWN); + return true; +} + +// Fence runs - each tile validated, skipped if blocked +static void gen_fence_h(int tx, int ty, int length) { + for (int i = 0; i < length; i++) { + int x = tx + i; + if (occ_at(x, ty) & (OCC_SOLID | OCC_WATER | OCC_PATH | OCC_VISUAL)) + continue; + int col = (i == 0) ? 0 : (i == length - 1) ? 3 : ((i % 2 == 1) ? 1 : 2); + add_decoration((float)x * 16, (float)ty * 16, &g_spr_fences, + true, 0, 4, 16, 12, col, 0); + occ_mark(x, ty, 1, 1, OCC_SOLID); + } +} + +static void gen_fence_v(int tx, int ty, int length) { + for (int i = 0; i < length; i++) { + int y = ty + i; + if (occ_at(tx, y) & (OCC_SOLID | OCC_WATER | OCC_PATH | OCC_VISUAL)) + continue; + int row = (i == 0) ? 0 : (i == length - 1) ? 3 : ((i % 2 == 1) ? 1 : 2); + add_decoration((float)tx * 16, (float)y * 16, &g_spr_fences, + true, 4, 0, 12, 16, 0, row); + occ_mark(tx, y, 1, 1, OCC_SOLID); + } +} + +// ---- Main world generation ---- + +static void generate_world() { + // Clear occupancy grid + for (int i = 0; i < MAP_W * MAP_H; i++) + g_occ[i] = 0; + + // Initialize tilemap + g_map.alloc(MAP_W, MAP_H, TILE_SIZE); + g_map.add_type(&g_spr_grass, 0, 0, TILE_SIZE, TILE_SIZE, false); + g_map.add_type(&g_spr_path, 0, 0, TILE_SIZE, TILE_SIZE, false); + g_map.add_type(&g_spr_water, 0, 0, TILE_SIZE, TILE_SIZE, true); + for (int i = 0; i < MAP_W * MAP_H; i++) + g_map.data[i] = TILE_GRASS; + + // ==== Phase 1: Water bodies ==== + + // Lake (top-right, ellipse centered at tile 40,7) + for (int y = 2; y < 12; y++) { + for (int x = 34; x < 47; x++) { + float dx = (float)(x - 40) / 6.0f; + float dy = (float)(y - 7) / 4.5f; + if (dx * dx + dy * dy < 1.0f) { + g_map.set(x, y, TILE_WATER); + occ_mark(x, y, 1, 1, OCC_WATER); + } + } + } + + // Pond (west side) + for (int y = 14; y < 18; y++) { + for (int x = 8; x < 13; x++) { + float dx = (float)(x - 10) / 2.5f; + float dy = (float)(y - 16) / 1.8f; + if (dx * dx + dy * dy < 1.0f) { + g_map.set(x, y, TILE_WATER); + occ_mark(x, y, 1, 1, OCC_WATER); + } + } + } + + // ==== Phase 2: Path network ==== + + set_tiles(0, 19, MAP_W - 1, 20, TILE_PATH, OCC_PATH); // main E-W road + set_tiles(24, 0, 25, MAP_H - 1, TILE_PATH, OCC_PATH); // main N-S road + set_tiles(25, 8, 42, 8, TILE_PATH, OCC_PATH); // lake spur + set_tiles(15, 19, 16, 30, TILE_PATH, OCC_PATH); // south branch + set_tiles(35, 20, 36, 26, TILE_PATH, OCC_PATH); // graveyard access + set_tiles(32, 26, 40, 33, TILE_PATH, OCC_PATH); // graveyard floor + + // ==== Phase 3: Houses ==== + // Village north of crossroads. Houses are 6x8 tiles. + // Positioned so bases (at ty+5..ty+7) don't overlap paths. + + place_house(18, 4); // left house + place_house(27, 4); // right house (gap at N-S path cols 24-25) + + // ==== Phase 4: Village fences ==== + // South boundary with gate at N-S path (cols 24-25) + + gen_fence_h(18, 16, 6); // left of gate: tiles 18-23 + gen_fence_h(26, 16, 7); // right of gate: tiles 26-32 + gen_fence_v(18, 12, 5); // left side + gen_fence_v(32, 12, 5); // right side + + // ==== Phase 5: Trees ==== + + // Northwest forest (4x5 tile spacing to prevent overlap) + place_tree(0, 0); + place_tree(5, 0); + place_tree(10, 0); + place_tree(2, 5); + place_tree(7, 5); + place_tree(12, 5); + place_tree(0, 10); + + // South wilderness + place_tree(2, 26); + place_tree(8, 30); + place_tree(28, 26); + place_tree(43, 28); + place_tree(44, 23); + + // East side + place_tree(46, 13); + place_tree(42, 21); + + // ==== Phase 6: Gravestones ==== + // Two rows of three within graveyard zone (32-40, 26-33) + + place_gravestone(34, 28); + place_gravestone(36, 28); + place_gravestone(38, 28); + place_gravestone(34, 31); + place_gravestone(36, 31); + place_gravestone(38, 31); + + // ==== Phase 7: Chests ==== + + place_chest_at(4, 3); // forest clearing + place_chest_at(43, 12); // near lake + place_chest_at(1, 32); // southern wilderness + place_chest_at(46, 35); // far corner + place_chest_at(26, 14); // village yard + + // ==== Phase 8: Enemies ==== + + // Skeletons patrol the graveyard + place_enemy_at(34, 29, 0); + place_enemy_at(37, 29, 0); + place_enemy_at(35, 32, 0); + + // Slimes in the northwest forest + place_enemy_at(4, 6, 1); + place_enemy_at(10, 6, 1); + + // Skeleton guard near lake + place_enemy_at(38, 14, 0); +} + +// ============================================================================ +// Update +// ============================================================================ + +static void update_camera() { + // Center camera on player + float target_x = g_player.x - (float)(WIN_W / SCALE) / 2.0f + SPR_W / 2.0f; + float target_y = g_player.y - (float)(WIN_H / SCALE) / 2.0f + SPR_H / 2.0f; + + // Smooth follow + g_cam_x += (target_x - g_cam_x) * 6.0f * g_engine.dt; + g_cam_y += (target_y - g_cam_y) * 6.0f * g_engine.dt; + + // Clamp to map bounds + float max_x = (float)(MAP_W * TILE_SIZE) - (float)(WIN_W / SCALE); + float max_y = (float)(MAP_H * TILE_SIZE) - (float)(WIN_H / SCALE); + if (g_cam_x < 0) g_cam_x = 0; + if (g_cam_y < 0) g_cam_y = 0; + if (g_cam_x > max_x) g_cam_x = max_x; + if (g_cam_y > max_y) g_cam_y = max_y; +} + +static bool check_decoration_collision(float x, float y, float w, float h) { + for (int i = 0; i < g_decor_count; i++) { + Decoration& d = g_decor[i]; + if (!d.solid) continue; + AABB a = { x, y, w, h }; + AABB b = { d.x + d.col_x, d.y + d.col_y, d.col_w, d.col_h }; + if (a.overlaps(b)) return true; + } + return false; +} + +static void update_player(float dt) { + float dx = 0, dy = 0; + bool moving = false; + + if (g_input.key_held(key::W) || g_input.key_held(key::UP)) { dy = -1; g_player.direction = DIR_UP; moving = true; } + if (g_input.key_held(key::S) || g_input.key_held(key::DOWN)) { dy = 1; g_player.direction = DIR_DOWN; moving = true; } + if (g_input.key_held(key::A) || g_input.key_held(key::LEFT)) { dx = -1; g_player.direction = DIR_LEFT; moving = true; } + if (g_input.key_held(key::D) || g_input.key_held(key::RIGHT)) { dx = 1; g_player.direction = DIR_RIGHT; moving = true; } + + // Normalize diagonal movement + if (dx != 0 && dy != 0) { + dx *= 0.707f; + dy *= 0.707f; + } + + dx *= PLAYER_SPEED * dt; + dy *= PLAYER_SPEED * dt; + + // Collision box: lower portion of sprite (feet area) + float bx = g_player.x + 8; + float by = g_player.y + 20; + float bw = 16; + float bh = 10; + + // Tilemap collision + resolve_tilemap_collision(g_map, bx, by, bw, bh, dx, dy); + + // Decoration collision - X axis + if (dx != 0 && check_decoration_collision(bx + dx, by, bw, bh)) + dx = 0; + // Decoration collision - Y axis + if (dy != 0 && check_decoration_collision(bx + dx, by + dy, bw, bh)) + dy = 0; + + // World bounds + float new_x = g_player.x + dx; + float new_y = g_player.y + dy; + if (new_x < 0) new_x = 0; + if (new_y < 0) new_y = 0; + if (new_x > (MAP_W - 2) * TILE_SIZE) new_x = (float)((MAP_W - 2) * TILE_SIZE); + if (new_y > (MAP_H - 2) * TILE_SIZE) new_y = (float)((MAP_H - 2) * TILE_SIZE); + + g_player.x = new_x; + g_player.y = new_y; + g_player.moving = moving; + + // Animation + if (moving) { + g_player.sprite.play(g_player.anim_walk[g_player.direction]); + } else { + g_player.sprite.play(g_player.anim_idle[g_player.direction]); + } + g_player.sprite.update(dt); + + // Invincibility timer + if (g_player.hit_timer > 0) + g_player.hit_timer -= dt; +} + +static void update_enemies(float dt) { + for (int i = 0; i < g_enemy_count; i++) { + Enemy& e = g_enemies[i]; + if (!e.active) continue; + + // Simple patrol AI + e.patrol_timer += dt; + if (e.patrol_timer >= e.patrol_duration) { + e.patrol_timer = 0; + e.direction = (e.direction + 1) % 4; + // Randomize duration slightly using frame count + e.patrol_duration = 1.5f + (float)(g_engine.frame_count % 3) * 0.5f; + } + + float dx = 0, dy = 0; + switch (e.direction) { + case DIR_DOWN: dy = ENEMY_SPEED * dt; break; + case DIR_UP: dy = -ENEMY_SPEED * dt; break; + case DIR_RIGHT: dx = ENEMY_SPEED * dt; break; + case DIR_LEFT: dx = -ENEMY_SPEED * dt; break; + } + + // Tilemap collision + float bx = e.x + 8; + float by = e.y + 20; + float bw = 16; + float bh = 10; + resolve_tilemap_collision(g_map, bx, by, bw, bh, dx, dy); + + // Reverse direction if blocked + if (dx == 0 && dy == 0) { + e.direction = (e.direction + 2) % 4; + e.patrol_timer = 0; + } + + // World bounds + float new_x = e.x + dx; + float new_y = e.y + dy; + if (new_x < 0 || new_x > (MAP_W - 2) * TILE_SIZE) { + e.direction = (e.direction + 2) % 4; + e.patrol_timer = 0; + new_x = e.x; + } + if (new_y < 0 || new_y > (MAP_H - 2) * TILE_SIZE) { + e.direction = (e.direction + 2) % 4; + e.patrol_timer = 0; + new_y = e.y; + } + + e.x = new_x; + e.y = new_y; + + // Animation + if (e.type == 0) { + e.sprite.play(e.anim_walk[e.direction]); + } + e.sprite.update(dt); + + // Damage player on contact + AABB pa = { g_player.x + 4, g_player.y + 8, 24, 22 }; + AABB ea = { e.x + 4, e.y + 8, 24, 22 }; + if (pa.overlaps(ea) && g_player.hit_timer <= 0) { + g_player.health -= 10; + g_player.hit_timer = 1.0f; // 1 second invincibility + if (g_player.health < 0) g_player.health = 0; + + // Knockback with collision checking + float kx = g_player.x - e.x; + float ky = g_player.y - e.y; + float len = 1.0f; + if (kx != 0 || ky != 0) { + float ax = kx < 0 ? -kx : kx; + float ay = ky < 0 ? -ky : ky; + len = ax > ay ? ax + ay * 0.4f : ay + ax * 0.4f; + } + float kb_dx = (kx / len) * 16.0f; + float kb_dy = (ky / len) * 16.0f; + + // Check collision before applying knockback + float bx = g_player.x + 8; + float by = g_player.y + 20; + resolve_tilemap_collision(g_map, bx, by, 16, 10, kb_dx, kb_dy); + float new_px = g_player.x + kb_dx; + float new_py = g_player.y + kb_dy; + if (new_px < 0) new_px = 0; + if (new_py < 0) new_py = 0; + if (new_px > (MAP_W - 2) * TILE_SIZE) new_px = (float)((MAP_W - 2) * TILE_SIZE); + if (new_py > (MAP_H - 2) * TILE_SIZE) new_py = (float)((MAP_H - 2) * TILE_SIZE); + g_player.x = new_px; + g_player.y = new_py; + + // Sound effect + g_audio.play_tone(200, 80, 30); + } + } +} + +static void update_interactions() { + g_prompt[0] = '\0'; + + // Check chest proximity + for (int i = 0; i < g_chest_count; i++) { + Chest& c = g_chests[i]; + if (c.collected) continue; + + float dx = g_player.x + 16 - (c.x + 8); + float dy = g_player.y + 16 - (c.y + 8); + float dist = dx * dx + dy * dy; + + if (dist < 40 * 40) { + if (!c.opened) { + snprintf(g_prompt, sizeof(g_prompt), "Press E to open chest"); + if (g_input.key_just_pressed(key::E)) { + c.opened = true; + c.collected = true; + g_player.score += 100; + snprintf(g_dialog_text, sizeof(g_dialog_text), + "You found treasure! +100 points"); + g_dialog_timer = 2.0f; + g_audio.play_tone(523, 100, 25); // C5 + g_audio.play_tone(659, 100, 25); // E5 + g_audio.play_tone(784, 150, 25); // G5 + } + } + } + } + + // Dialog timer + if (g_dialog_timer > 0) { + g_dialog_timer -= g_engine.dt; + if (g_dialog_timer <= 0) g_dialog_text[0] = '\0'; + } +} + +static void update(float dt) { + if (g_player.health <= 0) { + // Game over - respawn after pressing space + if (g_input.key_just_pressed(key::SPACE)) { + g_player.health = PLAYER_HP; + g_player.x = 24 * TILE_SIZE; + g_player.y = 22 * TILE_SIZE; + } + return; + } + + update_player(dt); + update_enemies(dt); + update_interactions(); + update_camera(); +} + +// ============================================================================ +// Rendering +// ============================================================================ + +// Y-sort helper for draw ordering +struct DrawEntry { + float y; + int type; // 0=decoration, 1=player, 2=enemy, 3=chest + int index; +}; + +static DrawEntry g_draw_list[MAX_DECORATIONS + MAX_ENEMIES + MAX_CHESTS + 1]; +static int g_draw_count = 0; + +static void sort_draw_list() { + // Simple insertion sort (small N) + for (int i = 1; i < g_draw_count; i++) { + DrawEntry tmp = g_draw_list[i]; + int j = i - 1; + while (j >= 0 && g_draw_list[j].y > tmp.y) { + g_draw_list[j + 1] = g_draw_list[j]; + j--; + } + g_draw_list[j + 1] = tmp; + } +} + +static void render() { + int cam_x = (int)g_cam_x; + int cam_y = (int)g_cam_y; + + // Draw tilemap + g_map.draw(g_engine.pixels, g_engine.screen_w, g_engine.screen_h, + cam_x, cam_y, SCALE); + + // Build Y-sorted draw list for entities and decorations + g_draw_count = 0; + + for (int i = 0; i < g_decor_count; i++) { + Decoration& dc = g_decor[i]; + float h = 0; + if (dc.sheet) { + h = (dc.frame_col >= 0) ? (float)dc.sheet->frame_h + : (float)dc.sheet->height; + } + g_draw_list[g_draw_count++] = { dc.y + h, 0, i }; + } + + for (int i = 0; i < g_chest_count; i++) { + g_draw_list[g_draw_count++] = { g_chests[i].y + TILE_SIZE, 3, i }; + } + + g_draw_list[g_draw_count++] = { g_player.y + SPR_H, 1, 0 }; + + for (int i = 0; i < g_enemy_count; i++) { + if (!g_enemies[i].active) continue; + float bottom = g_enemies[i].y + (g_enemies[i].type == 1 ? 64 : SPR_H); + g_draw_list[g_draw_count++] = { bottom, 2, i }; + } + + sort_draw_list(); + + // Render sorted entities + for (int i = 0; i < g_draw_count; i++) { + DrawEntry& de = g_draw_list[i]; + + if (de.type == 0) { + // Decoration + Decoration& d = g_decor[de.index]; + if (d.sheet && d.sheet->pixels) { + int sx = (int)(d.x * SCALE) - cam_x * SCALE; + int sy = (int)(d.y * SCALE) - cam_y * SCALE; + if (d.frame_col >= 0 && d.frame_row >= 0) { + d.sheet->draw_frame(g_engine.pixels, g_engine.screen_w, + g_engine.screen_h, d.frame_col, + d.frame_row, sx, sy, SCALE); + } else { + d.sheet->draw_region(g_engine.pixels, g_engine.screen_w, + g_engine.screen_h, 0, 0, + d.sheet->width, d.sheet->height, + sx, sy, SCALE); + } + } + } else if (de.type == 1) { + // Player + int px = (int)(g_player.x * SCALE) - cam_x * SCALE; + int py = (int)(g_player.y * SCALE) - cam_y * SCALE; + + // Flash when invincible + bool visible = true; + if (g_player.hit_timer > 0) { + visible = ((int)(g_player.hit_timer * 10) % 2 == 0); + } + + if (visible) { + g_player.sprite.draw(g_engine.pixels, g_engine.screen_w, + g_engine.screen_h, px, py, SCALE); + } + } else if (de.type == 2) { + // Enemy + Enemy& e = g_enemies[de.index]; + int ex = (int)(e.x * SCALE) - cam_x * SCALE; + int ey = (int)(e.y * SCALE) - cam_y * SCALE; + e.sprite.draw(g_engine.pixels, g_engine.screen_w, + g_engine.screen_h, ex, ey, SCALE); + + // Health bar above enemy + if (e.health < e.max_health) { + draw_bar(g_engine, ex + 4, ey - 8, 56, 6, + e.health, e.max_health, + 0xFFCC3333, 0xFF333333, 0xFF000000); + } + } else if (de.type == 3) { + // Chest + Chest& c = g_chests[de.index]; + int cx = (int)(c.x * SCALE) - cam_x * SCALE; + int cy = (int)(c.y * SCALE) - cam_y * SCALE; + + if (!c.collected && g_spr_chest.pixels) { + // Draw chest (frame 0 = closed, use tinting for opened) + g_spr_chest.draw_frame(g_engine.pixels, g_engine.screen_w, + g_engine.screen_h, 0, 0, + cx, cy, SCALE); + if (c.opened) { + // Darken opened chests slightly + g_engine.fill_rect_alpha(cx, cy, TILE_SIZE * SCALE, + TILE_SIZE * SCALE, 0x40000000); + } + } + } + } + + // ---- HUD ---- + + // Health bar + draw_bar(g_engine, 8, 8, 160, 20, + g_player.health, g_player.max_health, + 0xFFCC3333, 0xFF444444, 0xFF000000); + + char hp_text[32]; + snprintf(hp_text, sizeof(hp_text), "HP: %d/%d", g_player.health, g_player.max_health); + draw_hud_text(g_engine, 12, 9, hp_text, + Color::from_rgb(0xFF, 0xFF, 0xFF), 14); + + // Score + char score_text[32]; + snprintf(score_text, sizeof(score_text), "Score: %d", g_player.score); + draw_hud_text(g_engine, 8, 34, score_text, + Color::from_rgb(0xFF, 0xDD, 0x44), 14); + + // FPS counter + if (g_engine.dt > 0) { + char fps_text[16]; + int fps = (int)(1.0f / g_engine.dt); + snprintf(fps_text, sizeof(fps_text), "%d FPS", fps); + int fw = g_engine.text_width(fps_text, 12); + draw_hud_text(g_engine, g_engine.screen_w - fw - 8, 8, fps_text, + Color::from_rgb(0xAA, 0xAA, 0xAA), 12); + } + + // Interaction prompt + if (g_prompt[0]) { + draw_prompt(g_engine, g_prompt, 14); + } + + // Dialog text + if (g_dialog_text[0] && g_dialog_timer > 0) { + int dw = 300; + int dh = 50; + int dx = (g_engine.screen_w - dw) / 2; + int dy = g_engine.screen_h - 100; + draw_dialog(g_engine, dx, dy, dw, dh, g_dialog_text, + Color::from_rgb(0x33, 0x33, 0x33), 14); + } + + // Game over screen + if (g_player.health <= 0) { + g_engine.fill_rect_alpha(0, 0, g_engine.screen_w, g_engine.screen_h, + 0xAA000000); + const char* go_text = "Game Over"; + int tw = g_engine.text_width(go_text, 32); + g_engine.draw_text((g_engine.screen_w - tw) / 2, + g_engine.screen_h / 2 - 30, + go_text, Color::from_rgb(0xFF, 0x44, 0x44), 32); + + const char* restart_text = "Press SPACE to respawn"; + tw = g_engine.text_width(restart_text, 16); + g_engine.draw_text((g_engine.screen_w - tw) / 2, + g_engine.screen_h / 2 + 10, + restart_text, Color::from_rgb(0xCC, 0xCC, 0xCC), 16); + + char final_score[64]; + snprintf(final_score, sizeof(final_score), "Final Score: %d", g_player.score); + tw = g_engine.text_width(final_score, 18); + g_engine.draw_text((g_engine.screen_w - tw) / 2, + g_engine.screen_h / 2 + 40, + final_score, Color::from_rgb(0xFF, 0xDD, 0x44), 18); + } +} + +// ============================================================================ +// Entry point +// ============================================================================ + +extern "C" void _start() { + // Initialize engine + if (!g_engine.init("Montauk Quest", WIN_W, WIN_H)) { + montauk::print("Failed to create game window\n"); + montauk::exit(1); + } + + g_input.init(); + + // Initialize audio (non-fatal if it fails) + g_audio.init(); + + // Load assets + if (!load_assets()) { + montauk::print("Failed to load game assets\n"); + g_engine.shutdown(); + montauk::exit(1); + } + + // Initialize game world + generate_world(); + init_player(); + + // Main game loop + while (g_engine.running) { + g_engine.update_timing(); + g_input.begin_frame(); + + // Process all pending events + Montauk::WinEvent ev; + while (g_engine.poll(&ev)) { + g_input.handle_event(ev); + + // Escape to quit + if (ev.type == 0 && ev.key.pressed && ev.key.scancode == key::ESC) + g_engine.running = false; + } + + // Update + update(g_engine.dt); + + // Render + render(); + g_engine.present(); + + // Yield to avoid burning CPU when there's no input + montauk::sleep_ms(1); + } + + g_audio.shutdown(); + g_engine.shutdown(); + montauk::exit(0); +} diff --git a/programs/src/rpgdemo/manifest.toml b/programs/src/rpgdemo/manifest.toml new file mode 100644 index 0000000..69519f7 --- /dev/null +++ b/programs/src/rpgdemo/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Montauk Quest" +binary = "rpgdemo.elf" +icon = "icon.svg" + +[menu] +category = "Games" +visible = true diff --git a/programs/src/rpgdemo/stb_impl.cpp b/programs/src/rpgdemo/stb_impl.cpp new file mode 100644 index 0000000..1d30d13 --- /dev/null +++ b/programs/src/rpgdemo/stb_impl.cpp @@ -0,0 +1,69 @@ +/* + * stb_impl.cpp + * Single compilation unit for stb_image (PNG) and stb_truetype + * for MontaukOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +// ============================================================================ +// stb_image - PNG decoder +// ============================================================================ + +#define STBI_ONLY_PNG +#define STBI_NO_JPEG +#define STBI_NO_BMP +#define STBI_NO_PSD +#define STBI_NO_TGA +#define STBI_NO_GIF +#define STBI_NO_HDR +#define STBI_NO_PIC +#define STBI_NO_PNM +#define STBI_NO_LINEAR +#define STBI_NO_STDIO +#define STBI_NO_THREAD_LOCALS + +#define STBI_MALLOC(sz) malloc(sz) +#define STBI_FREE(p) free(p) +#define STBI_REALLOC(p, newsz) realloc(p, newsz) + +#define STBI_ASSERT(x) ((void)(x)) + +#define STB_IMAGE_IMPLEMENTATION +#include + +// ============================================================================ +// stb_truetype - TrueType font renderer +// ============================================================================ + +#define STBTT_ifloor(x) ((int) stb_floor(x)) +#define STBTT_iceil(x) ((int) stb_ceil(x)) +#define STBTT_sqrt(x) stb_sqrt(x) +#define STBTT_pow(x,y) stb_pow(x,y) +#define STBTT_fmod(x,y) stb_fmod(x,y) +#define STBTT_cos(x) stb_cos(x) +#define STBTT_acos(x) stb_acos(x) +#define STBTT_fabs(x) stb_fabs(x) + +#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x)) +#define STBTT_free(x,u) ((void)(u), montauk::mfree(x)) + +#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n) +#define STBTT_memset(d,v,n) montauk::memset(d,v,n) + +#define STBTT_strlen(x) montauk::slen(x) + +#define STBTT_assert(x) ((void)(x)) + +#define STB_TRUETYPE_IMPLEMENTATION +#include diff --git a/scripts/install_apps.sh b/scripts/install_apps.sh index f6222b9..b896026 100755 --- a/scripts/install_apps.sh +++ b/scripts/install_apps.sh @@ -29,6 +29,7 @@ APPS=( "music|apps/scalable/audio-player.svg" "video|apps/scalable/multimedia-video-player.svg" "bluetooth|apps/scalable/bluetooth.svg" + "rpgdemo|apps/scalable/utilities-terminal.svg" ) installed=0