feat: add experimental game engine & demo game, fix ACPI events
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user