feat: add experimental game engine & demo game, fix ACPI events
@@ -6,7 +6,7 @@ MAKEFLAGS += -rR
|
|||||||
ARCH := x86_64
|
ARCH := x86_64
|
||||||
|
|
||||||
# Default user QEMU flags. These are appended to the QEMU command calls.
|
# 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)
|
override IMAGE_NAME := montauk-$(ARCH)
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,7 @@ namespace Hal {
|
|||||||
KernelLogStream(INFO, "ACPI") << "Sleep button pressed";
|
KernelLogStream(INFO, "ACPI") << "Sleep button pressed";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send EOI
|
// Note: EOI is sent by HalIrqDispatch after this handler returns.
|
||||||
LocalApic::SendEOI();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -88,7 +87,13 @@ namespace Hal {
|
|||||||
// Initialize
|
// 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_pm1aEventBlock = fadt.PM1aEventBlock;
|
||||||
g_pm1bEventBlock = fadt.PM1bEventBlock;
|
g_pm1bEventBlock = fadt.PM1bEventBlock;
|
||||||
g_pm1EventLength = fadt.PM1EventLength;
|
g_pm1EventLength = fadt.PM1EventLength;
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ namespace Hal {
|
|||||||
|
|
||||||
// Initialize ACPI event handling: route the SCI interrupt,
|
// Initialize ACPI event handling: route the SCI interrupt,
|
||||||
// register the handler, and enable fixed events (power button).
|
// register the handler, and enable fixed events (power button).
|
||||||
// Must be called after APIC and FADT initialization.
|
// Must be called after APIC initialization.
|
||||||
void Initialize(const FADT::ParsedFADT& fadt);
|
void Initialize(ACPI::CommonSDTHeader* xsdt);
|
||||||
|
|
||||||
// Re-enable ACPI events after S3 resume.
|
// Re-enable ACPI events after S3 resume.
|
||||||
void Reinitialize();
|
void Reinitialize();
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ namespace Hal {
|
|||||||
// Initialize S3 suspend support (reads FACS and \_S3_ from namespace)
|
// Initialize S3 suspend support (reads FACS and \_S3_ from namespace)
|
||||||
AcpiSleep::Initialize(xsdt);
|
AcpiSleep::Initialize(xsdt);
|
||||||
|
|
||||||
// Initialize ACPI event handling (SCI interrupt, power button)
|
// Note: ACPI event handling (SCI, power button) is initialized
|
||||||
AcpiEvents::Initialize(fadt);
|
// from Main.cpp after the APIC subsystem is ready.
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsAvailable() {
|
bool IsAvailable() {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include <Memory/Paging.hpp>
|
#include <Memory/Paging.hpp>
|
||||||
#include <ACPI/ACPI.hpp>
|
#include <ACPI/ACPI.hpp>
|
||||||
#include <ACPI/AcpiShutdown.hpp>
|
#include <ACPI/AcpiShutdown.hpp>
|
||||||
|
#include <ACPI/AcpiEvents.hpp>
|
||||||
#include <Hal/Apic/ApicInit.hpp>
|
#include <Hal/Apic/ApicInit.hpp>
|
||||||
#include <Pci/Pci.hpp>
|
#include <Pci/Pci.hpp>
|
||||||
#include <Timekeeping/ApicTimer.hpp>
|
#include <Timekeeping/ApicTimer.hpp>
|
||||||
@@ -144,6 +145,9 @@ extern "C" void kmain() {
|
|||||||
|
|
||||||
Hal::ApicInitialize(g_acpi.GetXSDT());
|
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());
|
Pci::Initialize(g_acpi.GetXSDT());
|
||||||
|
|
||||||
Drivers::ProbeEarly();
|
Drivers::ProbeEarly();
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ BINDIR := bin
|
|||||||
PROGRAMS := $(notdir $(wildcard src/*))
|
PROGRAMS := $(notdir $(wildcard src/*))
|
||||||
|
|
||||||
# Programs with custom Makefiles (built separately).
|
# 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))
|
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||||
|
|
||||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
||||||
@@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
|
|||||||
# Common shared assets (wallpapers, etc.)
|
# Common shared assets (wallpapers, etc.)
|
||||||
COMMONKEEP := $(BINDIR)/common/.keep
|
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).
|
# 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)
|
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||||
@@ -184,12 +184,16 @@ icons:
|
|||||||
fonts:
|
fonts:
|
||||||
../scripts/copy_fonts.sh
|
../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.
|
# Build doom via its own Makefile.
|
||||||
doom:
|
doom:
|
||||||
$(MAKE) -C src/doom
|
$(MAKE) -C src/doom
|
||||||
|
|
||||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
||||||
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
|
../scripts/install_apps.sh
|
||||||
|
|
||||||
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
# 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/volume clean
|
||||||
$(MAKE) -C src/music clean
|
$(MAKE) -C src/music clean
|
||||||
$(MAKE) -C src/bluetooth clean
|
$(MAKE) -C src/bluetooth clean
|
||||||
|
$(MAKE) -C src/rpgdemo clean
|
||||||
|
|||||||
|
After Width: | Height: | Size: 622 B |
|
After Width: | Height: | Size: 983 B |
|
After Width: | Height: | Size: 735 B |
|
After Width: | Height: | Size: 748 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 386 B |
|
After Width: | Height: | Size: 585 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 921 B |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 648 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 371 B |
|
After Width: | Height: | Size: 99 B |
|
After Width: | Height: | Size: 99 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 99 B |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -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!
|
||||||
@@ -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 <cstdint>
|
||||||
|
#include <montauk/syscall.h>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <cstdint>
|
||||||
|
#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
|
||||||
@@ -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 <cstdint>
|
||||||
|
#include <montauk/syscall.h>
|
||||||
|
#include <montauk/heap.h>
|
||||||
|
#include <montauk/string.h>
|
||||||
|
#include <gui/gui.hpp>
|
||||||
|
#include <gui/truetype.hpp>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <stdio.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <cstdint>
|
||||||
|
#include <montauk/string.h>
|
||||||
|
#include <Api/Syscall.hpp>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <cstdint>
|
||||||
|
#include <montauk/heap.h>
|
||||||
|
#include <montauk/string.h>
|
||||||
|
#include "engine/engine.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <gui/stb_image.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <cstdint>
|
||||||
|
#include <montauk/heap.h>
|
||||||
|
#include <montauk/string.h>
|
||||||
|
#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
|
||||||
@@ -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 <cstdint>
|
||||||
|
#include "engine/engine.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <stdio.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[app]
|
||||||
|
name = "Montauk Quest"
|
||||||
|
binary = "rpgdemo.elf"
|
||||||
|
icon = "icon.svg"
|
||||||
|
|
||||||
|
[menu]
|
||||||
|
category = "Games"
|
||||||
|
visible = true
|
||||||
@@ -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 <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <montauk/heap.h>
|
||||||
|
#include <montauk/string.h>
|
||||||
|
#include <gui/stb_math.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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 <gui/stb_image.h>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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 <gui/stb_truetype.h>
|
||||||
@@ -29,6 +29,7 @@ APPS=(
|
|||||||
"music|apps/scalable/audio-player.svg"
|
"music|apps/scalable/audio-player.svg"
|
||||||
"video|apps/scalable/multimedia-video-player.svg"
|
"video|apps/scalable/multimedia-video-player.svg"
|
||||||
"bluetooth|apps/scalable/bluetooth.svg"
|
"bluetooth|apps/scalable/bluetooth.svg"
|
||||||
|
"rpgdemo|apps/scalable/utilities-terminal.svg"
|
||||||
)
|
)
|
||||||
|
|
||||||
installed=0
|
installed=0
|
||||||
|
|||||||