diff --git a/src/main.zig b/src/main.zig index 15628b4..102d247 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,25 +1,110 @@ const w4 = @import("wasm4.zig"); +const sh0rk = @import("sh0rk.zig"); +const sprites = @import("sprites.zig"); -const smiley = [8]u8{ - 0b11000011, - 0b10000001, - 0b00100100, - 0b00100100, - 0b00000000, - 0b00100100, - 0b10011001, - 0b11000011, -}; +const Direction = sh0rk.Direction; +const Point = sh0rk.Point; +const Rect = sh0rk.Rect; + +var frame_count: u32 = 0; +var mara_direction: Direction = Direction.Right; +var mara_frame: bool = false; +var mara_box: Rect = Rect{.base = Point{.x = 60, .y = 60}, .width = 16, .height = 16}; +var mara_speed: i32 = 0; +var screen = Rect{.base = Point{.x = 0, .y = 0}, .width = 160, .height = 160}; + +export fn start() void { + w4.m.palette.* = .{ + w4.Color{.blue = 0xAF, .green = 0xB0, .red = 0xD9}, + //0xD9B0AF, + w4.Color{.blue = 0x83, .green = 0x63, .red = 0xAA}, + //0xAA6383, + w4.Color{.blue = 0x83, .green = 0x3C, .red = 0xAA}, + //0x6A3C4F, + w4.Color{.blue = 0x2D, .green = 0x23, .red = 0x36}, + //0x36232D, + }; +} export fn update() void { - w4.DRAW_COLORS.* = 2; - w4.text("Hello from Zig!", 10, 10); + w4.m.colors._0 = .p3; + w4.m.colors._1 = .p0; + w4.text("Mara 2:\nTamamo's Fury", 10, 10); - const gamepad = w4.GAMEPAD1.*; - if (gamepad & w4.BUTTON_1 != 0) { - w4.DRAW_COLORS.* = 4; + const gamepad = w4.m.gamepads[0]; + if (gamepad.a) { + w4.m.colors._0 = .p2; + } + + mara_speed = 0; + if (gamepad.up) { + mara_direction = Direction.Up; + mara_speed = 2; + } else if (gamepad.down) { + mara_direction = Direction.Down; + mara_speed = 2; + } else if (gamepad.left) { + mara_direction = Direction.Left; + mara_speed = 2; + } else if (gamepad.right) { + mara_direction = Direction.Right; + mara_speed = 2; + } + + switch (mara_direction) { + .Up => { + mara_box.base.y -= mara_speed; + }, + .Down => { + mara_box.base.y += mara_speed; + }, + .Left => { + mara_box.base.x -= mara_speed; + }, + .Right => { + mara_box.base.x += mara_speed; + }, + } + + if (mara_box.base.x < 0) { + mara_box.base.x = 0; + } else if (mara_box.base.x > 160 - mara_box.width) { + mara_box.base.x = 160 - mara_box.width; + } + if (mara_box.base.y < 0) { + mara_box.base.y = 0; + } else if (mara_box.base.y > 160 - mara_box.height) { + mara_box.base.y = 160 - mara_box.height; } - w4.blit(&smiley, 76, 76, 8, 8, w4.BLIT_1BPP); w4.text("Press X to blink", 16, 90); + + w4.m.colors._0 = .transparent; + w4.m.colors._1 = .p0; + w4.m.colors._2 = .p1; + w4.m.colors._3 = .p3; + + var flags: w4.BlitFlags = w4.BlitFlags { + .two_bits = true, + }; + + if (mara_direction == Direction.Left) { + flags.flip_x = true; + } + + if (frame_count % 15 == 0 and mara_speed > 0) { + mara_frame = !mara_frame; + } + var frame: u32 = switch (mara_direction) { + .Left => 4, + .Right => 4, + .Up => 2, + .Down => 0, + }; + var step: u32 = if (mara_frame) 1 else 0; + + w4.blitSub(&sprites.Mara, @intCast(u32, mara_box.base.x), @intCast(u32, mara_box.base.y), 16, 16, 16 * (frame + step), 0, 96, flags); + + frame_count += 1; } + diff --git a/src/sh0rk.zig b/src/sh0rk.zig new file mode 100644 index 0000000..e08ed68 --- /dev/null +++ b/src/sh0rk.zig @@ -0,0 +1,44 @@ +pub const Point = packed struct { + x: i32, + y: i32, + pub fn init(x: i32, y: i32) @This() { + return @This() { + .x = x, + .y = y, + }; + } + + pub fn equals(this: @This(), other: @This()) bool { + return this.x == other.x and this.y == other.y; + } +}; + +pub const Rect = packed struct { + base: Point, + width: i32, + height: i32, + pub fn init(base: Point, width: i32, height: i32) @This() { + return @This() { + .base = base, + .width = width, + .height = height, + }; + } + + pub fn inside(this: @This(), point: Point) bool { + return point.x >= this.base.x and point.x < this.base.x + this.width and point.y >= this.base.y and point.y < this.base.y + this.height; + } + + pub fn collides(this: @This(), other: @This()) bool { + return this.base.x < other.base.x + other.width and this.base.x + this.width > other.base.x and this.base.y < other.base.y + other.height and this.base.y + this.height > other.base.y; + } +}; + +pub const Direction = enum(u2) { + Up, + Down, + Left, + Right, +}; + + diff --git a/src/sprites.zig b/src/sprites.zig new file mode 100644 index 0000000..7c9e35a --- /dev/null +++ b/src/sprites.zig @@ -0,0 +1,7 @@ +// Mara +pub const Mara_width = 96; +pub const Mara_height = 16; +pub const Mara_flags = 1; // BLIT_2BPP +pub const Mara = [384]u8{ 0x3f,0x00,0x00,0xfc,0x3f,0x00,0x00,0xfc,0x3f,0x00,0x00,0xfc,0x3f,0x00,0x00,0xfc,0x00,0x00,0xf0,0x00,0x00,0x00,0xf0,0x00,0x36,0xff,0xc3,0x9c,0x36,0xff,0xc3,0x9c,0x3a,0xc3,0xff,0xac,0x3a,0xc3,0xff,0xac,0x00,0x00,0xec,0x00,0x00,0x00,0xec,0x00,0x35,0xd5,0xbe,0x5c,0x35,0xd6,0xbe,0x5c,0x3a,0xbf,0xab,0xac,0x3a,0xbf,0xab,0xac,0x30,0x00,0xeb,0x00,0x30,0x00,0xeb,0x00,0x3a,0xd6,0x56,0xac,0x3a,0xd9,0x56,0xac,0x3a,0xe6,0x66,0xec,0x3a,0xe6,0x66,0xec,0xdc,0x03,0xe9,0xf0,0xdc,0x03,0xe9,0xf0,0x0f,0x59,0x5e,0xf0,0x0f,0x65,0x5e,0xf0,0x0f,0xb9,0x9d,0xf0,0x0f,0xb9,0x9d,0xf0,0xe7,0x0e,0x65,0x5c,0xe7,0x0e,0x65,0x5c,0x03,0x65,0x5e,0xc0,0x03,0xb5,0x5e,0xc0,0x03,0xde,0x77,0xc0,0x03,0xde,0x77,0xc0,0xe7,0x0d,0xa7,0x5c,0xe7,0x0d,0xa7,0x5c,0x03,0xd7,0xd6,0xc0,0x03,0xd7,0xd6,0xc0,0x03,0xe7,0xdb,0xc0,0x03,0xe7,0xdb,0xc0,0xe9,0xcd,0xa7,0x5c,0xe9,0xcd,0xa7,0x5c,0x00,0xe5,0x5b,0x00,0x00,0xe5,0x5b,0x00,0x00,0xe9,0x6b,0x00,0x00,0xe9,0x6b,0x00,0x3a,0x7d,0xa5,0x7c,0x3a,0x7d,0xa5,0x7c,0x03,0xff,0xff,0xc0,0x03,0xff,0xff,0xc0,0x03,0xfa,0xaf,0xc0,0x03,0xfa,0xaf,0xc0,0x3a,0xc3,0xe9,0xf0,0x3a,0xc3,0xe9,0xf0,0x0d,0xaa,0xaa,0x70,0x0d,0xaa,0xaa,0x70,0x0d,0xae,0xba,0x70,0x0d,0xae,0xba,0x70,0x3b,0x00,0xff,0xc0,0x3b,0x00,0xff,0xc0,0x0d,0xea,0xab,0x70,0x0d,0xea,0xab,0x70,0x0d,0xee,0xbb,0x70,0x0d,0xee,0xbb,0x70,0xeb,0x03,0xb7,0xb0,0xeb,0x03,0xb7,0xb0,0x0d,0xea,0xab,0x70,0x0d,0xea,0xab,0x70,0x0d,0xee,0xbb,0x70,0x0d,0xee,0xbb,0x70,0xea,0xff,0xde,0xb0,0xea,0xff,0xad,0xf0,0x03,0x37,0xdc,0xc0,0x03,0x37,0xdc,0xc0,0x03,0x3e,0xbc,0xc0,0x03,0x3e,0xbc,0xc0,0x3a,0xaa,0xfa,0xb0,0x3a,0xaa,0xab,0xb0,0x00,0x37,0xdc,0x00,0x00,0x37,0xdc,0x00,0x00,0x3f,0xfc,0x00,0x00,0x3f,0xfc,0x00,0x3a,0xaf,0xd5,0xf0,0x3a,0xaf,0xd5,0xf0,0x00,0x37,0x30,0x00,0x00,0x0c,0xdc,0x00,0x00,0x37,0x30,0x00,0x00,0x0c,0xdc,0x00,0x0f,0xf0,0x37,0x00,0x0f,0xf0,0x37,0x00,0x00,0x0c,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x0c,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x03,0x8c,0xb0,0x00,0x00,0xee,0xc0 }; + + diff --git a/src/wasm4.zig b/src/wasm4.zig index 322dc92..97cdd6f 100644 --- a/src/wasm4.zig +++ b/src/wasm4.zig @@ -1,13 +1,25 @@ // // WASM-4: https://wasm4.org/docs +const std = @import("std"); + +fn assert_equal(a: anytype, b: anytype, comptime error_msg: []const u8) void { + if (a != b) @compileError(std.fmt.comptimePrint("{} != {} {s}", .{a, b, error_msg})); +} + +comptime { + const builtin = @import("builtin"); + const native_endian = builtin.target.cpu.arch.endian(); + assert_equal(native_endian, .Little, "Bit flags need little endian"); +} + // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ // │ Platform Constants │ // │ │ // └───────────────────────────────────────────────────────────────────────────┘ -pub const CANVAS_SIZE: u32 = 160; +pub const SCREEN_SIZE: u32 = 160; // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ @@ -15,31 +27,99 @@ pub const CANVAS_SIZE: u32 = 160; // │ │ // └───────────────────────────────────────────────────────────────────────────┘ -pub const PALETTE: *[4]u32 = @intToPtr(*[4]u32, 0x04); -pub const DRAW_COLORS: *u16 = @intToPtr(*u16, 0x14); -pub const GAMEPAD1: *const u8 = @intToPtr(*const u8, 0x16); -pub const GAMEPAD2: *const u8 = @intToPtr(*const u8, 0x17); -pub const GAMEPAD3: *const u8 = @intToPtr(*const u8, 0x18); -pub const GAMEPAD4: *const u8 = @intToPtr(*const u8, 0x19); -pub const MOUSE_X: *const i16 = @intToPtr(*const i16, 0x1a); -pub const MOUSE_Y: *const i16 = @intToPtr(*const i16, 0x1c); -pub const MOUSE_BUTTONS: *const u8 = @intToPtr(*const u8, 0x1e); -pub const SYSTEM_FLAGS: *u8 = @intToPtr(*u8, 0x1f); -pub const FRAMEBUFFER: *[6400]u8 = @intToPtr(*[6400]u8, 0xA0); +pub const Memory = packed struct { + _padding: [4]u8, + palette: [4]Color, + colors: DrawColors, + gamepads: [4]GamePad, + mouse: Mouse, + system: SystemFlags, + _reserved: [128]u8, + framebuffer: [6400]u8, + userdata: [58976]u8, +}; -pub const BUTTON_1: u8 = 1; -pub const BUTTON_2: u8 = 2; -pub const BUTTON_LEFT: u8 = 16; -pub const BUTTON_RIGHT: u8 = 32; -pub const BUTTON_UP: u8 = 64; -pub const BUTTON_DOWN: u8 = 128; +comptime { + assert_equal(@bitSizeOf(Memory), 64 * 1024 * 8, "Memory layout wrong"); +} -pub const MOUSE_LEFT: u8 = 1; -pub const MOUSE_RIGHT: u8 = 2; -pub const MOUSE_MIDDLE: u8 = 4; +const dummym: Memory = undefined; -pub const SYSTEM_PRESERVE_FRAMEBUFFER: u8 = 1; -pub const SYSTEM_HIDE_GAMEPAD_OVERLAY: u8 = 2; +pub const m = struct { + pub const palette = @intToPtr(*@TypeOf(dummym.palette ), 0 + @offsetOf(Memory, "palette" )); + pub const colors = @intToPtr(*@TypeOf(dummym.colors ), 0 + @offsetOf(Memory, "colors" )); + pub const gamepads = @intToPtr(*@TypeOf(dummym.gamepads ), 0 + @offsetOf(Memory, "gamepads" )); + pub const mouse = @intToPtr(*@TypeOf(dummym.mouse ), 0 + @offsetOf(Memory, "mouse" )); + pub const system = @intToPtr(*@TypeOf(dummym.system ), 0 + @offsetOf(Memory, "system" )); + pub const framebuffer = @intToPtr(*@TypeOf(dummym.framebuffer), 0 + @offsetOf(Memory, "framebuffer")); + pub const userdata = @intToPtr(*@TypeOf(dummym.userdata ), 0 + @offsetOf(Memory, "userdata" )); +}; + +pub const Color = packed struct { + blue : u8, + green : u8, + red : u8, + _reserved: u8 = 0, +}; + +pub const DrawColors = packed struct { + _0: ColorIndex, + _1: ColorIndex, + _2: ColorIndex, + _3: ColorIndex, +}; + +pub const ColorIndex = enum(u4) { + transparent = 0, + /// Use palette[0] + p0 = 1, + /// Use palette[1] + p1 = 2, + /// Use palette[2] + p2 = 3, + /// Use palette[3] + p3 = 4, +}; + +pub const GamePad = packed struct { + a: bool, + b: bool, + _reserved: u2, + left: bool, + right: bool, + up: bool, + down: bool, +}; + +pub const Mouse = packed struct { + x: i16, + y: i16, + /// primary button + b0: bool, + /// secondary button + b1: bool, + /// third button + b2: bool, + _reserved: u5, +}; + +pub const SystemFlags = packed struct { + preserve_framebuffer: bool, + hide_gamepad_overlay: bool, + _reserved: u6, +}; + +// ┌───────────────────────────────────────────────────────────────────────────┐ +// │ │ +// │ Raw Functions │ +// │ │ +// └───────────────────────────────────────────────────────────────────────────┘ + +const raw_api = struct { + extern fn blit(sprite: [*]const u8, x: u32, y: u32, width: u32, height: u32, flags: u32) void; + extern fn blitSub(sprite: [*]const u8, x: u32, y: u32, width: u32, height: u32, src_x: u32, src_y: u32, stride: u32, flags: u32) void; + extern fn tone(frequency: u32, duration: u32, volume: u32, flags: u32) void; +}; // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ @@ -47,38 +127,49 @@ pub const SYSTEM_HIDE_GAMEPAD_OVERLAY: u8 = 2; // │ │ // └───────────────────────────────────────────────────────────────────────────┘ +pub const BlitFlags = packed struct { + /// one or two bits per pixel + two_bits: bool = false, + flip_x: bool = false, + flip_y: bool = false, + rotate: bool = false, + _reserved: u28 = 0, +}; + /// Copies pixels to the framebuffer. -pub extern fn blit(sprite: [*]const u8, x: i32, y: i32, width: i32, height: i32, flags: u32) void; +pub fn blit(sprite: []const u8, x: u32, y: u32, width: u32, height: u32, flags: BlitFlags) void { + + raw_api.blit(sprite.ptr, x, y, width, height, @bitCast(u32, flags)); +} /// Copies a subregion within a larger sprite atlas to the framebuffer. -pub extern fn blitSub(sprite: [*]const u8, x: i32, y: i32, width: i32, height: i32, src_x: u32, src_y: u32, stride: i32, flags: u32) void; - -pub const BLIT_2BPP: u32 = 1; -pub const BLIT_1BPP: u32 = 0; -pub const BLIT_FLIP_X: u32 = 2; -pub const BLIT_FLIP_Y: u32 = 4; -pub const BLIT_ROTATE: u32 = 8; +/// srcX: Source X position of the sprite region. +/// srcY: Source Y position of the sprite region. +/// stride: Total width of the overall sprite atlas. This is typically larger than width. +pub fn blitSub(sprite: []const u8, x: u32, y: u32, width: u32, height: u32, src_x: u32, src_y: u32, stride: u32, flags: BlitFlags) void { + raw_api.blitSub(sprite.ptr, x, y, width, height, src_x, src_y, stride, @bitCast(u32, flags)); +} /// Draws a line between two points. -pub extern fn line(x1: i32, y1: i32, x2: i32, y2: i32) void; +pub extern fn line(x1: u32, y1: u32, x2: u32, y2: u32) void; /// Draws an oval (or circle). -pub extern fn oval(x: i32, y: i32, width: i32, height: i32) void; +pub extern fn oval(x: u32, y: u32, width: u32, height: u32) void; /// Draws a rectangle. -pub extern fn rect(x: i32, y: i32, width: u32, height: u32) void; +pub extern fn rect(x: u32, y: u32, width: u32, height: u32) void; /// Draws text using the built-in system font. -pub fn text(str: []const u8, x: i32, y: i32) void { +pub fn text(str: []const u8, x: u32, y: u32) void { textUtf8(str.ptr, str.len, x, y); } -extern fn textUtf8(strPtr: [*]const u8, strLen: usize, x: i32, y: i32) void; +extern fn textUtf8(strPtr: [*]const u8, strLen: usize, x: u32, y: u32) void; /// Draws a vertical line -pub extern fn vline(x: i32, y: i32, len: u32) void; +pub extern fn vline(x: u32, y: u32, len: u32) void; /// Draws a horizontal line -pub extern fn hline(x: i32, y: i32, len: u32) void; +pub extern fn hline(x: u32, y: u32, len: u32) void; // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ @@ -87,16 +178,81 @@ pub extern fn hline(x: i32, y: i32, len: u32) void; // └───────────────────────────────────────────────────────────────────────────┘ /// Plays a sound tone. -pub extern fn tone(frequency: u32, duration: u32, volume: u32, flags: u32) void; +/// frequency: Wave frequency in hertz. +/// duration: Duration of the tone in frames (1/60th of a second), up to 255 frames. +/// volume: Volume of the sustain and attack durations, between 0 and 100. +pub fn tone(frequency: Tone.Frequency, duration: Tone.Duration, volume: Tone.Volume, flags: Tone.Flags) void { + std.debug.assert(volume.is_valid()); + tone(@bitCast(u32, frequency), @bitCast(u32, duration), @bitCast(u32, volume), @bitCast(u32, flags)); +} -pub const TONE_PULSE1: u32 = 0; -pub const TONE_PULSE2: u32 = 1; -pub const TONE_TRIANGLE: u32 = 2; -pub const TONE_NOISE: u32 = 3; -pub const TONE_MODE1: u32 = 0; -pub const TONE_MODE2: u32 = 4; -pub const TONE_MODE3: u32 = 8; -pub const TONE_MODE4: u32 = 12; +pub const Tone = struct { + /// Wave frequency in hertz. + pub const Frequency = packed struct { + start: u16, + end: u16 = 0, + }; + + /// Duration of ADSR of note (unit: frames) + /// + /// ^ + /// / \ decay + /// attack / \ + /// / \-------- + /// / sustain \ release + /// / \ + pub const Duration = packed struct { + sustain : u8 = 0, + release : u8 = 0, + decay : u8 = 0, + attack : u8 = 0, + }; + + /// Volume of note (0 to 100) + /// + /// ^ <-- attack volume + /// / \ + /// / \ + /// / \-------- <-- sustain valume + /// / \ + /// / \ + pub const Volume = packed struct { + sustain : u8, + attack : u8 = 100, + + pub fn is_valid(volume: @This()) bool { + return (0 <= volume.sustain) and (volume.sustain <= 100) + and (0 <= volume.attack ) and (volume.attack <= 100); + } + }; + + pub const Flags = packed struct { + channel: Channel, + pulse_duty: DutyCycle = 2, + pan: Pan = .both, + }; + + pub const Channel = enum(u2) { + pulse0 = 0, + pulse1 = 1, + triangle = 2, + noise = 3, + }; + + pub const DutyCycle = enum(u2) { + @"1/8" = 0, + @"1/4" = 1, + @"1/2" = 2, + }; + + /// Which stereo channel(s) to play audio in + pub const Pan = enum(u2) { + /// play audio in both stereo channels + both = 0, + left = 1, + right = 2, + }; +}; // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ @@ -121,13 +277,3 @@ pub fn trace(x: []const u8) void { traceUtf8(x.ptr, x.len); } extern fn traceUtf8(strPtr: [*]const u8, strLen: usize) void; - -/// Use with caution, as there's no compile-time type checking. -/// -/// * %c, %d, and %x expect 32-bit integers. -/// * %f expects 64-bit floats. -/// * %s expects a *zero-terminated* string pointer. -/// -/// See https://github.com/aduros/wasm4/issues/244 for discussion and type-safe -/// alternatives. -pub extern fn tracef(x: [*:0]const u8, ...) void;