diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 21 | ||||
-rw-r--r-- | src/all.c | 184 | ||||
-rw-r--r-- | src/index.html.in | 52 | ||||
-rw-r--r-- | src/tick.c | 19 | ||||
-rw-r--r-- | src/types.c | 3 |
6 files changed, 226 insertions, 54 deletions
@@ -8,3 +8,4 @@ x64 *.vcxproj.filters *.vcxproj.user *.sln.DotSettings.user +.ccls-cache @@ -1,8 +1,8 @@ -build/main: src/all.c src/all.h build/images.c src/types.c src/levels.c src/tick.c +build/main: src/all.c build/images.c build/music.c src/types.c src/levels.c src/tick.c mkdir -p build gcc -Wall -Wextra -Wpedantic -DSDL $$(pkg-config --libs sdl3) -o build/main src/all.c -build/main.wasm: src/all.c src/all.h +build/main.wasm: src/all.c src/types.c src/levels.c src/tick.c mkdir -p build clang --target=wasm32 -nostdlib -DWASM -Wall -Wextra -Wpedantic \ -Wl,--no-entry -fno-builtin -o build/main.wasm src/all.c @@ -11,11 +11,16 @@ build/music.mp3.b64: res/music.mp3 mkdir -p build (printf '"data:audio/mpeg;base64,'; base64 < res/music.mp3 | tr -d '\n'; printf '"') > build/music.mp3.b64 +build/%.png.b64: res/%.png + mkdir -p build + (printf '"data:image/png;base64,'; base64 < $< | tr -d '\n'; printf '"') > $@ + build/main.wasm.b64: build/main.wasm mkdir -p build (printf '"'; base64 < build/main.wasm | tr -d '\n'; printf '"') > build/main.wasm.b64 -build/index.html: src/index.html.in build/main.wasm.b64 build/music.mp3.b64 +build/index.html: src/index.html.in build/main.wasm.b64 build/music.mp3.b64 \ + build/continue.png.b64 build/exit.png.b64 build/pause.png.b64 build/play.png.b64 build/restart.png.b64 mkdir -p build clang -E -P -undef -nostdinc -x c -o build/index.html src/index.html.in @@ -27,6 +32,16 @@ build/%.qoi: res/%.png mkdir -p build magick $< $@ +build/music.pcm: res/music.mp3 + ffmpeg -i res/music.mp3 -acodec pcm_s16le -f s16le build/music.pcm + +build/music.c: build/music.pcm + ( \ + echo 'unsigned char musicBytes[] = {' && \ + xxd -i < build/music.pcm && \ + echo '};' \ + ) > build/music.c + build/images.c: build/continue.qoi build/exit.qoi build/pause.qoi build/play.qoi build/restart.qoi build/img mkdir -p build (\ @@ -101,40 +101,76 @@ static const Button buttons[N_BUTTONS] = { [BUTTON_CONTINUE] = {.x = BUTTON_SPACING, .y = BUTTON_SPACER(BUTTON_CONTINUE), .w = BUTTON_SIZE, .h = BUTTON_SIZE}, }; -static DrawList *render(State *state, UI *ui, Arena *a) { - (void) ui; +static int getPlaceAction(int currentColor, int cellx, int celly, float cellWidth, float cellHeight) { + int hoverColor = EMPTY; + + switch (currentColor) { + case BLACK: + if (cellx < cellWidth / 4 && cellx < celly && cellx < cellHeight - celly) { + hoverColor = RED_LEFT; + } else if (celly < cellHeight / 4 && celly < cellx && celly < cellWidth - cellx) { + hoverColor = RED_UP; + } else if (cellx > cellWidth / 4 * 3 && cellWidth - cellx < celly && cellWidth - cellx < cellHeight - celly) { + hoverColor = RED_RIGHT; + } else if (celly > cellHeight / 4 * 3 && cellHeight - celly < cellx && cellHeight - celly < cellWidth - cellx) { + hoverColor = RED_DOWN; + } else { + hoverColor = RED; + } + break; + case EMPTY: + hoverColor = BLACK; + break; + } + return hoverColor; +} + +static DrawList *render(State *state, UI *ui, Arena *a) { DrawList *drawList = new(a, 1, DrawList); - int cellWidth = (ui->width - GRID_OFFSET_X) / GRIDWIDTH; - int cellHeight = ui->height / GRIDHEIGHT; + float cellWidth = (float) (ui->width - GRID_OFFSET_X) / GRIDWIDTH; + float cellHeight = (float) ui->height / GRIDHEIGHT; for (int x = 0; x < GRIDWIDTH; x++) { for (int y = 0; y < GRIDHEIGHT; y++) { - drawList->els[drawList->len++] = (DrawElement) { - .x = cellWidth * x + GRID_OFFSET_X, - .y = cellHeight * y, - .w = cellWidth, - .h = cellHeight, - .fill = {0, 0, 0, 0}, - .border = {0, 0, 0, 255}, - }; for (int subx = 0; subx < 2; subx++) { for (int suby = 0; suby < 2; suby++) { drawList->els[drawList->len++] = (DrawElement) { .x = cellWidth * x + GRID_OFFSET_X + subx * cellWidth / 2, .y = cellHeight * y + suby * cellHeight / 2, - .w = cellWidth / 2, - .h = cellHeight / 2, + .w = cellWidth / 2 + (1 - subx), + .h = cellHeight / 2 + (1 - suby), .fill = colors[state->grid[x + GRIDWIDTH * y]][subx + 2 * suby], - .border = {0, 0, 0, 0}, }; } } } } - // render end cell + // Vertical grid lines + for (int x = 1; x < GRIDWIDTH; x++) { + drawList->els[drawList->len++] = (DrawElement) { + .x = cellWidth * x + GRID_OFFSET_X, + .y = 0, + .w = 1, + .h = ui->height, + .fill = {0, 0, 0, 255}, + }; + } + + // Horizontal grid lines + for (int y = 1; y < GRIDHEIGHT; y++) { + drawList->els[drawList->len++] = (DrawElement) { + .x = GRID_OFFSET_X, + .y = y * cellHeight, + .w = ui->width - GRID_OFFSET_X, + .h = 1, + .fill = {0, 0, 0, 255}, + }; + } + + // Goal drawList->els[drawList->len++] = (DrawElement) { .x = cellWidth * state->goalx + GRID_OFFSET_X, .y = cellHeight * state->goaly, @@ -142,10 +178,44 @@ static DrawList *render(State *state, UI *ui, Arena *a) { .h = cellHeight, .fill = {255, 0, 0, 63}, .border = {255, 0, 0, 255}, - .image = 5, }; - // render side panel + // Hover + if (ui->mousex >= GRID_OFFSET_X) { + int hoverx = (ui->mousex - GRID_OFFSET_X) * GRIDWIDTH / (ui->width - GRID_OFFSET_X); + int hovery = ui->mousey * GRIDHEIGHT / ui->height; + + int cellx = ui->mousex - GRID_OFFSET_X - hoverx * cellWidth; + int celly = ui->mousey - hovery * cellHeight; + + int hoverColor = getPlaceAction( + state->grid[hoverx + hovery * GRIDWIDTH], + cellx, + celly, + cellWidth, + cellHeight + ); + + if (hoverColor != EMPTY) { + int subCellWidth = cellWidth / 2; + int subCellHeight = cellHeight / 2; + for (int subx = 0; subx < 2; subx++) { + for (int suby = 0; suby < 2; suby++) { + Color fill = colors[hoverColor][subx + 2 * suby]; + fill.a = 127; + drawList->els[drawList->len++] = (DrawElement) { + .x = cellWidth * hoverx + GRID_OFFSET_X + (subx * subCellWidth), + .y = cellHeight * hovery + (suby * subCellHeight), + .w = subCellWidth, + .h = subCellHeight, + .fill = fill, + }; + } + } + } + } + + // Side panel drawList->els[drawList->len++] = (DrawElement) { .x = 0, .y = 0, @@ -243,19 +313,32 @@ static void update(Game *game, uint64_t now, Arena a) { switch (game->input) { int x, y; case INPUT_CLICK: - x = (game->mousex - GRID_OFFSET_X) * GRIDWIDTH / offset_width; - y = game->mousey * GRIDHEIGHT / game->ui.height; - if (x >= 0) { // TODO - 0 isn't far left of grid for some reason, it's slightly more - game->state.grid[x + y * GRIDWIDTH] = (game->state.grid[x + y * GRIDWIDTH] + 1) % (sizeof(colors) / sizeof(colors[0])); - // TODO - some ignore list for which cells we have left to place - game->state.placedCells[0] = BLACK; + x = (game->ui.mousex - GRID_OFFSET_X) * GRIDWIDTH / offset_width; + y = game->ui.mousey * GRIDHEIGHT / game->ui.height; + if (game->ui.mousex >= GRID_OFFSET_X) { + float cellWidth = (float) (game->ui.width - GRID_OFFSET_X) / GRIDWIDTH; + float cellHeight = (float) game->ui.height / GRIDHEIGHT; + int cellx = game->ui.mousex - GRID_OFFSET_X - x * cellWidth; + int celly = game->ui.mousey - y * cellHeight; + // Keeping this around for testing purposes + // game->state.grid[x + y * GRIDWIDTH] = (game->state.grid[x + y * GRIDWIDTH] + 1) % (sizeof(colors) / sizeof(colors[0])); + game->state.grid[x + y * GRIDWIDTH] = getPlaceAction( + game->state.grid[x + y * GRIDWIDTH], + cellx, + celly, + cellWidth, + cellHeight + ); } + // TODO - some ignore list for which cells we have left to place + game->state.placedCells[0] = BLACK; + for (int i = 0; i < N_BUTTONS; i++) { if ( - game->mousex > buttons[i].x && game->mousex < buttons[i].x + buttons[i].w && - game->mousey > game->ui.height - buttons[i].y && - game->mousey < game->ui.height - buttons[i].y + buttons[i].h + game->ui.mousex > buttons[i].x && game->ui.mousex < buttons[i].x + buttons[i].w && + game->ui.mousey > game->ui.height - buttons[i].y && + game->ui.mousey < game->ui.height - buttons[i].y + buttons[i].h ) { // TODO - CLICK THINGS //game->state.buttonStates[i] = BUTTON_STATE_PRESSED; @@ -280,8 +363,8 @@ static void update(Game *game, uint64_t now, Arena a) { } break; case INPUT_RCLICK: - x = (game->mousex - GRID_OFFSET_X) * GRIDWIDTH / offset_width; - y = game->mousey * GRIDHEIGHT / game->ui.height; + x = (game->ui.mousex - GRID_OFFSET_X) * GRIDWIDTH / (game->ui.width - GRID_OFFSET_X); + y = game->ui.mousey * GRIDHEIGHT / game->ui.height; game->state.grid[x + y * GRIDWIDTH] = EMPTY; break; case INPUT_PAUSE_PLAY: @@ -315,6 +398,10 @@ typedef struct { SDL_Texture *textures[sizeof(images) / sizeof(images[0])]; +#include "../build/music.c" + +SDL_AudioStream *stream; + int main(int argc, char **argv) { (void) argc; (void) argv; @@ -326,20 +413,19 @@ int main(int argc, char **argv) { }; Game *game = new(&a, 1, Game); - xmemcpy(&game->state.grid, &levels[0].grid, sizeof(game->state.grid)); - game->state.goalx = levels[0].goalx; - game->state.goaly = levels[0].goaly; game->state.currentLevel = 0; - game->state.playing = 0; + restart_level(game); game->ui = (UI) { .width = 640, .height = 480, + .mousex = 0, + .mousey = 0, }; for (int i = 0; i < N_BUTTONS; i++) { game->state.buttonStates[i] = BUTTON_STATE_IDLE; } - SDL_Init(SDL_INIT_VIDEO); + SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO); SDL_Window *w = SDL_CreateWindow( "LDJam 57", game->ui.width, @@ -369,6 +455,15 @@ int main(int argc, char **argv) { } SDL_UpdateTexture(textures[j], NULL, pixels, images[j].width * 4); } + + SDL_AudioSpec audioSpec = { + .format = SDL_AUDIO_S16LE, + .channels = 2, + .freq = 48000, + }; + stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audioSpec, NULL, NULL); + SDL_PutAudioStreamData(stream, musicBytes, sizeof(musicBytes)); + SDL_ResumeAudioStreamDevice(stream); for (;;) { uint64_t now = SDL_GetTicks(); @@ -379,14 +474,19 @@ int main(int argc, char **argv) { case SDL_EVENT_QUIT: return 0; case SDL_EVENT_MOUSE_BUTTON_DOWN: - game->mousex = (int) e.button.x; - game->mousey = (int) e.button.y; + game->ui.mousex = (int) e.button.x; + game->ui.mousey = (int) e.button.y; if (e.button.button == 1) { game->input = INPUT_CLICK; } else if (e.button.button == 3) { game->input = INPUT_RCLICK; } break; + case SDL_EVENT_MOUSE_MOTION: + game->ui.mousex = (int) e.motion.x; + game->ui.mousey = (int) e.motion.y; + game->input = INPUT_MOVE; + break; case SDL_EVENT_KEY_DOWN: switch (e.key.key) { case SDLK_SPACE: @@ -441,6 +541,10 @@ int main(int argc, char **argv) { update(game, now, a); + if (SDL_GetAudioStreamQueued(stream) < (int) sizeof(musicBytes) / 8) { + SDL_PutAudioStreamData(stream, musicBytes, sizeof(musicBytes)); + } + SDL_RenderPresent(r); } @@ -466,9 +570,11 @@ void game_init(void) { } __attribute((export_name("game_render"))) -DrawList *game_render(int width, int height) { +DrawList *game_render(int width, int height, int mousex, int mousey) { game->ui.width = width; game->ui.height = height; + game->ui.mousex = mousex; + game->ui.mousey = mousey; Arena scratch = perm; return render(&game->state, &game->ui, &scratch); } @@ -476,8 +582,8 @@ DrawList *game_render(int width, int height) { __attribute((export_name("game_update"))) void game_update(int input, int mousex, int mousey, int now) { game->input = input; - game->mousex = mousex; - game->mousey = mousey; + game->ui.mousex = mousex; + game->ui.mousey = mousey; update(game, now, perm); } diff --git a/src/index.html.in b/src/index.html.in index aca8b6d..14731f6 100644 --- a/src/index.html.in +++ b/src/index.html.in @@ -32,6 +32,8 @@ const INPUT_NONE = 0; const INPUT_CLICK = 1; const INPUT_RCLICK = 2; const INPUT_PAUSE_PLAY = 3; +const INPUT_MOVE = 4; +const INPUT_RESTART = 5; const WASM = #include "../build/main.wasm.b64" @@ -39,6 +41,19 @@ const WASM = const MUSIC = #include "../build/music.mp3.b64" +const IMAGES = [ +#include "../build/continue.png.b64" +, +#include "../build/exit.png.b64" +, +#include "../build/pause.png.b64" +, +#include "../build/play.png.b64" +, +#include "../build/restart.png.b64" +, +]; + async function main() { let bytes = Uint8Array.from(atob(WASM), function(c) { return c.charCodeAt(0); @@ -51,12 +66,22 @@ async function main() { let ctx = canvas.getContext("2d"); let memory = exports.memory; + let mousex = 0; + let mousey = 0; + const audio = new Audio(); audio.src = MUSIC; audio.volume = 0.2; audio.loop = true; let musicPlaying = false; + let images = [null]; + for (let i = 0; i < IMAGES.length; i++) { + const image = new Image(); + image.src = IMAGES[i]; + images.push(image); + } + const start = Date.now(); function now() { return Date.now() - start; @@ -73,13 +98,16 @@ async function main() { function render() { let width = canvas.width = min(html.clientWidth, max_width()); let height = canvas.height = width; - let ptr = exports.game_render(width, height); + let ptr = exports.game_render(width, height, mousex, mousey); let dl = new Int32Array(memory.buffer, ptr); let len = dl[0]; let ops = dl.subarray(1); + ctx.fillStyle = "#000000"; + ctx.fillRect(0, 0, width, height); + console.log("frame"); for (let i = 0; i < len; i++) { - let op = ops.subarray(6*i, 6*i+6); + let op = ops.subarray(7*i, 7*i+7); const color = new Uint8Array(new Uint32Array(op.subarray(4, 6)).buffer); ctx.fillStyle = `#${color[0].toString(16).padStart(2, "0")}${color[1].toString(16).padStart(2, "0")}${color[2].toString(16).padStart(2, "0")}`; ctx.globalAlpha = color[3] / 255; @@ -87,6 +115,10 @@ async function main() { ctx.strokeStyle = `#${color[4].toString(16).padStart(2, "0")}${color[5].toString(16).padStart(2, "0")}${color[6].toString(16).padStart(2, "0")}`; ctx.globalAlpha = color[7] / 255; ctx.strokeRect(op[0], op[1], op[2], op[3]); + if (op[6] !== 0) { + ctx.globalAlpha = 1; + ctx.drawImage(images[op[6]], op[0], op[1], op[2], op[3]); + } } } @@ -94,9 +126,15 @@ async function main() { window.addEventListener("resize", onresize); onresize(); + canvas.addEventListener("mousemove", function(e) { + mousex = e.clientX; + mousey = e.clientY; + exports.game_update(INPUT_MOVE, mousex, mousey, now()); + }); + canvas.addEventListener("mousedown", function(e) { - const mousex = e.clientX; - const mousey = e.clientY; + mousex = e.clientX; + mousey = e.clientY; if (e.button == 0) { exports.game_update(INPUT_CLICK, mousex, mousey, now()); } else if (e.button == 2) { @@ -115,14 +153,16 @@ async function main() { document.addEventListener("keydown", function (e) { if (e.key === " ") { - exports.game_update(INPUT_PAUSE_PLAY, 0, 0, now()); + exports.game_update(INPUT_PAUSE_PLAY, mousex, mousey, now()); + } else if (e.key == "r") { + exports.game_update(INPUT_RESTART, mousex, mousey, now()); } }); function animate() { // TODO: stop requesting frames when state is static requestAnimationFrame(animate); - exports.game_update(INPUT_NONE, 0, 0, now()); + exports.game_update(INPUT_NONE, mousex, mousey, now()); render(); } requestAnimationFrame(animate); @@ -302,30 +302,39 @@ static void tick(Game *game, Arena a) { } break; case EMPTY: - // TODO: same as multiple reds + blues = 0; if ( x > 0 && lastState->grid[x - 1 + y * GRIDWIDTH] == BLUE_RIGHT ) { - game->state.grid[x + y * GRIDWIDTH] = BLUE_RIGHT; + blues++; + blue = BLUE_RIGHT; } if ( x < GRIDWIDTH - 1 && lastState->grid[x + 1 + y * GRIDWIDTH] == BLUE_LEFT ) { - game->state.grid[x + y * GRIDWIDTH] = BLUE_LEFT; + blues++; + blue = BLUE_LEFT; } if ( y > 0 && lastState->grid[x + (y - 1) * GRIDWIDTH] == BLUE_DOWN ) { - game->state.grid[x + y * GRIDWIDTH] = BLUE_DOWN; + blues++; + blue = BLUE_DOWN; } if ( y < GRIDHEIGHT - 1 && lastState->grid[x + (y + 1) * GRIDWIDTH] == BLUE_UP ) { - game->state.grid[x + y * GRIDWIDTH] = BLUE_UP; + blues++; + blue = BLUE_UP; + } + if (blues == 1) { + game->state.grid[x + y * GRIDWIDTH] = blue; + } else if (blues >= 2) { + game->state.grid[x + y * GRIDWIDTH] = BLUE; } break; } diff --git a/src/types.c b/src/types.c index bd6b858..bee6b05 100644 --- a/src/types.c +++ b/src/types.c @@ -68,6 +68,7 @@ enum { typedef struct { int width, height; + int mousex, mousey; } UI; typedef enum { @@ -102,6 +103,7 @@ enum { INPUT_CLICK, INPUT_RCLICK, INPUT_PAUSE_PLAY, + INPUT_MOVE, INPUT_RESTART, }; @@ -109,7 +111,6 @@ typedef struct { State state; UI ui; int input; - int mousex, mousey; } Game; #define new(a, c, t) ((t *) alloc(a, c, sizeof(t), _Alignof(t))) |