feat: add experimental game engine & demo game, fix ACPI events

This commit is contained in:
2026-03-18 14:26:50 +01:00
parent ffffb6a0c5
commit b5d2cc515c
42 changed files with 2237 additions and 12 deletions
+9 -4
View File
@@ -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/<name>/.
@@ -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/<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
# 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
Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

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!
+81
View File
@@ -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
+48
View File
@@ -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
+200
View File
@@ -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
+87
View File
@@ -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
+264
View File
@@ -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
+134
View File
@@ -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
+114
View File
@@ -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
+116
View File
@@ -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)
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Montauk Quest"
binary = "rpgdemo.elf"
icon = "icon.svg"
[menu]
category = "Games"
visible = true
+69
View File
@@ -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>