master
Louis Pearson 2022-02-01 00:44:51 -07:00
parent 50060dfa33
commit d0d47d850d
3 changed files with 1043 additions and 959 deletions

981
src/game.zig Normal file
View File

@ -0,0 +1,981 @@
const std = @import("std");
const w4 = @import("wasm4.zig");
const assets = @import("assets");
const input = @import("input.zig");
const util = @import("util.zig");
const Circuit = @import("circuit.zig");
const Map = @import("map.zig");
const Music = @import("music.zig");
const State = @import("main.zig").State;
const Vec2 = util.Vec2;
const Vec2f = util.Vec2f;
const AABB = util.AABB;
const Anim = @import("anim.zig");
// Components
const Pos = struct {
pos: Vec2f,
last: Vec2f,
pinned: bool = false,
pub fn init(pos: Vec2f) @This() {
return @This(){ .pos = pos, .last = pos };
}
pub fn initVel(pos: Vec2f, vel: Vec2f) @This() {
return @This(){ .pos = pos, .last = pos - vel };
}
};
const Control = struct {
controller: enum { player },
state: enum { stand, walk, jump, fall, wallSlide },
facing: enum { left, right, up, down } = .right,
grabbing: ?struct { id: usize, which: usize } = null,
};
const Sprite = struct {
offset: Vec2 = Vec2{ 0, 0 },
size: w4.Vec2,
index: usize,
flags: w4.BlitFlags,
};
const StaticAnim = Anim;
const ControlAnim = struct { anims: []AnimData, state: Anim };
const Kinematic = struct {
col: AABB,
move: Vec2f = Vec2f{ 0, 0 },
lastCol: Vec2f = Vec2f{ 0, 0 },
pub fn inAir(this: @This()) bool {
return approxEqAbs(f32, this.lastCol[1], 0, 0.01);
}
pub fn onFloor(this: @This()) bool {
return approxEqAbs(f32, this.move[1], 0, 0.01) and this.lastCol[1] > 0;
}
pub fn isFalling(this: @This()) bool {
return this.move[1] > 0 and approxEqAbs(f32, this.lastCol[1], 0, 0.01);
}
pub fn onWall(this: @This()) bool {
return this.isFalling() and !approxEqAbs(f32, this.lastCol[0], 0, 0.01);
}
};
const Wire = struct {
nodes: std.BoundedArray(Pos, 32) = std.BoundedArray(Pos, 32).init(0),
enabled: bool = false,
pub fn begin(this: *@This()) *Pos {
return &this.nodes.slice()[0];
}
pub fn end(this: *@This()) *Pos {
return &this.nodes.slice()[this.nodes.len - 1];
}
pub fn straighten(this: *@This()) void {
const b = this.begin().pos;
const e = this.end().pos;
const size = e - b;
for (this.nodes.slice()) |*node, i| {
if (i == 0 or i == this.nodes.len - 1) continue;
node.pos = b + @splat(2, @intToFloat(f32, i)) * size / @splat(2, @intToFloat(f32, this.nodes.len));
}
}
};
const Physics = struct { gravity: Vec2f, friction: Vec2f };
const Player = struct {
pos: Pos,
control: Control,
sprite: Sprite,
controlAnim: ControlAnim,
kinematic: Kinematic,
physics: Physics,
};
// const World = ecs.World(Component);
const Particle = struct {
pos: Pos,
life: i32,
pub fn init(pos: Pos, life: i32) @This() {
return @This(){
.pos = pos,
.life = life,
};
}
};
const ParticleSystem = struct {
const MAXPARTICLES = 32;
particles: std.BoundedArray(Particle, MAXPARTICLES),
pub fn init() @This() {
return @This(){
.particles = std.BoundedArray(Particle, MAXPARTICLES).init(0) catch unreachable,
};
}
pub fn update(this: *@This()) void {
var physics = .{ .gravity = Vec2f{ 0, 0.1 }, .friction = Vec2f{ 0.1, 0.1 } };
var remove = std.BoundedArray(usize, MAXPARTICLES).init(0) catch unreachable;
for (this.particles.slice()) |*part, i| {
if (!inView(part.pos.pos)) {
remove.append(i) catch unreachable;
continue;
}
velocityProcess(1, &part.pos);
physicsProcess(1, &part.pos, &physics);
part.life -= 1;
if (part.life == 0) remove.append(i) catch unreachable;
}
while (remove.popOrNull()) |i| {
_ = this.particles.swapRemove(i);
}
}
pub fn draw(this: @This()) void {
for (this.particles.constSlice()) |*part| {
w4.DRAW_COLORS.* = 0x0002;
w4.oval(util.vec2fToVec2(part.pos.pos) - camera * Map.tile_size, Vec2{ 2, 2 });
}
}
pub fn createRandom(this: *@This(), pos: Vec2f) void {
if (this.particles.len == this.particles.capacity()) return;
const vel = Vec2f{ randRangeF(-1, 1), randRangeF(-2, 0) };
const posComp = Pos.initVel(pos, vel);
const life = randRange(10, 50);
const part = Particle.init(posComp, life);
this.particles.append(part) catch unreachable;
}
pub fn createNRandom(this: *@This(), pos: Vec2f, n: usize) void {
var i: usize = 0;
while (i < n) : (i += 1) {
this.createRandom(pos);
}
}
};
fn inView(vec: Vec2f) bool {
return @reduce(
.And,
@divTrunc(util.world2cell(vec), @splat(2, @as(i32, 20))) * @splat(2, @as(i32, 20)) == camera,
);
}
fn randRange(min: i32, max: i32) i32 {
return random.intRangeLessThanBiased(i32, min, max);
}
fn randRangeF(min: f32, max: f32) f32 {
return min + (random.float(f32) * (max - min));
}
// Global vars
var map: Map = undefined;
var circuit: Circuit = undefined;
var particles: ParticleSystem = undefined;
var prng = std.rand.DefaultPrng.init(0);
var random = prng.random();
var player: Player = undefined;
var music = Music.Procedural.init(.C3, &Music.Minor, 83);
var wires = std.BoundedArray(Wire, 10).init(0) catch unreachable;
var camera = Vec2{ 0, 0 };
const Coin = struct { pos: Pos, sprite: Sprite, anim: Anim, area: AABB };
var coins = std.BoundedArray(Coin, 20).init(0) catch unreachable;
var score: u8 = 0;
var ScoreCoin = Sprite{
.size = Map.tile_size,
.index = 4,
.flags = .{ .bpp = .b2 },
};
var solids_mutable = assets.solid;
var conduit_mutable = assets.conduit;
var conduitLevels_mutable: [conduit_mutable.len]u8 = undefined;
const anim_store = struct {
const stand = Anim.frame(8);
const walk = Anim.simple(4, &[_]usize{ 9, 10, 11, 12 });
const jump = Anim.frame(13);
const fall = Anim.frame(14);
const wallSlide = Anim.frame(15);
const coin = Anim.simple(15, &[_]usize{ 4, 5, 6 });
};
const AnimData = []const Anim.Ops;
const playerAnim = pac: {
var animArr = std.BoundedArray(AnimData, 100).init(0) catch unreachable;
animArr.append(&anim_store.stand) catch unreachable;
animArr.append(&anim_store.walk) catch unreachable;
animArr.append(&anim_store.jump) catch unreachable;
animArr.append(&anim_store.fall) catch unreachable;
animArr.append(&anim_store.wallSlide) catch unreachable;
break :pac animArr.slice();
};
fn showErr(msg: []const u8) noreturn {
w4.traceNoF(msg);
unreachable;
}
pub fn start() void {
particles = ParticleSystem.init();
std.mem.set(u8, &conduitLevels_mutable, 0);
circuit = Circuit.init(&conduit_mutable, &conduitLevels_mutable, assets.conduit_size);
map = Map.init(&solids_mutable, assets.solid_size);
camera = @divTrunc(assets.spawn, @splat(2, @as(i32, 20))) * @splat(2, @as(i32, 20));
const tile_size = Vec2{ 8, 8 };
const offset = Vec2{ 4, 8 };
player = .{
.pos = Pos.init(util.vec2ToVec2f(assets.spawn * tile_size + offset)),
.control = .{ .controller = .player, .state = .stand },
.sprite = .{ .offset = .{ -4, -8 }, .size = .{ 8, 8 }, .index = 8, .flags = .{ .bpp = .b2 } },
.physics = .{ .friction = Vec2f{ 0.15, 0.1 }, .gravity = Vec2f{ 0, 0.25 } },
.controlAnim = ControlAnim{
.anims = playerAnim,
.state = Anim{ .anim = &.{} },
},
.kinematic = .{ .col = .{ .pos = .{ -3, -6 }, .size = .{ 5, 5 } } },
};
for (assets.wire) |wire| {
var w = wires.addOne() catch showErr("New wire");
const divisions = wire.divisions;
var i: usize = 0;
while (i <= divisions) : (i += 1) {
w.nodes.append(Pos.init(Vec2f{ 0, 0 })) catch showErr("Appending nodes");
}
w.begin().pos = util.vec2ToVec2f(wire.p1);
w.end().pos = util.vec2ToVec2f(wire.p2);
w.begin().pinned = wire.a1;
w.end().pinned = wire.a2;
w.straighten();
}
for (assets.sources) |source| {
circuit.addSource(source);
}
for (assets.doors) |door| {
circuit.addDoor(door);
}
// _ = w4.diskw("", 0);
if (!load()) {
for (assets.coins) |coin| {
coins.append(.{
.pos = Pos.init(util.vec2ToVec2f(coin * tile_size)),
.sprite = .{ .offset = .{ 0, 0 }, .size = .{ 8, 8 }, .index = 4, .flags = .{ .bpp = .b2 } },
.anim = Anim{ .anim = &anim_store.coin },
.area = .{ .pos = .{ 0, 0 }, .size = .{ 8, 8 } },
}) catch showErr("Appending coin");
}
}
updateCircuit();
}
var indicator: ?Interaction = null;
pub fn update(time: usize) State {
for (wires.slice()) |*wire| {
wirePhysicsProcess(1, wire);
if (wire.enabled) {
if (music.isDrumBeat()) {
if (!wire.begin().pinned) particles.createNRandom(wire.begin().pos, 8);
if (!wire.end().pinned) particles.createNRandom(wire.end().pos, 8);
}
}
}
velocityProcess(1, &player.pos);
physicsProcess(1, &player.pos, &player.physics);
manipulationProcess(&player.pos, &player.control);
controlProcess(1, &player.pos, &player.control, &player.physics, &player.kinematic);
kinematicProcess(1, &player.pos, &player.kinematic);
controlAnimProcess(1, &player.sprite, &player.controlAnim, &player.control);
particles.update();
// Drawing
w4.DRAW_COLORS.* = 0x0004;
w4.rect(.{ 0, 0 }, .{ 160, 160 });
drawProcess(1, &player.pos, &player.sprite);
{
var shouldSave = false;
var remove = std.BoundedArray(usize, 10).init(0) catch unreachable;
for (coins.slice()) |*coin, i| {
staticAnimProcess(1, &coin.sprite, &coin.anim);
drawProcess(1, &coin.pos, &coin.sprite);
if (coin.area.addv(coin.pos.pos).overlaps(player.kinematic.col.addv(player.pos.pos))) {
score += 1;
remove.append(i) catch unreachable;
music.playCollect(score);
shouldSave = true;
}
}
while (remove.popOrNull()) |i| {
_ = coins.swapRemove(i);
}
// We save here to prevent duplicate coins
if (shouldSave) save();
}
const newCamera = @divTrunc(util.world2cell(player.pos.pos), @splat(2, @as(i32, 20))) * @splat(2, @as(i32, 20));
if (!@reduce(.And, newCamera == camera)) {
save();
}
camera = newCamera;
map.draw(camera);
circuit.draw(camera);
for (wires.slice()) |*wire| {
wireDrawProcess(1, wire);
}
particles.draw();
{
const pos = util.world2cell(player.pos.pos);
const shouldHum = circuit.isEnabled(pos) or
circuit.isEnabled(pos + util.Dir.up) or
circuit.isEnabled(pos + util.Dir.down) or
circuit.isEnabled(pos + util.Dir.left) or
circuit.isEnabled(pos + util.Dir.right);
if (shouldHum) {
w4.tone(.{ .start = 60 }, .{ .release = 255, .sustain = 0 }, 1, .{ .channel = .pulse1, .mode = .p50 });
}
}
if (indicator) |details| {
const pos = details.pos - (camera * Map.tile_size);
const stage = @divTrunc((time % 60), 30);
var size = Vec2{ 0, 0 };
switch (stage) {
0 => size = Vec2{ 6, 6 },
else => size = Vec2{ 8, 8 },
}
if (details.active) {
// w4.tone(.{ .start = 60 }, .{ .release = 255, .sustain = 0 }, 10, .{ .channel = .pulse1, .mode = .p50 });
// music.newIntensity = .danger;
w4.DRAW_COLORS.* = 0x0020;
} else {
w4.DRAW_COLORS.* = 0x0030;
}
var half = Vec2{ @divTrunc(size[0], 2), @divTrunc(size[1], 2) };
switch (details.details) {
.wire => w4.oval(pos - half, size),
.plug => w4.rect(pos - half, size),
.lever => w4.rect(pos - half, size),
}
}
// Score UI
{
const playerPos = util.vec2fToVec2(player.pos.pos) - camera * Map.tile_size;
const textOffset = Vec2{ 9, 1 };
const textChars = 3;
const size = Vec2{ 8 * textChars, 8 } + textOffset;
const scorePos = Vec2{
if (playerPos[0] > 80) 0 else 160 - size[0],
if (playerPos[1] > 80) 0 else 160 - size[1],
};
// Manually convert score to text
var scoreDigits = [textChars]u8{ 'x', '0', '0' };
scoreDigits[1] = '0' + @divTrunc(score, 10);
scoreDigits[2] = '0' + score % 10;
// Clear background of score
w4.DRAW_COLORS.* = 0x0004;
w4.rect(scorePos, size);
// Draw coin
draw_sprite(scorePos, ScoreCoin);
w4.DRAW_COLORS.* = 0x0042;
w4.text(&scoreDigits, scorePos + Vec2{ 9, 1 });
}
// Music
const musicCommand = music.getNext(1);
for (musicCommand.constSlice()) |sfx| {
w4.tone(sfx.freq, sfx.duration, sfx.volume, sfx.flags);
}
indicator = null;
return .Game;
}
fn write_diff(writer: anytype, stride: usize, initial: []const u8, mapBuf: []const u8) !u8 {
var written: u8 = 0;
for (initial) |init_tile, i| {
if (mapBuf[i] != init_tile) {
const x = @intCast(u8, i % @intCast(usize, stride));
const y = @intCast(u8, @divTrunc(i, @intCast(usize, stride)));
const temp = [3]u8{ x, y, mapBuf[i] };
try writer.writeAll(&temp);
written += 1;
}
}
return written;
}
pub fn load_diff(mapBuf: []u8, stride: usize, diff: []const u8) void {
var i: usize = 0;
while (i < diff.len) : (i += 3) {
const x = diff[i];
const y = diff[i + 1];
const tile = diff[i + 2];
const a = x + y * stride;
mapBuf[a] = tile;
// this.set_cell(Cell{ x, y }, tile);
}
}
fn load() bool {
var load_buf: [1024]u8 = undefined;
const read = w4.diskr(&load_buf, 1024);
w4.tracef("%d bytes read", read);
// if (true) return false;
if (read <= 0) return false;
// for (load_buf[0 .. read - 1]) |byte| w4.tracef("%d", byte);
var stream = std.io.fixedBufferStream(load_buf[0..read]);
var reader = stream.reader();
var header: [5]u8 = undefined;
_ = reader.read(&header) catch w4.tracef("couldn't load header");
w4.tracef("%s", &header);
if (!std.mem.eql(u8, "wired", &header)) return false; // w4.tracef("did not load, incorrect header bytes");
score = reader.readByte() catch return false;
const obj_len = reader.readByte() catch return false;
// const map_len = reader.readByte() catch return false;
const conduit_len = reader.readByte() catch return false;
var i: usize = 0;
while (i < obj_len) : (i += 1) {
const b = reader.readByte() catch return false;
const obj = @intToEnum(SaveObj, @truncate(u4, b));
const id = @truncate(u4, b >> 4);
const x = reader.readIntBig(u16) catch return false;
const y = reader.readIntBig(u16) catch return false;
var pos = Pos.init(util.vec2ToVec2f(Vec2{ x, y }));
switch (obj) {
.Player => {
w4.tracef("player at %d, %d", x, y);
player.pos = pos;
// player.pos.pos += Vec2f{ 4, 6 };
},
.Coin => {
coins.append(.{
.pos = pos,
.sprite = .{ .offset = .{ 0, 0 }, .size = .{ 8, 8 }, .index = 4, .flags = .{ .bpp = .b2 } },
.anim = Anim{ .anim = &anim_store.coin },
.area = .{ .pos = .{ 0, 0 }, .size = .{ 8, 8 } },
}) catch unreachable;
},
.WireBeginPinned => {
var begin = wires.slice()[id].begin();
begin.* = pos;
begin.pinned = true;
wires.slice()[id].straighten();
},
.WireBeginLoose => {
var begin = wires.slice()[id].begin();
begin.* = pos;
begin.pinned = false;
wires.slice()[id].straighten();
},
.WireEndPinned => {
var end = wires.slice()[id].end();
end.* = pos;
end.pinned = true;
wires.slice()[id].straighten();
},
.WireEndLoose => {
var end = wires.slice()[id].end();
end.* = pos;
end.pinned = false;
wires.slice()[id].straighten();
},
}
}
// Load map
var buf: [256]u8 = undefined;
// const len = reader.readByte() catch return;
// const bytes_map = reader.read(buf[0 .. map_len * 3]) catch return false;
// w4.tracef("loading %d map diffs... %d bytes", map_len, bytes_map);
// load_diff(&solids_mutable, assets.solid_size[0], buf[0..bytes_map]);
// Load conduit
// const conduit_len = reader.readByte() catch return;
const bytes_conduit = reader.read(buf[0 .. conduit_len * 3]) catch return false;
w4.tracef("loading %d conduit diffs... %d bytes", conduit_len, bytes_conduit);
for (buf[0..bytes_conduit]) |byte| w4.tracef("%d", byte);
load_diff(&conduit_mutable, assets.conduit_size[0], buf[0..bytes_conduit]);
return true;
}
const SaveObj = enum(u4) {
Player,
Coin,
WireBeginPinned,
WireBeginLoose,
WireEndPinned,
WireEndLoose,
};
fn cell2u8(cell: util.Cell) [2]u8 {
return [_]u8{ @intCast(u8, cell[0]), @intCast(u8, cell[1]) };
}
fn vec2u16(vec2: util.Vec2) [2]u16 {
return [_]u16{ @intCast(u16, vec2[0]), @intCast(u16, vec2[1]) };
}
fn save() void {
var save_buf: [1024]u8 = undefined;
var save_stream = std.io.fixedBufferStream(&save_buf);
var save_writer = save_stream.writer();
save_writer.writeAll("wired") catch return w4.tracef("Couldn't write header");
save_writer.writeByte(score) catch return w4.tracef("Couldn't save score");
w4.tracef("score %d written", score);
// Write temporary length values
const lengths_start = save_stream.getPos() catch return w4.tracef("Couldn't get pos");
save_writer.writeByte(0) catch return w4.tracef("Couldn't write obj length");
// save_writer.writeByte(0) catch return w4.tracef("Couldn't write map length");
save_writer.writeByte(0) catch return w4.tracef("Couldn't write conduit length");
// Write player
const playerPos = vec2u16(util.vec2fToVec2(player.pos.pos));
save_writer.writeByte(@enumToInt(SaveObj.Player)) catch return w4.tracef("Player");
save_writer.writeIntBig(u16, playerPos[0]) catch return;
save_writer.writeIntBig(u16, playerPos[1]) catch return;
// save_writer.writeAll(&[_]u8{ @enumToInt(SaveObj.Player), @intCast(u8, player
var obj_len: u8 = 1;
for (coins.slice()) |coin, i| {
obj_len += 1;
const id = @intCast(u8, @truncate(u4, i)) << 4;
// const cell = util.world2cell(coin.pos.pos);
save_writer.writeByte(@enumToInt(SaveObj.Coin) | id) catch return w4.tracef("Couldn't save coin");
const pos = vec2u16(util.vec2fToVec2(coin.pos.pos));
save_writer.writeIntBig(u16, pos[0]) catch return;
save_writer.writeIntBig(u16, pos[1]) catch return;
// save_writer.writeInt(&) catch return;
}
// Write wires
for (wires.slice()) |*wire, i| {
const id = @intCast(u8, @truncate(u4, i)) << 4;
const begin = wire.begin();
const end = wire.end();
obj_len += 1;
if (begin.pinned) {
// const cell = util.world2cell(begin.pos);
save_writer.writeByte(@enumToInt(SaveObj.WireBeginPinned) | id) catch return w4.tracef("Couldn't save wire");
// const pos = cell2u16(cell);
const pos = vec2u16(util.vec2fToVec2(begin.pos));
save_writer.writeIntBig(u16, pos[0]) catch return;
save_writer.writeIntBig(u16, pos[1]) catch return;
// save_writer.writeAll(&cell2u8(cell)) catch return;
} else {
// const cell = util.world2cell(begin.pos);
save_writer.writeByte(@enumToInt(SaveObj.WireBeginLoose) | id) catch return w4.tracef("Couldn't save wire");
// const pos = cell2u16(cell);
const pos = vec2u16(util.vec2fToVec2(begin.pos));
save_writer.writeIntBig(u16, pos[0]) catch return;
save_writer.writeIntBig(u16, pos[1]) catch return;
// save_writer.writeAll(&cell2u8(cell)) catch return;
}
obj_len += 1;
if (end.pinned) {
// const cell = util.world2cell(end.pos);
save_writer.writeByte(@enumToInt(SaveObj.WireEndPinned) | id) catch return w4.tracef("Couldn't save wire");
// const pos = cell2u16(cell);
const pos = vec2u16(util.vec2fToVec2(end.pos));
save_writer.writeIntBig(u16, pos[0]) catch return;
save_writer.writeIntBig(u16, pos[1]) catch return;
// save_writer.writeAll(&cell2u8(cell)) catch return;
} else {
// const cell = util.world2cell(end.pos);
save_writer.writeByte(@enumToInt(SaveObj.WireEndLoose) | id) catch return w4.tracef("Couldn't save wire");
// const pos = cell2u16(cell);
const pos = vec2u16(util.vec2fToVec2(end.pos));
save_writer.writeIntBig(u16, pos[0]) catch return;
save_writer.writeIntBig(u16, pos[1]) catch return;
// save_writer.writeAll(&cell2u8(cell)) catch return;
}
}
// Write map
// const map_len = write_diff(save_writer, assets.solid_size[0], &assets.solid, &solids_mutable) catch return w4.tracef("Couldn't save map diff");
// Write conduit
const conduit_len = write_diff(save_writer, assets.conduit_size[0], &assets.conduit, &conduit_mutable) catch return w4.tracef("Couldn't save map diff");
const endPos = save_stream.getPos() catch return;
save_stream.seekTo(lengths_start) catch w4.tracef("Couldn't seek");
save_writer.writeByte(obj_len) catch return w4.tracef("Couldn't write obj length");
// save_writer.writeByte(map_len) catch return w4.tracef("Couldn't write map length");
save_writer.writeByte(conduit_len) catch return w4.tracef("Couldn't write conduit length");
save_stream.seekTo(endPos) catch return;
const save_slice = save_stream.getWritten();
const written = w4.diskw(save_slice.ptr, save_slice.len);
w4.tracef("%d bytes written", written);
for (save_buf[0..written]) |byte| w4.tracef("%d", byte);
}
const Interaction = struct {
pos: Vec2,
details: union(enum) {
wire: struct { id: usize, which: usize },
plug: struct { wireID: usize, which: usize },
lever,
},
active: bool = false,
};
fn getNearestCircuitInteraction(pos: Vec2f) ?Interaction {
const cell = util.world2cell(pos);
if (circuit.get_cell(cell)) |tile| {
if (Circuit.is_switch(tile)) {
return Interaction{ .details = .lever, .pos = cell * Map.tile_size + Vec2{ 4, 4 } };
}
}
return null;
}
fn getNearestWireInteraction(pos: Vec2f, range: f32) ?Interaction {
var newIndicator: ?Interaction = null;
var minDistance: f32 = range;
for (wires.slice()) |*wire, wireID| {
const begin = wire.begin().pos;
const end = wire.end().pos;
var dist = util.distancef(begin, pos);
if (dist < minDistance) {
minDistance = dist;
newIndicator = Interaction{
.details = .{ .wire = .{ .id = wireID, .which = 0 } },
.pos = vec2ftovec2(begin),
.active = wire.enabled,
};
}
dist = util.distancef(end, pos);
if (dist < minDistance) {
minDistance = dist;
newIndicator = .{
.details = .{ .wire = .{ .id = wireID, .which = wire.nodes.len - 1 } },
.pos = vec2ftovec2(end),
.active = wire.enabled,
};
}
}
return newIndicator;
}
fn manipulationProcess(pos: *Pos, control: *Control) void {
var offset = switch (control.facing) {
.left => Vec2f{ -6, 0 },
.right => Vec2f{ 6, 0 },
.up => Vec2f{ 0, -8 },
.down => Vec2f{ 0, 8 },
};
// TODO: add centered property
const centeredPos = pos.pos + Vec2f{ 0, -4 };
const offsetPos = centeredPos + offset;
if (control.grabbing == null) {
if (getNearestWireInteraction(offsetPos, 8)) |i| {
indicator = i;
} else if (getNearestWireInteraction(centeredPos - offset, 8)) |i| {
indicator = i;
} else if (getNearestCircuitInteraction(offsetPos)) |i| {
indicator = i;
} else if (getNearestCircuitInteraction(centeredPos)) |i| {
indicator = i;
} else if (getNearestCircuitInteraction(centeredPos - offset)) |i| {
indicator = i;
}
} else if (control.grabbing) |details| {
const cell = util.world2cell(offsetPos);
var wire = &wires.slice()[details.id];
var nodes = wire.nodes.slice();
var maxLength = wireMaxLength(wire);
var length = wireLength(wire);
if (length > maxLength * 1.5) {
nodes[details.which].pinned = false;
control.grabbing = null;
} else {
nodes[details.which].pos = pos.pos + Vec2f{ 0, -4 };
}
if (Circuit.is_plug(circuit.get_cell(cell) orelse 0)) {
const active = circuit.isEnabled(cell);
indicator = .{
.details = .{ .plug = .{ .wireID = details.id, .which = details.which } },
.pos = cell * Map.tile_size + Vec2{ 4, 4 },
.active = active,
};
} else if (input.btnp(.one, .two)) {
nodes[details.which].pinned = false;
control.grabbing = null;
}
}
if (input.btnp(.one, .two)) {
if (indicator) |i| {
switch (i.details) {
.wire => |wire| {
control.grabbing = .{ .id = wire.id, .which = wire.which };
wires.slice()[wire.id].nodes.slice()[wire.which].pos = pos.pos + Vec2f{ 0, -4 };
wires.slice()[wire.id].nodes.slice()[wire.which].pinned = false;
updateCircuit();
},
.plug => |plug| {
wires.slice()[plug.wireID].nodes.slice()[plug.which].pos = vec2tovec2f(indicator.?.pos);
wires.slice()[plug.wireID].nodes.slice()[plug.which].pinned = true;
control.grabbing = null;
updateCircuit();
},
.lever => {
const cell = @divTrunc(i.pos, Map.tile_size);
circuit.toggle(cell);
updateCircuit();
},
}
}
}
}
fn updateCircuit() void {
circuit.clear();
for (wires.slice()) |*wire, wireID| {
wire.enabled = false;
if (!wire.begin().pinned or !wire.end().pinned) continue;
const nodes = wire.nodes.constSlice();
const cellBegin = util.world2cell(nodes[0].pos);
const cellEnd = util.world2cell(nodes[nodes.len - 1].pos);
circuit.bridge(.{ cellBegin, cellEnd }, wireID);
}
_ = circuit.fill();
for (wires.slice()) |*wire| {
const begin = wire.begin();
const end = wire.end();
const cellBegin = util.world2cell(begin.pos);
const cellEnd = util.world2cell(end.pos);
if ((circuit.isEnabled(cellBegin) and begin.pinned) or
(circuit.isEnabled(cellEnd) and end.pinned)) wire.enabled = true;
}
map.reset(&assets.solid);
const enabledDoors = circuit.enabledDoors();
for (enabledDoors.constSlice()) |door| {
map.set_cell(door, 0);
}
}
fn wirePhysicsProcess(dt: f32, wire: *Wire) void {
var nodes = wire.nodes.slice();
if (nodes.len == 0) return;
if (!inView(wire.begin().pos) and !inView(wire.end().pos)) return;
var physics = Physics{ .gravity = Vec2f{ 0, 0.25 }, .friction = Vec2f{ 0.1, 0.1 } };
var kinematic = Kinematic{ .col = AABB{ .pos = Vec2f{ -1, -1 }, .size = Vec2f{ 1, 1 } } };
for (nodes) |*node| {
velocityProcess(dt, node);
physicsProcess(dt, node, &physics);
kinematicProcess(dt, node, &kinematic);
}
var iterations: usize = 0;
while (iterations < 4) : (iterations += 1) {
var left: usize = 1;
while (left < nodes.len) : (left += 1) {
// Left side
constrainNodes(&nodes[left - 1], &nodes[left]);
kinematicProcess(dt, &nodes[left - 1], &kinematic);
kinematicProcess(dt, &nodes[left], &kinematic);
}
}
}
const wireSegmentMaxLength = 4;
fn wireMaxLength(wire: *Wire) f32 {
return @intToFloat(f32, wire.nodes.len) * wireSegmentMaxLength;
}
fn wireLength(wire: *Wire) f32 {
var nodes = wire.nodes.slice();
var length: f32 = 0;
var i: usize = 1;
while (i < nodes.len) : (i += 1) {
length += util.distancef(nodes[i - 1].pos, nodes[i].pos);
}
return length;
}
fn constrainNodes(prevNode: *Pos, node: *Pos) void {
var diff = prevNode.pos - node.pos;
var dist = util.distancef(node.pos, prevNode.pos);
var difference: f32 = 0;
if (dist > 0) {
difference = (wireSegmentMaxLength - dist) / dist;
}
var translate = diff * @splat(2, 0.5 * difference);
if (!prevNode.pinned) prevNode.pos += translate;
if (!node.pinned) node.pos -= translate;
}
fn wireDrawProcess(_: f32, wire: *Wire) void {
var nodes = wire.nodes.slice();
if (nodes.len == 0) return;
if (!inView(wire.begin().pos) and !inView(wire.end().pos)) return;
w4.DRAW_COLORS.* = if (wire.enabled) 0x0002 else 0x0003;
for (nodes) |node, i| {
if (i == 0) continue;
const offset = (camera * Map.tile_size);
w4.line(vec2ftovec2(nodes[i - 1].pos) - offset, vec2ftovec2(node.pos) - offset);
}
}
fn vec2tovec2f(vec2: w4.Vec2) Vec2f {
return Vec2f{ @intToFloat(f32, vec2[0]), @intToFloat(f32, vec2[1]) };
}
fn vec2ftovec2(vec2f: Vec2f) w4.Vec2 {
return w4.Vec2{ @floatToInt(i32, vec2f[0]), @floatToInt(i32, vec2f[1]) };
}
fn drawProcess(_: f32, pos: *Pos, sprite: *Sprite) void {
if (!inView(pos.pos)) return;
const ipos = (util.vec2fToVec2(pos.pos) + sprite.offset) - camera * Map.tile_size;
draw_sprite(ipos, sprite.*);
}
fn draw_sprite(pos: Vec2, sprite: Sprite) void {
w4.DRAW_COLORS.* = 0x2210;
const index = sprite.index;
const t = w4.Vec2{ @intCast(i32, (index * 8) % 128), @intCast(i32, (index * 8) / 128) };
w4.blitSub(&assets.tiles, pos, sprite.size, t, 128, sprite.flags);
}
fn staticAnimProcess(_: f32, sprite: *Sprite, anim: *StaticAnim) void {
anim.update(&sprite.index);
}
fn controlAnimProcess(_: f32, sprite: *Sprite, anim: *ControlAnim, control: *Control) void {
const a: usize = switch (control.state) {
.stand => 0,
.walk => 1,
.jump => 2,
.fall => 3,
.wallSlide => 4,
};
if (a != 0) music.walking = true else music.walking = false;
sprite.flags.flip_x = (control.facing == .left);
anim.state.play(anim.anims[a]);
anim.state.update(&sprite.index);
}
const approxEqAbs = std.math.approxEqAbs;
fn controlProcess(_: f32, pos: *Pos, control: *Control, physics: *Physics, kinematic: *Kinematic) void {
var delta = Vec2f{ 0, 0 };
if (approxEqAbs(f32, kinematic.move[1], 0, 0.01) and kinematic.lastCol[1] > 0) {
if (input.btnp(.one, .one)) delta[1] -= 23;
if (input.btn(.one, .left)) delta[0] -= 1;
if (input.btn(.one, .right)) delta[0] += 1;
if (delta[0] != 0 or delta[1] != 0) {
control.state = .walk;
} else {
control.state = .stand;
}
} else if (kinematic.move[1] > 0 and !approxEqAbs(f32, kinematic.lastCol[0], 0, 0.01) and approxEqAbs(f32, kinematic.lastCol[1], 0, 0.01)) {
// w4.trace("{}, {}", .{ kinematic.move, kinematic.lastCol });
if (kinematic.lastCol[0] > 0 and input.btnp(.one, .one)) delta = Vec2f{ -10, -15 };
if (kinematic.lastCol[0] < 0 and input.btnp(.one, .one)) delta = Vec2f{ 10, -15 };
physics.gravity = Vec2f{ 0, 0.05 };
control.state = .wallSlide;
} else {
if (input.btn(.one, .left)) delta[0] -= 1;
if (input.btn(.one, .right)) delta[0] += 1;
physics.gravity = Vec2f{ 0, 0.25 };
if (kinematic.move[1] < 0) control.state = .jump else control.state = .fall;
}
if (delta[0] > 0) control.facing = .right;
if (delta[0] < 0) control.facing = .left;
if (input.btn(.one, .up)) control.facing = .up;
if (input.btn(.one, .down)) control.facing = .down;
var move = delta * @splat(2, @as(f32, 0.2));
pos.pos += move;
}
fn kinematicProcess(_: f32, pos: *Pos, kinematic: *Kinematic) void {
var next = pos.last;
next[0] = pos.pos[0];
var hcol = map.collide(kinematic.col.addv(next));
if (hcol.len > 0) {
kinematic.lastCol[0] = next[0] - pos.last[0];
next[0] = pos.last[0];
} else if (!approxEqAbs(f32, next[0] - pos.last[0], 0, 0.01)) {
kinematic.lastCol[0] = 0;
}
next[1] = pos.pos[1];
var vcol = map.collide(kinematic.col.addv(next));
if (vcol.len > 0) {
kinematic.lastCol[1] = next[1] - pos.last[1];
next[1] = pos.last[1];
} else if (!approxEqAbs(f32, next[1] - pos.last[1], 0, 0.01)) {
kinematic.lastCol[1] = 0;
}
var colPosAbs = next + kinematic.lastCol;
var lastCol = map.collide(kinematic.col.addv(colPosAbs));
if (lastCol.len == 0) {
kinematic.lastCol = Vec2f{ 0, 0 };
}
kinematic.move = next - pos.last;
pos.pos = next;
}
fn velocityProcess(_: f32, pos: *Pos) void {
if (pos.pinned) return;
var vel = pos.pos - pos.last;
vel = @minimum(Vec2f{ 8, 8 }, @maximum(Vec2f{ -8, -8 }, vel));
pos.last = pos.pos;
pos.pos += vel;
}
fn physicsProcess(_: f32, pos: *Pos, physics: *Physics) void {
if (pos.pinned) return;
var friction = @splat(2, @as(f32, 1)) - physics.friction;
pos.pos = pos.last + (pos.pos - pos.last) * friction;
pos.pos += physics.gravity;
}

