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))) | 
