#ifndef INCLUDE_ALL_C #define INCLUDE_ALL_C #include #include #include "types.c" #include "levels.c" #include "tick.c" static const Color colors[N_COLORS][4] = { { {255, 255, 255, 255}, {255, 255, 255, 255}, {255, 255, 255, 255}, {255, 255, 255, 255}, }, { {0, 0, 0, 255}, {0, 0, 0, 255}, {0, 0, 0, 255}, {0, 0, 0, 255}, }, { {255, 0, 0, 255}, {255, 0, 0, 255}, {255, 0, 0, 255}, {255, 0, 0, 255}, }, { {255, 255, 0, 255}, {255, 255, 0, 255}, {255, 255, 0, 255}, {255, 255, 0, 255}, }, { {255, 0, 0, 255}, {255, 0, 0, 255}, {255, 255, 0, 255}, {255, 255, 0, 255}, }, { {255, 255, 0, 255}, {255, 255, 0, 255}, {255, 0, 0, 255}, {255, 0, 0, 255}, }, { {255, 0, 0, 255}, {255, 255, 0, 255}, {255, 0, 0, 255}, {255, 255, 0, 255}, }, { {255, 255, 0, 255}, {255, 0, 0, 255}, {255, 255, 0, 255}, {255, 0, 0, 255}, }, { {0, 0, 255, 255}, {0, 0, 255, 255}, {0, 0, 255, 255}, {0, 0, 255, 255}, }, { {0, 0, 255, 255}, {0, 0, 255, 255}, {255, 255, 255, 255}, {255, 255, 255, 255}, }, { {255, 255, 255, 255}, {255, 255, 255, 255}, {0, 0, 255, 255}, {0, 0, 255, 255}, }, { {0, 0, 255, 255}, {255, 255, 255, 255}, {0, 0, 255, 255}, {255, 255, 255, 255}, }, { {255, 255, 255, 255}, {0, 0, 255, 255}, {255, 255, 255, 255}, {0, 0, 255, 255}, }, }; #define BUTTON_SIZE 32 #define BUTTON_SPACING 16 #define BUTTON_SPACER(i) (BUTTON_SPACING + BUTTON_SIZE + (BUTTON_SIZE * (i) + (i) * BUTTON_SPACING)) // reverse order as they're anchored to bottom static const Button buttons[N_BUTTONS] = { [BUTTON_BACK] = {.x = BUTTON_SPACING, .y = BUTTON_SPACER(BUTTON_BACK), .w = BUTTON_SIZE, .h = BUTTON_SIZE}, [BUTTON_RETRY] = {.x = BUTTON_SPACING, .y = BUTTON_SPACER(BUTTON_RETRY), .w = BUTTON_SIZE, .h = BUTTON_SIZE}, [BUTTON_PLAY] = {.x = BUTTON_SPACING, .y = BUTTON_SPACER(BUTTON_PLAY), .w = BUTTON_SIZE, .h = BUTTON_SIZE}, [BUTTON_CONTINUE] = {.x = BUTTON_SPACING, .y = BUTTON_SPACER(BUTTON_CONTINUE), .w = BUTTON_SIZE, .h = BUTTON_SIZE}, }; // This returns a positive color index or a negated image index static int getPlaceAction(int currentColor, int cellx, int celly, float cellWidth, float cellHeight) { int hoverColor = EMPTY; // TODO yellow when there are no reds available 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); float cellWidth = (float) (ui->width - GRID_OFFSET_X) / GRIDWIDTH; float cellHeight = (float) ui->height / GRIDHEIGHT; // Actual cells for (int x = 0; x < GRIDWIDTH; x++) { for (int y = 0; y < GRIDHEIGHT; y++) { if (colorImages[(int) state->grid[x + GRIDWIDTH * y]]) { drawList->els[drawList->len++] = (DrawElement) { .x = cellWidth * x + GRID_OFFSET_X, .y = cellHeight * y, .w = cellWidth, .h = cellHeight, .image = (ImageRef) { .index = colorImages[(int) state->grid[x + GRIDWIDTH * y]], .opacity = 255, }, }; } else { 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 + (1 - subx), .h = cellHeight / 2 + (1 - suby), .fill = colors[(int) state->grid[x + GRIDWIDTH * y]][subx + 2 * suby], }; } } } } } // 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}, }; } // Spawner if (state->spawnerx != 0 && state->spawnery != 0) { drawList->els[drawList->len++] = (DrawElement) { .x = cellWidth * state->spawnerx + GRID_OFFSET_X, .y = cellHeight * state->spawnery, .w = cellWidth, .h = cellHeight, .fill = {0, 0, 255, 63}, .border = {0, 0, 255, 255}, }; } // Goal if (state->levelComplete) { drawList->els[drawList->len++] = (DrawElement) { .x = cellWidth * state->goalx + GRID_OFFSET_X, .y = cellHeight * state->goaly, .w = cellWidth, .h = cellHeight, .fill = {0, 255, 0, 63}, .border = {0, 255, 0, 255}, }; } else { drawList->els[drawList->len++] = (DrawElement) { .x = cellWidth * state->goalx + GRID_OFFSET_X, .y = cellHeight * state->goaly, .w = cellWidth, .h = cellHeight, .fill = {255, 0, 0, 63}, .border = {255, 0, 0, 255}, }; if (state->need9) { for (int i = 0; i < state->blues; i++) { drawList->els[drawList->len++] = (DrawElement) { .x = cellWidth * state->goalx + GRID_OFFSET_X + cellWidth / 3 * (i % 3), .y = cellHeight * state->goaly + cellHeight / 3 * (i / 3), .w = cellWidth / 3, .h = cellHeight / 3, .fill = {196, 255, 196, 255}, .border = {0, 200, 0, 255}, }; } } } // 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) { for (int i = 0; i < MAX_PLACEABLE_CELLS; i++) { if (state->placeableCells[i] == hoverColor) { if (colorImages[hoverColor]) { drawList->els[drawList->len++] = (DrawElement) { .x = cellWidth * hoverx + GRID_OFFSET_X, .y = cellHeight * hovery, .w = cellWidth, .h = cellHeight, .image = { .index = colorImages[hoverColor], .opacity = 127, }, }; } else { 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, }; } } } break; } } } } // Side panel drawList->els[drawList->len++] = (DrawElement) { .x = 0, .y = 0, .w = GRID_OFFSET_X, .h = ui->height, .fill = {100, 100, 100, 255}, .border = {0, 0, 0, 255}, }; // render buttons for (int i = 0; i < N_BUTTONS; i++) { Color colour = {0, 0, 0, 255}; switch (state->buttonStates[i]) { case BUTTON_STATE_IDLE: colour = (Color) {0, 0, 0, 255}; break; case BUTTON_STATE_HOVERED: colour = (Color) {255, 255, 0, 255}; break; case BUTTON_STATE_PRESSED: colour = (Color) {255, 0, 0, 255}; break; } int image = IMAGE_NULL; switch (i) { case BUTTON_CONTINUE: image = IMAGE_CONTINUE; break; case BUTTON_BACK: image = IMAGE_EXIT; break; case BUTTON_RETRY: image = IMAGE_RETRY; break; case BUTTON_PLAY: image = state->playing ? IMAGE_PAUSE : IMAGE_PLAY; break; } Color fill = {0}; if (i == BUTTON_CONTINUE && state->levelComplete) { fill = (Color) {127, 255, 127, 200}; }; drawList->els[drawList->len++] = (DrawElement) { .x = buttons[i].x, .y = ui->height - buttons[i].y, .w = buttons[i].w, .h = buttons[i].h, .fill = fill, .image = (ImageRef) { .index = image, .opacity = 255, }, }; } // Placeable cells for (int i = 0; i < MAX_PLACEABLE_CELLS; i++) { if (state->placeableCells[i] == EMPTY) { break; } if (colorImages[(int) state->placeableCells[i]]) { drawList->els[drawList->len++] = (DrawElement) { .x = BUTTON_SIZE / 2, .y = BUTTON_SIZE / 2 + i * (BUTTON_SIZE + BUTTON_SPACING), .w = BUTTON_SIZE, .h = BUTTON_SIZE, .image = { .index = colorImages[(int) state->placeableCells[i]], .opacity = 255, }, }; } else { for (int x = 0; x < 2; x++) { for (int y = 0; y < 2; y++) { drawList->els[drawList->len++] = (DrawElement) { .x = BUTTON_SIZE / 2 + x * (BUTTON_SIZE / 2), .y = BUTTON_SIZE / 2 + i * (BUTTON_SIZE + BUTTON_SPACING) + y * (BUTTON_SIZE / 2), .w = BUTTON_SIZE / 2, .h = BUTTON_SIZE / 2, .fill = colors[(int) state->placeableCells[i]][x + 2 * y], }; } } } } return drawList; } static void restart_level(Game* game) { const int level = game->state.currentLevel; xmemcpy(&game->state.grid, &levels[level].grid, sizeof(game->state.grid)); game->state.goalx = levels[level].goalx; game->state.goaly = levels[level].goaly; game->state.spawnerx = levels[level].spawnerx; game->state.spawnery = levels[level].spawnery; game->state.spawnerCount = 0; game->state.playing = 0; game->state.blues = 0; game->state.need9 = levels[level].need9; xmemcpy(&game->state.placeableCells, &levels[level].placeableCells, sizeof(game->state.placeableCells)); } static int update(Game *game, uint64_t now, Arena a) { const int offset_width = game->ui.width - GRID_OFFSET_X; int tutorial = 0; switch (game->input) { int x, y; case INPUT_CLICK: 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])); char placed = getPlaceAction( game->state.grid[x + y * GRIDWIDTH], cellx, celly, cellWidth, cellHeight ); if (placed != EMPTY) { for (int i = 0; i < MAX_PLACEABLE_CELLS; i++) { if (game->state.placeableCells[i] == placed) { game->state.grid[x + y * GRIDWIDTH] = placed; for (int j = i; j < MAX_PLACEABLE_CELLS - 1; j++) { game->state.placeableCells[j] = game->state.placeableCells[j + 1]; } game->state.placeableCells[MAX_PLACEABLE_CELLS - 1] = EMPTY; break; } } } } for (int i = 0; i < N_BUTTONS; i++) { if ( 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; switch (i) { case BUTTON_RETRY: restart_level(game); break; case BUTTON_CONTINUE: if ( !game->state.levelComplete || game->state.currentLevel + 1 >= sizeof(levels) / sizeof(levels[0]) ) { break; } game->state.levelComplete = 0; game->state.currentLevel++; restart_level(game); break; case BUTTON_BACK: tutorial = 1; break; case BUTTON_PLAY: game->state.playing = !game->state.playing; break; } } } break; case INPUT_RCLICK: if (game->ui.mousex > GRID_OFFSET_X) { x = (game->ui.mousex - GRID_OFFSET_X) * GRIDWIDTH / (game->ui.width - GRID_OFFSET_X); y = game->ui.mousey * GRIDHEIGHT / game->ui.height; if (game->state.grid[x + y * GRIDWIDTH] != BLACK) { break; } for (int i = 0; i < MAX_PLACEABLE_CELLS; i++) { if (game->state.placeableCells[i] == YELLOW) { game->state.grid[x + y * GRIDWIDTH] = YELLOW; for (int j = i; j < MAX_PLACEABLE_CELLS - 1; j++) { game->state.placeableCells[j] = game->state.placeableCells[j + 1]; } game->state.placeableCells[MAX_PLACEABLE_CELLS - 1] = EMPTY; break; } } } break; case INPUT_PAUSE_PLAY: game->state.playing = !game->state.playing; break; case INPUT_RESTART: restart_level(game); break; default: break; } if (game->state.playing && game->state.lastTick + TICK_LENGTH <= now) { game->state.lastTick = now; tick(game, a); } return tutorial; } #if SDL #include typedef struct { int width, height; Color colors[16]; const char *pixels; } Image; #include "../build/images.c" 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; char *mem = SDL_malloc(MEM_SIZE); Arena a = { .start = mem, .end = mem + MEM_SIZE, }; Game *game = new(&a, 1, Game); game->state.currentLevel = 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_AUDIO); SDL_Window *w = SDL_CreateWindow( "LDJam 57", game->ui.width, game->ui.height, SDL_WINDOW_RESIZABLE ); SDL_PropertiesID renderProps = SDL_CreateProperties(); SDL_SetPointerProperty(renderProps, SDL_PROP_RENDERER_CREATE_WINDOW_POINTER, w); SDL_SetNumberProperty(renderProps, SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_NUMBER, 1); SDL_Renderer *r = SDL_CreateRendererWithProperties(renderProps); SDL_DestroyProperties(renderProps); SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND); for (int j = 1; j < (int) (sizeof(images) / sizeof(images[0])); j++) { textures[j] = SDL_CreateTexture( r, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STATIC, images[j].width, images[j].height ); SDL_SetTextureScaleMode(textures[j], SDL_SCALEMODE_NEAREST); Arena scratch = a; Color *pixels = new(&scratch, images[j].width * images[j].height, Color); for (int i = 0; i < images[j].width * images[j].height; i++) { pixels[i] = images[j].colors[(int) images[j].pixels[i]]; } 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(); game->input = INPUT_NONE; SDL_Event e = {0}; while (SDL_PollEvent(&e)) { switch (e.type) { case SDL_EVENT_QUIT: return 0; case SDL_EVENT_MOUSE_BUTTON_DOWN: 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: game->input = INPUT_PAUSE_PLAY; break; case SDLK_R: game->input = INPUT_RESTART; break; } break; case SDL_EVENT_WINDOW_RESIZED: SDL_GetWindowSize(w, &game->ui.width, &game->ui.height); break; } } Arena scratch = a; DrawList *drawList = render(&game->state, &game->ui, &scratch); SDL_SetRenderDrawColor(r, 0, 0, 0, 255); SDL_RenderFillRect(r, NULL); for (int i = 0; i < drawList->len; i++) { SDL_FRect rect = { .x = (float) drawList->els[i].x, .y = (float) drawList->els[i].y, .w = (float) drawList->els[i].w, .h = (float) drawList->els[i].h }; SDL_SetRenderDrawColor( r, drawList->els[i].fill.r, drawList->els[i].fill.g, drawList->els[i].fill.b, drawList->els[i].fill.a ); SDL_RenderFillRect(r, &rect); SDL_SetRenderDrawColor( r, drawList->els[i].border.r, drawList->els[i].border.g, drawList->els[i].border.b, drawList->els[i].border.a ); SDL_RenderRect(r, &rect); if (drawList->els[i].image.index != 0) { SDL_SetTextureAlphaMod(textures[(int) drawList->els[i].image.index], drawList->els[i].image.opacity); SDL_RenderTexture(r, textures[(int) drawList->els[i].image.index], NULL, &rect); } } update(game, now, a); if (SDL_GetAudioStreamQueued(stream) < (int) sizeof(musicBytes) / 8) { SDL_PutAudioStreamData(stream, musicBytes, sizeof(musicBytes)); } SDL_RenderPresent(r); } return 0; } #elif WASM static Game *game; static Arena perm; __attribute((export_name("game_init"))) void game_init(void) { static char heap[MEM_SIZE]; perm.start = heap; perm.end = heap + MEM_SIZE; game = new(&perm, 1, Game); game->state.currentLevel = 0; restart_level(game); for (int i = 0; i < N_BUTTONS; i++) { game->state.buttonStates[i] = BUTTON_STATE_IDLE; } } __attribute((export_name("game_render"))) 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); } __attribute((export_name("game_update"))) int game_update(int input, int mousex, int mousey, int now) { game->input = input; game->ui.mousex = mousex; game->ui.mousey = mousey; return update(game, now, perm); } #endif #endif