├── README.md ├── assets ├── alagard.ttf ├── proj.png └── sounds │ ├── bullet_fire.ogg │ ├── fire.ogg │ ├── flamethrower.ogg │ ├── foot_steps.ogg │ ├── fright_bg.ogg │ ├── frost_wave.mp3 │ ├── health_pickup.wav │ ├── heartbeat.wav │ ├── hurt.wav │ ├── orb.wav │ ├── splinter.wav │ └── upgrade.wav ├── game.c ├── game.h └── z_build.sh /README.md: -------------------------------------------------------------------------------- 1 | # Blob Survival 2 | A small chaotic vampire-survivors inspired game written in C and raylib. It uses a quad-tree for effecient collisions and is capable of handling thousands of enemies. 3 | 4 | You can play a wasm build of the game here - [https://bones-ai.itch.io/blob-survival](https://bones-ai.itch.io/blob-survival) 5 | 6 | ![untitled2(3)](https://github.com/user-attachments/assets/417b7127-6406-4429-8f7a-973c52bdc935) 7 | 8 | 9 | # Small demo on YT 10 | [![youtube](https://img.youtube.com/vi/kHefr2VUDvw/0.jpg)](https://youtu.be/kHefr2VUDvw) 11 | 12 | 13 | # Read this 14 | - You won't readily be able to run the code because I'm not allowed to redistribute some of the assets used in the game, i've put in some placeholder assets for you to replace. 15 | - The entire game is within a single `game.c` file, I use `:` tags to quickly jump between different sections, I find this to be more effective than splitting it into separate files, and there's also some `// MARK: ` that helps if you're using vscode 16 | - This is the trimmed down version of readme file I used to document my todo and other things while building the game 17 | - Refer to the end of the file for credits and links to assets and other resources I used 18 | - Use the `z_build.sh` to run the game 19 | 20 | # Done 21 | - clean up the heartbeat audio noise 22 | - reduce the spawn duration 23 | - have a internal volume thingy that allows for the heartbeat to be heard easily over all other sounds 24 | - hurt sounds for enemy bullets 25 | - hurt sound effect 26 | - heart pickup sound effect 27 | - reduce the number of heart pickups 28 | - disable the escape key behaviour on release builds 29 | - bug: spike toast always says unlocked, flame always upgraded 30 | - heart beat music when the health drops below a certain value 31 | - Clean up old enemies that are far behind 32 | - Update music for frost wave 33 | - Music/Sounds 2, on upgrade, 34 | - splinter new music 35 | - frost wave 36 | - orbs 37 | - upgrade 38 | - Make the main menu fancy? 39 | - Pause menu with stats and other stuff and redirect to shop menu? 40 | - More upgrades like pickup range etc. ? 41 | - UI for when a weapon will shoot (-) 42 | - Lazer attack (-) 43 | - some simple analytics for when someone plays the game? 44 | - Adjust the number of particles for the area attacks based on upgrades 45 | - Different shiny enemies (-) 46 | - Enemy drop different value of mana (-) 47 | - More pickup types 48 | - Some way to clean up the pickups that are too old 49 | - Make enemy bullets collide with player 50 | - Color pallete change? 51 | - Player takes damage 52 | - Make pickup range a circle 53 | - Upgrades post a point will be random 54 | - Volume controls in the pause menu 55 | - Settings menu with volume controls 56 | - Global gamestate 57 | - Save system (-) 58 | - Change the clock to a distance based thing (-) 59 | - Toast system 60 | - Main menu 61 | - New upgrade system 62 | - TOWN (no town for now) 63 | - (didn't think i need this) Make gamestate init happen on game start, and have a pregamestate for before the game starts? 64 | - Music/Sounds 65 | - Refactor 66 | - Camera system 67 | - Fix enemy spawning around player 68 | - Quadtree for collisions 69 | - Single file arch 70 | - Fixed map size, clamp player within map 71 | - Random map decorations 72 | - Custom font 73 | - Draw map borders 74 | - Simple enemy rampup system 75 | - Pixelate everything 76 | - Letter boxing ie black padding and resize handling 77 | - Virtual joystick, mobile and touch controls 78 | - Decorations again 79 | - Shiny enemies 80 | - Different attack types 81 | - Enemy drop loot 82 | - Track picked up mana and display a progress bar that fills up 83 | - Level up as you collect mana, each level up needs higher mana 84 | - Upgrade system on each level up 85 | - Particle system 86 | - Animations? (like collecting pickups) 87 | - Sprite x flip and sprite bob 88 | - Frost wave and slowing enemies down 89 | - Enemy variants, Enemies with better health/values 90 | - Unique enemy behaviours 91 | - slime - one shotable, def speed, nothing special, default spawn 92 | - deer - fast, low health, charge at you?, 0.2 prob @ 1-2 mins? 93 | - emu - med speed, fires bullets, med health, 0.2 prob @ 5 mins 94 | - dino - spawns pups, fires bullets, 0.1 prob @ 10 mins, max of 5 can exist 95 | 96 | # Optimisations 97 | - Clean up enemies that are too far away 98 | - Stop updating enemies out of the screen? 99 | - Don't boid enemies outside the screen 100 | - Music files in ogg have lower size, idk if converting everything to ogg is better 101 | 102 | # Other things 103 | - learn lighting? 104 | - Blood splats 105 | - Fog for far away enemies? 106 | - Make boid separation based on a value for each enemy, shiny and important enemies should be prioritized? 107 | 108 | # Crashes 109 | - When you have a lot of enemies, we get this crash, this should technically not happen since the player can't be alive for that long ??? 110 | - To replicate the crash, spawn a lot of enemies and stand inplace 111 | - This happens when the qtree is reset with points 112 | - Potential cause - the quadtree is oversplit at the player pos to the point that the boundary becomes invalid? idk 113 | ``` 114 | Process 18590 stopped 115 | * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=2, address=0x16f603ff8) 116 | frame #0: 0x000000010000373c game_debug`is_rect_contains_point(rect=(x = , y = , w = 0, h = 0), pt=(x = , y = , id = )) at game.c:876 117 | 873 118 | 874 // MARK: :utils 119 | 875 120 | -> 876 bool is_rect_contains_point(QRect rect, QPoint pt) { 121 | 877 return (pt.x >= rect.x - rect.w && pt.x <= rect.x + rect.w) && 122 | 878 (pt.y >= rect.y - rect.h && pt.y <= rect.y + rect.h); 123 | 879 } 124 | Target 0: (game_debug) stopped 125 | ``` 126 | - The pickups qtree range is 32k, anything beyond that wont be recognized 127 | 128 | # Ideas 129 | - For new attacks 130 | - Orbs (blobs of light that scatter around randomly) 131 | - Rockets - just fire random bullets that do area damage 132 | - Lasers 133 | - Bomb, drop inplace and it blows up after awhile 134 | - Meteors 135 | - Yoyo like attack? spawns and comes back to the player 136 | - A giant ball stuck to player with a chain 137 | - Fire trail? or trails in general 138 | - Utility types 139 | - Shield, Invinsibility, 140 | 141 | # Links 142 | - Raylib wasm build - https://www.youtube.com/watch?v=j6akryezlzc 143 | - emcc to run at 120fps, from karl odin raylib wasm template - https://github.com/karl-zylinski/odin-raylib-web 144 | - Lospec pallete - https://lospec.com/palette-list/laser-lab 145 | - new color pallete - https://lospec.com/palette-list/joker-6 146 | - Quadtree CodingTrain - https://www.youtube.com/watch?v=OJxEcs0w_kE, code - https://editor.p5js.org/codingtrain/sketches/g7LnWQ42x 147 | - Alagard font by Hewett Tsoi - https://www.dafont.com/alagard.font 148 | - Boid separation - https://www.youtube.com/watch?v=mhjuuHl6qHM 149 | - Pixel camera - https://github.com/raysan5/raylib/blob/master/examples/core/core_smooth_pixelperfect.c, demo - https://www.raylib.com/examples/core/loader.html?name=core_smooth_pixelperfect 150 | - Raylib letterboxing (black bars around the viewport when window is resized) - https://github.com/raysan5/raylib/blob/master/examples/core/core_window_letterbox.c, demo - https://www.raylib.com/examples/core/loader.html?name=core_window_letterbox 151 | - Enemy assets - https://cmski.itch.io/tabletop-armies-asset-pack 152 | - Grass & Stone assets - https://egordorichev.itch.io/toi 153 | - Player assets - https://ibirothe.itch.io/roguelike1bit16x16assetpack 154 | - bg music - https://makotohiramatsu.itch.io/north-sea 155 | - impact sounds - https://kenney.nl/assets/impact-sounds 156 | - sounds - https://opengameart.org/content/fire-crackling, https://opengameart.org/content/catching-fire, https://pixabay.com/sound-effects/flamethrower-19895/ 157 | - heartbeat - https://pixabay.com/sound-effects/heartbeat-loud-242421/, https://pixabay.com/sound-effects/heartbeat-loop-96879/ 158 | - buildings in town - https://iknowkingrabbit.itch.io/rts-micro-asset 159 | - random sprites from urizen one bit sprite sheet - https://vurmux.itch.io/urizen-onebit-tileset 160 | - more sound effects - https://brainzplayz.itch.io/retro-sounds-32-bit 161 | - more sound effects - https://lmglolo.itch.io/free-fps-sfx 162 | - more sound fx - https://kronbits.itch.io/freesfx 163 | - more sound fx - https://pixabay.com/sound-effects/short-fireball-woosh-6146/ 164 | - heart beat fx - https://pixabay.com/sound-effects/heartbeat-loud-242421/ 165 | - brackeys-platformer-bundle - https://brackeysgames.itch.io/brackeys-platformer-bundle 166 | -------------------------------------------------------------------------------- /assets/alagard.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/alagard.ttf -------------------------------------------------------------------------------- /assets/proj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/proj.png -------------------------------------------------------------------------------- /assets/sounds/bullet_fire.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/bullet_fire.ogg -------------------------------------------------------------------------------- /assets/sounds/fire.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/fire.ogg -------------------------------------------------------------------------------- /assets/sounds/flamethrower.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/flamethrower.ogg -------------------------------------------------------------------------------- /assets/sounds/foot_steps.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/foot_steps.ogg -------------------------------------------------------------------------------- /assets/sounds/fright_bg.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/fright_bg.ogg -------------------------------------------------------------------------------- /assets/sounds/frost_wave.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/frost_wave.mp3 -------------------------------------------------------------------------------- /assets/sounds/health_pickup.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/health_pickup.wav -------------------------------------------------------------------------------- /assets/sounds/heartbeat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/heartbeat.wav -------------------------------------------------------------------------------- /assets/sounds/hurt.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/hurt.wav -------------------------------------------------------------------------------- /assets/sounds/orb.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/orb.wav -------------------------------------------------------------------------------- /assets/sounds/splinter.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/splinter.wav -------------------------------------------------------------------------------- /assets/sounds/upgrade.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones-ai/c-blob-survival/f642d9f4b09dfc41ada9e019700079b5099da6e8/assets/sounds/upgrade.wav -------------------------------------------------------------------------------- /game.c: -------------------------------------------------------------------------------- 1 | #include "raylib.h" 2 | #include "raymath.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #ifdef WASM 12 | #include 13 | #else 14 | #include 15 | #include 16 | #endif 17 | 18 | #include "game.h" 19 | 20 | // global state 21 | // most funcs assume state pointer is valid when accessed 22 | // init properly before use 23 | GameState *state = NULL; 24 | 25 | // MARK: :init :malloc 26 | 27 | void gamestate_create() { 28 | state = malloc(sizeof(GameState)); 29 | if (!state) { 30 | printe("Error malloc game state"); 31 | return; 32 | } 33 | 34 | Vec2 world_center = (Vec2) { 35 | (WORLD_W * TILE_SIZE * DEFAULT_SPRITE_SCALE) / 2.0f, 36 | (WORLD_H * TILE_SIZE * DEFAULT_SPRITE_SCALE) / 2.0f 37 | }; 38 | 39 | // Pixel camera setup 40 | float virtual_ratio = (float) SCREEN_WIDTH / (float) VIRT_WIDTH; 41 | 42 | *state = (GameState) { 43 | .camera = { 44 | .target = (Vec2) { VIRT_WIDTH/2.0f, VIRT_HEIGHT/2.0f }, 45 | .offset = (Vec2) { VIRT_WIDTH/2.0f, VIRT_HEIGHT/2.0f }, 46 | .rotation = 0.0f, 47 | .zoom = 1.0f 48 | }, 49 | .sprite_sheet = LoadTexture(SPRITE_SHEET_PATH), 50 | .custom_font = LoadFontEx(CUSTOM_FONT_PATH, 64 * 2, 0, 250), 51 | .render_texture = LoadRenderTexture(VIRT_WIDTH, VIRT_HEIGHT), 52 | .source_rect = (Rect) { 53 | 0.0f, 0.0f, 54 | (float) VIRT_WIDTH, -(float) VIRT_HEIGHT 55 | }, 56 | .dest_rect = (Rect) { 57 | -virtual_ratio, -virtual_ratio, 58 | SCREEN_WIDTH + (virtual_ratio*2), SCREEN_HEIGHT + (virtual_ratio*2) 59 | }, 60 | 61 | // Sound 62 | .sound_intensity = 1.0, 63 | 64 | // General 65 | .screen = MAIN_MENU, 66 | .timer = (Timers) { 67 | .game_start_ts = get_current_time_millis(), 68 | .pause_ts = 0, 69 | .pickups_cleanup_ts = 0, 70 | .last_hurt_sound_ts = 0, 71 | .last_heart_spawn_ts = 0, 72 | 73 | .bullet_ts = 0, 74 | .splinter_ts = 0, 75 | .spike_ts = 0, 76 | .flame_ts = 0, 77 | .frost_wave_ts = 0, 78 | .orbs_ts = 0, 79 | 80 | .qtree_update_ts = 0, 81 | .enemy_spawn_ts = 0, 82 | .enemy_spawn_interval = DEFAULT_ENEMY_SPAWN_INTERVAL_MS, 83 | }, 84 | .stats = (Stats) { 85 | .bullet_interval = get_base_stat_value(BULLET_INTERVAL), 86 | .bullet_count = get_base_stat_value(BULLET_COUNT), 87 | .bullet_spread = get_base_stat_value(BULLET_SPREAD), 88 | .bullet_range = get_base_stat_value(BULLET_RANGE), 89 | .bullet_damage = get_base_stat_value(BULLET_DAMAGE), 90 | .bullet_penetration = get_base_stat_value(BULLET_PENETRATION), 91 | 92 | .splinter_interval = get_base_stat_value(SPLINTER_INTERVAL), 93 | .splinter_range = get_base_stat_value(SPLINTER_RANGE), 94 | .splinter_count = get_base_stat_value(SPLINTER_COUNT), 95 | .splinter_damage = get_base_stat_value(SPLINTER_DAMAGE), 96 | .splinter_penetration = get_base_stat_value(SPLINTER_PENETRATION), 97 | 98 | .spike_interval = get_base_stat_value(SPIKE_INTERVAL), 99 | .spike_speed = get_base_stat_value(SPIKE_SPEED), 100 | .spike_count = get_base_stat_value(SPIKE_COUNT), 101 | .spike_damage = get_base_stat_value(SPIKE_DAMAGE), 102 | 103 | .flame_interval = get_base_stat_value(FLAME_INTERVAL), 104 | .flame_lifetime = get_base_stat_value(FLAME_LIFETIME), 105 | .flame_spread = get_base_stat_value(FLAME_SPREAD), 106 | .flame_distance = get_base_stat_value(FLAME_DISTANCE), 107 | .flame_damage = get_base_stat_value(FLAME_DAMAGE), 108 | 109 | .frost_wave_interval = get_base_stat_value(FROST_WAVE_INTERVAL), 110 | .frost_wave_lifetime = get_base_stat_value(FROST_WAVE_LIFETIME), 111 | .frost_wave_damage = get_base_stat_value(FROST_WAVE_DAMAGE), 112 | 113 | .orbs_interval = get_base_stat_value(ORBS_INTERVAL), 114 | .orbs_damage = get_base_stat_value(ORBS_DAMAGE), 115 | .orbs_count = get_base_stat_value(ORBS_COUNT), 116 | .orbs_size = get_base_stat_value(ORBS_SIZE), 117 | 118 | .player_speed = get_base_stat_value(PLAYER_SPEED), 119 | .shiny_chance = get_base_stat_value(SHINY_CHANCE), 120 | }, 121 | .upgrades = (Upgrades) { 122 | .bullet_level = 1, 123 | .splinter_level = 0, 124 | .spike_level = GOD ? 1 : 0, 125 | .flame_level = 0, 126 | .frost_wave_level = 0, 127 | .orbs_level = 0, 128 | .speed_level = 0, 129 | .shiny_level = 0, 130 | }, 131 | .available_upgrades = (Upgrades) { 132 | .bullet_level = 0, 133 | .splinter_level = 0, 134 | .spike_level = 0, 135 | .flame_level = 0, 136 | .frost_wave_level = 0, 137 | .orbs_level = 0, 138 | .speed_level = 0, 139 | .shiny_level = 0, 140 | }, 141 | .toasts = (Toast*) malloc(MAX_NUM_TOASTS * sizeof(Toast)), 142 | .toasts_count = 0, 143 | .wave_index = 0, 144 | .player_level = 1, 145 | .kill_count = 0, 146 | .is_fullscreen = false, 147 | 148 | // Player 149 | .player_pos = world_center, 150 | .player_health = MAX_PLAYER_HEALTH, 151 | .player_heading_dir = (Vec2) { 1, 0 }, 152 | .is_joystick_enabled = false, 153 | .touch_down_pos = Vector2Zero(), 154 | .joystick_direction = Vector2Zero(), 155 | .mana_count = 0, 156 | .mana_particles = (Vec2*) malloc(MAX_MANA_PARTICLES * sizeof(Vec2)), 157 | .mana_particles_count = 0, 158 | 159 | // Weapon 160 | .bullets = (Bullet*) malloc(MAX_BULLETS * sizeof(Bullet)), 161 | .bullet_count = 0, 162 | .enemy_bullets = (Bullet*) malloc(MAX_ENEMY_BULLETS * sizeof(Bullet)), 163 | .enemy_bullet_count = 0, 164 | .flame_particles = (Particle*) malloc(MAX_FLAME_PARTICLES * sizeof(Particle)), 165 | .flame_particles_count = 0, 166 | .frost_wave_particles = (Particle*) malloc(MAX_FROST_WAVE_PARTICLES * sizeof(Particle)), 167 | 168 | // Enemy 169 | .enemies = (Enemy*) malloc(MAX_ENEMIES * sizeof(Enemy)), 170 | .enemy_count = 0, 171 | .enemy_qtree = qtree_create(get_visible_rect(world_center, 1.0)), 172 | .num_enemies_per_tick = 1, 173 | 174 | // World 175 | .world_dims = (Rect) { 176 | 0, 0, 177 | WORLD_W * TILE_SIZE * DEFAULT_SPRITE_SCALE, 178 | WORLD_H * TILE_SIZE * DEFAULT_SPRITE_SCALE 179 | }, 180 | .decorations = (Decoration*) malloc(NUM_DECORATIONS * sizeof(Decoration)), 181 | .pickups = (Pickup*) malloc(MAX_PICKUPS * sizeof(Pickup)), 182 | .pickups_count = 0, 183 | .pickups_qtree = qtree_create( 184 | (QRect) { 185 | world_center.x, world_center.y, 186 | 32000, 32000 187 | } 188 | ), 189 | 190 | // UI 191 | .master_volume = 1.0f, 192 | .is_master_volume_dragging = false, 193 | .main_menu_enemies = (Enemy*) malloc(MAX_MAIN_MENU_ENEMIES * sizeof(Enemy)), 194 | .main_menu_enemies_count = 0, 195 | 196 | // Others 197 | .query_points = malloc(MAX_ENEMIES * sizeof(QPoint)), 198 | .num_query_points = 0, 199 | 200 | .is_show_debug_gui = false, 201 | .temp = (Temp) { 202 | .ct = 0, 203 | .num_enemies_drawn = 0 204 | }, 205 | }; 206 | 207 | if (!state->bullets || !state->enemies || !state->query_points || !state->enemy_qtree) { 208 | free(state->bullets); 209 | free(state->query_points); 210 | free(state->enemies); 211 | qtree_destroy(state->enemy_qtree); 212 | free(state); 213 | 214 | printe("Error malloc game state components"); 215 | return; 216 | } 217 | 218 | // idk if I need this 219 | SetTextureFilter(state->render_texture.texture, TEXTURE_FILTER_POINT); 220 | 221 | reset_main_menu_enemies(); 222 | handle_window_resize(); 223 | sounds_init(); 224 | decorations_init(); 225 | post_analytics_async(GAME_OPEN); 226 | } 227 | 228 | // :deinit :destroy 229 | void gamestate_destroy() { 230 | if (state == NULL) { 231 | printe("state empty"); 232 | return; 233 | } 234 | 235 | free(state->toasts); 236 | state->toasts_count = 0; 237 | 238 | free(state->mana_particles); 239 | state->mana_particles_count = 0; 240 | 241 | free(state->bullets); 242 | state->bullet_count = 0; 243 | free(state->enemy_bullets); 244 | state->enemy_bullet_count = 0; 245 | free(state->flame_particles); 246 | state->flame_particles_count = 0; 247 | free(state->frost_wave_particles); 248 | state->frost_wave_particles_count = 0; 249 | 250 | free(state->enemies); 251 | state->enemy_count = 0; 252 | qtree_destroy(state->enemy_qtree); 253 | 254 | free(state->decorations); 255 | free(state->pickups); 256 | state->pickups_count = 0; 257 | qtree_destroy(state->pickups_qtree); 258 | 259 | free(state->main_menu_enemies); 260 | state->main_menu_enemies_count = 0; 261 | 262 | free(state->query_points); 263 | state->num_query_points = 0; 264 | 265 | UnloadTexture(state->sprite_sheet); 266 | UnloadRenderTexture(state->render_texture); 267 | UnloadFont(state->custom_font); 268 | 269 | sounds_destroy(); 270 | 271 | free(state); 272 | state = NULL; 273 | } 274 | 275 | // MARK: :pause :resume :timer 276 | 277 | void pause_timers() { 278 | state->timer.pause_ts = get_current_time_millis(); 279 | } 280 | 281 | void resume_timers() { 282 | int delta = get_current_time_millis() - state->timer.pause_ts; 283 | state->timer.pause_ts = 0; 284 | state->timer.game_start_ts += delta; 285 | state->timer.pickups_cleanup_ts += delta; 286 | state->timer.last_hurt_sound_ts += delta; 287 | 288 | state->timer.bullet_ts += delta; 289 | state->timer.splinter_ts += delta; 290 | state->timer.spike_ts += delta; 291 | state->timer.flame_ts += delta; 292 | state->timer.frost_wave_ts += delta; 293 | state->timer.orbs_ts += delta; 294 | 295 | state->timer.qtree_update_ts += delta; 296 | state->timer.enemy_spawn_ts += delta; 297 | } 298 | 299 | // MARK: :render :draw 300 | 301 | void draw_game() { 302 | ClearBackground(BLACK); 303 | 304 | // game draw 305 | BeginTextureMode(state->render_texture); 306 | { 307 | ClearBackground(COLOR_DARK_BLUE); 308 | BeginMode2D(state->camera); 309 | { 310 | draw_decorations(); 311 | draw_pickups(); 312 | draw_enemies(); 313 | draw_particles(); 314 | draw_bullets(); 315 | draw_player(); 316 | } 317 | EndMode2D(); 318 | } 319 | EndTextureMode(); 320 | 321 | DrawTexturePro( 322 | state->render_texture.texture, 323 | state->source_rect, 324 | state->dest_rect, 325 | Vector2Zero(), 0.0f, WHITE 326 | ); 327 | 328 | draw_ui(); 329 | draw_toasts(); 330 | draw_upgrade_menu(); 331 | 332 | // Debug 333 | { 334 | float xpos = 10; 335 | float ypos = 200; 336 | float ypadding = 30; 337 | Color color = GRAY; 338 | int font_size = 20; 339 | if (state->is_show_debug_gui) { 340 | DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), ColorAlpha(BLACK, 0.2)); 341 | DrawTextEx( 342 | state->custom_font, 343 | TextFormat("Fps: %i", GetFPS()), 344 | (Vec2){ xpos, ypos }, font_size, 2, color 345 | ); 346 | ypos += ypadding; 347 | DrawTextEx( 348 | state->custom_font, 349 | TextFormat("#level: %i", state->player_level), 350 | (Vec2){ xpos, ypos }, font_size, 2, color 351 | ); 352 | ypos += ypadding; 353 | DrawTextEx( 354 | state->custom_font, 355 | TextFormat("#Enemies: %i", state->enemy_count), 356 | (Vec2){ xpos, ypos }, font_size, 2, color 357 | ); 358 | ypos += ypadding; 359 | DrawTextEx( 360 | state->custom_font, 361 | TextFormat("#Bullets: %i", state->bullet_count), 362 | (Vec2){ xpos, ypos }, font_size, 2, color 363 | ); 364 | ypos += ypadding; 365 | DrawTextEx( 366 | state->custom_font, 367 | TextFormat("ppos: %f, %f", state->player_pos.x, state->player_pos.y), 368 | (Vec2){ xpos, ypos }, font_size, 2, color 369 | ); 370 | ypos += ypadding; 371 | DrawTextEx( 372 | state->custom_font, 373 | TextFormat("Wave: %d", state->wave_index), 374 | (Vec2){ xpos, ypos }, font_size, 2, color 375 | ); 376 | ypos += ypadding; 377 | DrawTextEx( 378 | state->custom_font, 379 | TextFormat("#Enemies on screen: %d", state->temp.num_enemies_drawn), 380 | (Vec2){ xpos, ypos }, font_size, 2, color 381 | ); 382 | ypos += ypadding; 383 | DrawTextEx( 384 | state->custom_font, 385 | TextFormat("Spawn rate: %d", state->num_enemies_per_tick), 386 | (Vec2){ xpos, ypos }, font_size, 2, color 387 | ); 388 | ypos += ypadding; 389 | DrawTextEx( 390 | state->custom_font, 391 | TextFormat("#Pickups: %d", state->pickups_count), 392 | (Vec2){ xpos, ypos }, font_size, 2, color 393 | ); 394 | ypos += ypadding; 395 | DrawTextEx( 396 | state->custom_font, 397 | TextFormat("mana: %d", state->mana_count), 398 | (Vec2){ xpos, ypos }, font_size, 2, color 399 | ); 400 | ypos += ypadding; 401 | DrawTextEx( 402 | state->custom_font, 403 | TextFormat("kills: %ld", state->kill_count), 404 | (Vec2){ xpos, ypos }, font_size, 2, color 405 | ); 406 | ypos += ypadding; 407 | DrawTextEx( 408 | state->custom_font, 409 | TextFormat("#mana-particles: %d", state->mana_particles_count), 410 | (Vec2){ xpos, ypos }, font_size, 2, color 411 | ); 412 | ypos += ypadding; 413 | DrawTextEx( 414 | state->custom_font, 415 | TextFormat("#flame-particles: %d", state->flame_particles_count), 416 | (Vec2){ xpos, ypos }, font_size, 2, color 417 | ); 418 | ypos += ypadding; 419 | DrawTextEx( 420 | state->custom_font, 421 | TextFormat("health: %f", state->player_health), 422 | (Vec2){ xpos, ypos }, font_size, 2, color 423 | ); 424 | ypos += ypadding; 425 | } 426 | 427 | // show fps 428 | if (DEBUG) { 429 | DrawTextEx( 430 | state->custom_font, 431 | TextFormat("%d", GetFPS()), 432 | (Vec2) { GetScreenWidth() - 30, 4 }, 433 | 15, 2, ColorAlpha(COLOR_BROWN, 0.5) 434 | ); 435 | } 436 | } 437 | } 438 | 439 | void draw_player() { 440 | draw_sprite(&state->sprite_sheet, (Vec2) {0, 9}, state->player_pos, DEFAULT_SPRITE_SCALE, 0, WHITE); 441 | DrawCircleLinesV(state->player_pos, 80, ColorAlpha(COLOR_BROWN, 0.1)); 442 | 443 | // :healthbar 444 | Vec2 size = { 16, 1 }; 445 | DrawRectangleV( 446 | (Vec2) { state->player_pos.x - size.x/2, state->player_pos.y + 10 }, 447 | size, COLOR_BROWN 448 | ); 449 | DrawRectangleV( 450 | (Vec2) { state->player_pos.x - size.x/2, state->player_pos.y + 10 }, 451 | (Vec2) { size.x * (state->player_health / MAX_PLAYER_HEALTH), size.y }, 452 | COLOR_WHITE 453 | ); 454 | } 455 | 456 | void draw_enemies() { 457 | { 458 | // Draw culling for enemies 459 | state->num_query_points = 0; 460 | qtree_query( 461 | state->enemy_qtree, 462 | state->enemy_qtree->boundary, 463 | state->query_points, 464 | &state->num_query_points 465 | ); 466 | 467 | state->temp.num_enemies_drawn = state->num_query_points; 468 | for (int i = 0; i < state->num_query_points; i++) { 469 | QPoint pt = state->query_points[i]; 470 | Enemy *enemy = &state->enemies[pt.id]; 471 | Vec2 loc = get_enemy_sprite_pos(enemy->type, enemy->is_shiny); 472 | bool is_flip_x = enemy->pos.x < state->player_pos.x; 473 | 474 | Color flash = WHITE; 475 | if (enemy->is_frozen) { 476 | flash = COLOR_FLASH_FROST; 477 | } 478 | if (enemy->is_taking_damage) { 479 | flash = COLOR_FLASH_HURT; 480 | } 481 | 482 | draw_spritev( 483 | &state->sprite_sheet, 484 | (Vec2) { loc.x, loc.y }, 485 | enemy->pos, 486 | get_enemy_scale(enemy->type), 487 | 0, 488 | is_flip_x, 489 | true, 490 | flash, 491 | pt.id 492 | ); 493 | 494 | // this chokes when you draw for too many enemies 495 | // draw_health_bar(enemy->pos, enemy->health, get_enemy_health(enemy->type, enemy->is_shiny)); 496 | } 497 | } 498 | } 499 | 500 | void draw_particles() { 501 | // Mana pickups 502 | for (int i = 0; i < state->mana_particles_count; i ++) { 503 | draw_sprite( 504 | &state->sprite_sheet, 505 | (Vec2) {1, 9}, 506 | state->mana_particles[i], 507 | 0.4, 0, WHITE 508 | ); 509 | } 510 | 511 | // Flame 512 | for (int i = 0; i < state->flame_particles_count; i++) { 513 | draw_sprite( 514 | &state->sprite_sheet, 515 | get_attack_sprite(FLAME), 516 | state->flame_particles[i].pos, 517 | 0.6, 0, COLOR_RED 518 | ); 519 | } 520 | 521 | // Frost wave 522 | for (int i = 0; i < state->frost_wave_particles_count; i++) { 523 | draw_sprite( 524 | &state->sprite_sheet, 525 | get_attack_sprite(FROST_WAVE), 526 | state->frost_wave_particles[i].pos, 527 | 0.6, 0, COLOR_FLASH_FROST 528 | ); 529 | } 530 | } 531 | 532 | void draw_bullets() { 533 | // Player bullets 534 | for (int i = 0; i < state->bullet_count; i++) { 535 | float angle = atan2f( 536 | state->bullets[i].direction.y, 537 | state->bullets[i].direction.x) * (180.0f / PI); 538 | 539 | bool is_spin = state->bullets[i].type == SPIKE || state->bullets[i].type == ORBS; 540 | if (is_spin) 541 | angle = GetRandomValue(0, 360); 542 | 543 | float button_scale = 1.2f; 544 | if (state->bullets[i].type == ORBS) 545 | button_scale = 1.2f + (state->upgrades.orbs_level * 0.1f); 546 | 547 | draw_sprite( 548 | &state->sprite_sheet, 549 | get_attack_sprite(state->bullets[i].type), 550 | state->bullets[i].pos, 551 | button_scale, angle, WHITE 552 | ); 553 | } 554 | 555 | // Enemy bullets 556 | for (int i = 0; i < state->enemy_bullet_count; i++) { 557 | draw_sprite( 558 | &state->sprite_sheet, 559 | get_attack_sprite(state->enemy_bullets[i].type), 560 | state->enemy_bullets[i].pos, 561 | 0.8, 0, COLOR_RED 562 | ); 563 | } 564 | } 565 | 566 | void draw_decorations() { 567 | for (int i = 0; i < NUM_DECORATIONS; i++) { 568 | Vec2 sprite_pos = (Vec2) { 569 | ((state->decorations[i].decoration_idx) % 10) + 1, 570 | (state->decorations[i].decoration_idx + 1 > 9) ? 1 : 0 571 | }; 572 | draw_sprite( 573 | &state->sprite_sheet, 574 | sprite_pos, 575 | state->decorations[i].pos, 576 | 0.8, 0, WHITE 577 | ); 578 | } 579 | } 580 | 581 | void draw_pickups() { 582 | state->num_query_points = 0; 583 | qtree_query( 584 | state->pickups_qtree, 585 | // this will cull the off screen pickups 586 | state->enemy_qtree->boundary, 587 | state->query_points, 588 | &state->num_query_points 589 | ); 590 | 591 | for (int i = 0; i < state->num_query_points; i++) { 592 | QPoint pt = state->query_points[i]; 593 | Vec2 sprite_pos = { 1, 9 }; 594 | float sprite_scale = 0.4; 595 | if (state->pickups[pt.id].type == MANA_SHINY) { 596 | sprite_pos = (Vec2) { 2, 9 }; 597 | } 598 | if (state->pickups[pt.id].type == HEALTH) { 599 | sprite_pos = (Vec2) { 5, 9 }; 600 | sprite_scale = 0.5; 601 | } 602 | 603 | draw_sprite( 604 | &state->sprite_sheet, sprite_pos, 605 | (Vec2) { pt.x, pt.y }, sprite_scale, 0, WHITE 606 | ); 607 | } 608 | } 609 | 610 | // :toast 611 | void draw_toasts() { 612 | float ww = (float) GetScreenWidth(); 613 | float wh = (float) GetScreenHeight(); 614 | int font_size = 24; 615 | int spacing = 45; 616 | int padding = 10; 617 | 618 | for (int i = 0; i < state->toasts_count; i++) { 619 | Toast t = state->toasts[i]; 620 | Vec2 size = MeasureTextEx(state->custom_font, t.message, font_size, 2); 621 | Vec2 pos = { 622 | .x = ww/2 - size.x/2, 623 | // .x = 20, 624 | .y = (wh * 0.90) + (i * spacing) 625 | }; 626 | Vec2 pos_offset = { 627 | pos.x - padding, 628 | pos.y - padding 629 | }; 630 | 631 | DrawRectangleV( 632 | pos_offset, 633 | Vector2AddValue(size, padding * 2), 634 | ColorAlpha(COLOR_BROWN, 0.3) 635 | ); 636 | DrawTextEx( 637 | state->custom_font, 638 | t.message, 639 | pos, 640 | font_size, 2, WHITE 641 | ); 642 | } 643 | } 644 | 645 | void draw_health_bar(Vec2 pos, float health, float max_health) { 646 | DrawRectangleV((Vec2) {pos.x - 8, pos.y + 8}, (Vec2) { 10, 2 }, COLOR_BROWN); 647 | DrawRectangleV((Vec2) {pos.x - 8, pos.y + 8}, (Vec2) { 10 * (health / max_health), 2 }, COLOR_WHITE); 648 | } 649 | 650 | void reset_main_menu_enemies() { 651 | state->main_menu_enemies_count = 0; 652 | for (int i = 0; i < MAX_MAIN_MENU_ENEMIES; i ++) { 653 | state->main_menu_enemies[i] = (Enemy) { 654 | .pos = (Vec2) { 655 | .x = GetRandomValue(0, SCREEN_WIDTH), 656 | .y = GetRandomValue(0, SCREEN_HEIGHT) 657 | }, 658 | .health = 1, 659 | // .type = GetRandomValue(0, 100) > 70 ? RAM : BAT, 660 | .type = BAT, 661 | .speed = get_enemy_speed(DEMON_PUP), 662 | .spawn_ts = 0, 663 | .is_shiny = false, 664 | .is_player_found = false, 665 | .player_found_ts = 0, 666 | // charge dir is used for main menu 667 | // to make enemies move 668 | .charge_dir = get_rand_unit_vec2(), 669 | .last_spawn_ts = 0, 670 | .is_frozen = false, 671 | .is_taking_damage = false, 672 | }; 673 | state->main_menu_enemies_count += 1; 674 | } 675 | } 676 | 677 | // this updates and draws the main menu enemies 678 | void update_main_menu_enemies() { 679 | float ww = (float) GetScreenWidth(); 680 | float wh = (float) GetScreenHeight(); 681 | 682 | for (int i = 0; i < state->main_menu_enemies_count; i++) { 683 | Enemy *enemy = &state->main_menu_enemies[i]; 684 | bool is_flip_x = enemy->pos.x > ww / 2.0f; 685 | Vec2 velocity = Vector2Scale(enemy->charge_dir, 0.3); 686 | enemy->pos = Vector2Add(enemy->pos, velocity); 687 | 688 | if (enemy->pos.x <= 0 || enemy->pos.x >= ww) { 689 | enemy->charge_dir.x *= -1; 690 | } 691 | if (enemy->pos.y <= 0 || enemy->pos.y >= wh) { 692 | enemy->charge_dir.y *= -1; 693 | } 694 | 695 | Vec2 loc = get_enemy_sprite_pos(enemy->type, false); 696 | draw_spritev( 697 | &state->sprite_sheet, 698 | (Vec2) { loc.x, loc.y }, 699 | enemy->pos, 700 | get_enemy_scale(enemy->type) * 2.5, 701 | 0, 702 | is_flip_x, 703 | true, 704 | ColorAlpha(WHITE, 0.2), 705 | i 706 | ); 707 | } 708 | } 709 | 710 | // MARK: :menu 711 | void draw_main_menu() { 712 | if (state->screen != MAIN_MENU) return; 713 | ClearBackground(COLOR_DARK_BLUE); 714 | 715 | float ww = (float) GetScreenWidth(); 716 | float wh = (float) GetScreenHeight(); 717 | float time = GetTime(); 718 | float amplitude = 10; 719 | float speed = 2; 720 | 721 | // version 722 | { 723 | DrawTextEx( 724 | state->custom_font, 725 | TextFormat("%s", VERSION), 726 | (Vec2) { 20, wh - 35 }, 20, 2, WHITE 727 | ); 728 | } 729 | 730 | // title 731 | { 732 | { 733 | const char *title = "Blob"; 734 | int font_size = 80; 735 | Vec2 title_size = MeasureTextEx(state->custom_font, title, font_size, 2); 736 | Vec2 title_pos = (Vec2) { 737 | ww / 2 - title_size.x / 2, 738 | wh * 0.2 + sinf(time * speed) * amplitude 739 | }; 740 | 741 | DrawTextEx( 742 | state->custom_font, 743 | title, 744 | Vector2AddValue(title_pos, 4), 745 | font_size, 3, COLOR_RED 746 | ); 747 | DrawTextEx( 748 | state->custom_font, 749 | title, 750 | title_pos, 751 | font_size, 3, COLOR_WHITE 752 | ); 753 | } 754 | { 755 | const char *title = "Survival"; 756 | int font_size = 80; 757 | Vec2 title_size = MeasureTextEx(state->custom_font, title, font_size, 2); 758 | Vec2 title_pos = (Vec2) { 759 | ww/2 - title_size.x/2, 760 | (wh * 0.2 + 80) + sinf(time * speed) * amplitude 761 | }; 762 | DrawTextEx( 763 | state->custom_font, 764 | title, 765 | Vector2AddValue(title_pos, 4), 766 | font_size, 3, COLOR_RED 767 | ); 768 | DrawTextEx( 769 | state->custom_font, 770 | title, 771 | title_pos, 772 | font_size, 3, COLOR_WHITE 773 | ); 774 | } 775 | } 776 | 777 | float y_start = wh * 0.7; 778 | int font_size = 35; 779 | // play btn 780 | { 781 | const char *text = "Play"; 782 | Vec2 btn_size = (Vec2) { 250, 70 }; 783 | Vec2 pos = (Vec2) { ww/2 - btn_size.x/2, y_start }; 784 | bool is_play = main_menu_button((Button) { 785 | .label = text, 786 | .pos = pos, 787 | .size = btn_size, 788 | .bg = COLOR_BROWN, 789 | .fg = COLOR_WHITE, 790 | .shadow = COLOR_CEMENT, 791 | .font_size = font_size 792 | }); 793 | y_start += 100; 794 | 795 | // :start :play 796 | if (is_play || IsKeyPressed(KEY_ENTER)) { 797 | state->screen = IN_GAME; 798 | state->timer.game_start_ts = get_current_time_millis(); 799 | post_analytics_async(GAME_START); 800 | } 801 | } 802 | } 803 | 804 | // MARK: :ui 805 | 806 | void draw_ui() { 807 | float ww = (float) GetScreenWidth(); 808 | float wh = (float) GetScreenHeight(); 809 | GameScreen *screen = &state->screen; 810 | 811 | // Pause 812 | { 813 | if (*screen == PAUSE_MENU) { 814 | 815 | // bg 816 | DrawRectangleV( 817 | (Vec2) { 0, 0 }, 818 | (Vec2) { ww, wh }, 819 | ColorAlpha(COLOR_DARK_BLUE, 0.7) 820 | ); 821 | 822 | // Title 823 | { 824 | const char *text = "Game Paused"; 825 | int font_size = 35; 826 | Vec2 text_size = MeasureTextEx(state->custom_font, text, font_size, 2); 827 | Vec2 pos = { (ww/2) - (text_size.x/2), wh * 0.1 }; 828 | draw_text_with_shadow(text, pos, font_size, COLOR_WHITE, COLOR_RED); 829 | } 830 | 831 | // Volume controls 832 | { 833 | const char *text = "Master Volume"; 834 | slider( 835 | 0.4f, text, 836 | &state->master_volume, 837 | &state->is_master_volume_dragging 838 | ); 839 | SetMasterVolume(state->master_volume); 840 | } 841 | 842 | // Unpause button 843 | { 844 | const char *text = "Back"; 845 | float y_start = wh * 0.8; 846 | int font_size = 35; 847 | Vec2 btn_size = (Vec2) { 250, 70 }; 848 | Vec2 pos = (Vec2) { ww/2 - btn_size.x/2, y_start }; 849 | bool is_back = upgrade_menu_button((Button) { 850 | .label = text, 851 | .pos = pos, 852 | .size = btn_size, 853 | .bg = COLOR_BROWN, 854 | .fg = COLOR_WHITE, 855 | .shadow = COLOR_BROWN, 856 | .font_size = font_size 857 | }); 858 | 859 | if (is_back || IsKeyPressed(KEY_ESCAPE)) { 860 | resume_timers(); 861 | state->screen = IN_GAME; 862 | } 863 | } 864 | 865 | return; 866 | } 867 | } 868 | 869 | // In game 870 | { 871 | if (*screen == IN_GAME) { 872 | // Mana bar 873 | { 874 | int max_current_level_mana = get_level_mana_threshold(state->player_level); 875 | int ww = GetScreenWidth(); 876 | float percentage_mana = (float) state->mana_count / max_current_level_mana; 877 | DrawRectangleV(Vector2Zero(), (Vec2) { percentage_mana * ww, 10 }, COLOR_LIGHT_PINK); 878 | } 879 | 880 | // Kill count 881 | { 882 | int font_size = 25; 883 | const char *text = TextFormat("%ld", state->kill_count); 884 | draw_sprite(&state->sprite_sheet, (Vec2) {0, 10}, (Vec2) {20, 40}, 5, 0, WHITE); 885 | DrawTextEx( 886 | state->custom_font, text, 887 | (Vec2) { 50, 40 - 7 }, font_size, 2, COLOR_WHITE 888 | ); 889 | } 890 | 891 | // Timer 892 | { 893 | // can't draw this other states are the time needs to be reset 894 | // at the end of the pause/resume cycle 895 | int now = get_current_time_millis(); 896 | int elapsed = now - state->timer.game_start_ts; 897 | int minutes = elapsed / 60000; 898 | int seconds = (elapsed / 1000) % 60; 899 | 900 | int font_size = 35; 901 | const char *text = TextFormat("%d:%02d", minutes, seconds); 902 | Vec2 text_size = MeasureTextEx(state->custom_font, text, font_size, 2); 903 | Vec2 pos = (Vec2) { (ww/2) - (text_size.x/2), 30 }; 904 | 905 | draw_text_with_shadow(text, pos, font_size, COLOR_WHITE, COLOR_RED); 906 | } 907 | 908 | // Pause button 909 | { 910 | Vec2 pos = (Vec2) { ww - 40, 40 }; 911 | float scale = 2.0f; 912 | float sprite_size = TILE_SIZE * scale; 913 | draw_sprite( 914 | &state->sprite_sheet, 915 | (Vec2) {6, 10}, Vector2AddValue(pos, 1.5), 916 | DEFAULT_SPRITE_SCALE * scale, 0, WHITE 917 | ); 918 | draw_sprite( 919 | &state->sprite_sheet, 920 | (Vec2) {5, 10}, pos, 921 | DEFAULT_SPRITE_SCALE * scale, 0, WHITE 922 | ); 923 | 924 | bool is_mouse_hovered = CheckCollisionPointRec( 925 | GetMousePosition(), (Rect) { 926 | pos.x - sprite_size / 2, pos.y - sprite_size / 2, 927 | sprite_size, sprite_size 928 | } 929 | ); 930 | if (is_mouse_hovered && IsMouseButtonPressed(0)) { 931 | pause_timers(); 932 | state->screen = PAUSE_MENU; 933 | state->is_joystick_enabled = false; 934 | } 935 | } 936 | } 937 | } 938 | 939 | // :death 940 | // Death 941 | { 942 | if (*screen == DEATH) { 943 | // bg 944 | DrawRectangleV( 945 | (Vec2) { 0, 0 }, 946 | (Vec2) { ww, wh }, 947 | ColorAlpha(COLOR_DARK_BLUE, 0.7) 948 | ); 949 | 950 | // text 951 | int font_size = 35; 952 | 953 | { 954 | const char *text = "You Died"; 955 | Vec2 text_size = MeasureTextEx(state->custom_font, text, font_size, 2); 956 | Vec2 pos = (Vec2) { (ww/2) - (text_size.x/2), wh * 0.1 }; 957 | draw_text_with_shadow(text, pos, font_size, COLOR_WHITE, COLOR_RED); 958 | } 959 | { 960 | const char *text = "You survived for"; 961 | Vec2 text_size = MeasureTextEx(state->custom_font, text, font_size, 2); 962 | Vec2 pos = (Vec2) { (ww/2) - (text_size.x/2), wh * 0.3 }; 963 | draw_text_with_shadow(text, pos, font_size, COLOR_WHITE, COLOR_CEMENT); 964 | } 965 | { 966 | int elapsed = state->death_ts - state->timer.game_start_ts; 967 | int minutes = elapsed / 60000; 968 | int seconds = (elapsed / 1000) % 60; 969 | const char *text = TextFormat("%d minutes and %d seconds", minutes, seconds); 970 | Vec2 text_size = MeasureTextEx(state->custom_font, text, font_size, 2); 971 | Vec2 pos = (Vec2) { (ww/2) - (text_size.x/2), wh * 0.35 }; 972 | draw_text_with_shadow(text, pos, font_size, COLOR_WHITE, COLOR_CEMENT); 973 | } 974 | { 975 | const char *text = TextFormat("You Killed %d enemies", state->kill_count); 976 | Vec2 text_size = MeasureTextEx(state->custom_font, text, font_size, 2); 977 | Vec2 pos = (Vec2) { (ww/2) - (text_size.x/2), wh * 0.5 }; 978 | draw_text_with_shadow(text, pos, font_size, COLOR_WHITE, COLOR_CEMENT); 979 | } 980 | 981 | // btn 982 | { 983 | const char *text = "Retry"; 984 | float y_start = wh * 0.8; 985 | Vec2 btn_size = (Vec2) { 250, 70 }; 986 | Vec2 pos = (Vec2) { ww/2 - btn_size.x/2, y_start }; 987 | bool is_play = upgrade_menu_button((Button) { 988 | .label = text, 989 | .pos = pos, 990 | .size = btn_size, 991 | .bg = COLOR_BROWN, 992 | .fg = COLOR_WHITE, 993 | .shadow = COLOR_BROWN, 994 | .font_size = font_size 995 | }); 996 | 997 | // :retry :restart 998 | if (is_play || IsKeyPressed(KEY_ENTER)) { 999 | gamestate_destroy(); 1000 | gamestate_create(); 1001 | state->screen = IN_GAME; 1002 | post_analytics_async(GAME_START); 1003 | } 1004 | } 1005 | } 1006 | } 1007 | 1008 | // Joystick 1009 | { 1010 | if (state->is_joystick_enabled) { 1011 | DrawCircleV(state->touch_down_pos, 30, ColorAlpha(COLOR_BROWN, 0.5)); 1012 | DrawCircleV( 1013 | Vector2Add( 1014 | Vector2Scale(state->joystick_direction, 15), 1015 | state->touch_down_pos 1016 | ), 1017 | 10, ColorAlpha(COLOR_WHITE, 0.5) 1018 | ); 1019 | } 1020 | } 1021 | 1022 | } 1023 | 1024 | // MARK: :upgrade 1025 | 1026 | void draw_upgrade_menu() { 1027 | if (state->screen != UPGRADE_MENU) return; 1028 | 1029 | float ww = (float) GetScreenWidth(); 1030 | float wh = (float) GetScreenHeight(); 1031 | 1032 | // auto upgrade 1033 | if (state->player_level > 18) { 1034 | int min = BULLET_UPGRADE; 1035 | int max = SHINY_UPGRADE; 1036 | UpgradeType rand_type = (UpgradeType) GetRandomValue(min, max); 1037 | 1038 | upgrade_stat(rand_type); 1039 | state->screen = IN_GAME; 1040 | resume_timers(); 1041 | play_sound_modulated(&state->sound_upgrade, 0.3); 1042 | return; 1043 | } 1044 | 1045 | // title 1046 | { 1047 | const char *text = "Pick an Upgrade"; 1048 | int font_size = 35; 1049 | Vec2 text_size = MeasureTextEx(state->custom_font, text, font_size, 2); 1050 | Vec2 pos = { (GetScreenWidth()/2) - (text_size.x/2), wh * 0.15 }; 1051 | Vec2 pos_offset = Vector2AddValue(pos, 2); 1052 | 1053 | DrawTextEx(state->custom_font, text, pos_offset, font_size, 2, COLOR_RED); 1054 | DrawTextEx(state->custom_font, text, pos, font_size, 2, COLOR_WHITE); 1055 | } 1056 | 1057 | // options 1058 | { 1059 | // size values are repeated in the draw func 1060 | Vec2 size = (Vec2) { 400, 50 }; 1061 | Vec2 pos = (Vec2) { 1062 | ww/2 - size.x/2, 1063 | wh * 0.3 1064 | }; 1065 | 1066 | Upgrades *level = &state->available_upgrades; 1067 | draw_upgrade_option(BULLET_UPGRADE, level->bullet_level, &pos); 1068 | draw_upgrade_option(SPLINTER_UPGRADE, level->splinter_level, &pos); 1069 | draw_upgrade_option(SPIKE_UPGRADE, level->spike_level, &pos); 1070 | draw_upgrade_option(FLAME_UPGRADE, level->flame_level, &pos); 1071 | draw_upgrade_option(FROST_UPGRADE, level->frost_wave_level, &pos); 1072 | draw_upgrade_option(ORBS_UPGRADE, level->orbs_level, &pos); 1073 | draw_upgrade_option(SPEED_UPGRADE, level->speed_level, &pos); 1074 | draw_upgrade_option(SHINY_UPGRADE, level->shiny_level, &pos); 1075 | } 1076 | } 1077 | 1078 | void draw_upgrade_option(UpgradeType type, int level, Vec2 *pos) { 1079 | // This is a draw func but it also updates the stats and levels of the upgrades 1080 | 1081 | int font_size = 30; 1082 | Vec2 size = (Vec2) { 400, 60 }; 1083 | float gap = 80.0f; 1084 | 1085 | char *label = "Err"; 1086 | switch (type) { 1087 | case BULLET_UPGRADE: 1088 | label = "Bullet"; 1089 | break; 1090 | case SPLINTER_UPGRADE: 1091 | label = "Splinter"; 1092 | break; 1093 | case SPIKE_UPGRADE: 1094 | label = "Spike"; 1095 | break; 1096 | case FLAME_UPGRADE: 1097 | label = "Flame"; 1098 | break; 1099 | case FROST_UPGRADE: 1100 | label = "Frost Wave"; 1101 | break; 1102 | case ORBS_UPGRADE: 1103 | label = "Orbs"; 1104 | break; 1105 | case SPEED_UPGRADE: 1106 | label = "Player Speed"; 1107 | break; 1108 | case SHINY_UPGRADE: 1109 | label = "Shiny Probability"; 1110 | break; 1111 | } 1112 | 1113 | bool is_enabled = false; 1114 | if (level > 0) { 1115 | is_enabled = upgrade_menu_button((Button) { 1116 | .label = label, 1117 | .pos = *pos, .size = size, 1118 | .bg = COLOR_BROWN, .fg = COLOR_WHITE, .shadow = COLOR_BROWN, 1119 | .font_size = font_size 1120 | }); 1121 | pos->y += gap; 1122 | } 1123 | 1124 | if (is_enabled) { 1125 | state->screen = IN_GAME; 1126 | resume_timers(); 1127 | upgrade_stat(type); 1128 | play_sound_modulated(&state->sound_upgrade, 0.5); 1129 | } 1130 | } 1131 | 1132 | // :sprite 1133 | void draw_sprite(Texture2D *sprite_sheet, Vec2 tile, Vec2 pos, float scale, float rotation, Color tint) { 1134 | draw_spritev(sprite_sheet, tile, pos, scale, rotation, false, false, tint, 0); 1135 | } 1136 | 1137 | void draw_spritev(Texture2D *sprite_sheet, Vec2 tile, Vec2 pos, float scale, float rotation, bool is_flip_x, bool is_bob, Color tint, int bob_id) { 1138 | float bob_amplitude = 7; 1139 | float bob_speed = 3; 1140 | int time = GetTime() - bob_id; 1141 | float width = TILE_SIZE * (is_flip_x ? -1 : 1); 1142 | rotation = is_bob ? sinf(time * bob_speed) * bob_amplitude : rotation; 1143 | 1144 | DrawTexturePro( 1145 | *sprite_sheet, 1146 | (Rect) { tile.x * TILE_SIZE, tile.y * TILE_SIZE, width, TILE_SIZE }, 1147 | (Rect) { pos.x, pos.y, TILE_SIZE * scale, TILE_SIZE * scale }, 1148 | (Vec2) { (TILE_SIZE/2.0f) * scale, (TILE_SIZE/2.0f) * scale }, 1149 | rotation, 1150 | tint 1151 | ); 1152 | } 1153 | 1154 | // MARK: :update 1155 | 1156 | void update_game() { 1157 | update_toasts(); 1158 | sounds_update(); 1159 | 1160 | if (state->screen != IN_GAME) { 1161 | return; 1162 | } 1163 | 1164 | handle_player_input(); 1165 | handle_virtual_joystick_input(); 1166 | 1167 | update_bullets(); 1168 | update_enemies(); 1169 | update_particles(); 1170 | update_player(); 1171 | update_pickups(); 1172 | update_decorations(); 1173 | 1174 | if (state->player_health <= 0) { 1175 | state->screen = DEATH; 1176 | state->death_ts = get_current_time_millis(); 1177 | pause_timers(); 1178 | post_analytics_async(GAME_OVER); 1179 | } 1180 | 1181 | // :wave 1182 | // Update wave 1183 | { 1184 | int now = get_current_time_millis(); 1185 | int time_elapsed = now - state->timer.game_start_ts; 1186 | int next_state_ts = (WAVE_DURATION_MILLIS * state->wave_index) + 1187 | (WAVE_DURATION_INCREMENT_MILLIS * state->wave_index); 1188 | int wave_increment = GOD ? 100 : get_num_enemies_per_tick(); 1189 | if (time_elapsed > next_state_ts) { 1190 | state->wave_index += 1; 1191 | state->num_enemies_per_tick = state->wave_index * wave_increment; 1192 | 1193 | if (time_elapsed > 5 * 60 * 1000) { 1194 | state->timer.enemy_spawn_interval = DEFAULT_ENEMY_SPAWN_INTERVAL_MS - (state->wave_index * 10); 1195 | } 1196 | } 1197 | 1198 | } 1199 | } 1200 | 1201 | // :player 1202 | void update_player() { 1203 | // Camera follow 1204 | state->camera.target = (Vec2) { state->player_pos.x, state->player_pos.y }; 1205 | 1206 | // Player hurt 1207 | // :hurt 1208 | { 1209 | state->num_query_points = 0; 1210 | qtree_query( 1211 | state->enemy_qtree, 1212 | (QRect) { 1213 | state->player_pos.x, 1214 | state->player_pos.y, 1215 | 10, 10 1216 | }, 1217 | state->query_points, 1218 | &state->num_query_points 1219 | ); 1220 | for (int i = 0; i < state->num_query_points; i++) { 1221 | QPoint pt = state->query_points[i]; 1222 | float damage = get_enemy_damage(state->enemies[pt.id].type); 1223 | if (Vector2DistanceSqr(state->player_pos, state->enemies[pt.id].pos) <= 50) { 1224 | state->player_health -= damage; 1225 | if (get_current_time_millis() - state->timer.last_hurt_sound_ts > 400) { 1226 | play_sound_modulated(&state->sound_hurt, 0.5); 1227 | state->timer.last_hurt_sound_ts = get_current_time_millis(); 1228 | } 1229 | } 1230 | } 1231 | } 1232 | 1233 | // Player enemy bullet collisions 1234 | { 1235 | for (int i = 0; i < state->enemy_bullet_count; i++) { 1236 | Bullet *b = &state->enemy_bullets[i]; 1237 | float dist = Vector2DistanceSqr(state->player_pos, b->pos); 1238 | if (dist < 50) { 1239 | state->player_health -= b->strength; 1240 | b->penetration = 0; 1241 | b->strength = 0; 1242 | if (get_current_time_millis() - state->timer.last_hurt_sound_ts > 400) { 1243 | play_sound_modulated(&state->sound_hurt, 0.5); 1244 | state->timer.last_hurt_sound_ts = get_current_time_millis(); 1245 | } 1246 | } 1247 | } 1248 | } 1249 | 1250 | // Slow auto-heal 1251 | { 1252 | if (state->player_health < MAX_PLAYER_HEALTH) { 1253 | if (GetRandomValue(0, 100) > 90) { 1254 | state->player_health += 0.05; 1255 | } 1256 | } 1257 | } 1258 | } 1259 | 1260 | // :enemy 1261 | void update_enemies() { 1262 | int ww = get_window_width(); 1263 | int wh = get_window_height(); 1264 | 1265 | Vec2 player_pos = state->player_pos; 1266 | Bullet *bullets = state->bullets; 1267 | Enemy *enemies = state->enemies; 1268 | int bullet_count = state->bullet_count; 1269 | int *enemy_count = &state->enemy_count; 1270 | 1271 | // :reset 1272 | // Reset the Quadtree 1273 | { 1274 | int now = get_current_time_millis(); 1275 | if (now - state->timer.qtree_update_ts > QTREE_UPDATE_INTERVAL_MILLIS) { 1276 | state->timer.qtree_update_ts = now; 1277 | qtree_reset_boundary( 1278 | state->enemy_qtree, 1279 | get_visible_rect(state->player_pos, state->camera.zoom) 1280 | ); 1281 | qtree_clear(state->enemy_qtree); 1282 | 1283 | /** 1284 | * Cleanup dead enemies 1285 | * 1286 | * Dead enemies should only be cleaned up during qtree reset, 1287 | * This is so that the unordered enemy array shifting while deleting them 1288 | * doesn't cause the ids in the qtree to be invalid 1289 | */ 1290 | for (int i = 0; i < *enemy_count; i++) { 1291 | // :mana :health :heart 1292 | bool should_drop_mana = (GetRandomValue(0, 100) > 50) || enemies[i].is_shiny; 1293 | bool is_kill_enemy = true; 1294 | 1295 | if (enemies[i].health > 0) { 1296 | is_kill_enemy = false; 1297 | int lifetime = get_current_time_millis() - enemies[i].spawn_ts; 1298 | // if the enemy is far away cull it 1299 | if (lifetime > 10 * 1000) { 1300 | int dist = Vector2DistanceSqr(enemies[i].pos, state->player_pos); 1301 | if (dist > 200 * 200) { 1302 | should_drop_mana = false; 1303 | is_kill_enemy = true; 1304 | } 1305 | } 1306 | } 1307 | 1308 | if (is_kill_enemy && should_drop_mana) { 1309 | // valid kill, leave mana behind 1310 | if (state->pickups_count < MAX_PICKUPS) { 1311 | state->pickups[state->pickups_count] = (Pickup) { 1312 | .pos = (Vec2) { enemies[i].pos.x, enemies[i].pos.y }, 1313 | .type = get_pickup_spawn_type(enemies[i].is_shiny), 1314 | .spawn_ts = get_current_time_millis(), 1315 | .is_collected = false 1316 | }; 1317 | 1318 | qtree_insert(state->pickups_qtree, (QPoint) { 1319 | .x = enemies[i].pos.x, 1320 | .y = enemies[i].pos.y, 1321 | .id = state->pickups_count 1322 | }); 1323 | state->pickups_count ++; 1324 | } 1325 | } 1326 | 1327 | if (is_kill_enemy) { 1328 | enemies[i] = enemies[*enemy_count - 1]; 1329 | *enemy_count -= 1; 1330 | } 1331 | } 1332 | 1333 | for (int i = 0; i < *enemy_count; i++) { 1334 | qtree_insert(state->enemy_qtree, (QPoint) { 1335 | state->enemies[i].pos.x, 1336 | state->enemies[i].pos.y, 1337 | i 1338 | }); 1339 | } 1340 | } 1341 | } 1342 | 1343 | // Bullet collision 1344 | // :collision 1345 | for (int i = 0; i < bullet_count; i++) { 1346 | if (bullets[i].strength <= 0 || bullets[i].penetration <= 0) { 1347 | continue; 1348 | } 1349 | int bullet_size = 5; 1350 | 1351 | if (bullets[i].type == ORBS) { 1352 | bullet_size = state->stats.orbs_size; 1353 | } 1354 | 1355 | state->num_query_points = 0; 1356 | qtree_query( 1357 | state->enemy_qtree, 1358 | (QRect) { 1359 | bullets[i].pos.x - 2.5f, 1360 | bullets[i].pos.y - 2.5f, 1361 | bullet_size, bullet_size 1362 | }, 1363 | state->query_points, 1364 | &state->num_query_points 1365 | ); 1366 | 1367 | for (int j = 0; j < state->num_query_points; j++) { 1368 | QPoint pt = state->query_points[j]; 1369 | float damage = fmin(bullets[i].strength, enemies[pt.id].health); 1370 | if (enemies[pt.id].health <= 0) { 1371 | continue; 1372 | } 1373 | 1374 | enemies[pt.id].health -= damage; 1375 | enemies[pt.id].is_taking_damage = true; 1376 | enemies[pt.id].damage_ts = get_current_time_millis(); 1377 | // bullets[i].strength -= damage; 1378 | bullets[i].penetration -= 1; 1379 | 1380 | // idk how else to do this in a better way 1381 | if (enemies[pt.id].health <= 0) { 1382 | state->kill_count += 1; 1383 | } 1384 | 1385 | // let one bullet hurt only one enemy 1386 | if (bullets[i].penetration <= 0) break; 1387 | } 1388 | } 1389 | 1390 | // Area collisions 1391 | // :area 1392 | { 1393 | // Flame area damage 1394 | if (get_current_time_millis() - state->timer.flame_ts < state->stats.flame_lifetime) { 1395 | Vec2 flame_dir = state->player_heading_dir; 1396 | // range buffer to account for tringle and cone differences 1397 | float flame_range = (state->stats.flame_distance * (PARTICLE_LIFETIME / 1000.0f)) + 10; 1398 | Vec2 flame_dest = point_at_dist(state->player_pos, state->player_heading_dir, flame_range); 1399 | 1400 | // flame triangle 1401 | Vec2 left_bound = Vector2Add( 1402 | player_pos, 1403 | Vector2Scale( 1404 | rotate_vector(flame_dir, -state->stats.flame_spread), 1405 | flame_range 1406 | ) 1407 | ); 1408 | Vec2 right_bound = Vector2Add( 1409 | player_pos, 1410 | Vector2Scale(rotate_vector(flame_dir, state->stats.flame_spread), flame_range) 1411 | ); 1412 | 1413 | // query rect 1414 | Vec2 rect_center = get_line_center(flame_dest, state->player_pos); 1415 | // 20 buffer 1416 | float dist = Vector2Distance(flame_dest, state->player_pos) + 20; 1417 | QRect rect = { rect_center.x, rect_center.y, dist/2, dist/2 }; 1418 | 1419 | state->num_query_points = 0; 1420 | qtree_query( 1421 | state->enemy_qtree, rect, 1422 | state->query_points, &state->num_query_points 1423 | ); 1424 | 1425 | for (int i = 0; i < state->num_query_points; i++) { 1426 | QPoint pt = state->query_points[i]; 1427 | if (enemies[pt.id].health <= 0) { 1428 | continue; 1429 | } 1430 | 1431 | // this is a cone approximation 1432 | // there's 2 triangles, as the spread angle grows, 1433 | // we need the 2nd minor triangle to handle the other section of the cone 1434 | bool is_in_major_triangle = is_point_in_triangle( 1435 | enemies[pt.id].pos, state->player_pos, left_bound, right_bound); 1436 | bool is_in_minor_triangle = is_point_in_triangle( 1437 | enemies[pt.id].pos, flame_dest, left_bound, right_bound); 1438 | 1439 | if (is_in_major_triangle || is_in_minor_triangle) { 1440 | enemies[pt.id].health -= state->stats.flame_damage; 1441 | enemies[pt.id].is_taking_damage = true; 1442 | enemies[pt.id].damage_ts = get_current_time_millis(); 1443 | if (enemies[pt.id].health <= 0) { 1444 | state->kill_count += 1; 1445 | } 1446 | } 1447 | } 1448 | } 1449 | 1450 | // Frost area slowdown 1451 | { 1452 | if (get_current_time_millis() - state->timer.frost_wave_ts < state->stats.frost_wave_lifetime) { 1453 | float dist = 100; 1454 | QRect rect = { player_pos.x, player_pos.y, dist/2, dist/2 }; 1455 | state->num_query_points = 0; 1456 | qtree_query( 1457 | state->enemy_qtree, rect, 1458 | state->query_points, &state->num_query_points 1459 | ); 1460 | 1461 | for (int i = 0; i < state->num_query_points; i++) { 1462 | QPoint pt = state->query_points[i]; 1463 | bool can_frost = !enemies[pt.id].is_frozen; 1464 | if (can_frost) { 1465 | float cur_dist = Vector2DistanceSqr(enemies[pt.id].pos, state->player_pos); 1466 | if (cur_dist <= dist/2 * dist/2) { 1467 | enemies[pt.id].is_frozen = true; 1468 | enemies[pt.id].speed /= 2.0f; 1469 | enemies[pt.id].health -= state->stats.frost_wave_damage; 1470 | enemies[pt.id].frozen_ts = get_current_time_millis(); 1471 | } 1472 | } 1473 | } 1474 | } 1475 | } 1476 | } 1477 | 1478 | // Enemies update 1479 | { 1480 | for (int i = 0; i < *enemy_count; i++) { 1481 | Vec2 player_dir = Vector2Subtract(player_pos, enemies[i].pos); 1482 | player_dir = Vector2Normalize(player_dir); 1483 | 1484 | if (enemies[i].health <= 0) { 1485 | continue; 1486 | } 1487 | 1488 | // :seperation :separation :boid 1489 | Vec2 separation = Vector2Zero(); 1490 | { 1491 | float perception_radius = 10.0f; 1492 | 1493 | // adjust how often we want to do separation 1494 | int separation_threshold = 10; 1495 | if (state->enemy_count < 100) { 1496 | separation_threshold = 50; 1497 | } else if (state->enemy_count < 200) { 1498 | separation_threshold = 70; 1499 | } else if (state->enemy_count < 500) { 1500 | separation_threshold = 90; 1501 | } else { 1502 | separation_threshold = 99; 1503 | } 1504 | 1505 | // TODO do i not do separation at some point? 1506 | // maybe when the fps drops too low? 1507 | // this may result in the qtree crashing due to many items in a single tile 1508 | // if (state->enemy_count > 5000) { 1509 | // // don't do any separation 1510 | // separation_threshold = 101; 1511 | // } 1512 | 1513 | // separation is applied randomly 1514 | if (GetRandomValue(0, 100) > separation_threshold) { 1515 | state->num_query_points = 0; 1516 | qtree_query( 1517 | state->enemy_qtree, 1518 | (QRect) { 1519 | enemies[i].pos.x, 1520 | enemies[i].pos.y, 1521 | perception_radius * 2, 1522 | perception_radius * 2 1523 | }, 1524 | state->query_points, 1525 | &state->num_query_points 1526 | ); 1527 | 1528 | for (int j = 0; j < state->num_query_points; j++) { 1529 | QPoint pt = state->query_points[j]; 1530 | if (pt.id != i) { 1531 | Vec2 neighbor = Vector2Subtract(enemies[pt.id].pos, enemies[i].pos); 1532 | float dist = Vector2Length(neighbor); 1533 | 1534 | if (dist < perception_radius && dist > 0) { 1535 | // repulsion force, stronger at closer distances 1536 | float repulsion_strength = (1.0f - (dist / perception_radius)) * 0.5f; 1537 | Vec2 repulsion = Vector2Normalize(neighbor); 1538 | repulsion = Vector2Scale(repulsion, -repulsion_strength * ENEMY_SPEED); 1539 | separation = Vector2Add(separation, repulsion); 1540 | } 1541 | } 1542 | } 1543 | } 1544 | } 1545 | 1546 | // Enemy pos update 1547 | // :behaviour 1548 | 1549 | // Unique enemy updates 1550 | switch (enemies[i].type) { 1551 | 1552 | /** 1553 | * Default enemy behaviour is in the defualt case 1554 | * Jump to the defualt case for all enemies 1555 | */ 1556 | 1557 | case RAM: 1558 | { 1559 | // Gets close to the player and pauses 1560 | // Then charges with a lot of speed for sometime 1561 | 1562 | float dist_to_player = Vector2DistanceSqr(state->player_pos, enemies[i].pos); 1563 | if (dist_to_player > 50 * 50) { 1564 | enemies[i].is_player_found = false; 1565 | enemies[i].player_found_ts = 0; 1566 | } else if (!enemies[i].is_player_found) { 1567 | enemies[i].is_player_found = true; 1568 | enemies[i].player_found_ts = get_current_time_millis(); 1569 | } 1570 | 1571 | if (enemies[i].is_player_found) { 1572 | int time_elapsed = get_current_time_millis() - enemies[i].player_found_ts; 1573 | if (time_elapsed < 500) { 1574 | // wait for some time 1575 | enemies[i].charge_dir = player_dir; 1576 | break; 1577 | } else if (time_elapsed < 1500) { 1578 | // charge in last player dir for some time 1579 | Vec2 velocity = Vector2Scale(enemies[i].charge_dir, enemies[i].speed * 2.5f); 1580 | velocity = Vector2Add(velocity, separation); 1581 | velocity = Vector2Scale(velocity, GetFrameTime()); 1582 | enemies[i].pos = Vector2Add(enemies[i].pos, velocity); 1583 | break; 1584 | } else { 1585 | // reset 1586 | enemies[i].is_player_found = false; 1587 | enemies[i].player_found_ts = 0; 1588 | } 1589 | } 1590 | goto default_behaviour; 1591 | } 1592 | case MAGE: 1593 | { 1594 | // Gets close to the player 1595 | // Then shoots bullets until the player is in vision 1596 | 1597 | float dist_to_player = Vector2DistanceSqr(state->player_pos, enemies[i].pos); 1598 | if (dist_to_player > 75 * 75) { 1599 | enemies[i].is_player_found = false; 1600 | enemies[i].player_found_ts = 0; 1601 | } else if (!enemies[i].is_player_found) { 1602 | enemies[i].is_player_found = true; 1603 | enemies[i].player_found_ts = get_current_time_millis(); 1604 | } 1605 | 1606 | if (enemies[i].is_player_found) { 1607 | int time_elapsed = get_current_time_millis() - enemies[i].player_found_ts; 1608 | if (time_elapsed > 1000 && state->enemy_bullet_count < MAX_ENEMY_BULLETS) { 1609 | // fire a bullet 1610 | state->enemy_bullets[state->enemy_bullet_count] = (Bullet) { 1611 | .pos = enemies[i].pos, 1612 | .direction = player_dir, 1613 | .spawnTs = get_current_time_millis(), 1614 | .strength = 10, 1615 | .penetration = 1, 1616 | .type = MAGE_BULLET, 1617 | .speed = get_attack_speed(MAGE_BULLET), 1618 | .angle = 0 1619 | }; 1620 | state->enemy_bullet_count += 1; 1621 | 1622 | // reset time acts as a bullet interval 1623 | enemies[i].player_found_ts = get_current_time_millis(); 1624 | } 1625 | 1626 | // don't approach the player once found 1627 | break; 1628 | } 1629 | goto default_behaviour; 1630 | } 1631 | case DEMON: 1632 | { 1633 | // Gets close to the player 1634 | // Spawns pup enemies and spawns many bullets 1635 | 1636 | float dist_to_player = Vector2DistanceSqr(state->player_pos, enemies[i].pos); 1637 | if (dist_to_player > 90 * 90) { 1638 | enemies[i].is_player_found = false; 1639 | enemies[i].player_found_ts = 0; 1640 | } else if (!enemies[i].is_player_found) { 1641 | enemies[i].is_player_found = true; 1642 | enemies[i].player_found_ts = get_current_time_millis(); 1643 | } 1644 | 1645 | if (enemies[i].is_player_found) { 1646 | int time_elapsed = get_current_time_millis() - enemies[i].player_found_ts; 1647 | if (time_elapsed > 2000) { 1648 | 1649 | // fire bullets 1650 | int num_bullets = 5; 1651 | float spread_angle = 60.0f; 1652 | float half_angle = spread_angle / 2.0f; 1653 | float angle_increment = spread_angle / (num_bullets - 1); 1654 | 1655 | for (int j = 0; j < num_bullets; j++) { 1656 | float angle = -half_angle + j * angle_increment; 1657 | Vec2 bullet_dir = rotate_vector(player_dir, angle); 1658 | 1659 | if (state->enemy_bullet_count > MAX_ENEMY_BULLETS) break; 1660 | 1661 | state->enemy_bullets[state->enemy_bullet_count] = (Bullet) { 1662 | .pos = enemies[i].pos, 1663 | .direction = bullet_dir, 1664 | .spawnTs = get_current_time_millis(), 1665 | .strength = 10, 1666 | .penetration = 1, 1667 | .type = DEMON_BULLET, 1668 | .speed = get_attack_speed(DEMON_BULLET), 1669 | .angle = 0 1670 | }; 1671 | state->enemy_bullet_count += 1; 1672 | } 1673 | 1674 | // reset time acts as a bullet interval 1675 | enemies[i].player_found_ts = get_current_time_millis(); 1676 | } 1677 | 1678 | // don't approach the player once found 1679 | break; 1680 | } 1681 | 1682 | // Spawn pups 1683 | // :pups 1684 | { 1685 | int num_pups_to_spawn = 5; 1686 | int time_elapsed = (get_current_time_millis() - enemies[i].last_spawn_ts); 1687 | float spawn_distance = 20.0f; 1688 | bool can_spawn = GetRandomValue(0, 100) > 90 && state->enemy_count < MAX_ENEMIES; 1689 | // bool can_spawn = state->enemy_count < MAX_ENEMIES; 1690 | if (can_spawn && time_elapsed > 10000 && dist_to_player < 100 * 100) { 1691 | enemies[i].last_spawn_ts = get_current_time_millis(); 1692 | for (int j = 0; j < num_pups_to_spawn; j++) { 1693 | float angle = (2.0f * PI / num_pups_to_spawn) * j; 1694 | Vector2 spawn_offset = (Vec2) { 1695 | .x = cosf(angle) * spawn_distance, 1696 | .y = sinf(angle) * spawn_distance 1697 | }; 1698 | 1699 | // Spawn the pup 1700 | enemies[*enemy_count] = (Enemy) { 1701 | .pos = (Vec2) { 1702 | .x = enemies[i].pos.x + spawn_offset.x, 1703 | .y = enemies[i].pos.y + spawn_offset.y 1704 | }, 1705 | .health = get_enemy_health(DEMON_PUP, false), 1706 | .type = DEMON_PUP, 1707 | .speed = get_enemy_speed(DEMON_PUP), 1708 | .spawn_ts = get_current_time_millis(), 1709 | .is_shiny = false, 1710 | .is_player_found = false, 1711 | .player_found_ts = 0, 1712 | .charge_dir = Vector2Zero(), 1713 | .last_spawn_ts = 0, 1714 | .is_frozen = false, 1715 | .is_taking_damage = false, 1716 | }; 1717 | 1718 | *enemy_count += 1; 1719 | } 1720 | } 1721 | } 1722 | 1723 | goto default_behaviour; 1724 | } 1725 | default: 1726 | default_behaviour: 1727 | { 1728 | // default behaviour, approach player 1729 | Vec2 velocity = Vector2Scale(player_dir, enemies[i].speed); 1730 | velocity = Vector2Add(velocity, separation); 1731 | velocity = Vector2Scale(velocity, GetFrameTime()); 1732 | enemies[i].pos = Vector2Add(enemies[i].pos, velocity); 1733 | break; 1734 | } 1735 | } 1736 | 1737 | // Extra stuff 1738 | if (enemies[i].is_frozen) { 1739 | if (get_current_time_millis() - enemies[i].frozen_ts > 2000) { 1740 | enemies[i].is_frozen = false; 1741 | enemies[i].frozen_ts = 0; 1742 | enemies[i].speed = get_enemy_speed(enemies[i].type); 1743 | } 1744 | } 1745 | 1746 | if (enemies[i].is_taking_damage) { 1747 | if (get_current_time_millis() - enemies[i].damage_ts > 100) { 1748 | enemies[i].is_taking_damage = false; 1749 | enemies[i].damage_ts = 0; 1750 | } 1751 | } 1752 | } 1753 | } 1754 | 1755 | // Auto spawn enemies 1756 | // :spawn 1757 | { 1758 | int elapsed = get_current_time_millis() - state->timer.enemy_spawn_ts; 1759 | if (elapsed > state->timer.enemy_spawn_interval) { 1760 | for (int i = 0; i < state->num_enemies_per_tick; i++) { 1761 | if (*enemy_count >= MAX_ENEMIES) { 1762 | break; 1763 | } 1764 | 1765 | Vec2 randPos = Vector2Zero(); 1766 | float diagonal_length = sqrt(ww * ww + wh * wh) / 2; 1767 | randPos = get_rand_pos_around_point(player_pos, diagonal_length, diagonal_length + 30); 1768 | 1769 | int rand_enemy = get_next_enemy_spawn_type(); 1770 | bool is_shiny = GetRandomValue(1, 100) > (100 - state->stats.shiny_chance); 1771 | state->timer.enemy_spawn_ts = get_current_time_millis(); 1772 | enemies[*enemy_count] = (Enemy) { 1773 | .pos = randPos, 1774 | .health = get_enemy_health(rand_enemy, is_shiny), 1775 | .type = rand_enemy, 1776 | .speed = get_enemy_speed(rand_enemy), 1777 | .spawn_ts = get_current_time_millis(), 1778 | .is_shiny = is_shiny, 1779 | .is_player_found = false, 1780 | .player_found_ts = 0, 1781 | .charge_dir = Vector2Zero(), 1782 | .last_spawn_ts = 0, 1783 | .is_frozen = false, 1784 | .is_taking_damage = false 1785 | }; 1786 | 1787 | *enemy_count += 1; 1788 | } 1789 | } 1790 | } 1791 | } 1792 | 1793 | // :particle 1794 | void update_particles() { 1795 | Vec2 player_pos = state->player_pos; 1796 | 1797 | // Mana pickup particles 1798 | { 1799 | int i = 0; 1800 | while (i < state->mana_particles_count) { 1801 | 1802 | // reached player 1803 | float dist = Vector2DistanceSqr(player_pos, state->mana_particles[i]); 1804 | if (dist < 10) { 1805 | state->mana_particles[i] = state->mana_particles[state->mana_particles_count - 1]; 1806 | state->mana_particles_count -= 1; 1807 | i++; 1808 | continue; 1809 | } 1810 | 1811 | // move towards player 1812 | Vec2 dir = Vector2Subtract(player_pos, state->mana_particles[i]); 1813 | dir = Vector2Normalize(dir); 1814 | state->mana_particles[i].x += dir.x * GetFrameTime() * 200; 1815 | state->mana_particles[i].y += dir.y * GetFrameTime() * 200; 1816 | 1817 | i++; 1818 | } 1819 | } 1820 | 1821 | // Flame particles 1822 | { 1823 | int i = 0; 1824 | while (i < state->flame_particles_count) { 1825 | Particle *p = &state->flame_particles[i]; 1826 | 1827 | int time_alive = get_current_time_millis() - p->spawn_ts; 1828 | if (time_alive > p->lifetime) { 1829 | state->flame_particles[i] = state->flame_particles[state->flame_particles_count - 1]; 1830 | state->flame_particles_count -= 1; 1831 | i ++; 1832 | continue; 1833 | } 1834 | 1835 | Vec2 dir = state->flame_particles[i].dir; 1836 | state->flame_particles[i].pos.x += dir.x * GetFrameTime() * state->stats.flame_distance; 1837 | state->flame_particles[i].pos.y += dir.y * GetFrameTime() * state->stats.flame_distance; 1838 | 1839 | i ++; 1840 | } 1841 | } 1842 | 1843 | // Frost wave particles 1844 | { 1845 | int i = 0; 1846 | while (i < state->frost_wave_particles_count) { 1847 | Particle *p = &state->frost_wave_particles[i]; 1848 | 1849 | int time_alive = get_current_time_millis() - p->spawn_ts; 1850 | if (time_alive > p->lifetime) { 1851 | state->frost_wave_particles[i] = state->frost_wave_particles[state->frost_wave_particles_count - 1]; 1852 | state->frost_wave_particles_count -= 1; 1853 | i ++; 1854 | continue; 1855 | } 1856 | 1857 | Vec2 dir = state->frost_wave_particles[i].dir; 1858 | state->frost_wave_particles[i].pos.x += dir.x * GetFrameTime() * 120; 1859 | state->frost_wave_particles[i].pos.y += dir.y * GetFrameTime() * 120; 1860 | 1861 | i ++; 1862 | } 1863 | } 1864 | } 1865 | 1866 | // :bullet :gun :weapon 1867 | void update_bullets() { 1868 | Bullet *bullets = state->bullets; 1869 | Bullet *enemy_bullets = state->enemy_bullets; 1870 | Enemy *enemies = state->enemies; 1871 | 1872 | Vec2 player_pos = state->player_pos; 1873 | int *bullet_count = &state->bullet_count; 1874 | int *enemy_bullet_count = &state->enemy_bullet_count; 1875 | 1876 | // Auto shoot weapon 1877 | // :fire :shoot 1878 | { 1879 | // Simple Bullet 1880 | bool is_simple_shoot = get_current_time_millis() - state->timer.bullet_ts > state->stats.bullet_interval; 1881 | if (is_simple_shoot) { 1882 | float min_dist = FLT_MAX; 1883 | bool enemy_found = false; 1884 | Vec2 enemy_pos = Vector2Zero(); 1885 | 1886 | play_sound_modulated(&state->sound_bullet_fire, 0.1); 1887 | 1888 | state->num_query_points = 0; 1889 | qtree_query( 1890 | state->enemy_qtree, 1891 | (QRect) { player_pos.x, player_pos.y, GUN_VISION, GUN_VISION }, 1892 | state->query_points, 1893 | &state->num_query_points 1894 | ); 1895 | 1896 | for (int i = 0; i < state->num_query_points; i++) { 1897 | QPoint pt = state->query_points[i]; 1898 | float dist = Vector2DistanceSqr(player_pos, enemies[pt.id].pos); 1899 | if (dist < min_dist) { 1900 | min_dist = dist; 1901 | enemy_pos = enemies[pt.id].pos; 1902 | enemy_found = true; 1903 | } 1904 | } 1905 | 1906 | Vec2 dir = Vector2Subtract(enemy_pos, player_pos); 1907 | dir = Vector2Normalize(dir); 1908 | 1909 | if (!enemy_found) { 1910 | dir = get_rand_unit_vec2(); 1911 | } 1912 | 1913 | for (int i = 0; i < state->stats.bullet_count; i++) { 1914 | state->timer.bullet_ts = get_current_time_millis(); 1915 | if (*bullet_count >= MAX_BULLETS) { 1916 | break; 1917 | } 1918 | 1919 | // first bullet always hits the target 1920 | float angle_spread = 0; 1921 | if (i != 0) { 1922 | int spread = (int) state->stats.bullet_spread; 1923 | // TODO probably have to clamp the spread angle here 1924 | angle_spread = GetRandomValue(-spread, spread); 1925 | } 1926 | 1927 | dir = rotate_vector(dir, angle_spread); 1928 | bullets[*bullet_count] = (Bullet) { 1929 | .pos = player_pos, 1930 | .direction = dir, 1931 | .spawnTs = get_current_time_millis(), 1932 | .strength = state->stats.bullet_damage, 1933 | .penetration = state->stats.bullet_penetration, 1934 | .type = BULLET, 1935 | .speed = get_attack_speed(BULLET), 1936 | .angle = 0 1937 | }; 1938 | *bullet_count += 1; 1939 | } 1940 | } 1941 | 1942 | // :splinter 1943 | // Revolving bullets are fired when the old ones die 1944 | 1945 | bool is_shoot_splinter = get_current_time_millis() - state->timer.splinter_ts > state->stats.splinter_interval; 1946 | if (is_shoot_splinter && state->upgrades.splinter_level > 0) { 1947 | play_sound_modulated(&state->sound_splinter, 0.3); 1948 | for (int i = 0; i < state->stats.splinter_count; i++) { 1949 | Vec2 dir = get_rand_unit_vec2(); 1950 | if (*bullet_count < MAX_BULLETS) { 1951 | bullets[*bullet_count] = (Bullet){ 1952 | .pos = player_pos, 1953 | .direction = dir, 1954 | .spawnTs = get_current_time_millis(), 1955 | .strength = state->stats.splinter_damage, 1956 | .penetration = state->stats.splinter_penetration, 1957 | .type = SPLINTER, 1958 | .speed = get_attack_speed(SPLINTER), 1959 | .angle = 0 1960 | }; 1961 | *bullet_count += 1; 1962 | } 1963 | state->timer.splinter_ts = get_current_time_millis(); 1964 | } 1965 | } 1966 | 1967 | // :orbs 1968 | bool is_shoot_orbs = get_current_time_millis() - state->timer.orbs_ts > state->stats.orbs_interval; 1969 | if (is_shoot_orbs && state->upgrades.orbs_level > 0) { 1970 | for (int i = 0; i < state->stats.orbs_count; i ++) { 1971 | Vec2 dir = get_rand_unit_vec2(); 1972 | if (*bullet_count < MAX_BULLETS) { 1973 | bullets[*bullet_count] = (Bullet) { 1974 | .pos = player_pos, 1975 | .direction = dir, 1976 | .spawnTs = get_current_time_millis(), 1977 | .strength = state->stats.orbs_damage, 1978 | .penetration = INT_MAX, 1979 | .type = ORBS, 1980 | .speed = get_attack_speed(ORBS), 1981 | .angle = 0 1982 | }; 1983 | *bullet_count += 1; 1984 | } 1985 | } 1986 | state->timer.orbs_ts = get_current_time_millis(); 1987 | play_sound_modulated(&state->sound_orb, 0.3); 1988 | } 1989 | } 1990 | 1991 | // Bullet update 1992 | int num_spikes = 0; 1993 | { 1994 | int i = 0; 1995 | while (i < *bullet_count) { 1996 | Bullet *b = &bullets[i]; 1997 | int now = get_current_time_millis(); 1998 | 1999 | bool is_kill_bullet = b->penetration <= 0 || b->strength <= 0 || 2000 | (now - b->spawnTs) > get_attack_range(b->type); 2001 | // this kills slow orb bullets when reversing 2002 | bool is_slow_bullet = b->speed < -20; 2003 | if (is_kill_bullet || is_slow_bullet) { 2004 | // Unordered remove 2005 | // Swap the current bullet with the last one and then pop the last bullet 2006 | bullets[i] = bullets[*bullet_count - 1]; 2007 | *bullet_count -= 1; 2008 | i++; 2009 | continue; 2010 | } 2011 | 2012 | if (b->type == SPIKE) { 2013 | b->angle += state->stats.spike_speed * GetFrameTime(); 2014 | if (b->angle > 2 * PI) { 2015 | b->angle -= 2 * PI; 2016 | } 2017 | 2018 | b->pos.x = state->player_pos.x + SPIKE_RADIUS * cosf(b->angle); 2019 | b->pos.y = state->player_pos.y + SPIKE_RADIUS * sinf(b->angle); 2020 | 2021 | i++; 2022 | num_spikes ++; 2023 | continue; 2024 | } 2025 | if (b->type == ORBS) { 2026 | b->speed -= 1; 2027 | } 2028 | 2029 | b->pos.x += b->direction.x * GetFrameTime() * b->speed; 2030 | b->pos.y += b->direction.y * GetFrameTime() * b->speed; 2031 | i++; 2032 | } 2033 | } 2034 | 2035 | // Launch revolving bullets 2036 | // :spike 2037 | { 2038 | bool is_shoot_spike = get_current_time_millis() - state->timer.spike_ts > state->stats.spike_interval; 2039 | if (state->upgrades.spike_level > 0 && is_shoot_spike && num_spikes < state->stats.spike_count) { 2040 | Vec2 dir = get_rand_unit_vec2(); 2041 | if (*bullet_count < MAX_BULLETS) { 2042 | bullets[*bullet_count] = (Bullet){ 2043 | .pos = player_pos, 2044 | .direction = dir, 2045 | .spawnTs = get_current_time_millis(), 2046 | .strength = state->stats.spike_damage, 2047 | .penetration = 10, 2048 | .type = SPIKE, 2049 | .speed = get_attack_speed(SPIKE), 2050 | .angle = GetRandomValue(0, 360) 2051 | }; 2052 | *bullet_count += 1; 2053 | } 2054 | state->timer.spike_ts = get_current_time_millis(); 2055 | } 2056 | } 2057 | 2058 | // Flamethrower 2059 | // :flamethrower :flame 2060 | { 2061 | bool is_shoot_flame = get_current_time_millis() - state->timer.flame_ts > state->stats.flame_interval; 2062 | bool is_post_shoot = get_current_time_millis() - state->timer.flame_ts < state->stats.flame_lifetime; 2063 | 2064 | if (state->upgrades.flame_level > 0 && (is_shoot_flame || is_post_shoot)) { 2065 | if (state->flame_particles_count <= MAX_FLAME_PARTICLES) { 2066 | for (int i = 0; i < 2; i ++) { 2067 | state->flame_particles[state->flame_particles_count] = (Particle) { 2068 | .pos = state->player_pos, 2069 | .dir = rotate_vector( 2070 | state->player_heading_dir, 2071 | GetRandomValue(-state->stats.flame_spread, state->stats.flame_spread) 2072 | ), 2073 | .lifetime = PARTICLE_LIFETIME, 2074 | .spawn_ts = get_current_time_millis() 2075 | }; 2076 | state->flame_particles_count += 1; 2077 | } 2078 | } 2079 | 2080 | if (is_shoot_flame) { 2081 | // TODO playing this on a low interval breaks my speakers 2082 | if (!GOD) 2083 | play_sound_modulated(&state->sound_flamethrower, 0.9); 2084 | 2085 | state->timer.flame_ts = get_current_time_millis(); 2086 | } 2087 | } 2088 | } 2089 | 2090 | // Frost wave 2091 | // :frost 2092 | { 2093 | int num_frost_particles = 3; 2094 | bool is_shoot_frost_wave = get_current_time_millis() - state->timer.frost_wave_ts > state->stats.frost_wave_interval; 2095 | bool is_post_shoot = get_current_time_millis() - state->timer.frost_wave_ts < state->stats.frost_wave_lifetime; 2096 | 2097 | if (state->upgrades.frost_wave_level > 0 && (is_shoot_frost_wave || is_post_shoot)) { 2098 | for (int i = 0; i < num_frost_particles; i++) { 2099 | if (state->frost_wave_particles_count <= MAX_FROST_WAVE_PARTICLES) { 2100 | state->frost_wave_particles[state->frost_wave_particles_count] = (Particle) { 2101 | .pos = state->player_pos, 2102 | .dir = get_rand_unit_vec2(), 2103 | .lifetime = state->stats.frost_wave_lifetime, 2104 | .spawn_ts = get_current_time_millis() 2105 | }; 2106 | state->frost_wave_particles_count += 1; 2107 | } 2108 | } 2109 | 2110 | if (is_shoot_frost_wave) { 2111 | state->timer.frost_wave_ts = get_current_time_millis(); 2112 | play_sound_modulated(&state->sound_frost_wave, 0.4); 2113 | } 2114 | } 2115 | } 2116 | 2117 | // Enemy bullets update 2118 | { 2119 | int i = 0; 2120 | while (i < *enemy_bullet_count) { 2121 | Bullet *b = &enemy_bullets[i]; 2122 | int now = get_current_time_millis(); 2123 | 2124 | if ((now - b->spawnTs) > get_attack_range(b->type) || b->penetration <= 0) { 2125 | // Unordered remove 2126 | // Swap the current bullet with the last one and then pop the last bullet 2127 | enemy_bullets[i] = enemy_bullets[*enemy_bullet_count - 1]; 2128 | *enemy_bullet_count -= 1; 2129 | i++; 2130 | continue; 2131 | } 2132 | 2133 | b->pos.x += b->direction.x * GetFrameTime() * get_attack_speed(b->type); 2134 | b->pos.y += b->direction.y * GetFrameTime() * get_attack_speed(b->type); 2135 | i++; 2136 | } 2137 | } 2138 | } 2139 | 2140 | // :pickups 2141 | void update_pickups() { 2142 | // Cleanup collectd pickups 2143 | { 2144 | int now = get_current_time_millis(); 2145 | if (now - state->timer.pickups_cleanup_ts > PICKUPS_CLEANUP_INTERVAL) { 2146 | state->timer.pickups_cleanup_ts = now; 2147 | 2148 | for (int i = 0; i < state->pickups_count; i++) { 2149 | bool too_old = get_current_time_millis() - state->pickups[i].spawn_ts >= PICKUPS_LIFETIME; 2150 | if (too_old || state->pickups[i].is_collected) { 2151 | state->pickups[i] = state->pickups[state->pickups_count - 1]; 2152 | state->pickups_count -= 1; 2153 | } 2154 | } 2155 | } 2156 | } 2157 | 2158 | // Grab pickups 2159 | { 2160 | float perception_radius = 25.0f; 2161 | state->num_query_points = 0; 2162 | qtree_query( 2163 | state->pickups_qtree, 2164 | (QRect) { 2165 | state->player_pos.x, 2166 | state->player_pos.y, 2167 | perception_radius, 2168 | perception_radius 2169 | }, 2170 | state->query_points, 2171 | &state->num_query_points 2172 | ); 2173 | 2174 | for (int j = 0; j < state->num_query_points; j++) { 2175 | QPoint pt = state->query_points[j]; 2176 | float dist = Vector2DistanceSqr(state->player_pos, (Vec2) { pt.x, pt.y }); 2177 | PickupType pickup_type = state->pickups[pt.id].type; 2178 | if (dist > perception_radius * perception_radius) { 2179 | continue; 2180 | } 2181 | 2182 | // remove the pickup item 2183 | state->pickups[pt.id].is_collected = true; 2184 | qtree_remove(state->pickups_qtree, pt); 2185 | 2186 | if (pickup_type == MANA || pickup_type == MANA_SHINY) { 2187 | int mana_value = get_mana_value(); 2188 | state->mana_count += pickup_type == MANA_SHINY ? 7 * mana_value : mana_value; 2189 | 2190 | // mana pickup particle 2191 | // TODO there's a bug here, sometimes particles that are far away are being attracted to the player 2192 | // I tried doing a (&& state->pickups[pt.id].is_collected) but it doesn't help 2193 | // Hence doing a distance check for now, 2194 | // It doesn't fully work properly and a few pickups don't have pickup particle effect 2195 | float perception_range = perception_radius * perception_radius; 2196 | if (state->mana_particles_count < MAX_MANA_PARTICLES && dist <= perception_range) { 2197 | state->mana_particles[state->mana_particles_count] = (Vec2) { pt.x, pt.y }; 2198 | state->mana_particles_count += 1; 2199 | } 2200 | 2201 | // :levelup 2202 | int level_mana_count = get_level_mana_threshold(state->player_level); 2203 | if (state->mana_count >= level_mana_count) { 2204 | state->mana_count -= level_mana_count; 2205 | state->player_level += 1; 2206 | 2207 | update_available_upgrades(); 2208 | state->screen = UPGRADE_MENU; 2209 | pause_timers(); 2210 | } 2211 | } else if (pickup_type == HEALTH) { 2212 | play_sound_modulated(&state->sound_health_pickup, 0.9); 2213 | state->player_health = Clamp(state->player_health + 100, 0, MAX_PLAYER_HEALTH); 2214 | } else { 2215 | printe("Picked up an unhandled pickup"); 2216 | } 2217 | } 2218 | } 2219 | } 2220 | 2221 | void update_toasts() { 2222 | if (state->toasts_count <= 0) { 2223 | return; 2224 | } 2225 | 2226 | int now = get_current_time_millis(); 2227 | int elapsed = now - state->toasts[0].spawn_ts; 2228 | if (elapsed <= TOAST_LIEFTIME_MS) { 2229 | return; 2230 | } 2231 | 2232 | for (int i = 1; i < state->toasts_count; i++) { 2233 | state->toasts[i - 1] = state->toasts[i]; 2234 | state->toasts[i - 1].spawn_ts = now; 2235 | } 2236 | state->toasts_count -= 1; 2237 | } 2238 | 2239 | // MARK: :sound :music 2240 | 2241 | void sounds_init() { 2242 | InitAudioDevice(); 2243 | 2244 | // bg 2245 | state->sound_bg = LoadMusicStream(SOUND_BG_1); 2246 | state->sound_bg.looping = true; 2247 | 2248 | // heartbeat 2249 | state->sound_heartbeat = LoadMusicStream(SOUND_HEARTBEAT); 2250 | state->sound_heartbeat.looping = true; 2251 | 2252 | // impact 2253 | state->sound_upgrade = LoadSound(SOUND_UPGRADE); 2254 | state->sound_bullet_fire = LoadSound(SOUND_BULLET_FIRE); 2255 | state->sound_splinter = LoadSound(SOUND_SPLINTER); 2256 | state->sound_flamethrower = LoadSound(SOUND_FLAME_THROWER); 2257 | state->sound_frost_wave = LoadSound(SOUND_FROST_WAVE); 2258 | state->sound_orb = LoadSound(SOUND_ORB); 2259 | state->sound_hurt= LoadSound(SOUND_HURT); 2260 | state->sound_health_pickup= LoadSound(SOUND_HEALTH_PICKUP); 2261 | 2262 | PlayMusicStream(state->sound_bg); 2263 | PlayMusicStream(state->sound_heartbeat); 2264 | SetMusicVolume(state->sound_heartbeat, 0.0f); 2265 | } 2266 | 2267 | void sounds_destroy() { 2268 | StopMusicStream(state->sound_bg); 2269 | UnloadMusicStream(state->sound_bg); 2270 | 2271 | StopMusicStream(state->sound_heartbeat); 2272 | UnloadMusicStream(state->sound_heartbeat); 2273 | 2274 | UnloadSound(state->sound_upgrade); 2275 | UnloadSound(state->sound_bullet_fire); 2276 | UnloadSound(state->sound_splinter); 2277 | UnloadSound(state->sound_flamethrower); 2278 | UnloadSound(state->sound_frost_wave); 2279 | UnloadSound(state->sound_orb); 2280 | UnloadSound(state->sound_hurt); 2281 | UnloadSound(state->sound_health_pickup); 2282 | 2283 | // Closing audio before unloading sound crashes the game on android 2284 | CloseAudioDevice(); 2285 | } 2286 | 2287 | void sounds_update() { 2288 | UpdateMusicStream(state->sound_bg); 2289 | UpdateMusicStream(state->sound_heartbeat); 2290 | 2291 | // :heartbeat 2292 | { 2293 | float health_ratio = state->player_health / MAX_PLAYER_HEALTH; 2294 | if (health_ratio < 0.7) { 2295 | state->sound_intensity = health_ratio + 0.3; 2296 | float tempo = 1.0f + (1.0f - health_ratio * 2.0); 2297 | tempo = fmax(1.0f, fmin(2.0f, tempo)); 2298 | 2299 | SetMusicVolume(state->sound_heartbeat, 0.3f + (1.0f - health_ratio)); 2300 | SetMusicPitch(state->sound_heartbeat, tempo); 2301 | SetMusicVolume(state->sound_bg, health_ratio + 0.3); 2302 | } else { 2303 | SetMusicVolume(state->sound_heartbeat, 0.0f); 2304 | } 2305 | 2306 | if (state->player_health <= 0) { 2307 | SetMusicVolume(state->sound_heartbeat, 0.0f); 2308 | SetMusicPitch(state->sound_heartbeat, 1.0f); 2309 | SetMusicVolume(state->sound_bg, 1.0); 2310 | } 2311 | } 2312 | } 2313 | 2314 | void play_sound_modulated(Sound *sound, float volume) { 2315 | float pitch = 0.8f + (GetRandomValue(0, 1000) / 1000.0f) * 0.4f; 2316 | // float pan = -1.0f + (GetRandomValue(0, 1000) / 1000.0f) * 2.0f; 2317 | 2318 | // TODO do we need pan at all? 2319 | SetSoundPitch(*sound, pitch); 2320 | SetSoundVolume(*sound, volume * state->sound_intensity); 2321 | // SetSoundPan(*sound, pan); 2322 | PlaySound(*sound); 2323 | } 2324 | 2325 | // MARK: :decorations 2326 | 2327 | void decorations_init() { 2328 | int num_decorations_in_view = NUM_DECORATIONS / 3; 2329 | for (int i = 0; i < num_decorations_in_view; i ++) { 2330 | int rand_decoration = GetRandomValue(0, TOTAL_NUM_DECORATIONS); 2331 | state->decorations[i] = (Decoration) { 2332 | .pos = (Vec2) { 2333 | GetRandomValue(0, state->world_dims.width), 2334 | GetRandomValue(0, state->world_dims.height) 2335 | }, 2336 | .decoration_idx = rand_decoration 2337 | }; 2338 | } 2339 | 2340 | for (int i = num_decorations_in_view; i < (NUM_DECORATIONS - num_decorations_in_view); i ++) { 2341 | int rand_decoration = GetRandomValue(0, TOTAL_NUM_DECORATIONS); 2342 | state->decorations[i] = (Decoration) { 2343 | .pos = (Vec2) { 2344 | GetRandomValue(0, state->world_dims.width), 2345 | GetRandomValue(0, state->world_dims.height) 2346 | }, 2347 | .decoration_idx = rand_decoration 2348 | }; 2349 | } 2350 | } 2351 | 2352 | void update_decorations() { 2353 | for (int i = 0; i < NUM_DECORATIONS; i ++) { 2354 | Decoration *d = &state->decorations[i]; 2355 | float dist = Vector2DistanceSqr(state->player_pos, d->pos); 2356 | float dist_threshold = 300.0f; 2357 | if (dist > dist_threshold * dist_threshold) { 2358 | int rand_decoration = GetRandomValue(0, TOTAL_NUM_DECORATIONS); 2359 | Vec2 rand_pos = get_rand_pos_around_point(state->player_pos, 200.0f, 300.0f); 2360 | state->decorations[i] = (Decoration) { 2361 | .pos = rand_pos, 2362 | .decoration_idx = rand_decoration 2363 | }; 2364 | } 2365 | } 2366 | } 2367 | 2368 | // MARK: :input 2369 | 2370 | void handle_player_input() { 2371 | Vec2 *player_pos = &state->player_pos; 2372 | bool is_dir_invalid = true; 2373 | Vec2 old_pos = *player_pos; 2374 | 2375 | // wasd movement 2376 | { 2377 | Vec2 dir = Vector2Zero(); 2378 | if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) { dir.y -= 1; } 2379 | if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) { dir.y += 1; } 2380 | if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) { dir.x -= 1; } 2381 | if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) { dir.x += 1; } 2382 | 2383 | dir = Vector2Normalize(dir); 2384 | is_dir_invalid = is_vec2_zero(dir); 2385 | 2386 | player_pos->x += dir.x * GetFrameTime() * state->stats.player_speed; 2387 | player_pos->y += dir.y * GetFrameTime() * state->stats.player_speed; 2388 | 2389 | if (!is_dir_invalid) { 2390 | state->player_heading_dir = Vector2Normalize( 2391 | Vector2Subtract(*player_pos, old_pos)); 2392 | } 2393 | } 2394 | 2395 | // joystick movement 2396 | { 2397 | if (state->is_joystick_enabled && is_dir_invalid) { 2398 | player_pos->x += state->joystick_direction.x * GetFrameTime() * state->stats.player_speed; 2399 | player_pos->y += state->joystick_direction.y * GetFrameTime() * state->stats.player_speed; 2400 | state->player_heading_dir = Vector2Normalize(Vector2Subtract(*player_pos, old_pos)); 2401 | } 2402 | } 2403 | } 2404 | 2405 | void handle_extra_inputs() { 2406 | GameScreen *screen = &state->screen; 2407 | // pause 2408 | if (IsKeyPressed(KEY_SPACE)) { 2409 | if (*screen == PAUSE_MENU) { 2410 | resume_timers(); 2411 | *screen = IN_GAME; 2412 | } else if (*screen == IN_GAME) { 2413 | pause_timers(); 2414 | *screen = PAUSE_MENU; 2415 | } 2416 | return; 2417 | } 2418 | } 2419 | 2420 | void handle_debug_inputs() { 2421 | if (!DEBUG) return; 2422 | 2423 | GameScreen *screen = &state->screen; 2424 | if (IsKeyPressed(KEY_TAB)) { 2425 | state->is_show_debug_gui = !state->is_show_debug_gui; 2426 | } 2427 | if (IsKeyPressed(KEY_RIGHT_CONTROL)) { 2428 | state->wave_index += 10; 2429 | state->num_enemies_per_tick = state->wave_index; 2430 | } 2431 | if (IsKeyPressed(KEY_RIGHT_SHIFT) || IsKeyPressedRepeat(KEY_RIGHT_SHIFT)) { 2432 | state->mana_count += 10; 2433 | } 2434 | if (IsKeyPressed(KEY_BACKSLASH) || IsKeyPressedRepeat(KEY_BACKSLASH)) { 2435 | state->timer.game_start_ts -= 1000; 2436 | } 2437 | if (IsKeyPressed(KEY_ENTER) || IsKeyPressedRepeat(KEY_ENTER)) { 2438 | state->player_health = MAX_PLAYER_HEALTH; 2439 | } 2440 | if (IsKeyPressed(KEY_GRAVE) && *screen == IN_GAME) { 2441 | toast("This is a sample Toast"); 2442 | } 2443 | 2444 | // Camera scroll 2445 | float mouse_diff = GetMouseWheelMove(); 2446 | if (mouse_diff != 0 && *screen == IN_GAME) { 2447 | if (mouse_diff > 0) { 2448 | state->camera.zoom += 0.1; 2449 | } else { 2450 | state->camera.zoom -= 0.1; 2451 | } 2452 | } 2453 | } 2454 | 2455 | // :resize 2456 | void handle_window_resize() { 2457 | int ww = GetScreenWidth(); 2458 | int wh = GetScreenHeight(); 2459 | 2460 | float target_aspect = (float)VIRT_WIDTH / (float)VIRT_HEIGHT; 2461 | int scaled_width, scaled_height; 2462 | 2463 | if ((float)ww / wh > target_aspect) { 2464 | // Window wider than target aspect ratio 2465 | scaled_height = wh; 2466 | scaled_width = (int)(scaled_height * target_aspect); 2467 | } else { 2468 | // Window taller than target aspect ratio 2469 | scaled_width = ww; 2470 | scaled_height = (int)(scaled_width / target_aspect); 2471 | } 2472 | 2473 | state->dest_rect = (Rect) { 2474 | (ww - scaled_width) / 2, 2475 | (wh - scaled_height) / 2, 2476 | scaled_width, scaled_height 2477 | }; 2478 | } 2479 | 2480 | // :joystick 2481 | void handle_virtual_joystick_input() { 2482 | // button 0 / touch input -> left click 2483 | 2484 | if (IsMouseButtonPressed(0)) { 2485 | state->is_joystick_enabled = true; 2486 | state->touch_down_pos = GetMousePosition(); 2487 | } 2488 | if (IsMouseButtonReleased(0)) { 2489 | state->is_joystick_enabled = false; 2490 | state->touch_down_pos = Vector2Zero(); 2491 | } 2492 | 2493 | if (state->is_joystick_enabled && IsMouseButtonDown(0)) { 2494 | state->joystick_direction = Vector2Subtract(GetMousePosition(), state->touch_down_pos); 2495 | state->joystick_direction = Vector2Normalize(state->joystick_direction); 2496 | } 2497 | } 2498 | 2499 | // MARK: :quadtree :qtree 2500 | /** 2501 | * This is a simple quadtree impl following 2502 | * https://www.youtube.com/watch?v=OJxEcs0w_kE 2503 | * Original code: https://editor.p5js.org/codingtrain/sketches/CDMjU0GIK 2504 | * 2505 | * Its used to store all enemy locations that are currently visible on the screen, 2506 | * When the player moves around, the boundaries of the qtree are also updated 2507 | * TODO might have to actually free the quadtree after certain clears/boundary-resets? 2508 | */ 2509 | 2510 | QTree *qtree_create(QRect boundary) { 2511 | QTree *qtree = malloc(sizeof(QTree)); 2512 | if (!qtree) return NULL; 2513 | 2514 | qtree->points = malloc(POINTS_PER_QUAD * sizeof(QPoint)); 2515 | qtree->boundary = boundary; 2516 | qtree->is_divided = false; 2517 | qtree->num_points = 0; 2518 | qtree->tl = qtree->tr = qtree->bl = qtree->br = NULL; 2519 | 2520 | return qtree; 2521 | } 2522 | 2523 | void qtree_destroy(QTree *qtree) { 2524 | if (!qtree) return; 2525 | 2526 | if (qtree->is_divided) { 2527 | qtree_destroy(qtree->tl); 2528 | qtree_destroy(qtree->tr); 2529 | qtree_destroy(qtree->bl); 2530 | qtree_destroy(qtree->br); 2531 | } 2532 | 2533 | free(qtree->points); 2534 | free(qtree); 2535 | } 2536 | 2537 | void qtree_clear(QTree *qtree) { 2538 | if (!qtree) return; 2539 | 2540 | qtree->num_points = 0; 2541 | if (qtree->is_divided) { 2542 | qtree_clear(qtree->tl); 2543 | qtree_clear(qtree->tr); 2544 | qtree_clear(qtree->bl); 2545 | qtree_clear(qtree->br); 2546 | } 2547 | } 2548 | 2549 | void qtree_reset_boundary(QTree *qtree, QRect rect) { 2550 | qtree->boundary.x = rect.x; 2551 | qtree->boundary.y = rect.y; 2552 | qtree->boundary.w = rect.w; 2553 | qtree->boundary.h = rect.h; 2554 | 2555 | if (qtree->is_divided) { 2556 | float w = rect.w/2; 2557 | float h = rect.h/2; 2558 | 2559 | qtree->tl->boundary.x = rect.x - w/2; 2560 | qtree->tl->boundary.y = rect.y - h/2; 2561 | qtree->tl->boundary.w = w; 2562 | qtree->tl->boundary.h = h; 2563 | qtree_reset_boundary( 2564 | qtree->tl, 2565 | (QRect) { qtree->tl->boundary.x, qtree->tl->boundary.y, w, h } 2566 | ); 2567 | 2568 | qtree->tr->boundary.x = rect.x + w/2; 2569 | qtree->tr->boundary.y = rect.y - h/2; 2570 | qtree->tr->boundary.w = w; 2571 | qtree->tr->boundary.h = h; 2572 | qtree_reset_boundary( 2573 | qtree->tr, 2574 | (QRect) { qtree->tr->boundary.x, qtree->tr->boundary.y, w, h } 2575 | ); 2576 | 2577 | qtree->bl->boundary.x = rect.x - w/2; 2578 | qtree->bl->boundary.y = rect.y + h/2; 2579 | qtree->bl->boundary.w = w; 2580 | qtree->bl->boundary.h = h; 2581 | qtree_reset_boundary( 2582 | qtree->bl, 2583 | (QRect) { qtree->bl->boundary.x, qtree->bl->boundary.y, w, h } 2584 | ); 2585 | 2586 | qtree->br->boundary.x = rect.x + w/2; 2587 | qtree->br->boundary.y = rect.y + h/2; 2588 | qtree->br->boundary.w = w; 2589 | qtree->br->boundary.h = h; 2590 | qtree_reset_boundary( 2591 | qtree->br, 2592 | (QRect) { qtree->br->boundary.x, qtree->br->boundary.y, w, h } 2593 | ); 2594 | } 2595 | } 2596 | 2597 | bool qtree_insert(QTree *tree, QPoint pt) { 2598 | if (!tree) 2599 | return false; 2600 | if (!is_rect_contains_point(tree->boundary, pt)) 2601 | return false; 2602 | 2603 | if (tree->num_points < POINTS_PER_QUAD) { 2604 | tree->points[tree->num_points] = pt; 2605 | tree->num_points += 1; 2606 | return true; 2607 | } 2608 | 2609 | if (!tree->is_divided) { 2610 | _qtree_subdivide(tree); 2611 | for (int i = 0; i < tree->num_points; i++) { 2612 | qtree_insert(tree->tl, tree->points[i]); 2613 | qtree_insert(tree->tr, tree->points[i]); 2614 | qtree_insert(tree->bl, tree->points[i]); 2615 | qtree_insert(tree->br, tree->points[i]); 2616 | } 2617 | tree->num_points = 0; 2618 | } 2619 | 2620 | return qtree_insert(tree->tl, pt) || 2621 | qtree_insert(tree->tr, pt) || 2622 | qtree_insert(tree->bl, pt) || 2623 | qtree_insert(tree->br, pt); 2624 | } 2625 | 2626 | bool qtree_remove(QTree *qtree, QPoint pt) { 2627 | if (!qtree || !is_rect_contains_point(qtree->boundary, pt)) 2628 | return false; 2629 | 2630 | for (int i = 0; i < qtree->num_points; i++) { 2631 | 2632 | // we don't do the id check here since the id of the items aren't valid 2633 | // ie when you remove a pickup item, the id of other pickups may change 2634 | if (qtree->points[i].x != pt.x && qtree->points[i].y != pt.y) { 2635 | continue; 2636 | } 2637 | 2638 | // Don't do an unordered remove here since the id values in the qtree are hardcoded 2639 | qtree->points[i] = qtree->points[qtree->num_points - 1]; 2640 | qtree->num_points--; 2641 | return true; 2642 | } 2643 | 2644 | if (qtree->is_divided) { 2645 | return qtree_remove(qtree->tl, pt) || 2646 | qtree_remove(qtree->tr, pt) || 2647 | qtree_remove(qtree->bl, pt) || 2648 | qtree_remove(qtree->br, pt); 2649 | } 2650 | return false; 2651 | } 2652 | 2653 | void qtree_query(QTree *qtree, QRect range, QPoint *result, int *num_points) { 2654 | if (!qtree || !result || !num_points) 2655 | return; 2656 | if (*num_points >= MAX_ENEMIES) 2657 | return; 2658 | if (!is_rect_overlap(qtree->boundary, range)) 2659 | return; 2660 | 2661 | for (int i = 0; i < qtree->num_points; i ++) { 2662 | if (is_rect_contains_point(range, qtree->points[i])) { 2663 | result[*num_points] = qtree->points[i]; 2664 | *num_points += 1; 2665 | } 2666 | } 2667 | 2668 | if (qtree->is_divided) { 2669 | qtree_query(qtree->tl, range, result, num_points); 2670 | qtree_query(qtree->tr, range, result, num_points); 2671 | qtree_query(qtree->bl, range, result, num_points); 2672 | qtree_query(qtree->br, range, result, num_points); 2673 | } 2674 | } 2675 | 2676 | void _qtree_subdivide(QTree *qtree) { 2677 | float x = qtree->boundary.x; 2678 | float y = qtree->boundary.y; 2679 | float w = qtree->boundary.w; 2680 | float h = qtree->boundary.h; 2681 | 2682 | QRect tl = (QRect) { x - w/2, y - h/2, w/2, h/2 }; 2683 | QRect tr = (QRect) { x + w/2, y - h/2, w/2, h/2 }; 2684 | QRect bl = (QRect) { x - w/2, y + h/2, w/2, h/2 }; 2685 | QRect br = (QRect) { x + w/2, y + h/2, w/2, h/2 }; 2686 | 2687 | qtree->tl = qtree_create(tl); 2688 | qtree->tr = qtree_create(tr); 2689 | qtree->bl = qtree_create(bl); 2690 | qtree->br = qtree_create(br); 2691 | 2692 | if (!qtree->tl || !qtree->tr || !qtree->bl || !qtree->br) { 2693 | free(qtree->tl); 2694 | free(qtree->tr); 2695 | free(qtree->bl); 2696 | free(qtree->br); 2697 | qtree->tl = qtree->tr = qtree->bl = qtree->br = NULL; 2698 | return; 2699 | } 2700 | 2701 | qtree->is_divided = true; 2702 | } 2703 | 2704 | // MARK: :data :switch 2705 | 2706 | Vec2 get_enemy_sprite_pos(EnemyType type, bool is_shiny) { 2707 | switch (type) { 2708 | case BAT: 2709 | return is_shiny ? (Vec2) {0, 4} : (Vec2) {0, 3}; 2710 | case RAM: 2711 | return (Vec2) {1, 3}; 2712 | case MAGE: 2713 | return (Vec2) {2, 3}; 2714 | case DEMON: 2715 | return (Vec2) {4, 3}; 2716 | case DEMON_PUP: 2717 | return (Vec2) {5, 3}; 2718 | case REAPER: 2719 | return (Vec2) {3, 3}; 2720 | default: 2721 | return (Vec2) {0, 0}; 2722 | } 2723 | } 2724 | 2725 | float get_enemy_scale(EnemyType type) { 2726 | switch (type) { 2727 | case BAT: 2728 | return DEFAULT_SPRITE_SCALE; 2729 | case RAM: 2730 | return DEFAULT_SPRITE_SCALE + 0.3; 2731 | case MAGE: 2732 | return DEFAULT_SPRITE_SCALE + 0.3; 2733 | case REAPER: 2734 | return DEFAULT_SPRITE_SCALE + 0.3; 2735 | case DEMON: 2736 | return DEFAULT_SPRITE_SCALE + 0.6; 2737 | case DEMON_PUP: 2738 | return DEFAULT_SPRITE_SCALE; 2739 | default: 2740 | return DEFAULT_SPRITE_SCALE; 2741 | } 2742 | } 2743 | 2744 | float get_enemy_health(EnemyType type, bool is_shiny) { 2745 | switch (type) { 2746 | case BAT: 2747 | return is_shiny ? 200 : 15; 2748 | case RAM: 2749 | return 150; 2750 | case MAGE: 2751 | return 80; 2752 | case DEMON: 2753 | return 500; 2754 | case DEMON_PUP: 2755 | return 5; 2756 | case REAPER: 2757 | return 50000; 2758 | default: 2759 | return 100; 2760 | } 2761 | } 2762 | 2763 | float get_enemy_speed(EnemyType type) { 2764 | switch (type) { 2765 | case BAT: 2766 | return ENEMY_SPEED * 0.9; 2767 | case RAM: 2768 | return ENEMY_SPEED * 1.2; 2769 | case MAGE: 2770 | return ENEMY_SPEED * 0.7; 2771 | case DEMON: 2772 | return ENEMY_SPEED * 1.0; 2773 | case REAPER: 2774 | return ENEMY_SPEED * 0.6; 2775 | default: 2776 | return ENEMY_SPEED * 0.9; 2777 | } 2778 | } 2779 | 2780 | float get_enemy_damage(EnemyType type) { 2781 | switch (type) { 2782 | case BAT: 2783 | return 1; 2784 | case RAM: 2785 | return 3; 2786 | case MAGE: 2787 | return 2; 2788 | case DEMON: 2789 | return 3; 2790 | case REAPER: 2791 | return 5; 2792 | default: 2793 | return 1; 2794 | } 2795 | } 2796 | 2797 | Vec2 get_attack_sprite(AttackType type) { 2798 | switch (type) { 2799 | case BULLET: 2800 | return (Vec2) {0, 6}; 2801 | case MAGE_BULLET: 2802 | return (Vec2) {4, 6}; 2803 | case DEMON_BULLET: 2804 | return (Vec2) {4, 6}; 2805 | case SPLINTER: 2806 | return (Vec2) {1, 6}; 2807 | case SPIKE: 2808 | return (Vec2) {2, 6}; 2809 | case FLAME: 2810 | case FROST_WAVE: 2811 | return (Vec2) {3, 6}; 2812 | case ORBS: 2813 | return (Vec2) {6, 6}; 2814 | default: 2815 | return (Vec2) {0, 0}; 2816 | } 2817 | } 2818 | 2819 | int get_attack_range(AttackType type) { 2820 | switch (type) { 2821 | case BULLET: 2822 | return state->stats.bullet_range; 2823 | case SPLINTER: 2824 | return state->stats.splinter_range; 2825 | case SPIKE: 2826 | return INT_MAX; 2827 | case ORBS: 2828 | return 3000; 2829 | case MAGE_BULLET: 2830 | return 1200; 2831 | case DEMON_BULLET: 2832 | return 3500; 2833 | default: 2834 | return 200; 2835 | } 2836 | } 2837 | 2838 | int get_attack_speed(AttackType type) { 2839 | switch (type) { 2840 | case BULLET: 2841 | return 400; 2842 | case MAGE_BULLET: 2843 | return 100; 2844 | case DEMON_BULLET: 2845 | return 35; 2846 | case SPLINTER: 2847 | return 200; 2848 | case SPIKE: 2849 | // this is a dummy value, 2850 | // there's a stat for this 2851 | return 5; 2852 | case ORBS: 2853 | return 120; 2854 | default: 2855 | return 100; 2856 | } 2857 | } 2858 | 2859 | // MARK: :stat 2860 | 2861 | float get_base_stat_value(ShopUpgradeType type) { 2862 | switch (type) { 2863 | case BULLET_INTERVAL: return 400; 2864 | case BULLET_COUNT: return GOD ? 10 : 1; 2865 | case BULLET_SPREAD: return GOD ? 30 : 15; 2866 | case BULLET_RANGE: return GOD ? 400 : 200; 2867 | case BULLET_DAMAGE: return 15; 2868 | case BULLET_PENETRATION: return 1; 2869 | 2870 | case SPLINTER_INTERVAL: return 1000; 2871 | case SPLINTER_RANGE: return 1000; 2872 | case SPLINTER_COUNT: return GOD ? 20 : 3; 2873 | case SPLINTER_DAMAGE: return 10; 2874 | case SPLINTER_PENETRATION: return 3; 2875 | 2876 | case SPIKE_INTERVAL: return GOD ? 1 : 200; 2877 | case SPIKE_SPEED: return 5; 2878 | case SPIKE_COUNT: return GOD ? 5000 : 3; 2879 | case SPIKE_DAMAGE: return 3; 2880 | 2881 | case FLAME_INTERVAL: return GOD ? 100 : 4800; 2882 | case FLAME_LIFETIME: return 800; 2883 | case FLAME_SPREAD: return 25; 2884 | case FLAME_DISTANCE: return 200; 2885 | case FLAME_DAMAGE: return 0.5f; 2886 | 2887 | case FROST_WAVE_INTERVAL: return 3000; 2888 | case FROST_WAVE_LIFETIME: return 300; 2889 | case FROST_WAVE_DAMAGE: return 5; 2890 | 2891 | case ORBS_INTERVAL: return 3200; 2892 | case ORBS_DAMAGE: return 1; 2893 | case ORBS_COUNT: return 2; 2894 | case ORBS_SIZE: return 5; 2895 | 2896 | case PLAYER_SPEED: return 40; 2897 | case SHINY_CHANCE: return 1; 2898 | 2899 | default: 2900 | return 0; 2901 | } 2902 | } 2903 | 2904 | // :increment 2905 | float get_stat_increment(ShopUpgradeType type) { 2906 | switch (type) { 2907 | case BULLET_INTERVAL: return -5; 2908 | case BULLET_COUNT: return 1; 2909 | case BULLET_SPREAD: return 1; 2910 | case BULLET_RANGE: return 4; 2911 | case BULLET_DAMAGE: return 3; 2912 | case BULLET_PENETRATION: return 1; 2913 | 2914 | case SPLINTER_INTERVAL: return -3; 2915 | case SPLINTER_RANGE: return 4; 2916 | case SPLINTER_COUNT: return 2; 2917 | case SPLINTER_DAMAGE: return 1; 2918 | case SPLINTER_PENETRATION: return 0; 2919 | 2920 | case SPIKE_INTERVAL: return -10; 2921 | case SPIKE_SPEED: return 0.2; 2922 | case SPIKE_COUNT: return 5; 2923 | case SPIKE_DAMAGE: return 5; 2924 | 2925 | case FLAME_INTERVAL: return -20; 2926 | case FLAME_LIFETIME: return 25; 2927 | case FLAME_SPREAD: return 0.5; 2928 | case FLAME_DISTANCE: return 3; 2929 | case FLAME_DAMAGE: return 0.2f; 2930 | 2931 | case FROST_WAVE_INTERVAL: return -100; 2932 | case FROST_WAVE_LIFETIME: return 10; 2933 | case FROST_WAVE_DAMAGE: return 1; 2934 | 2935 | case ORBS_INTERVAL: return -20; 2936 | case ORBS_DAMAGE: return 1; 2937 | case ORBS_COUNT: return 1; 2938 | case ORBS_SIZE: return 1; 2939 | 2940 | case PLAYER_SPEED: return 1; 2941 | case SHINY_CHANCE: return 1; 2942 | 2943 | default: return 1.0; 2944 | } 2945 | } 2946 | 2947 | // MARK: :impl :func 2948 | 2949 | EnemyType get_next_enemy_spawn_type() { 2950 | int time_elapsed = get_current_time_millis() - state->timer.game_start_ts; 2951 | int prob = GetRandomValue(0, 100); 2952 | 2953 | /** 2954 | * Remember to sequence the following logic in the reverse time order 2955 | */ 2956 | 2957 | // more reaper 2958 | if (time_elapsed > 60 * 12 * 1000) { 2959 | if (prob > 99) return REAPER; 2960 | if (prob > 98) return DEMON; 2961 | if (prob > 97) return MAGE; 2962 | if (prob > 96) return RAM; 2963 | return BAT; 2964 | } 2965 | 2966 | // reaper 2967 | if (time_elapsed > 60 * 10 * 1000) { 2968 | if (prob > 99) { 2969 | if (GetRandomValue(0, 100) > 80) return REAPER; 2970 | } 2971 | if (prob > 98) return DEMON; 2972 | if (prob > 97) return MAGE; 2973 | if (prob > 96) return RAM; 2974 | return BAT; 2975 | } 2976 | 2977 | // more mages 2978 | if (time_elapsed > 60 * 5 * 1000) { 2979 | if (prob > 99) return DEMON; 2980 | if (prob > 98) return MAGE; 2981 | if (prob > 97) return RAM; 2982 | return BAT; 2983 | } 2984 | 2985 | // mage 2986 | if (time_elapsed > 60 * 3 * 1000) { 2987 | if (prob > 99) return MAGE; 2988 | if (prob > 98) return RAM; 2989 | return BAT; 2990 | } 2991 | 2992 | // rams 2993 | if (time_elapsed > 60 * 1 * 1000) { 2994 | if (prob > 99) return RAM; 2995 | } 2996 | 2997 | return BAT; 2998 | } 2999 | 3000 | int get_num_enemies_per_tick() { 3001 | int elapsed = get_current_time_millis() - state->timer.game_start_ts; 3002 | int mins = elapsed / (60 * 1000); 3003 | 3004 | if (mins < 3) { 3005 | return 3; 3006 | } 3007 | 3008 | if (mins < 7) { 3009 | return mins - 3 + 8; 3010 | } 3011 | 3012 | return mins - 5 + 25; 3013 | } 3014 | 3015 | int get_mana_value() { 3016 | // int elapsed = get_current_time_millis() - state->timer.game_start_ts; 3017 | // int mins = elapsed / (60 * 1000); 3018 | 3019 | // if (mins < 3) { 3020 | // return 1; 3021 | // } 3022 | 3023 | // if (mins < 5) { 3024 | // return 2; 3025 | // } 3026 | 3027 | // if (mins < 8) { 3028 | // return 3; 3029 | // } 3030 | 3031 | // if (mins < 10) { 3032 | // return 5; 3033 | // } 3034 | 3035 | return 1; 3036 | } 3037 | 3038 | PickupType get_pickup_spawn_type(bool is_shiny) { 3039 | if (is_shiny) return MANA_SHINY; 3040 | if (GetRandomValue(0, 8000) > 7999) { 3041 | if (get_current_time_millis() - state->timer.last_heart_spawn_ts > 20 * 1000) { 3042 | state->timer.last_heart_spawn_ts = get_current_time_millis(); 3043 | return HEALTH; 3044 | } 3045 | } 3046 | return MANA; 3047 | } 3048 | 3049 | void update_available_upgrades() { 3050 | // TODO use the state to decide which ones are upgradable 3051 | 3052 | // reset options 3053 | state->available_upgrades.bullet_level = 0; 3054 | state->available_upgrades.splinter_level = 0; 3055 | state->available_upgrades.spike_level = 0; 3056 | state->available_upgrades.flame_level = 0; 3057 | state->available_upgrades.frost_wave_level = 0; 3058 | state->available_upgrades.orbs_level = 0; 3059 | state->available_upgrades.speed_level = 0; 3060 | state->available_upgrades.shiny_level = 0; 3061 | 3062 | Upgrades *upgrades = &state->available_upgrades; 3063 | int *fields[] = { 3064 | &upgrades->bullet_level, 3065 | &upgrades->splinter_level, 3066 | &upgrades->spike_level, 3067 | &upgrades->flame_level, 3068 | &upgrades->frost_wave_level, 3069 | &upgrades->orbs_level, 3070 | &upgrades->speed_level, 3071 | &upgrades->shiny_level, 3072 | }; 3073 | size_t num_fields = sizeof(fields) / sizeof(fields[0]); 3074 | 3075 | // shuffle 3076 | for (size_t i = 0; i < num_fields - 1; i++) { 3077 | size_t j = i + GetRandomValue(0, num_fields - i - 1); 3078 | int *temp = fields[i]; 3079 | fields[i] = fields[j]; 3080 | fields[j] = temp; 3081 | } 3082 | 3083 | // pick 4 for now 3084 | for (size_t i = 0; i < 4; i++) { 3085 | *fields[i] = 1; 3086 | } 3087 | } 3088 | 3089 | void upgrade_stat(UpgradeType type) { 3090 | int rand_val = GetRandomValue(0, 100); 3091 | switch (type) 3092 | { 3093 | case BULLET_UPGRADE: { 3094 | state->stats.bullet_count += get_stat_increment(BULLET_COUNT); 3095 | if (rand_val > 50) state->stats.bullet_interval += get_stat_increment(BULLET_INTERVAL); 3096 | if (rand_val > 50) state->stats.bullet_spread += get_stat_increment(BULLET_SPREAD); 3097 | state->stats.bullet_damage += get_stat_increment(BULLET_DAMAGE); 3098 | state->stats.bullet_penetration += get_stat_increment(BULLET_PENETRATION); 3099 | state->upgrades.bullet_level += 1; 3100 | 3101 | toast("Bullet upgraded"); 3102 | break; 3103 | } 3104 | case SPLINTER_UPGRADE: { 3105 | state->stats.splinter_count += get_stat_increment(SPLINTER_COUNT); 3106 | if (rand_val > 50) state->stats.splinter_interval += get_stat_increment(SPLINTER_INTERVAL); 3107 | if (rand_val > 50) state->stats.splinter_range += get_stat_increment(SPLINTER_RANGE); 3108 | state->stats.splinter_damage += get_stat_increment(SPLINTER_DAMAGE); 3109 | state->stats.splinter_penetration += get_stat_increment(SPLINTER_PENETRATION); 3110 | state->upgrades.splinter_level += 1; 3111 | 3112 | (state->upgrades.splinter_level <= 1) 3113 | ? toast("Splinter unlocked!") 3114 | : toast("Splinter upgraded"); 3115 | break; 3116 | } 3117 | case SPIKE_UPGRADE: { 3118 | state->stats.spike_count += get_stat_increment(SPIKE_COUNT); 3119 | if (rand_val > 50) state->stats.spike_interval += get_stat_increment(SPIKE_INTERVAL); 3120 | state->stats.spike_speed += get_stat_increment(SPIKE_SPEED); 3121 | state->stats.spike_damage += get_stat_increment(SPIKE_DAMAGE); 3122 | state->upgrades.spike_level += 1; 3123 | 3124 | (state->upgrades.spike_level <= 1) 3125 | ? toast("Spike unlocked!") 3126 | : toast("Spike upgraded"); 3127 | break; 3128 | } 3129 | case FLAME_UPGRADE: { 3130 | if (rand_val > 50) state->stats.flame_interval += get_stat_increment(FLAME_INTERVAL); 3131 | state->stats.flame_lifetime += get_stat_increment(FLAME_LIFETIME); 3132 | if (rand_val > 50) state->stats.flame_spread += get_stat_increment(FLAME_SPREAD); 3133 | state->stats.flame_distance += get_stat_increment(FLAME_DISTANCE); 3134 | state->stats.flame_damage += get_stat_increment(FLAME_DAMAGE); 3135 | state->upgrades.flame_level += 1; 3136 | 3137 | (state->upgrades.flame_level <= 1) 3138 | ? toast("Flame unlocked!") 3139 | : toast("Flame upgraded"); 3140 | break; 3141 | } 3142 | case FROST_UPGRADE: { 3143 | if (rand_val > 50) state->stats.frost_wave_interval += get_stat_increment(FROST_WAVE_INTERVAL); 3144 | if (rand_val > 50) state->stats.frost_wave_lifetime += get_stat_increment(FROST_WAVE_LIFETIME); 3145 | state->stats.frost_wave_damage += get_stat_increment(FROST_WAVE_DAMAGE); 3146 | state->upgrades.frost_wave_level += 1; 3147 | 3148 | (state->upgrades.frost_wave_level <= 1) 3149 | ? toast("Frost wave unlocked!") 3150 | : toast("Frost wave upgraded"); 3151 | break; 3152 | } 3153 | case ORBS_UPGRADE: { 3154 | if (rand_val > 50) state->stats.orbs_interval += get_stat_increment(ORBS_INTERVAL); 3155 | state->stats.orbs_damage += get_stat_increment(ORBS_DAMAGE); 3156 | state->stats.orbs_count += get_stat_increment(ORBS_COUNT); 3157 | if (rand_val > 70) state->stats.orbs_size += get_stat_increment(ORBS_SIZE); 3158 | state->upgrades.orbs_level += 1; 3159 | 3160 | (state->upgrades.orbs_level <= 1) 3161 | ? toast("Orbs unlocked!") 3162 | : toast("Orbs upgraded"); 3163 | break; 3164 | } 3165 | case SPEED_UPGRADE: { 3166 | state->stats.player_speed += get_stat_increment(PLAYER_SPEED); 3167 | state->upgrades.speed_level += 1; 3168 | toast("Speed upgraded"); 3169 | break; 3170 | } 3171 | case SHINY_UPGRADE: { 3172 | state->stats.shiny_chance += get_stat_increment(SHINY_CHANCE); 3173 | state->upgrades.shiny_level += 1; 3174 | toast("Shiny Probability upgraded"); 3175 | break; 3176 | } 3177 | default: 3178 | printe(TextFormat("Unknown upgrade: %d", type)); 3179 | break; 3180 | } 3181 | } 3182 | 3183 | int get_level_mana_threshold(int level) { 3184 | return (int) ((5 * level) + (level * 4)); 3185 | } 3186 | 3187 | // MARK: :button :btn 3188 | 3189 | bool main_menu_button(Button btn) { 3190 | NPatchInfo patch_info = { 3191 | .source = {0, 16 * 12, 48, 48}, 3192 | .left = 16, .top = 16, .right = 16, .bottom = 16, 3193 | .layout = NPATCH_NINE_PATCH 3194 | }; 3195 | Vec2 text_size = MeasureTextEx(state->custom_font, btn.label, btn.font_size, 2); 3196 | Vec2 text_pos = { btn.pos.x + (btn.size.x - text_size.x) / 2, btn.pos.y + (btn.size.y - text_size.y) / 2 }; 3197 | 3198 | bool is_mouse_hovered = CheckCollisionPointRec( 3199 | GetMousePosition(), (Rect) {btn.pos.x, btn.pos.y, btn.size.x, btn.size.y}); 3200 | DrawTextureNPatch( 3201 | state->sprite_sheet, 3202 | patch_info, 3203 | (Rect) { btn.pos.x, btn.pos.y, btn.size.x, btn.size.y }, 3204 | Vector2Zero(), 0.0f, COLOR_WHITE 3205 | ); 3206 | draw_text_with_shadow(btn.label, text_pos, btn.font_size, COLOR_WHITE, COLOR_CEMENT); 3207 | 3208 | return is_mouse_hovered && IsMouseButtonPressed(0); 3209 | } 3210 | 3211 | bool upgrade_menu_button(Button btn) { 3212 | // 9patch 3213 | NPatchInfo patch_info = { 3214 | .source = {0, 16 * 12, 48, 48}, 3215 | .left = 16, .top = 16, .right = 16, .bottom = 16, 3216 | .layout = NPATCH_NINE_PATCH 3217 | }; 3218 | 3219 | int padding = 1; 3220 | DrawRectangleV( 3221 | Vector2SubtractValue(btn.pos, padding), 3222 | Vector2AddValue(btn.size, padding * 2), 3223 | COLOR_DARK_BLUE 3224 | ); 3225 | DrawTextureNPatch( 3226 | state->sprite_sheet, 3227 | patch_info, 3228 | (Rect) { btn.pos.x, btn.pos.y, btn.size.x, btn.size.y }, 3229 | Vector2Zero(), 0.0f, COLOR_WHITE 3230 | ); 3231 | 3232 | Vec2 text_size = MeasureTextEx(state->custom_font, btn.label, btn.font_size, 2); 3233 | Vec2 text_pos = { btn.pos.x + (btn.size.x - text_size.x) / 2, btn.pos.y + (btn.size.y - text_size.y) / 2 }; 3234 | DrawTextEx(state->custom_font, btn.label, text_pos, btn.font_size, 2, btn.fg); 3235 | 3236 | bool is_mouse_hovered = CheckCollisionPointRec( 3237 | GetMousePosition(), (Rect) { btn.pos.x, btn.pos.y, btn.size.x, btn.size.y }); 3238 | return is_mouse_hovered && IsMouseButtonPressed(0); 3239 | } 3240 | 3241 | bool button(Button btn) { 3242 | Vec2 text_size = MeasureTextEx(state->custom_font, btn.label, btn.font_size, 2); 3243 | Vec2 text_pos = { btn.pos.x + (btn.size.x - text_size.x) / 2, btn.pos.y + (btn.size.y - text_size.y) / 2 }; 3244 | Vec2 pos_offset = Vector2AddValue(btn.pos, 3); 3245 | Vec2 pos_offset_rev = Vector2SubtractValue(btn.pos, 3); 3246 | Vec2 size_offset = Vector2AddValue(btn.size, 6); 3247 | 3248 | bool is_mouse_hovered = CheckCollisionPointRec( 3249 | GetMousePosition(), (Rect) {btn.pos.x, btn.pos.y, btn.size.x, btn.size.y}); 3250 | 3251 | DrawRectangleV( 3252 | is_mouse_hovered ? pos_offset_rev : pos_offset, 3253 | is_mouse_hovered ? size_offset : btn.size, 3254 | is_mouse_hovered ? COLOR_WHITE : btn.shadow 3255 | ); 3256 | DrawRectangleV(btn.pos, btn.size, btn.bg); 3257 | DrawTextEx(state->custom_font, btn.label, text_pos, btn.font_size, 2, btn.fg); 3258 | 3259 | return is_mouse_hovered && IsMouseButtonPressed(0); 3260 | } 3261 | 3262 | //:slider 3263 | void slider(float ypos, const char *label, float *slider_offset, bool *is_drag) { 3264 | float ww = (float) GetScreenWidth(); 3265 | float wh = (float) GetScreenHeight(); 3266 | Vec2 slider_size = { 300, 10 }; 3267 | Vec2 handle_size = { 10, 30 }; 3268 | float max_offset_val = slider_size.x - handle_size.x; 3269 | int font_size = 35; 3270 | 3271 | Vec2 pos = { ww / 2 - slider_size.x / 2, wh * ypos }; 3272 | Vec2 text_pos = { pos.x, pos.y + 40 }; 3273 | Vec2 handle_pos = { 3274 | pos.x + (*slider_offset * max_offset_val), 3275 | pos.y - handle_size.y / 2 + slider_size.y / 2 3276 | }; 3277 | 3278 | if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { 3279 | Vec2 mouse_pos = GetMousePosition(); 3280 | Rectangle handle_rect = { handle_pos.x, handle_pos.y, handle_size.x, handle_size.y }; 3281 | 3282 | if (*is_drag || CheckCollisionPointRec(mouse_pos, handle_rect)) { 3283 | *is_drag = true; 3284 | *slider_offset = Clamp( 3285 | mouse_pos.x - pos.x - handle_size.x / 2, 0, max_offset_val); 3286 | *slider_offset /= max_offset_val; 3287 | } 3288 | } else { 3289 | *is_drag = false; 3290 | } 3291 | 3292 | DrawTextEx(state->custom_font, label, text_pos, font_size, 2, COLOR_WHITE); 3293 | DrawTextEx( 3294 | state->custom_font, 3295 | TextFormat("%d%%", (int) (*slider_offset * 100)), 3296 | (Vec2) { text_pos.x, text_pos.y + 40 }, 3297 | font_size, 2, COLOR_WHITE 3298 | ); 3299 | DrawRectangleV(pos, slider_size, COLOR_BROWN); 3300 | DrawRectangleV( 3301 | (Vec2) {pos.x + (*slider_offset * max_offset_val), handle_pos.y}, 3302 | handle_size, COLOR_WHITE 3303 | ); 3304 | } 3305 | 3306 | void toast(const char *message) { 3307 | if (state->toasts_count >= MAX_NUM_TOASTS) { 3308 | return; 3309 | } 3310 | 3311 | state->toasts[state->toasts_count] = (Toast) { 3312 | .message = message, 3313 | .spawn_ts = get_current_time_millis() 3314 | }; 3315 | state->toasts_count += 1; 3316 | } 3317 | 3318 | void draw_text_with_shadow(const char *text, Vec2 pos, int size, Color fg, Color bg) { 3319 | Vec2 pos_offset = Vector2AddValue(pos, 2); 3320 | DrawTextEx(state->custom_font, text, pos_offset, size, 2, bg); 3321 | DrawTextEx(state->custom_font, text, pos, size, 2, fg); 3322 | } 3323 | 3324 | // MARK: :utils 3325 | 3326 | int get_window_width() { 3327 | // return GetScreenWidth(); 3328 | return VIRT_WIDTH; 3329 | } 3330 | 3331 | int get_window_height() { 3332 | // return GetScreenHeight(); 3333 | return VIRT_HEIGHT; 3334 | } 3335 | 3336 | QRect get_visible_rect(Vec2 center, float zoom) { 3337 | float view_width = get_window_width(); 3338 | float view_height = get_window_height(); 3339 | return (QRect) { center.x, center.y, view_width + 10, view_height + 10 }; 3340 | } 3341 | 3342 | int get_current_time_millis() { 3343 | return GetTime() * 1000; 3344 | } 3345 | 3346 | bool is_vec2_zero(Vec2 vec) { 3347 | return vec.x == 0.0f && vec.y == 0.0f; 3348 | } 3349 | 3350 | Vec2 rotate_vector(Vec2 v, float angle) { 3351 | float angle_rad = angle * DEG2RAD; 3352 | float cosa = cosf(angle_rad); 3353 | float sina = sinf(angle_rad); 3354 | return (Vec2) { 3355 | v.x * cosa - v.y * sina, 3356 | v.x * sina + v.y * cosa 3357 | }; 3358 | } 3359 | 3360 | Vec2 get_line_center(Vec2 a, Vec2 b) { 3361 | return (Vec2) { 3362 | (a.x + b.x) / 2, 3363 | (a.y + b.y) / 2 3364 | }; 3365 | } 3366 | 3367 | Vec2 get_rand_unit_vec2() { 3368 | Vec2 rand_vec = (Vec2) { GetRandomValue(-100, 100), GetRandomValue(-100, 100) }; 3369 | return Vector2Normalize(rand_vec); 3370 | } 3371 | 3372 | bool is_rect_contains_point(QRect rect, QPoint pt) { 3373 | return (pt.x >= rect.x - rect.w && pt.x <= rect.x + rect.w) && 3374 | (pt.y >= rect.y - rect.h && pt.y <= rect.y + rect.h); 3375 | } 3376 | 3377 | bool is_rect_overlap(QRect first, QRect second) { 3378 | return !( 3379 | first.x - first.w > second.x + second.w || 3380 | first.x + first.w < second.x - second.w || 3381 | first.y - first.h > second.y + second.h || 3382 | first.y + first.h < second.y - second.h 3383 | ); 3384 | } 3385 | 3386 | bool are_colors_equal(Color a, Color b) { 3387 | return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; 3388 | } 3389 | 3390 | Vec2 get_rand_pos_around_point(Vec2 pt, float min_dist, float max_dist) { 3391 | float angle = GetRandomValue(0, 360) * (PI / 180.0f); 3392 | float distance = min_dist + (float) GetRandomValue(0, 100) / 100.0f * (max_dist - min_dist); 3393 | 3394 | return (Vec2) { 3395 | pt.x + distance * cosf(angle), 3396 | pt.y + distance * sinf(angle) 3397 | }; 3398 | } 3399 | 3400 | Vec2 point_at_dist(Vec2 pt, Vec2 dir, float dist) { 3401 | return (Vec2) { pt.x + dir.x * dist, pt.y + dir.y * dist }; 3402 | } 3403 | 3404 | bool is_point_in_triangle(Vec2 pt, Vec2 a, Vec2 b, Vec2 c) { 3405 | // Barycentric weights 3406 | // https://en.wikipedia.org/wiki/Barycentric_coordinate_system 3407 | float w1 = ((b.y - a.y) * (pt.x - a.x) - (b.x - a.x) * (pt.y - a.y)) / 3408 | ((b.y - a.y) * (c.x - a.x) - (b.x - a.x) * (c.y - a.y)); 3409 | float w2 = ((c.y - a.y) * (pt.x - a.x) - (c.x - a.x) * (pt.y - a.y)) / 3410 | ((c.y - a.y) * (b.x - a.x) - (c.x - a.x) * (b.y - a.y)); 3411 | return w1 >= 0 && w2 >= 0 && (w1 + w2) <= 1; 3412 | } 3413 | 3414 | float vec2_to_angle(Vec2 dir) { 3415 | float angle = atan2f(-dir.y, dir.x) * RAD2DEG; 3416 | if (angle < 0) angle += 360.0f; 3417 | return angle; 3418 | } 3419 | 3420 | void print(const char *text) { 3421 | if (!DEBUG) return; 3422 | TraceLog(LOG_INFO, text); 3423 | } 3424 | 3425 | void printe(const char *text) { 3426 | TraceLog(LOG_ERROR, text); 3427 | } 3428 | 3429 | // MARK: :ana 3430 | 3431 | void* post_analytics_thread(void* arg) { 3432 | AnalyticsType type = *(AnalyticsType*)arg; 3433 | free(arg); 3434 | 3435 | char url[256] = SERVER_URL; 3436 | switch (type) { 3437 | case GAME_OPEN: 3438 | sprintf(url, "%s/game-open", SERVER_URL); 3439 | break; 3440 | case GAME_START: 3441 | sprintf(url, "%s/game-start", SERVER_URL); 3442 | break; 3443 | case GAME_OVER: 3444 | sprintf(url, "%s/game-over", SERVER_URL); 3445 | break; 3446 | } 3447 | 3448 | if (DEBUG) { 3449 | sprintf(url, "%s/debug", url); 3450 | } else if (IS_MOBILE) { 3451 | sprintf(url, "%s/mobile", url); 3452 | } 3453 | 3454 | #ifndef WASM 3455 | { 3456 | CURL *curl = curl_easy_init(); 3457 | if (!curl) { 3458 | printe("Curl init failed"); 3459 | return NULL; 3460 | } 3461 | curl_easy_setopt(curl, CURLOPT_URL, url); 3462 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 3463 | CURLcode res = curl_easy_perform(curl); 3464 | if (res != CURLE_OK) { 3465 | printe(TextFormat("curl failed: %s\n", curl_easy_strerror(res))); 3466 | } 3467 | curl_easy_cleanup(curl); 3468 | } 3469 | #else 3470 | { 3471 | // WASM API GET request using emscripten_fetch 3472 | emscripten_fetch_attr_t attr; 3473 | emscripten_fetch_attr_init(&attr); 3474 | strcpy(attr.requestMethod, "GET"); 3475 | // no success/failure callbacks 3476 | // emscripten will automatically clean up the fetch resources 3477 | attr.onsuccess = NULL; 3478 | attr.onerror = NULL; 3479 | attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY; 3480 | emscripten_fetch(&attr, url); 3481 | } 3482 | #endif 3483 | 3484 | return NULL; 3485 | } 3486 | 3487 | void post_analytics_async(AnalyticsType type) { 3488 | #ifndef WASM 3489 | { 3490 | pthread_t thread; 3491 | AnalyticsType* data = malloc(sizeof(AnalyticsType)); 3492 | if (!data) { 3493 | printe("malloc for thread failed"); 3494 | return; 3495 | } 3496 | *data = type; 3497 | 3498 | if (pthread_create(&thread, NULL, post_analytics_thread, data) != 0) { 3499 | printe("failed to create thread"); 3500 | free(data); 3501 | } else { 3502 | // automatically cleans up resources when thread exits 3503 | pthread_detach(thread); 3504 | } 3505 | } 3506 | #else 3507 | { 3508 | AnalyticsType* data = malloc(sizeof(AnalyticsType)); 3509 | if (!data) { 3510 | printe("malloc for thread failed"); 3511 | return; 3512 | } 3513 | *data = type; 3514 | post_analytics_thread(data); 3515 | } 3516 | #endif 3517 | } 3518 | 3519 | // MARK: :main 3520 | 3521 | void game_update() { 3522 | BeginDrawing(); 3523 | 3524 | bool in_game = state->screen != MAIN_MENU; 3525 | if (in_game) { 3526 | update_game(); 3527 | draw_game(); 3528 | } 3529 | 3530 | if (state->screen == MAIN_MENU) { 3531 | update_main_menu_enemies(); 3532 | draw_main_menu(); 3533 | } 3534 | 3535 | handle_debug_inputs(); 3536 | handle_extra_inputs(); 3537 | EndDrawing(); 3538 | } 3539 | 3540 | #ifndef WASM 3541 | int main(void) { 3542 | // Init Window 3543 | // use FLAG_VSYNC_HINT for vsync 3544 | SetConfigFlags(FLAG_WINDOW_RESIZABLE); 3545 | InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "cgame1"); 3546 | SetWindowMinSize(VIRT_WIDTH, VIRT_HEIGHT); 3547 | SetTargetFPS(FPS); 3548 | 3549 | // disable escape key for non debug builds 3550 | if (!DEBUG) SetExitKey(KEY_NULL); 3551 | 3552 | gamestate_create(); 3553 | while (!WindowShouldClose()) { 3554 | if (IsWindowResized() || (state->is_fullscreen != IsWindowFullscreen())) { 3555 | handle_window_resize(); 3556 | state->is_fullscreen = IsWindowFullscreen(); 3557 | } 3558 | 3559 | game_update(); 3560 | } 3561 | 3562 | gamestate_destroy(); 3563 | CloseWindow(); 3564 | return 0; 3565 | } 3566 | #endif 3567 | -------------------------------------------------------------------------------- /game.h: -------------------------------------------------------------------------------- 1 | #ifndef GAME_H 2 | #define GAME_H 3 | 4 | // MARK: :flags 5 | 6 | #define IS_MOBILE false 7 | #define DEBUG true 8 | #define GOD false 9 | 10 | // MARK: :configs 11 | 12 | #define VERSION "v0.0.1" 13 | #if IS_MOBILE 14 | #define VIRT_WIDTH 144 15 | #define VIRT_HEIGHT 320 16 | #define SCREEN_WIDTH VIRT_WIDTH * 3 17 | #define SCREEN_HEIGHT VIRT_HEIGHT * 3 18 | #else 19 | #define VIRT_WIDTH 320 20 | #define VIRT_HEIGHT 180 21 | #define SCREEN_WIDTH VIRT_WIDTH * 4 22 | #define SCREEN_HEIGHT VIRT_HEIGHT * 4 23 | #endif 24 | #define FPS 144 25 | 26 | #define TILE_SIZE 16 27 | #define DEFAULT_SPRITE_SCALE 1 28 | #define SPRITE_SHEET_PATH "assets/proj.png" 29 | #define CUSTOM_FONT_PATH "assets/alagard.ttf" 30 | #define SOUND_BG_1 "assets/sounds/fright_bg.ogg" 31 | #define SOUND_BULLET_FIRE "assets/sounds/bullet_fire.ogg" 32 | #define SOUND_SPLINTER "assets/sounds/splinter.wav" 33 | #define SOUND_FROST_WAVE "assets/sounds/frost_wave.mp3" 34 | #define SOUND_ORB "assets/sounds/orb.wav" 35 | #define SOUND_HURT "assets/sounds/hurt.wav" 36 | #define SOUND_HEALTH_PICKUP "assets/sounds/health_pickup.wav" 37 | #define SOUND_UPGRADE "assets/sounds/upgrade.wav" 38 | #define SOUND_FLAME_THROWER "assets/sounds/flamethrower.ogg" 39 | #define SOUND_HEARTBEAT "assets/sounds/heartbeat.wav" 40 | 41 | #define WORLD_W 32 42 | #define WORLD_H 24 43 | #define NUM_DECORATIONS 25 44 | #define TOTAL_NUM_DECORATIONS 14 45 | 46 | #define MAX_PICKUPS 50000 47 | #define PICKUPS_CLEANUP_INTERVAL 1000 * 5 48 | #define PICKUPS_LIFETIME 1000 * 60 * 1 49 | 50 | #define PARTICLE_SPEED 200 51 | #define PARTICLE_LIFETIME 400 52 | #define MAX_MANA_PARTICLES 1000 53 | #define MAX_FLAME_PARTICLES 1000 54 | #define MAX_FROST_WAVE_PARTICLES 1000 55 | 56 | #define MAX_ENEMIES 50000 57 | #define MAX_MAIN_MENU_ENEMIES 200 58 | #define ENEMY_SPEED 30 59 | #define WAVE_DURATION_MILLIS 1000 * 15 60 | #define WAVE_DURATION_INCREMENT_MILLIS 1000 * 3 61 | #define QTREE_UPDATE_INTERVAL_MILLIS 100 62 | #define DEFAULT_ENEMY_SPAWN_INTERVAL_MS 1000 63 | 64 | #define MAX_BULLETS 10000 65 | #define MAX_ENEMY_BULLETS 5000 66 | #define GUN_VISION 70 67 | #define SPIKE_RADIUS 30 68 | 69 | #define POINTS_PER_QUAD 10 70 | 71 | #define TOAST_LIEFTIME_MS 1500 72 | #define MAX_NUM_TOASTS 15 73 | 74 | #define MAX_PLAYER_HEALTH 500.0f 75 | 76 | #define SERVER_URL "" 77 | 78 | // MARK: :colors 79 | 80 | #define COLOR_DARK_BLUE (Color) { 16, 20, 31, 255 } 81 | #define COLOR_BROWN (Color) { 123, 105, 96, 255 } 82 | #define COLOR_WHITE (Color) { 255, 248, 237, 255 } 83 | #define COLOR_RED (Color) { 255, 41, 97, 255 } 84 | #define COLOR_SKY_BLUE (Color) { 81, 202, 243, 255 } 85 | #define COLOR_CEMENT (Color) { 53, 52, 65, 255 } 86 | #define COLOR_LIGHT_PINK (Color) { 243, 216, 216, 255 } 87 | 88 | #define COLOR_FLASH_HURT (Color) { 212, 129, 129, 255 } 89 | #define COLOR_FLASH_FROST COLOR_SKY_BLUE 90 | 91 | // MARK: :alias 92 | 93 | typedef Vector2 Vec2; 94 | typedef Rectangle Rect; 95 | 96 | // MARK: :enums 97 | 98 | typedef enum { 99 | MAIN_MENU, 100 | UPGRADE_MENU, 101 | IN_GAME, 102 | PAUSE_MENU, 103 | DEATH 104 | } GameScreen; 105 | 106 | typedef enum { 107 | BAT, 108 | RAM, 109 | MAGE, 110 | REAPER, 111 | DEMON, 112 | DEMON_PUP, 113 | } EnemyType; 114 | 115 | typedef enum { 116 | // One shot bullets 117 | BULLET, 118 | SPLINTER, 119 | 120 | // Revolvers 121 | SPIKE, 122 | 123 | // Particle effects 124 | FLAME, 125 | FROST_WAVE, 126 | FIRE_WAVE, 127 | 128 | // Area 129 | ORBS, 130 | METEOR, 131 | GARLIC, 132 | 133 | // Enemy Attacks 134 | MAGE_BULLET, 135 | DEMON_BULLET, 136 | } AttackType; 137 | 138 | // :shop 139 | typedef enum { 140 | BULLET_INTERVAL, 141 | BULLET_COUNT, 142 | BULLET_SPREAD, 143 | BULLET_RANGE, 144 | BULLET_DAMAGE, 145 | BULLET_PENETRATION, 146 | 147 | SPLINTER_INTERVAL, 148 | SPLINTER_RANGE, 149 | SPLINTER_COUNT, 150 | SPLINTER_DAMAGE, 151 | SPLINTER_PENETRATION, 152 | 153 | SPIKE_INTERVAL, 154 | SPIKE_SPEED, 155 | SPIKE_COUNT, 156 | SPIKE_DAMAGE, 157 | 158 | FLAME_INTERVAL, 159 | FLAME_LIFETIME, 160 | FLAME_SPREAD, 161 | FLAME_DISTANCE, 162 | FLAME_DAMAGE, 163 | 164 | FROST_WAVE_INTERVAL, 165 | FROST_WAVE_LIFETIME, 166 | FROST_WAVE_DAMAGE, 167 | 168 | ORBS_INTERVAL, 169 | ORBS_DAMAGE, 170 | ORBS_COUNT, 171 | ORBS_SIZE, 172 | 173 | PLAYER_SPEED, 174 | SHINY_CHANCE 175 | } ShopUpgradeType; 176 | 177 | typedef enum { 178 | MANA, 179 | MANA_SHINY, 180 | HEALTH, 181 | MAGNET 182 | } PickupType; 183 | 184 | typedef enum { 185 | /** 186 | * When adding a new upgrade, 187 | * - add the enum value here 188 | * - add the new value to the draw switch statements 189 | * - actually call the draw func when showing options 190 | * - add the new options to the randomization list 191 | * - ensure the stats are initialized 192 | */ 193 | BULLET_UPGRADE, 194 | SPLINTER_UPGRADE, 195 | SPIKE_UPGRADE, 196 | FLAME_UPGRADE, 197 | FROST_UPGRADE, 198 | ORBS_UPGRADE, 199 | SPEED_UPGRADE, 200 | SHINY_UPGRADE, 201 | } UpgradeType; 202 | 203 | typedef enum { 204 | GAME_OPEN, 205 | GAME_START, 206 | GAME_OVER, 207 | } AnalyticsType; 208 | 209 | // MARK: :structs 210 | 211 | typedef struct { 212 | float x, y, w, h; 213 | } QRect; 214 | 215 | typedef struct { 216 | float x, y; 217 | int id; 218 | } QPoint; 219 | 220 | typedef struct QTree_ { 221 | bool is_divided; 222 | QRect boundary; 223 | QPoint *points; 224 | int num_points; 225 | 226 | struct QTree_ *tl; 227 | struct QTree_ *tr; 228 | struct QTree_ *bl; 229 | struct QTree_ *br; 230 | } QTree; 231 | 232 | // :bullet 233 | typedef struct { 234 | Vec2 pos; 235 | Vec2 direction; 236 | float strength; 237 | int penetration; 238 | int spawnTs; 239 | int speed; 240 | AttackType type; 241 | 242 | // For revolving type bullets 243 | float angle; 244 | } Bullet; 245 | 246 | typedef struct { 247 | EnemyType type; 248 | Vec2 pos; 249 | float health; 250 | float damage; 251 | float speed; 252 | int spawn_ts; 253 | bool is_shiny; 254 | 255 | // Flash 256 | bool is_frozen; 257 | int frozen_ts; 258 | bool is_taking_damage; 259 | int damage_ts; 260 | 261 | // Enemy type specific 262 | bool is_player_found; 263 | int player_found_ts; 264 | // Used by RAM 265 | Vec2 charge_dir; 266 | // Used by DEMON 267 | int last_spawn_ts; 268 | } Enemy; 269 | 270 | typedef struct { 271 | Vec2 pos; 272 | int decoration_idx; 273 | } Decoration; 274 | 275 | typedef struct { 276 | Vec2 pos; 277 | PickupType type; 278 | int spawn_ts; 279 | bool is_collected; 280 | } Pickup; 281 | 282 | typedef struct { 283 | Vec2 pos; 284 | Vec2 dir; 285 | int spawn_ts; 286 | int lifetime; 287 | } Particle; 288 | 289 | typedef struct { 290 | const char *label; 291 | Vec2 pos; 292 | Vec2 size; 293 | Color bg; 294 | Color fg; 295 | Color shadow; 296 | float font_size; 297 | } Button; 298 | 299 | // :timers 300 | typedef struct { 301 | /** 302 | * When adding new timers, 303 | * remember to update the timer state to handle 304 | * pause/resume properly 305 | */ 306 | 307 | // General 308 | int game_start_ts; 309 | int pause_ts; 310 | int pickups_cleanup_ts; 311 | int last_hurt_sound_ts; 312 | int last_heart_spawn_ts; 313 | 314 | // Weapon 315 | int bullet_ts; 316 | int splinter_ts; 317 | int spike_ts; 318 | int flame_ts; 319 | int frost_wave_ts; 320 | int orbs_ts; 321 | 322 | // Enemy 323 | int qtree_update_ts; 324 | int enemy_spawn_ts; 325 | int enemy_spawn_interval; 326 | 327 | } Timers; 328 | 329 | // :stat 330 | typedef struct { 331 | /** 332 | * Steps to add a new shop item: 333 | * - Add a new enum field 334 | * - Updated all :stat functions with the new enum 335 | * - Add a new stat var 336 | * - Init the newly added stat var 337 | * - Add item to :shop items list 338 | * - Add a new clause to the :shop switch 339 | */ 340 | float bullet_interval; 341 | float bullet_count; 342 | float bullet_spread; 343 | float bullet_range; 344 | float bullet_damage; 345 | float bullet_penetration; 346 | 347 | float splinter_interval; 348 | float splinter_range; 349 | float splinter_count; 350 | float splinter_damage; 351 | float splinter_penetration; 352 | 353 | float spike_interval; 354 | float spike_speed; 355 | float spike_count; 356 | float spike_damage; 357 | 358 | float flame_interval; 359 | float flame_lifetime; 360 | float flame_spread; 361 | float flame_distance; 362 | float flame_damage; 363 | 364 | float frost_wave_interval; 365 | float frost_wave_lifetime; 366 | float frost_wave_damage; 367 | 368 | float orbs_interval; 369 | float orbs_damage; 370 | float orbs_count; 371 | float orbs_size; 372 | 373 | float player_speed; 374 | float shiny_chance; 375 | } Stats; 376 | 377 | typedef struct { 378 | int bullet_level; 379 | int splinter_level; 380 | int spike_level; 381 | int flame_level; 382 | int frost_wave_level; 383 | int orbs_level; 384 | int speed_level; 385 | int shiny_level; 386 | } Upgrades; 387 | 388 | typedef struct { 389 | const char *message; 390 | int spawn_ts; 391 | } Toast; 392 | 393 | typedef struct { 394 | int ct; 395 | int num_enemies_drawn; 396 | } Temp; 397 | 398 | // MARK: :gamestate 399 | 400 | typedef struct { 401 | // Raylib 402 | Camera2D camera; 403 | Texture2D sprite_sheet; 404 | Font custom_font; 405 | RenderTexture2D render_texture; 406 | Rect source_rect; 407 | Rect dest_rect; 408 | 409 | // Music, Sound 410 | Music sound_bg; 411 | Music sound_heartbeat; 412 | Sound sound_upgrade; 413 | Sound sound_bullet_fire; 414 | Sound sound_splinter; 415 | Sound sound_flamethrower; 416 | Sound sound_frost_wave; 417 | Sound sound_orb; 418 | Sound sound_hurt; 419 | Sound sound_health_pickup; 420 | float sound_intensity; 421 | 422 | // General 423 | GameScreen screen; 424 | Timers timer; 425 | Stats stats; 426 | Upgrades upgrades; 427 | Upgrades available_upgrades; 428 | Toast *toasts; 429 | int toasts_count; 430 | int wave_index; 431 | int player_level; 432 | long kill_count; 433 | bool is_fullscreen; 434 | 435 | // Player 436 | Vec2 player_pos; 437 | float player_health; 438 | Vec2 player_heading_dir; 439 | bool is_joystick_enabled; 440 | Vec2 touch_down_pos; 441 | Vec2 joystick_direction; 442 | int mana_count; 443 | Vec2 *mana_particles; 444 | int mana_particles_count; 445 | int death_ts; 446 | 447 | // Weapon 448 | Bullet *bullets; 449 | int bullet_count; 450 | Bullet *enemy_bullets; 451 | int enemy_bullet_count; 452 | Particle *flame_particles; 453 | int flame_particles_count; 454 | Particle *frost_wave_particles; 455 | int frost_wave_particles_count; 456 | 457 | // Enemy 458 | Enemy *enemies; 459 | int enemy_count; 460 | QTree *enemy_qtree; 461 | int num_enemies_per_tick; 462 | 463 | // World 464 | Rect world_dims; 465 | Decoration *decorations; 466 | Pickup *pickups; 467 | int pickups_count; 468 | QTree *pickups_qtree; 469 | 470 | // UI 471 | float master_volume; 472 | bool is_master_volume_dragging; 473 | Enemy *main_menu_enemies; 474 | int main_menu_enemies_count; 475 | 476 | // Others 477 | QPoint *query_points; 478 | int num_query_points; 479 | 480 | // Debug 481 | bool is_show_debug_gui; 482 | Temp temp; 483 | 484 | } GameState; 485 | 486 | // MARK: :func 487 | 488 | void gamestate_create(); 489 | void gamestate_destroy(); 490 | 491 | void pause_timers(); 492 | void resume_timers(); 493 | 494 | // :draw :render 495 | void draw_main_menu(); 496 | void draw_game(); 497 | void draw_ui(); 498 | void draw_upgrade_menu(); 499 | void draw_upgrade_option(UpgradeType type, int level, Vec2 *pos); 500 | void draw_player(); 501 | void draw_enemies(); 502 | void draw_particles(); 503 | void draw_bullets(); 504 | void draw_decorations(); 505 | void draw_pickups(); 506 | void draw_toasts(); 507 | void draw_health_bar(Vec2 pos, float health, float max_health); 508 | void draw_sprite(Texture2D *sprite_sheet, Vec2 tile, Vec2 pos, float scale, float rotation, Color tint); 509 | void draw_spritev(Texture2D *sprite_sheet, Vec2 tile, Vec2 pos, float scale, float rotation, bool is_flip_x, bool is_bob, Color tint, int bob_id); 510 | 511 | // :update 512 | void update_game(); 513 | void update_player(); 514 | void update_enemies(); 515 | void update_particles(); 516 | void update_bullets(); 517 | void update_pickups(); 518 | void update_toasts(); 519 | 520 | // :sound :music 521 | void sounds_init(); 522 | void sounds_destroy(); 523 | void sounds_update(); 524 | void play_sound_modulated(Sound *sound, float volume); 525 | 526 | // :decorations 527 | void decorations_init(); 528 | void update_decorations(); 529 | 530 | // :input 531 | void handle_player_input(); 532 | void handle_debug_inputs(); 533 | void handle_extra_inputs(); 534 | void handle_window_resize(); 535 | void handle_virtual_joystick_input(); 536 | 537 | // :quadtree :qtree 538 | QTree* qtree_create(QRect boundary); 539 | void qtree_destroy(QTree *qtree); 540 | void qtree_clear(QTree *qtree); 541 | void qtree_reset_boundary(QTree *qtree, QRect rect); 542 | bool qtree_insert(QTree *qtree, QPoint pt); 543 | bool qtree_remove(QTree *qtree, QPoint pt); 544 | void qtree_query(QTree *qtree, QRect range, QPoint *result, int *num_points); 545 | void _qtree_subdivide(QTree *qtree); 546 | 547 | // :data 548 | Vec2 get_enemy_sprite_pos(EnemyType type, bool is_shiny); 549 | float get_enemy_scale(EnemyType type); 550 | float get_enemy_health(EnemyType type, bool is_shiny); 551 | float get_enemy_speed(EnemyType type); 552 | float get_enemy_damage(EnemyType type); 553 | Vec2 get_attack_sprite(AttackType type); 554 | int get_attack_range(AttackType type); 555 | int get_attack_speed(AttackType type); 556 | float get_base_stat_value(ShopUpgradeType type); 557 | float get_stat_increment(ShopUpgradeType type); 558 | 559 | // :impl 560 | EnemyType get_next_enemy_spawn_type(); 561 | int get_num_enemies_per_tick(); 562 | int get_mana_value(); 563 | PickupType get_pickup_spawn_type(bool is_shiny); 564 | void update_available_upgrades(); 565 | void upgrade_stat(UpgradeType type); 566 | int get_level_mana_threshold(int level); 567 | 568 | // :ui 569 | bool main_menu_button(Button btn); 570 | void reset_main_menu_enemies(); 571 | void update_main_menu_enemies(); 572 | bool upgrade_menu_button(Button btn); 573 | bool button(Button btn); 574 | void slider(float ypos, const char *label, float *slider_offset, bool *is_drag); 575 | void toast(const char *message); 576 | void draw_text_with_shadow(const char *text, Vec2 pos, int size, Color fg, Color bg); 577 | 578 | // :utils 579 | int get_window_width(); 580 | int get_window_height(); 581 | QRect get_visible_rect(Vec2 center, float zoom); 582 | int get_current_time_millis(); 583 | bool is_vec2_zero(Vec2 vec); 584 | Vec2 rotate_vector(Vec2 vec, float angle); 585 | Vec2 get_line_center(Vec2 a, Vec2 b); 586 | Vec2 get_rand_unit_vec2(); 587 | bool is_rect_contains_point(QRect rect, QPoint pt); 588 | bool is_rect_overlap(QRect first, QRect second); 589 | bool are_colors_equal(Color a, Color b); 590 | Vec2 get_rand_pos_around_point(Vec2 pt, float minDist, float maxDist); 591 | Vec2 point_at_dist(Vec2 pt, Vec2 dir, float dist); 592 | bool is_point_in_triangle(Vec2 pt, Vec2 a, Vec2 b, Vec2 c); 593 | float vec2_to_angle(Vec2 dir); 594 | void print(const char *text); 595 | void printe(const char *text); 596 | 597 | // :ana 598 | void* post_analytics_thread(void* arg); 599 | void post_analytics_async(AnalyticsType type); 600 | 601 | // :main 602 | void game_update(); 603 | 604 | #endif 605 | -------------------------------------------------------------------------------- /z_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RAYLIB_PATH="/Users/grey/softwares/raylib/src" 4 | 5 | # Need this for raygui to work 6 | RAYLIB_INCLUDE_PATH="/Users/grey/softwares/raylib/build/raylib/include" 7 | 8 | # Add libcurl dependency 9 | CURL_FLAGS=`pkg-config --cflags --libs libcurl` 10 | 11 | case "$1" in 12 | speed|release) 13 | echo "Building for release" 14 | gcc -O3 -march=native -s game.c -I$RAYLIB_INCLUDE_PATH `pkg-config --libs --cflags raylib` $CURL_FLAGS -o build/game_release && ./build/game_release 15 | ;; 16 | 17 | wasm) 18 | # we use -DWASM to pass a wasm flag to C 19 | # FETCH needed for emscripten api calls to work 20 | echo "Building for wasm" 21 | emcc -o build/wasm/game.html game.c web_game.c -Wall -std=c99 -D_DEFAULT_SOURCE -Wno-missing-braces -Wunused-result \ 22 | -O3 -DWASM -I$RAYLIB_INCLUDE_PATH -I "$RAYLIB_PATH" -I "$RAYLIB_PATH/external" \ 23 | -L. -L "$RAYLIB_PATH" \ 24 | --preload-file assets -s ASYNCIFY \ 25 | -s USE_GLFW=3 -s TOTAL_MEMORY=1073741824 -s FORCE_FILESYSTEM=1 \ 26 | -s MAX_WEBGL_VERSION=2 -s USE_WEBGL2=1 \ 27 | -s FETCH=1 \ 28 | --shell-file "$RAYLIB_PATH/minshell.html" "$RAYLIB_PATH/web/libraylib.a" \ 29 | -s 'EXPORTED_FUNCTIONS=["_free","_malloc","_main"]' -s EXPORTED_RUNTIME_METHODS=ccall 30 | ;; 31 | 32 | *) 33 | echo "Default build" 34 | gcc -g -O0 -Wall game.c -I$RAYLIB_INCLUDE_PATH `pkg-config --libs --cflags raylib` $CURL_FLAGS -o build/game_debug && ./build/game_debug 35 | ;; 36 | esac --------------------------------------------------------------------------------