View File

@ -3,215 +3,13 @@ const w4 = @import("wasm4.zig");
const assets = @import("assets"); const assets = @import("assets");
const input = @import("input.zig"); const input = @import("input.zig");
const util = @import("util.zig"); const util = @import("util.zig");
const Circuit = @import("circuit.zig");
const Map = @import("map.zig");
const Music = @import("music.zig");
const Vec2 = util.Vec2; const game = @import("game.zig");
const Vec2f = util.Vec2f; const menu = @import("menu.zig");
const AABB = util.AABB;
const Anim = @import("anim.zig");
// Components pub const State = enum {
const Pos = struct { Menu,
pos: Vec2f, Game,
last: Vec2f,
pinned: bool = false,
pub fn init(pos: Vec2f) @This() {
return @This(){ .pos = pos, .last = pos };
}
pub fn initVel(pos: Vec2f, vel: Vec2f) @This() {
return @This(){ .pos = pos, .last = pos - vel };
}
};
const Control = struct {
controller: enum { player },
state: enum { stand, walk, jump, fall, wallSlide },
facing: enum { left, right, up, down } = .right,
grabbing: ?struct { id: usize, which: usize } = null,
};
const Sprite = struct {
offset: Vec2 = Vec2{ 0, 0 },
size: w4.Vec2,
index: usize,
flags: w4.BlitFlags,
};
const StaticAnim = Anim;
const ControlAnim = struct { anims: []AnimData, state: Anim };
const Kinematic = struct {
col: AABB,
move: Vec2f = Vec2f{ 0, 0 },
lastCol: Vec2f = Vec2f{ 0, 0 },
pub fn inAir(this: @This()) bool {
return approxEqAbs(f32, this.lastCol[1], 0, 0.01);
}
pub fn onFloor(this: @This()) bool {
return approxEqAbs(f32, this.move[1], 0, 0.01) and this.lastCol[1] > 0;
}
pub fn isFalling(this: @This()) bool {
return this.move[1] > 0 and approxEqAbs(f32, this.lastCol[1], 0, 0.01);
}
pub fn onWall(this: @This()) bool {
return this.isFalling() and !approxEqAbs(f32, this.lastCol[0], 0, 0.01);
}
};
const Wire = struct {
nodes: std.BoundedArray(Pos, 32) = std.BoundedArray(Pos, 32).init(0),
enabled: bool = false,
pub fn begin(this: *@This()) *Pos {
return &this.nodes.slice()[0];
}
pub fn end(this: *@This()) *Pos {
return &this.nodes.slice()[this.nodes.len - 1];
}
pub fn straighten(this: *@This()) void {
const b = this.begin().pos;
const e = this.end().pos;
const size = e - b;
for (this.nodes.slice()) |*node, i| {
if (i == 0 or i == this.nodes.len - 1) continue;
node.pos = b + @splat(2, @intToFloat(f32, i)) * size / @splat(2, @intToFloat(f32, this.nodes.len));
}
}
};
const Physics = struct { gravity: Vec2f, friction: Vec2f };
const Player = struct {
pos: Pos,
control: Control,
sprite: Sprite,
controlAnim: ControlAnim,
kinematic: Kinematic,
physics: Physics,
};
// const World = ecs.World(Component);
const Particle = struct {
pos: Pos,
life: i32,
pub fn init(pos: Pos, life: i32) @This() {
return @This(){
.pos = pos,
.life = life,
};
}
};
const ParticleSystem = struct {
const MAXPARTICLES = 32;
particles: std.BoundedArray(Particle, MAXPARTICLES),
pub fn init() @This() {
return @This(){
.particles = std.BoundedArray(Particle, MAXPARTICLES).init(0) catch unreachable,
};
}
pub fn update(this: *@This()) void {
var physics = .{ .gravity = Vec2f{ 0, 0.1 }, .friction = Vec2f{ 0.1, 0.1 } };
var remove = std.BoundedArray(usize, MAXPARTICLES).init(0) catch unreachable;
for (this.particles.slice()) |*part, i| {
if (!inView(part.pos.pos)) {
remove.append(i) catch unreachable;
continue;
}
velocityProcess(1, &part.pos);
physicsProcess(1, &part.pos, &physics);
part.life -= 1;
if (part.life == 0) remove.append(i) catch unreachable;
}
while (remove.popOrNull()) |i| {
_ = this.particles.swapRemove(i);
}
}
pub fn draw(this: @This()) void {
for (this.particles.constSlice()) |*part| {
w4.DRAW_COLORS.* = 0x0002;
w4.oval(util.vec2fToVec2(part.pos.pos) - camera * Map.tile_size, Vec2{ 2, 2 });
}
}
pub fn createRandom(this: *@This(), pos: Vec2f) void {
if (this.particles.len == this.particles.capacity()) return;
const vel = Vec2f{ randRangeF(-1, 1), randRangeF(-2, 0) };
const posComp = Pos.initVel(pos, vel);
const life = randRange(10, 50);
const part = Particle.init(posComp, life);
this.particles.append(part) catch unreachable;
}
pub fn createNRandom(this: *@This(), pos: Vec2f, n: usize) void {
var i: usize = 0;
while (i < n) : (i += 1) {
this.createRandom(pos);
}
}
};
fn inView(vec: Vec2f) bool {
return @reduce(
.And,
@divTrunc(util.world2cell(vec), @splat(2, @as(i32, 20))) * @splat(2, @as(i32, 20)) == camera,
);
}
fn randRange(min: i32, max: i32) i32 {
return random.intRangeLessThanBiased(i32, min, max);
}
fn randRangeF(min: f32, max: f32) f32 {
return min + (random.float(f32) * (max - min));
}
// Global vars
var map: Map = undefined;
var circuit: Circuit = undefined;
var particles: ParticleSystem = undefined;
var prng = std.rand.DefaultPrng.init(0);
var random = prng.random();
var player: Player = undefined;
var music = Music.Procedural.init(.C3, &Music.Minor, 83);
var wires = std.BoundedArray(Wire, 10).init(0) catch unreachable;
var camera = Vec2{ 0, 0 };
const Coin = struct { pos: Pos, sprite: Sprite, anim: Anim, area: AABB };
var coins = std.BoundedArray(Coin, 20).init(0) catch unreachable;
var score: u8 = 0;
var ScoreCoin = Sprite{
.size = Map.tile_size,
.index = 4,
.flags = .{ .bpp = .b2 },
};
var solids_mutable = assets.solid;
var conduit_mutable = assets.conduit;
var conduitLevels_mutable: [conduit_mutable.len]u8 = undefined;
const anim_store = struct {
const stand = Anim.frame(8);
const walk = Anim.simple(4, &[_]usize{ 9, 10, 11, 12 });
const jump = Anim.frame(13);
const fall = Anim.frame(14);
const wallSlide = Anim.frame(15);
const coin = Anim.simple(15, &[_]usize{ 4, 5, 6 });
};
const AnimData = []const Anim.Ops;
const playerAnim = pac: {
var animArr = std.BoundedArray(AnimData, 100).init(0) catch unreachable;
animArr.append(&anim_store.stand) catch unreachable;
animArr.append(&anim_store.walk) catch unreachable;
animArr.append(&anim_store.jump) catch unreachable;
animArr.append(&anim_store.fall) catch unreachable;
animArr.append(&anim_store.wallSlide) catch unreachable;
break :pac animArr.slice();
}; };
fn showErr(msg: []const u8) noreturn { fn showErr(msg: []const u8) noreturn {
@ -219,764 +17,25 @@ fn showErr(msg: []const u8) noreturn {
unreachable; unreachable;
} }
var time: usize = 0;
var state: State = .Menu;
export fn start() void { export fn start() void {
particles = ParticleSystem.init(); menu.start();
std.mem.set(u8, &conduitLevels_mutable, 0);
circuit = Circuit.init(&conduit_mutable, &conduitLevels_mutable, assets.conduit_size);
map = Map.init(&solids_mutable, assets.solid_size);
camera = @divTrunc(assets.spawn, @splat(2, @as(i32, 20))) * @splat(2, @as(i32, 20));
const tile_size = Vec2{ 8, 8 };
const offset = Vec2{ 4, 8 };
player = .{
.pos = Pos.init(util.vec2ToVec2f(assets.spawn * tile_size + offset)),
.control = .{ .controller = .player, .state = .stand },
.sprite = .{ .offset = .{ -4, -8 }, .size = .{ 8, 8 }, .index = 8, .flags = .{ .bpp = .b2 } },
.physics = .{ .friction = Vec2f{ 0.15, 0.1 }, .gravity = Vec2f{ 0, 0.25 } },
.controlAnim = ControlAnim{
.anims = playerAnim,
.state = Anim{ .anim = &.{} },
},
.kinematic = .{ .col = .{ .pos = .{ -3, -6 }, .size = .{ 5, 5 } } },
};
for (assets.wire) |wire| {
var w = wires.addOne() catch showErr("New wire");
const divisions = wire.divisions;
var i: usize = 0;
while (i <= divisions) : (i += 1) {
w.nodes.append(Pos.init(Vec2f{ 0, 0 })) catch showErr("Appending nodes");
}
w.begin().pos = util.vec2ToVec2f(wire.p1);
w.end().pos = util.vec2ToVec2f(wire.p2);
w.begin().pinned = wire.a1;
w.end().pinned = wire.a2;
w.straighten();
}
for (assets.sources) |source| {
circuit.addSource(source);
}
for (assets.doors) |door| {
circuit.addDoor(door);
}
// _ = w4.diskw("", 0);
if (!load()) {
for (assets.coins) |coin| {
coins.append(.{
.pos = Pos.init(util.vec2ToVec2f(coin * tile_size)),
.sprite = .{ .offset = .{ 0, 0 }, .size = .{ 8, 8 }, .index = 4, .flags = .{ .bpp = .b2 } },
.anim = Anim{ .anim = &anim_store.coin },
.area = .{ .pos = .{ 0, 0 }, .size = .{ 8, 8 } },
}) catch showErr("Appending coin");
}
}
updateCircuit();
} }
var indicator: ?Interaction = null;
var time: usize = 0;
export fn update() void { export fn update() void {
for (wires.slice()) |*wire| { const newState = switch (state) {
wirePhysicsProcess(1, wire); .Menu => menu.update(),
if (wire.enabled) { .Game => game.update(time),
if (music.isDrumBeat()) { };
if (!wire.begin().pinned) particles.createNRandom(wire.begin().pos, 8); if (state != newState) {
if (!wire.end().pinned) particles.createNRandom(wire.end().pos, 8); state = newState;
} switch (newState) {
.Menu => menu.start(),
.Game => game.start(),
} }
} }
velocityProcess(1, &player.pos);
physicsProcess(1, &player.pos, &player.physics);
manipulationProcess(&player.pos, &player.control);
controlProcess(1, &player.pos, &player.control, &player.physics, &player.kinematic);
kinematicProcess(1, &player.pos, &player.kinematic);
controlAnimProcess(1, &player.sprite, &player.controlAnim, &player.control);
particles.update();
// Drawing
w4.DRAW_COLORS.* = 0x0004;
w4.rect(.{ 0, 0 }, .{ 160, 160 });
drawProcess(1, &player.pos, &player.sprite);
{
var shouldSave = false;
var remove = std.BoundedArray(usize, 10).init(0) catch unreachable;
for (coins.slice()) |*coin, i| {
staticAnimProcess(1, &coin.sprite, &coin.anim);
drawProcess(1, &coin.pos, &coin.sprite);
if (coin.area.addv(coin.pos.pos).overlaps(player.kinematic.col.addv(player.pos.pos))) {
score += 1;
remove.append(i) catch unreachable;
music.playCollect(score);
shouldSave = true;
}
}
while (remove.popOrNull()) |i| {
_ = coins.swapRemove(i);
}
// We save here to prevent duplicate coins
if (shouldSave) save();
}
const newCamera = @divTrunc(util.world2cell(player.pos.pos), @splat(2, @as(i32, 20))) * @splat(2, @as(i32, 20));
if (!@reduce(.And, newCamera == camera)) {
save();
}
camera = newCamera;
map.draw(camera);
circuit.draw(camera);
for (wires.slice()) |*wire| {
wireDrawProcess(1, wire);
}
particles.draw();
{
const pos = util.world2cell(player.pos.pos);
const shouldHum = circuit.isEnabled(pos) or
circuit.isEnabled(pos + util.Dir.up) or
circuit.isEnabled(pos + util.Dir.down) or
circuit.isEnabled(pos + util.Dir.left) or
circuit.isEnabled(pos + util.Dir.right);
if (shouldHum) {
w4.tone(.{ .start = 60 }, .{ .release = 255, .sustain = 0 }, 1, .{ .channel = .pulse1, .mode = .p50 });
}
}
if (indicator) |details| {
const pos = details.pos - (camera * Map.tile_size);
const stage = @divTrunc((time % 60), 30);
var size = Vec2{ 0, 0 };
switch (stage) {
0 => size = Vec2{ 6, 6 },
else => size = Vec2{ 8, 8 },
}
if (details.active) {
// w4.tone(.{ .start = 60 }, .{ .release = 255, .sustain = 0 }, 10, .{ .channel = .pulse1, .mode = .p50 });
// music.newIntensity = .danger;
w4.DRAW_COLORS.* = 0x0020;
} else {
w4.DRAW_COLORS.* = 0x0030;
}
var half = Vec2{ @divTrunc(size[0], 2), @divTrunc(size[1], 2) };
switch (details.details) {
.wire => w4.oval(pos - half, size),
.plug => w4.rect(pos - half, size),
.lever => w4.rect(pos - half, size),
}
}
// Score UI
{
const playerPos = util.vec2fToVec2(player.pos.pos) - camera * Map.tile_size;
const textOffset = Vec2{ 9, 1 };
const textChars = 3;
const size = Vec2{ 8 * textChars, 8 } + textOffset;
const scorePos = Vec2{
if (playerPos[0] > 80) 0 else 160 - size[0],
if (playerPos[1] > 80) 0 else 160 - size[1],
};
// Manually convert score to text
var scoreDigits = [textChars]u8{ 'x', '0', '0' };
scoreDigits[1] = '0' + @divTrunc(score, 10);
scoreDigits[2] = '0' + score % 10;
// Clear background of score
w4.DRAW_COLORS.* = 0x0004;
w4.rect(scorePos, size);
// Draw coin
draw_sprite(scorePos, ScoreCoin);
w4.DRAW_COLORS.* = 0x0042;
w4.text(&scoreDigits, scorePos + Vec2{ 9, 1 });
}
// Music
const musicCommand = music.getNext(1);
for (musicCommand.constSlice()) |sfx| {
w4.tone(sfx.freq, sfx.duration, sfx.volume, sfx.flags);
}
indicator = null;
input.update(); input.update();
time += 1; time += 1;
} }
fn write_diff(writer: anytype, stride: usize, initial: []const u8, mapBuf: []const u8) !u8 {
var written: u8 = 0;
for (initial) |init_tile, i| {
if (mapBuf[i] != init_tile) {
const x = @intCast(u8, i % @intCast(usize, stride));
const y = @intCast(u8, @divTrunc(i, @intCast(usize, stride)));
const temp = [3]u8{ x, y, mapBuf[i] };
try writer.writeAll(&temp);
written += 1;
}
}
return written;
}
pub fn load_diff(mapBuf: []u8, stride: usize, diff: []const u8) void {
var i: usize = 0;
while (i < diff.len) : (i += 3) {
const x = diff[i];
const y = diff[i + 1];
const tile = diff[i + 2];
const a = x + y * stride;
mapBuf[a] = tile;
// this.set_cell(Cell{ x, y }, tile);
}
}
fn load() bool {
var load_buf: [1024]u8 = undefined;
const read = w4.diskr(&load_buf, 1024);
w4.tracef("%d bytes read", read);
// if (true) return false;
if (read <= 0) return false;
// for (load_buf[0 .. read - 1]) |byte| w4.tracef("%d", byte);
var stream = std.io.fixedBufferStream(load_buf[0..read]);
var reader = stream.reader();
var header: [5]u8 = undefined;
_ = reader.read(&header) catch w4.tracef("couldn't load header");
w4.tracef("%s", &header);
if (!std.mem.eql(u8, "wired", &header)) return false; // w4.tracef("did not load, incorrect header bytes");
score = reader.readByte() catch return false;
const obj_len = reader.readByte() catch return false;
// const map_len = reader.readByte() catch return false;
const conduit_len = reader.readByte() catch return false;
var i: usize = 0;
while (i < obj_len) : (i += 1) {
const b = reader.readByte() catch return false;
const obj = @intToEnum(SaveObj, @truncate(u4, b));
const id = @truncate(u4, b >> 4);
const x = reader.readIntBig(u16) catch return false;
const y = reader.readIntBig(u16) catch return false;
var pos = Pos.init(util.vec2ToVec2f(Vec2{ x, y }));
switch (obj) {
.Player => {
w4.tracef("player at %d, %d", x, y);
player.pos = pos;
// player.pos.pos += Vec2f{ 4, 6 };
},
.Coin => {
coins.append(.{
.pos = pos,
.sprite = .{ .offset = .{ 0, 0 }, .size = .{ 8, 8 }, .index = 4, .flags = .{ .bpp = .b2 } },
.anim = Anim{ .anim = &anim_store.coin },
.area = .{ .pos = .{ 0, 0 }, .size = .{ 8, 8 } },
}) catch unreachable;
},
.WireBeginPinned => {
var begin = wires.slice()[id].begin();
begin.* = pos;
begin.pinned = true;
wires.slice()[id].straighten();
},
.WireBeginLoose => {
var begin = wires.slice()[id].begin();
begin.* = pos;
begin.pinned = false;
wires.slice()[id].straighten();
},
.WireEndPinned => {
var end = wires.slice()[id].end();
end.* = pos;
end.pinned = true;
wires.slice()[id].straighten();
},
.WireEndLoose => {
var end = wires.slice()[id].end();
end.* = pos;
end.pinned = false;
wires.slice()[id].straighten();
},
}
}
// Load map
var buf: [256]u8 = undefined;
// const len = reader.readByte() catch return;
// const bytes_map = reader.read(buf[0 .. map_len * 3]) catch return false;
// w4.tracef("loading %d map diffs... %d bytes", map_len, bytes_map);
// load_diff(&solids_mutable, assets.solid_size[0], buf[0..bytes_map]);
// Load conduit
// const conduit_len = reader.readByte() catch return;
const bytes_conduit = reader.read(buf[0 .. conduit_len * 3]) catch return false;
w4.tracef("loading %d conduit diffs... %d bytes", conduit_len, bytes_conduit);
for (buf[0..bytes_conduit]) |byte| w4.tracef("%d", byte);
load_diff(&conduit_mutable, assets.conduit_size[0], buf[0..bytes_conduit]);
return true;
}
const SaveObj = enum(u4) {
Player,
Coin,
WireBeginPinned,
WireBeginLoose,
WireEndPinned,
WireEndLoose,
};
fn cell2u8(cell: util.Cell) [2]u8 {
return [_]u8{ @intCast(u8, cell[0]), @intCast(u8, cell[1]) };
}
fn vec2u16(vec2: util.Vec2) [2]u16 {
return [_]u16{ @intCast(u16, vec2[0]), @intCast(u16, vec2[1]) };
}
fn save() void {
var save_buf: [1024]u8 = undefined;
var save_stream = std.io.fixedBufferStream(&save_buf);
var save_writer = save_stream.writer();
save_writer.writeAll("wired") catch return w4.tracef("Couldn't write header");
save_writer.writeByte(score) catch return w4.tracef("Couldn't save score");
w4.tracef("score %d written", score);
// Write temporary length values
const lengths_start = save_stream.getPos() catch return w4.tracef("Couldn't get pos");
save_writer.writeByte(0) catch return w4.tracef("Couldn't write obj length");
// save_writer.writeByte(0) catch return w4.tracef("Couldn't write map length");
save_writer.writeByte(0) catch return w4.tracef("Couldn't write conduit length");
// Write player
const playerPos = vec2u16(util.vec2fToVec2(player.pos.pos));
save_writer.writeByte(@enumToInt(SaveObj.Player)) catch return w4.tracef("Player");
save_writer.writeIntBig(u16, playerPos[0]) catch return;
save_writer.writeIntBig(u16, playerPos[1]) catch return;
// save_writer.writeAll(&[_]u8{ @enumToInt(SaveObj.Player), @intCast(u8, player
var obj_len: u8 = 1;
for (coins.slice()) |coin, i| {
obj_len += 1;
const id = @intCast(u8, @truncate(u4, i)) << 4;
// const cell = util.world2cell(coin.pos.pos);
save_writer.writeByte(@enumToInt(SaveObj.Coin) | id) catch return w4.tracef("Couldn't save coin");
const pos = vec2u16(util.vec2fToVec2(coin.pos.pos));
save_writer.writeIntBig(u16, pos[0]) catch return;
save_writer.writeIntBig(u16, pos[1]) catch return;
// save_writer.writeInt(&) catch return;
}
// Write wires
for (wires.slice()) |*wire, i| {
const id = @intCast(u8, @truncate(u4, i)) << 4;
const begin = wire.begin();
const end = wire.end();
obj_len += 1;
if (begin.pinned) {
// const cell = util.world2cell(begin.pos);
save_writer.writeByte(@enumToInt(SaveObj.WireBeginPinned) | id) catch return w4.tracef("Couldn't save wire");
// const pos = cell2u16(cell);
const pos = vec2u16(util.vec2fToVec2(begin.pos));
save_writer.writeIntBig(u16, pos[0]) catch return;
save_writer.writeIntBig(u16, pos[1]) catch return;
// save_writer.writeAll(&cell2u8(cell)) catch return;
} else {
// const cell = util.world2cell(begin.pos);
save_writer.writeByte(@enumToInt(SaveObj.WireBeginLoose) | id) catch return w4.tracef("Couldn't save wire");
// const pos = cell2u16(cell);
const pos = vec2u16(util.vec2fToVec2(begin.pos));
save_writer.writeIntBig(u16, pos[0]) catch return;
save_writer.writeIntBig(u16, pos[1]) catch return;
// save_writer.writeAll(&cell2u8(cell)) catch return;
}
obj_len += 1;
if (end.pinned) {
// const cell = util.world2cell(end.pos);
save_writer.writeByte(@enumToInt(SaveObj.WireEndPinned) | id) catch return w4.tracef("Couldn't save wire");
// const pos = cell2u16(cell);
const pos = vec2u16(util.vec2fToVec2(end.pos));
save_writer.writeIntBig(u16, pos[0]) catch return;
save_writer.writeIntBig(u16, pos[1]) catch return;
// save_writer.writeAll(&cell2u8(cell)) catch return;
} else {
// const cell = util.world2cell(end.pos);
save_writer.writeByte(@enumToInt(SaveObj.WireEndLoose) | id) catch return w4.tracef("Couldn't save wire");
// const pos = cell2u16(cell);
const pos = vec2u16(util.vec2fToVec2(end.pos));
save_writer.writeIntBig(u16, pos[0]) catch return;
save_writer.writeIntBig(u16, pos[1]) catch return;
// save_writer.writeAll(&cell2u8(cell)) catch return;
}
}
// Write map
// const map_len = write_diff(save_writer, assets.solid_size[0], &assets.solid, &solids_mutable) catch return w4.tracef("Couldn't save map diff");
// Write conduit
const conduit_len = write_diff(save_writer, assets.conduit_size[0], &assets.conduit, &conduit_mutable) catch return w4.tracef("Couldn't save map diff");
const endPos = save_stream.getPos() catch return;
save_stream.seekTo(lengths_start) catch w4.tracef("Couldn't seek");
save_writer.writeByte(obj_len) catch return w4.tracef("Couldn't write obj length");
// save_writer.writeByte(map_len) catch return w4.tracef("Couldn't write map length");
save_writer.writeByte(conduit_len) catch return w4.tracef("Couldn't write conduit length");
save_stream.seekTo(endPos) catch return;
const save_slice = save_stream.getWritten();
const written = w4.diskw(save_slice.ptr, save_slice.len);
w4.tracef("%d bytes written", written);
for (save_buf[0..written]) |byte| w4.tracef("%d", byte);
}
const Interaction = struct {
pos: Vec2,
details: union(enum) {
wire: struct { id: usize, which: usize },
plug: struct { wireID: usize, which: usize },
lever,
},
active: bool = false,
};
fn getNearestCircuitInteraction(pos: Vec2f) ?Interaction {
const cell = util.world2cell(pos);
if (circuit.get_cell(cell)) |tile| {
if (Circuit.is_switch(tile)) {
return Interaction{ .details = .lever, .pos = cell * Map.tile_size + Vec2{ 4, 4 } };
}
}
return null;
}
fn getNearestWireInteraction(pos: Vec2f, range: f32) ?Interaction {
var newIndicator: ?Interaction = null;
var minDistance: f32 = range;
for (wires.slice()) |*wire, wireID| {
const begin = wire.begin().pos;
const end = wire.end().pos;
var dist = util.distancef(begin, pos);
if (dist < minDistance) {
minDistance = dist;
newIndicator = Interaction{
.details = .{ .wire = .{ .id = wireID, .which = 0 } },
.pos = vec2ftovec2(begin),
.active = wire.enabled,
};
}
dist = util.distancef(end, pos);
if (dist < minDistance) {
minDistance = dist;
newIndicator = .{
.details = .{ .wire = .{ .id = wireID, .which = wire.nodes.len - 1 } },
.pos = vec2ftovec2(end),
.active = wire.enabled,
};
}
}
return newIndicator;
}
fn manipulationProcess(pos: *Pos, control: *Control) void {
var offset = switch (control.facing) {
.left => Vec2f{ -6, 0 },
.right => Vec2f{ 6, 0 },
.up => Vec2f{ 0, -8 },
.down => Vec2f{ 0, 8 },
};
// TODO: add centered property
const centeredPos = pos.pos + Vec2f{ 0, -4 };
const offsetPos = centeredPos + offset;
if (control.grabbing == null) {
if (getNearestWireInteraction(offsetPos, 8)) |i| {
indicator = i;
} else if (getNearestWireInteraction(centeredPos - offset, 8)) |i| {
indicator = i;
} else if (getNearestCircuitInteraction(offsetPos)) |i| {
indicator = i;
} else if (getNearestCircuitInteraction(centeredPos)) |i| {
indicator = i;
} else if (getNearestCircuitInteraction(centeredPos - offset)) |i| {
indicator = i;
}
} else if (control.grabbing) |details| {
const cell = util.world2cell(offsetPos);
var wire = &wires.slice()[details.id];
var nodes = wire.nodes.slice();
var maxLength = wireMaxLength(wire);
var length = wireLength(wire);
if (length > maxLength * 1.5) {
nodes[details.which].pinned = false;
control.grabbing = null;
} else {
nodes[details.which].pos = pos.pos + Vec2f{ 0, -4 };
}
if (Circuit.is_plug(circuit.get_cell(cell) orelse 0)) {
const active = circuit.isEnabled(cell);
indicator = .{
.details = .{ .plug = .{ .wireID = details.id, .which = details.which } },
.pos = cell * Map.tile_size + Vec2{ 4, 4 },
.active = active,
};
} else if (input.btnp(.one, .two)) {
nodes[details.which].pinned = false;
control.grabbing = null;
}
}
if (input.btnp(.one, .two)) {
if (indicator) |i| {
switch (i.details) {
.wire => |wire| {
control.grabbing = .{ .id = wire.id, .which = wire.which };
wires.slice()[wire.id].nodes.slice()[wire.which].pos = pos.pos + Vec2f{ 0, -4 };
wires.slice()[wire.id].nodes.slice()[wire.which].pinned = false;
updateCircuit();
},
.plug => |plug| {
wires.slice()[plug.wireID].nodes.slice()[plug.which].pos = vec2tovec2f(indicator.?.pos);
wires.slice()[plug.wireID].nodes.slice()[plug.which].pinned = true;
control.grabbing = null;
updateCircuit();
},
.lever => {
const cell = @divTrunc(i.pos, Map.tile_size);
circuit.toggle(cell);
updateCircuit();
},
}
}
}
}
fn updateCircuit() void {
circuit.clear();
for (wires.slice()) |*wire, wireID| {
wire.enabled = false;
if (!wire.begin().pinned or !wire.end().pinned) continue;
const nodes = wire.nodes.constSlice();
const cellBegin = util.world2cell(nodes[0].pos);
const cellEnd = util.world2cell(nodes[nodes.len - 1].pos);
circuit.bridge(.{ cellBegin, cellEnd }, wireID);
}
_ = circuit.fill();
for (wires.slice()) |*wire| {
const begin = wire.begin();
const end = wire.end();
const cellBegin = util.world2cell(begin.pos);
const cellEnd = util.world2cell(end.pos);
if ((circuit.isEnabled(cellBegin) and begin.pinned) or
(circuit.isEnabled(cellEnd) and end.pinned)) wire.enabled = true;
}
map.reset(&assets.solid);
const enabledDoors = circuit.enabledDoors();
for (enabledDoors.constSlice()) |door| {
map.set_cell(door, 0);
}
}
fn wirePhysicsProcess(dt: f32, wire: *Wire) void {
var nodes = wire.nodes.slice();
if (nodes.len == 0) return;
if (!inView(wire.begin().pos) and !inView(wire.end().pos)) return;
var physics = Physics{ .gravity = Vec2f{ 0, 0.25 }, .friction = Vec2f{ 0.1, 0.1 } };
var kinematic = Kinematic{ .col = AABB{ .pos = Vec2f{ -1, -1 }, .size = Vec2f{ 1, 1 } } };
for (nodes) |*node| {
velocityProcess(dt, node);
physicsProcess(dt, node, &physics);
kinematicProcess(dt, node, &kinematic);
}
var iterations: usize = 0;
while (iterations < 4) : (iterations += 1) {
var left: usize = 1;
while (left < nodes.len) : (left += 1) {
// Left side
constrainNodes(&nodes[left - 1], &nodes[left]);
kinematicProcess(dt, &nodes[left - 1], &kinematic);
kinematicProcess(dt, &nodes[left], &kinematic);
}
}
}
const wireSegmentMaxLength = 4;
fn wireMaxLength(wire: *Wire) f32 {
return @intToFloat(f32, wire.nodes.len) * wireSegmentMaxLength;
}
fn wireLength(wire: *Wire) f32 {
var nodes = wire.nodes.slice();
var length: f32 = 0;
var i: usize = 1;
while (i < nodes.len) : (i += 1) {
length += util.distancef(nodes[i - 1].pos, nodes[i].pos);
}
return length;
}
fn constrainNodes(prevNode: *Pos, node: *Pos) void {
var diff = prevNode.pos - node.pos;
var dist = util.distancef(node.pos, prevNode.pos);
var difference: f32 = 0;
if (dist > 0) {
difference = (wireSegmentMaxLength - dist) / dist;
}
var translate = diff * @splat(2, 0.5 * difference);
if (!prevNode.pinned) prevNode.pos += translate;
if (!node.pinned) node.pos -= translate;
}
fn wireDrawProcess(_: f32, wire: *Wire) void {
var nodes = wire.nodes.slice();
if (nodes.len == 0) return;
if (!inView(wire.begin().pos) and !inView(wire.end().pos)) return;
w4.DRAW_COLORS.* = if (wire.enabled) 0x0002 else 0x0003;
for (nodes) |node, i| {
if (i == 0) continue;
const offset = (camera * Map.tile_size);
w4.line(vec2ftovec2(nodes[i - 1].pos) - offset, vec2ftovec2(node.pos) - offset);
}
}
fn vec2tovec2f(vec2: w4.Vec2) Vec2f {
return Vec2f{ @intToFloat(f32, vec2[0]), @intToFloat(f32, vec2[1]) };
}
fn vec2ftovec2(vec2f: Vec2f) w4.Vec2 {
return w4.Vec2{ @floatToInt(i32, vec2f[0]), @floatToInt(i32, vec2f[1]) };
}
fn drawProcess(_: f32, pos: *Pos, sprite: *Sprite) void {
if (!inView(pos.pos)) return;
const ipos = (util.vec2fToVec2(pos.pos) + sprite.offset) - camera * Map.tile_size;
draw_sprite(ipos, sprite.*);
}
fn draw_sprite(pos: Vec2, sprite: Sprite) void {
w4.DRAW_COLORS.* = 0x2210;
const index = sprite.index;
const t = w4.Vec2{ @intCast(i32, (index * 8) % 128), @intCast(i32, (index * 8) / 128) };
w4.blitSub(&assets.tiles, pos, sprite.size, t, 128, sprite.flags);
}
fn staticAnimProcess(_: f32, sprite: *Sprite, anim: *StaticAnim) void {
anim.update(&sprite.index);
}
fn controlAnimProcess(_: f32, sprite: *Sprite, anim: *ControlAnim, control: *Control) void {
const a: usize = switch (control.state) {
.stand => 0,
.walk => 1,
.jump => 2,
.fall => 3,
.wallSlide => 4,
};
if (a != 0) music.walking = true else music.walking = false;
sprite.flags.flip_x = (control.facing == .left);
anim.state.play(anim.anims[a]);
anim.state.update(&sprite.index);
}
const approxEqAbs = std.math.approxEqAbs;
fn controlProcess(_: f32, pos: *Pos, control: *Control, physics: *Physics, kinematic: *Kinematic) void {
var delta = Vec2f{ 0, 0 };
if (approxEqAbs(f32, kinematic.move[1], 0, 0.01) and kinematic.lastCol[1] > 0) {
if (input.btnp(.one, .one)) delta[1] -= 23;
if (input.btn(.one, .left)) delta[0] -= 1;
if (input.btn(.one, .right)) delta[0] += 1;
if (delta[0] != 0 or delta[1] != 0) {
control.state = .walk;
} else {
control.state = .stand;
}
} else if (kinematic.move[1] > 0 and !approxEqAbs(f32, kinematic.lastCol[0], 0, 0.01) and approxEqAbs(f32, kinematic.lastCol[1], 0, 0.01)) {
// w4.trace("{}, {}", .{ kinematic.move, kinematic.lastCol });
if (kinematic.lastCol[0] > 0 and input.btnp(.one, .one)) delta = Vec2f{ -10, -15 };
if (kinematic.lastCol[0] < 0 and input.btnp(.one, .one)) delta = Vec2f{ 10, -15 };
physics.gravity = Vec2f{ 0, 0.05 };
control.state = .wallSlide;
} else {
if (input.btn(.one, .left)) delta[0] -= 1;
if (input.btn(.one, .right)) delta[0] += 1;
physics.gravity = Vec2f{ 0, 0.25 };
if (kinematic.move[1] < 0) control.state = .jump else control.state = .fall;
}
if (delta[0] > 0) control.facing = .right;
if (delta[0] < 0) control.facing = .left;
if (input.btn(.one, .up)) control.facing = .up;
if (input.btn(.one, .down)) control.facing = .down;
var move = delta * @splat(2, @as(f32, 0.2));
pos.pos += move;
}
fn kinematicProcess(_: f32, pos: *Pos, kinematic: *Kinematic) void {
var next = pos.last;
next[0] = pos.pos[0];
var hcol = map.collide(kinematic.col.addv(next));
if (hcol.len > 0) {
kinematic.lastCol[0] = next[0] - pos.last[0];
next[0] = pos.last[0];
} else if (!approxEqAbs(f32, next[0] - pos.last[0], 0, 0.01)) {
kinematic.lastCol[0] = 0;
}
next[1] = pos.pos[1];
var vcol = map.collide(kinematic.col.addv(next));
if (vcol.len > 0) {
kinematic.lastCol[1] = next[1] - pos.last[1];
next[1] = pos.last[1];
} else if (!approxEqAbs(f32, next[1] - pos.last[1], 0, 0.01)) {
kinematic.lastCol[1] = 0;
}
var colPosAbs = next + kinematic.lastCol;
var lastCol = map.collide(kinematic.col.addv(colPosAbs));
if (lastCol.len == 0) {
kinematic.lastCol = Vec2f{ 0, 0 };
}
kinematic.move = next - pos.last;
pos.pos = next;
}
fn velocityProcess(_: f32, pos: *Pos) void {
if (pos.pinned) return;
var vel = pos.pos - pos.last;
vel = @minimum(Vec2f{ 8, 8 }, @maximum(Vec2f{ -8, -8 }, vel));
pos.last = pos.pos;
pos.pos += vel;
}
fn physicsProcess(_: f32, pos: *Pos, physics: *Physics) void {
if (pos.pinned) return;
var friction = @splat(2, @as(f32, 1)) - physics.friction;
pos.pos = pos.last + (pos.pos - pos.last) * friction;
pos.pos += physics.gravity;
}

44
src/menu.zig Normal file
View File

@ -0,0 +1,44 @@
const input = @import("input.zig");
const w4 = @import("wasm4.zig");
const State = @import("main.zig").State;
const Vec2 = w4.Vec2;
var selected: i32 = 0;
const MenuOptions = enum(usize) {
Continue,
NewGame,
};
pub fn start() void {
selected = 0;
}
pub fn update() State {
w4.DRAW_COLORS.* = 0x0002;
var i: i32 = 1;
w4.text("WIRED", Vec2{ 16, i * 16 });
i += 1;
w4.text("Continue", Vec2{ 16, i * 16 });
i += 1;
w4.text("New Game", Vec2{ 16, i * 16 });
i += 1;
w4.text(">", Vec2{ 8, 32 + selected * 16 });
if (input.btnp(.one, .down)) selected += 1;
if (input.btnp(.one, .up)) selected -= 1;
selected = if (selected < 0) 1 else @mod(selected, 2);
if (input.btnp(.one, .one) or input.btnp(.one, .two)) {
switch (@intToEnum(MenuOptions, selected)) {
.Continue => return .Game,
.NewGame => {
_ = w4.diskw("", 0);
return .Game;
},
}
}
return .Menu;
}