├── LICENSE ├── README.md └── Source ├── PICO-8 ├── Classic.cs ├── Emulator.cs ├── Graphics │ ├── atlas.png │ ├── consolebg.png │ ├── font.png │ └── logo.png └── Readme.md └── Player ├── Player.cs └── Readme.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Noel Berry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Celeste 2 | This repo is used to track issues and bugs with [Celeste](http://www.celestegame.com/). Before submitting an issue, please check the [Celeste changelog](http://www.celestegame.com/changelog.html) and closed issues to see if it has already been fixed. 3 | 4 | ### Info & Links 5 | - Tech support and general inquiries should be sent to our email at [contact.celestegame@gmail.com](mailto:contact.celestegame@gmail.com) 6 | - A changelog of all releases can be [found here](http://www.celestegame.com/changelog.html) 7 | 8 | ### Source Code 9 | - We also will be releasing a few of the class files from Celeste, which you will later find in this repo. They're here as a learning resource and for general interest. 10 | - The Framework Celeste was developed in is also [open source and can be found here](https://bitbucket.org/MattThorson/monocle-engine/src). 11 | - NOTE: the MIT License only applies to the code in this repo and does not include the actual commercial Celeste game or assets. 12 | -------------------------------------------------------------------------------- /Source/PICO-8/Classic.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using System.Collections.Generic; 3 | 4 | namespace Celeste.Pico8 5 | { 6 | /// 7 | /// Attempting to Copy the Celeste Classic line-for-line here. 8 | /// Obviously some differences due to converting from LUA to C# 9 | /// 10 | /// This is not how I would recommend implementing a game like this from scratch in C#! 11 | /// It's simply trying to be 1-1 with the LUA version 12 | /// 13 | public class Classic 14 | { 15 | public Emulator E; 16 | 17 | // ~ celeste ~ 18 | // matt thorson + noel berry 19 | 20 | #region "global" variables 21 | 22 | private Point room; 23 | private List objects; 24 | public int freeze; 25 | private int shake; 26 | private bool will_restart; 27 | private int delay_restart; 28 | private HashSet got_fruit; 29 | private bool has_dashed; 30 | private int sfx_timer; 31 | private bool has_key; 32 | private bool pause_player; 33 | private bool flash_bg; 34 | private int music_timer; 35 | private bool new_bg; 36 | 37 | private int k_left = 0; 38 | private int k_right = 1; 39 | private int k_up = 2; 40 | private int k_down = 3; 41 | private int k_jump = 4; 42 | private int k_dash = 5; 43 | 44 | private int frames; 45 | private int seconds; 46 | private int minutes; 47 | private int deaths; 48 | private int max_djump; 49 | private bool start_game; 50 | private int start_game_flash; 51 | 52 | #endregion 53 | 54 | #region effects 55 | 56 | private class Cloud 57 | { 58 | public float x; 59 | public float y; 60 | public float spd; 61 | public float w; 62 | } 63 | private List clouds; 64 | 65 | private class Particle 66 | { 67 | public float x; 68 | public float y; 69 | public int s; 70 | public float spd; 71 | public float off; 72 | public int c; 73 | } 74 | private List particles; 75 | 76 | private class DeadParticle 77 | { 78 | public float x; 79 | public float y; 80 | public int t; 81 | public Vector2 spd; 82 | } 83 | private List dead_particles; 84 | 85 | #endregion 86 | 87 | #region entry point 88 | 89 | public void Init(Emulator emulator) 90 | { 91 | E = emulator; 92 | 93 | room = new Point(0, 0); 94 | objects = new List(); 95 | freeze = 0; 96 | will_restart = false; 97 | delay_restart = 0; 98 | got_fruit = new HashSet(); 99 | has_dashed = false; 100 | sfx_timer = 0; 101 | has_key = false; 102 | pause_player = false; 103 | flash_bg = false; 104 | music_timer = 0; 105 | new_bg = false; 106 | 107 | frames = 0; 108 | seconds = 0; 109 | minutes = 0; 110 | deaths = 0; 111 | max_djump = 1; 112 | start_game = false; 113 | start_game_flash = 0; 114 | 115 | clouds = new List(); 116 | for (int i = 0; i <= 16; i++) 117 | clouds.Add(new Cloud() 118 | { 119 | x = E.rnd(128), 120 | y = E.rnd(128), 121 | spd = 1 + E.rnd(4), 122 | w = 32 + E.rnd(32) 123 | }); 124 | 125 | particles = new List(); 126 | for (int i = 0; i <= 32; i++) 127 | particles.Add(new Particle() 128 | { 129 | x = E.rnd(128), 130 | y = E.rnd(128), 131 | s = 0 + E.flr(E.rnd(5) / 4), 132 | spd = 0.25f + E.rnd(5), 133 | off = E.rnd(1), 134 | c = 6 + E.flr(0.5f + E.rnd(1)) 135 | }); 136 | 137 | dead_particles = new List(); 138 | 139 | title_screen(); 140 | } 141 | 142 | private void title_screen() 143 | { 144 | got_fruit = new HashSet(); 145 | frames = 0; 146 | deaths = 0; 147 | max_djump = 1; 148 | start_game = false; 149 | start_game_flash = 0; 150 | E.music(40, 0, 7); 151 | load_room(7, 3); 152 | } 153 | 154 | private void begin_game() 155 | { 156 | frames = 0; 157 | seconds = 0; 158 | minutes = 0; 159 | music_timer = 0; 160 | start_game = false; 161 | E.music(0, 0, 7); 162 | load_room(0, 0); 163 | } 164 | 165 | private int level_index() 166 | { 167 | return room.X % 8 + room.Y * 8; 168 | } 169 | 170 | private bool is_title() 171 | { 172 | return level_index() == 31; 173 | } 174 | 175 | #endregion 176 | 177 | #region objects 178 | 179 | public class player : ClassicObject 180 | { 181 | public bool p_jump = false; 182 | public bool p_dash = false; 183 | public int grace = 0; 184 | public int jbuffer = 0; 185 | public int djump; 186 | public int dash_time = 0; 187 | public int dash_effect_time = 0; 188 | public Vector2 dash_target = new Vector2(0, 0); 189 | public Vector2 dash_accel = new Vector2(0, 0); 190 | public float spr_off = 0; 191 | public bool was_on_ground; 192 | public player_hair hair; 193 | 194 | public override void init(Classic g, Emulator e) 195 | { 196 | base.init(g, e); 197 | 198 | spr = 1; 199 | djump = g.max_djump; 200 | hitbox = new Rectangle(1, 3, 6, 5); 201 | } 202 | 203 | public override void update() 204 | { 205 | if (G.pause_player) return; 206 | var input = E.btn(G.k_right) ? 1 : (E.btn(G.k_left) ? -1 : 0); 207 | 208 | // spikes collide 209 | if (G.spikes_at(x + hitbox.X, y + hitbox.Y, hitbox.Width, hitbox.Height, spd.X, spd.Y)) 210 | G.kill_player(this); 211 | 212 | // bottom death 213 | if (y > 128) 214 | G.kill_player(this); 215 | 216 | var on_ground = is_solid(0, 1); 217 | var on_ice = is_ice(0, 1); 218 | 219 | // smoke particles 220 | if (on_ground && !was_on_ground) 221 | G.init_object(new smoke(), x, y + 4); 222 | 223 | var jump = E.btn(G.k_jump) && !p_jump; 224 | p_jump = E.btn(G.k_jump); 225 | if (jump) 226 | jbuffer = 4; 227 | else if (jbuffer > 0) 228 | jbuffer--; 229 | 230 | var dash = E.btn(G.k_dash) && !p_dash; 231 | p_dash = E.btn(G.k_dash); 232 | 233 | if (on_ground) 234 | { 235 | grace = 6; 236 | if (djump < G.max_djump) 237 | { 238 | G.psfx(54); 239 | djump = G.max_djump; 240 | } 241 | } 242 | else if (grace > 0) 243 | grace--; 244 | 245 | dash_effect_time--; 246 | if (dash_time > 0) 247 | { 248 | G.init_object(new smoke(), x, y); 249 | dash_time--; 250 | spd.X = G.appr(spd.X, dash_target.X, dash_accel.X); 251 | spd.Y = G.appr(spd.Y, dash_target.Y, dash_accel.Y); 252 | } 253 | else 254 | { 255 | // move 256 | var maxrun = 1; 257 | var accel = 0.6f; 258 | var deccel = 0.15f; 259 | 260 | if (!on_ground) 261 | accel = 0.4f; 262 | else if (on_ice) 263 | { 264 | accel = 0.05f; 265 | if (input == (flipX ? -1 : 1)) // this it how it was in the pico-8 cart but is redundant? 266 | accel = 0.05f; 267 | } 268 | 269 | if (E.abs(spd.X) > maxrun) 270 | spd.X = G.appr(spd.X, E.sign(spd.X) * maxrun, deccel); 271 | else 272 | spd.X = G.appr(spd.X, input * maxrun, accel); 273 | 274 | // facing 275 | if (spd.X != 0) 276 | flipX = (spd.X < 0); 277 | 278 | // gravity 279 | var maxfall = 2f; 280 | var gravity = 0.21f; 281 | 282 | if (E.abs(spd.Y) <= 0.15f) 283 | gravity *= 0.5f; 284 | 285 | // wall slide 286 | if (input != 0 && is_solid(input, 0) && !is_ice(input, 0)) 287 | { 288 | maxfall = 0.4f; 289 | if (E.rnd(10) < 2) 290 | G.init_object(new smoke(), x + input * 6, y); 291 | } 292 | 293 | if (!on_ground) 294 | spd.Y = G.appr(spd.Y, maxfall, gravity); 295 | 296 | // jump 297 | if (jbuffer > 0) 298 | { 299 | if (grace > 0) 300 | { 301 | // normal jump 302 | G.psfx(1); 303 | jbuffer = 0; 304 | grace = 0; 305 | spd.Y = -2; 306 | G.init_object(new smoke(), x, y + 4); 307 | } 308 | else 309 | { 310 | // wall jump 311 | var wall_dir = (is_solid(-3, 0) ? -1 : (is_solid(3, 0) ? 1 : 0)); 312 | if (wall_dir != 0) 313 | { 314 | G.psfx(2); 315 | jbuffer = 0; 316 | spd.Y = -2; 317 | spd.X = -wall_dir * (maxrun + 1); 318 | if (is_ice(wall_dir * 3, 0)) 319 | G.init_object(new smoke(), x + wall_dir * 6, y); 320 | } 321 | } 322 | } 323 | 324 | // dash 325 | var d_full = 5; 326 | var d_half = d_full * 0.70710678118f; 327 | 328 | if (djump > 0 && dash) 329 | { 330 | G.init_object(new smoke(), x, y); 331 | djump --; 332 | dash_time = 4; 333 | G.has_dashed = true; 334 | dash_effect_time = 10; 335 | 336 | var dash_x_input = E.dashDirectionX(flipX ? -1 : 1); 337 | var dash_y_input = E.dashDirectionY(flipX ? -1 : 1); 338 | 339 | if (dash_x_input != 0 && dash_y_input != 0) 340 | { 341 | spd.X = dash_x_input * d_half; 342 | spd.Y = dash_y_input * d_half; 343 | } 344 | else if (dash_x_input != 0) 345 | { 346 | spd.X = dash_x_input * d_full; 347 | spd.Y = 0; 348 | } 349 | else 350 | { 351 | spd.X = 0; 352 | spd.Y = dash_y_input * d_full; 353 | } 354 | 355 | G.psfx(3); 356 | G.freeze = 2; 357 | G.shake = 6; 358 | dash_target.X = 2 * E.sign(spd.X); 359 | dash_target.Y = 2 * E.sign(spd.Y); 360 | dash_accel.X = 1.5f; 361 | dash_accel.Y = 1.5f; 362 | 363 | if (spd.Y < 0) 364 | dash_target.Y *= 0.75f; 365 | if (spd.Y != 0) 366 | dash_accel.X *= 0.70710678118f; 367 | if (spd.X != 0) 368 | dash_accel.Y *= 0.70710678118f; 369 | } 370 | else if (dash && djump <= 0) 371 | { 372 | G.psfx(9); 373 | G.init_object(new smoke(), x, y); 374 | } 375 | } 376 | 377 | // animation 378 | spr_off += 0.25f; 379 | if (!on_ground) 380 | { 381 | if (is_solid(input, 0)) 382 | spr = 5; 383 | else 384 | spr = 3; 385 | } 386 | else if (E.btn(G.k_down)) 387 | spr = 6; 388 | else if (E.btn(G.k_up)) 389 | spr = 7; 390 | else if (spd.X == 0 || (!E.btn(G.k_left) && !E.btn(G.k_right))) 391 | spr = 1; 392 | else 393 | spr = 1 + spr_off % 4; 394 | 395 | // next level 396 | if (y < -4 && G.level_index() < 30) 397 | G.next_room(); 398 | 399 | // was on the ground 400 | was_on_ground = on_ground; 401 | } 402 | public override void draw() 403 | { 404 | // clamp in screen 405 | if (x < -1 || x > 121) 406 | { 407 | x = G.clamp(x, -1, 121); 408 | spd.X = 0; 409 | } 410 | 411 | hair.draw_hair(this, flipX ? -1 : 1, djump); 412 | G.draw_player(this, djump); 413 | } 414 | } 415 | 416 | private void psfx(int num) 417 | { 418 | if (sfx_timer <= 0) 419 | E.sfx(num); 420 | } 421 | 422 | private void draw_player(ClassicObject obj, int djump) 423 | { 424 | var spritePush = 0; 425 | if (djump == 2) 426 | { 427 | if (E.flr((frames / 3) % 2) == 0) 428 | spritePush = 10 * 16; 429 | else 430 | spritePush = 9 * 16; 431 | } 432 | else if (djump == 0) 433 | { 434 | spritePush = 8 * 16; 435 | } 436 | 437 | E.spr(obj.spr + spritePush, obj.x, obj.y, 1, 1, obj.flipX, obj.flipY); 438 | } 439 | 440 | public class player_hair 441 | { 442 | private class node 443 | { 444 | public float x; 445 | public float y; 446 | public float size; 447 | } 448 | 449 | private node[] hair = new node[5]; 450 | private Emulator E; 451 | private Classic G; 452 | 453 | public player_hair(ClassicObject obj) 454 | { 455 | E = obj.E; 456 | G = obj.G; 457 | for (var i = 0; i <= 4; i++) 458 | hair[i] = new node() { x = obj.x, y = obj.y, size = E.max(1, E.min(2, 3 - i)) }; 459 | } 460 | 461 | public void draw_hair(ClassicObject obj, int facing, int djump) 462 | { 463 | var c = (djump == 1 ? 8 : (djump == 2 ? (7 + E.flr((G.frames / 3) % 2) * 4) : 12)); 464 | var last = new Vector2(obj.x + 4 - facing * 2, obj.y + (E.btn(G.k_down) ? 4 : 3)); 465 | foreach (var h in hair) 466 | { 467 | h.x += (last.X - h.x) / 1.5f; 468 | h.y += (last.Y + 0.5f - h.y) / 1.5f; 469 | E.circfill(h.x, h.y, h.size, c); 470 | last = new Vector2(h.x, h.y); 471 | } 472 | } 473 | } 474 | 475 | public class player_spawn : ClassicObject 476 | { 477 | private Vector2 target; 478 | private int state; 479 | private int delay; 480 | private player_hair hair; 481 | 482 | public override void init(Classic g, Emulator e) 483 | { 484 | base.init(g, e); 485 | 486 | spr = 3; 487 | target = new Vector2(x, y); 488 | y = 128; 489 | spd.Y = -4; 490 | state = 0; 491 | delay = 0; 492 | solids = false; 493 | hair = new player_hair(this); 494 | E.sfx(4); 495 | } 496 | public override void update() 497 | { 498 | // jumping up 499 | if (state == 0) 500 | { 501 | if (y < target.Y + 16) 502 | { 503 | state = 1; 504 | delay = 3; 505 | } 506 | } 507 | // falling 508 | else if (state == 1) 509 | { 510 | spd.Y += 0.5f; 511 | if (spd.Y > 0 && delay > 0) 512 | { 513 | spd.Y = 0; 514 | delay --; 515 | } 516 | if (spd.Y > 0 && y > target.Y) 517 | { 518 | y = target.Y; 519 | spd = new Vector2(0, 0); 520 | state = 2; 521 | delay = 5; 522 | G.shake = 5; 523 | G.init_object(new smoke(), x, y + 4); 524 | E.sfx(5); 525 | } 526 | } 527 | // landing 528 | else if (state == 2) 529 | { 530 | delay--; 531 | spr = 6; 532 | if (delay < 0) 533 | { 534 | G.destroy_object(this); 535 | var player = G.init_object(new player(), x, y); 536 | player.hair = hair; 537 | } 538 | } 539 | } 540 | public override void draw() 541 | { 542 | hair.draw_hair(this, 1, G.max_djump); 543 | G.draw_player(this, G.max_djump); 544 | } 545 | } 546 | 547 | public class spring : ClassicObject 548 | { 549 | public int hide_in = 0; 550 | private int hide_for = 0; 551 | private int delay = 0; 552 | 553 | public override void update() 554 | { 555 | if (hide_for > 0) 556 | { 557 | hide_for--; 558 | if (hide_for <= 0) 559 | { 560 | spr = 18; 561 | delay = 0; 562 | } 563 | } 564 | else if (spr == 18) 565 | { 566 | var hit = collide(0, 0); 567 | if (hit != null && hit.spd.Y >= 0) 568 | { 569 | spr = 19; 570 | hit.y = y - 4; 571 | hit.spd.X *= 0.2f; 572 | hit.spd.Y = -3; 573 | hit.djump = G.max_djump; 574 | delay = 10; 575 | G.init_object(new smoke(), x, y); 576 | 577 | // breakable below us 578 | var below = collide(0, 1); 579 | if (below != null) 580 | G.break_fall_floor(below); 581 | 582 | G.psfx(8); 583 | } 584 | } 585 | else if (delay > 0) 586 | { 587 | delay--; 588 | if (delay <= 0) 589 | spr = 18; 590 | } 591 | 592 | // begin hiding 593 | if (hide_in > 0) 594 | { 595 | hide_in--; 596 | if (hide_in <= 0) 597 | { 598 | hide_for = 60; 599 | spr = 0; 600 | } 601 | } 602 | } 603 | } 604 | 605 | private void break_spring(spring obj) 606 | { 607 | obj.hide_in = 15; 608 | } 609 | 610 | public class balloon : ClassicObject 611 | { 612 | float offset; 613 | float start; 614 | float timer; 615 | 616 | public override void init(Classic g, Emulator e) 617 | { 618 | base.init(g, e); 619 | 620 | offset = E.rnd(1f); 621 | start = y; 622 | hitbox = new Rectangle(-1, -1, 10, 10); 623 | } 624 | public override void update() 625 | { 626 | if (spr == 22) 627 | { 628 | offset += 0.01f; 629 | y = start + E.sin(offset) * 2; 630 | var hit = collide(0, 0); 631 | if (hit != null && hit.djump < G.max_djump) 632 | { 633 | G.psfx(6); 634 | G.init_object(new smoke(), x, y); 635 | hit.djump = G.max_djump; 636 | spr = 0; 637 | timer = 60; 638 | } 639 | } 640 | else if (timer > 0) 641 | timer--; 642 | else 643 | { 644 | G.psfx(7); 645 | G.init_object(new smoke(), x, y); 646 | spr = 22; 647 | } 648 | } 649 | public override void draw() 650 | { 651 | if (spr == 22) 652 | { 653 | E.spr(13 + (offset * 8) % 3, x, y + 6); 654 | E.spr(spr, x, y); 655 | } 656 | } 657 | } 658 | 659 | public class fall_floor : ClassicObject 660 | { 661 | public int state = 0; 662 | public bool solid = true; 663 | public int delay = 0; 664 | 665 | public override void update() 666 | { 667 | if (state == 0) 668 | { 669 | if (check(0, -1) || check(-1, 0) || check(1, 0)) 670 | G.break_fall_floor(this); 671 | } 672 | else if (state == 1) 673 | { 674 | delay--; 675 | if (delay <= 0) 676 | { 677 | state = 2; 678 | delay = 60; //how long it hides for 679 | collideable = false; 680 | } 681 | } 682 | else if (state == 2) 683 | { 684 | delay--; 685 | if (delay <= 0 && !check(0, 0)) 686 | { 687 | G.psfx(7); 688 | state = 0; 689 | collideable = true; 690 | G.init_object(new smoke(), x, y); 691 | } 692 | } 693 | } 694 | public override void draw() 695 | { 696 | if (state != 2) 697 | { 698 | if (state != 1) 699 | E.spr(23, x, y); 700 | else 701 | E.spr(23 + (15 - delay) / 5, x, y); 702 | } 703 | } 704 | } 705 | 706 | private void break_fall_floor(fall_floor obj) 707 | { 708 | if (obj.state == 0) 709 | { 710 | psfx(15); 711 | obj.state = 1; 712 | obj.delay = 15; //how long until it falls 713 | init_object(new smoke(), obj.x, obj.y); 714 | var hit = obj.collide(0, -1); 715 | if (hit != null) 716 | break_spring(hit); 717 | } 718 | } 719 | 720 | public class smoke : ClassicObject 721 | { 722 | public override void init(Classic g, Emulator e) 723 | { 724 | base.init(g, e); 725 | 726 | spr = 29; 727 | spd.Y = -0.1f; 728 | spd.X = 0.3f + E.rnd(0.2f); 729 | x += -1 + E.rnd(2); 730 | y += -1 + E.rnd(2); 731 | flipX = G.maybe(); 732 | flipY = G.maybe(); 733 | solids = false; 734 | } 735 | public override void update() 736 | { 737 | spr += 0.2f; 738 | if (spr >= 32) 739 | G.destroy_object(this); 740 | } 741 | } 742 | 743 | public class fruit : ClassicObject 744 | { 745 | float start; 746 | float off; 747 | 748 | public override void init(Classic g, Emulator e) 749 | { 750 | base.init(g, e); 751 | 752 | spr = 26; 753 | start = y; 754 | off = 0; 755 | } 756 | 757 | public override void update() 758 | { 759 | var hit = collide(0, 0); 760 | if (hit != null) 761 | { 762 | hit.djump = G.max_djump; 763 | G.sfx_timer = 20; 764 | E.sfx(13); 765 | G.got_fruit.Add(1 + G.level_index()); 766 | G.init_object(new lifeup(), x, y); 767 | G.destroy_object(this); 768 | Stats.Increment(Stat.PICO_BERRIES); 769 | } 770 | off++; 771 | y = start + E.sin(off / 40f) * 2.5f; 772 | } 773 | } 774 | 775 | public class fly_fruit : ClassicObject 776 | { 777 | float start; 778 | bool fly = false; 779 | float step = 0.5f; 780 | float sfx_delay = 8; 781 | 782 | public override void init(Classic g, Emulator e) 783 | { 784 | base.init(g, e); 785 | 786 | start = y; 787 | solids = false; 788 | } 789 | 790 | public override void update() 791 | { 792 | // fly away 793 | if (fly) 794 | { 795 | if (sfx_delay > 0) 796 | { 797 | sfx_delay--; 798 | if (sfx_delay <= 0) 799 | { 800 | G.sfx_timer = 20; 801 | E.sfx(14); 802 | } 803 | } 804 | spd.Y = G.appr(spd.Y, -3.5f, 0.25f); 805 | if (y < -16) 806 | G.destroy_object(this); 807 | } 808 | // wait 809 | else 810 | { 811 | if (G.has_dashed) 812 | fly = true; 813 | step += 0.05f; 814 | spd.Y = E.sin(step) * 0.5f; 815 | } 816 | // collect 817 | var hit = collide(0, 0); 818 | if (hit != null) 819 | { 820 | hit.djump = G.max_djump; 821 | G.sfx_timer = 20; 822 | E.sfx(13); 823 | G.got_fruit.Add(1 + G.level_index()); 824 | G.init_object(new lifeup(), x, y); 825 | G.destroy_object(this); 826 | Stats.Increment(Stat.PICO_BERRIES); 827 | } 828 | } 829 | public override void draw() 830 | { 831 | var off = 0f; 832 | if (!fly) 833 | { 834 | var dir = E.sin(step); 835 | if (dir < 0) 836 | off = 1 + E.max(0, G.sign(y - start)); 837 | } 838 | else 839 | off = (off + 0.25f) % 3; 840 | E.spr(45 + off, x - 6, y - 2, 1, 1, true, false); 841 | E.spr(spr, x, y); 842 | E.spr(45 + off, x + 6, y - 2); 843 | } 844 | } 845 | 846 | public class lifeup : ClassicObject 847 | { 848 | int duration; 849 | float flash; 850 | 851 | public override void init(Classic g, Emulator e) 852 | { 853 | base.init(g, e); 854 | 855 | spd.Y = -0.25f; 856 | duration = 30; 857 | x -= 2; 858 | y -= 4; 859 | flash = 0; 860 | solids = false; 861 | } 862 | 863 | public override void update() 864 | { 865 | duration--; 866 | if (duration <= 0) 867 | G.destroy_object(this); 868 | } 869 | 870 | public override void draw() 871 | { 872 | flash += 0.5f; 873 | E.print("1000", x - 2, y, 7 + flash % 2); 874 | } 875 | } 876 | 877 | public class fake_wall : ClassicObject 878 | { 879 | public override void update() 880 | { 881 | hitbox = new Rectangle(-1, -1, 18, 18); 882 | var hit = collide(0, 0); 883 | if (hit != null && hit.dash_effect_time > 0) 884 | { 885 | hit.spd.X = -G.sign(hit.spd.X) * 1.5f; 886 | hit.spd.Y = -1.5f; 887 | hit.dash_time = -1; 888 | G.sfx_timer = 20; 889 | E.sfx(16); 890 | G.destroy_object(this); 891 | G.init_object(new smoke(), x, y); 892 | G.init_object(new smoke(), x + 8, y); 893 | G.init_object(new smoke(), x, y + 8); 894 | G.init_object(new smoke(), x + 8, y + 8); 895 | G.init_object(new fruit(), x + 4, y + 4); 896 | } 897 | hitbox = new Rectangle(0, 0, 16, 16); 898 | } 899 | public override void draw() 900 | { 901 | E.spr(64, x, y); 902 | E.spr(65, x + 8, y); 903 | E.spr(80, x, y + 8); 904 | E.spr(81, x + 8, y + 8); 905 | } 906 | } 907 | 908 | public class key : ClassicObject 909 | { 910 | public override void update() 911 | { 912 | var was = E.flr(spr); 913 | spr = 9 + (E.sin(G.frames / 30f) + 0.5f) * 1; 914 | var current = E.flr(spr); 915 | if (current == 10 && current != was) 916 | flipX = !flipX; 917 | if (check(0, 0)) 918 | { 919 | E.sfx(23); 920 | G.sfx_timer = 20; 921 | G.destroy_object(this); 922 | G.has_key = true; 923 | } 924 | } 925 | } 926 | 927 | public class chest : ClassicObject 928 | { 929 | float start; 930 | float timer; 931 | 932 | public override void init(Classic g, Emulator e) 933 | { 934 | base.init(g, e); 935 | x -= 4; 936 | start = x; 937 | timer = 20; 938 | } 939 | public override void update() 940 | { 941 | if (G.has_key) 942 | { 943 | timer--; 944 | x = start - 1 + E.rnd(3); 945 | if (timer <= 0) 946 | { 947 | G.sfx_timer = 20; 948 | E.sfx(16); 949 | G.init_object(new fruit(), x, y - 4); 950 | G.destroy_object(this); 951 | } 952 | } 953 | } 954 | } 955 | 956 | public class platform : ClassicObject 957 | { 958 | public float dir; 959 | float last; 960 | 961 | public override void init(Classic g, Emulator e) 962 | { 963 | base.init(g, e); 964 | x -= 4; 965 | solids = false; 966 | hitbox.Width = 16; 967 | last = x; 968 | } 969 | public override void update() 970 | { 971 | spd.X = dir * 0.65f; 972 | if (x < -16) x = 128; 973 | if (x > 128) x = -16; 974 | if (!check(0, 0)) 975 | { 976 | var hit = collide(0, -1); 977 | if (hit != null) 978 | hit.move_x((int)(x - last), 1); 979 | } 980 | last = x; 981 | } 982 | public override void draw() 983 | { 984 | E.spr(11, x, y - 1); 985 | E.spr(12, x + 8, y - 1); 986 | } 987 | } 988 | 989 | public class message : ClassicObject 990 | { 991 | float last = 0; 992 | float index = 0; 993 | public override void draw() 994 | { 995 | var text = "-- celeste mountain --#this memorial to those# perished on the climb"; 996 | if (check(4, 0)) 997 | { 998 | if (index < text.Length) 999 | { 1000 | index += 0.5f; 1001 | if (index >= last + 1) 1002 | { 1003 | last += 1; 1004 | E.sfx(35); 1005 | } 1006 | } 1007 | 1008 | var off = new Vector2(8, 96); 1009 | for (var i = 0; i < index; i ++) 1010 | { 1011 | if (text[i] != '#') 1012 | { 1013 | E.rectfill(off.X - 2, off.Y - 2, off.X + 7, off.Y + 6, 7); 1014 | E.print("" + text[i], off.X, off.Y, 0); 1015 | off.X += 5; 1016 | } 1017 | else 1018 | { 1019 | off.X = 8; 1020 | off.Y += 7; 1021 | } 1022 | } 1023 | } 1024 | else 1025 | { 1026 | index = 0; 1027 | last = 0; 1028 | } 1029 | } 1030 | } 1031 | 1032 | public class big_chest : ClassicObject 1033 | { 1034 | int state = 0; 1035 | float timer; 1036 | 1037 | private class particle 1038 | { 1039 | public float x; 1040 | public float y; 1041 | public float h; 1042 | public float spd; 1043 | } 1044 | private List particles; 1045 | 1046 | public override void init(Classic g, Emulator e) 1047 | { 1048 | base.init(g, e); 1049 | hitbox.Width = 16; 1050 | } 1051 | public override void draw() 1052 | { 1053 | if (state == 0) 1054 | { 1055 | var hit = collide(0, 8); 1056 | if (hit !=null && hit.is_solid(0, 1)) 1057 | { 1058 | E.music(-1, 500, 7); 1059 | E.sfx(37); 1060 | G.pause_player = true; 1061 | hit.spd.X = 0; 1062 | hit.spd.Y = 0; 1063 | state = 1; 1064 | G.init_object(new smoke(), x, y); 1065 | G.init_object(new smoke(), x + 8, y); 1066 | timer = 60; 1067 | particles = new List(); 1068 | } 1069 | E.spr(96, x, y); 1070 | E.spr(97, x + 8, y); 1071 | } 1072 | else if (state == 1) 1073 | { 1074 | timer--; 1075 | G.shake = 5; 1076 | G.flash_bg = true; 1077 | if (timer <= 45 && particles.Count < 50) 1078 | { 1079 | particles.Add(new particle() 1080 | { 1081 | x = 1 + E.rnd(14), 1082 | y = 0, 1083 | h = 32+E.rnd(32), 1084 | spd = 8+E.rnd(8) 1085 | }); 1086 | } 1087 | if (timer < 0) 1088 | { 1089 | state = 2; 1090 | particles.Clear(); 1091 | G.flash_bg = false; 1092 | G.new_bg = true; 1093 | G.init_object(new orb(), x + 4, y + 4); 1094 | G.pause_player = false; 1095 | } 1096 | foreach (var p in particles) 1097 | { 1098 | p.y += p.spd; 1099 | E.rectfill(x + p.x, y + 8 - p.y, x + p.x + 1, E.min(y + 8 - p.y + p.h, y + 8), 7); 1100 | } 1101 | } 1102 | 1103 | E.spr(112, x, y + 8); 1104 | E.spr(113, x + 8, y + 8); 1105 | } 1106 | } 1107 | 1108 | public class orb : ClassicObject 1109 | { 1110 | public override void init(Classic g, Emulator e) 1111 | { 1112 | base.init(g, e); 1113 | spd.Y = -4; 1114 | solids = false; 1115 | } 1116 | public override void draw() 1117 | { 1118 | spd.Y = G.appr(spd.Y, 0, 0.5f); 1119 | var hit = collide(0, 0); 1120 | if (spd.Y == 0 && hit != null) 1121 | { 1122 | G.music_timer = 45; 1123 | E.sfx(51); 1124 | G.freeze = 10; 1125 | G.shake = 10; 1126 | G.destroy_object(this); 1127 | G.max_djump = 2; 1128 | hit.djump = 2; 1129 | } 1130 | 1131 | E.spr(102, x, y); 1132 | var off = G.frames / 30f; 1133 | for (var i = 0; i <= 7; i++) 1134 | E.circfill(x + 4 + E.cos(off + i / 8f) * 8, y + 4 + E.sin(off + i / 8f) * 8, 1, 7); 1135 | } 1136 | } 1137 | 1138 | public class flag : ClassicObject 1139 | { 1140 | float score = 0; 1141 | bool show = false; 1142 | 1143 | public override void init(Classic g, Emulator e) 1144 | { 1145 | base.init(g, e); 1146 | x += 5; 1147 | score = G.got_fruit.Count; 1148 | 1149 | Stats.Increment(Stat.PICO_COMPLETES); 1150 | Achievements.Register(Achievement.PICO8); 1151 | } 1152 | public override void draw() 1153 | { 1154 | spr = 118 + (G.frames / 5f) % 3; 1155 | E.spr(spr, x, y); 1156 | if (show) 1157 | { 1158 | E.rectfill(32, 2, 96, 31, 0); 1159 | E.spr(26, 55, 6); 1160 | E.print("x" + score, 64, 9, 7); 1161 | G.draw_time(49, 16); 1162 | E.print("deaths:" + G.deaths, 48, 24, 7); 1163 | } 1164 | else if (check(0, 0)) 1165 | { 1166 | E.sfx(55); 1167 | G.sfx_timer = 30; 1168 | show = true; 1169 | } 1170 | } 1171 | } 1172 | 1173 | public class room_title : ClassicObject 1174 | { 1175 | float delay = 5; 1176 | public override void draw() 1177 | { 1178 | delay--; 1179 | if (delay < -30) 1180 | G.destroy_object(this); 1181 | else if (delay < 0) 1182 | { 1183 | E.rectfill(24, 58, 104, 70, 0); 1184 | if (G.room.X == 3 && G.room.Y == 1) 1185 | E.print("old site", 48, 62, 7); 1186 | else if (G.level_index() == 30) 1187 | E.print("summit", 52, 62, 7); 1188 | else 1189 | { 1190 | var level = (1 + G.level_index()) * 100; 1191 | E.print(level + "m", 52 + (level < 1000 ? 2 : 0), 62, 7); 1192 | } 1193 | 1194 | G.draw_time(4, 4); 1195 | } 1196 | } 1197 | } 1198 | 1199 | #endregion 1200 | 1201 | #region object functions 1202 | 1203 | public class ClassicObject 1204 | { 1205 | public Classic G; 1206 | public Emulator E; 1207 | 1208 | public int type; 1209 | public bool collideable = true; 1210 | public bool solids = true; 1211 | public float spr; 1212 | public bool flipX; 1213 | public bool flipY; 1214 | public float x; 1215 | public float y; 1216 | public Rectangle hitbox = new Rectangle(0, 0, 8, 8); 1217 | public Vector2 spd = new Vector2(0, 0); 1218 | public Vector2 rem = new Vector2(0, 0); 1219 | 1220 | public virtual void init(Classic g, Emulator e) 1221 | { 1222 | G = g; 1223 | E = e; 1224 | } 1225 | 1226 | public virtual void update() 1227 | { 1228 | 1229 | } 1230 | 1231 | public virtual void draw() 1232 | { 1233 | if (spr > 0) 1234 | E.spr(spr, x, y, 1, 1, flipX, flipY); 1235 | } 1236 | 1237 | public bool is_solid(int ox, int oy) 1238 | { 1239 | if (oy > 0 && !check(ox, 0) && check(ox, oy)) 1240 | return true; 1241 | return G.solid_at(x + hitbox.X + ox, y + hitbox.Y + oy, hitbox.Width, hitbox.Height) || 1242 | check(ox, oy) || 1243 | check(ox, oy); 1244 | } 1245 | 1246 | public bool is_ice(int ox, int oy) 1247 | { 1248 | return G.ice_at(x + hitbox.X + ox, y + hitbox.Y + oy, hitbox.Width, hitbox.Height); 1249 | } 1250 | 1251 | public T collide(int ox, int oy) where T : ClassicObject 1252 | { 1253 | var type = typeof(T); 1254 | foreach (var other in G.objects) 1255 | { 1256 | if (other != null && other.GetType() == type && other != this && other.collideable && 1257 | other.x + other.hitbox.X + other.hitbox.Width > x + hitbox.X + ox && 1258 | other.y + other.hitbox.Y + other.hitbox.Height > y + hitbox.Y + oy && 1259 | other.x + other.hitbox.X < x + hitbox.X + hitbox.Width + ox && 1260 | other.y + other.hitbox.Y < y + hitbox.Y + hitbox.Height + oy) 1261 | return other as T; 1262 | 1263 | } 1264 | return null; 1265 | } 1266 | 1267 | public bool check(int ox, int oy) where T : ClassicObject 1268 | { 1269 | return collide(ox, oy) != null; 1270 | } 1271 | 1272 | public void move(float ox, float oy) 1273 | { 1274 | int amount = 0; 1275 | // [x] get move amount 1276 | rem.X += ox; 1277 | amount = E.flr(rem.X + 0.5f); 1278 | rem.X -= amount; 1279 | move_x(amount, 0); 1280 | 1281 | // [y] get move amount 1282 | rem.Y += oy; 1283 | amount = E.flr(rem.Y + 0.5f); 1284 | rem.Y -= amount; 1285 | move_y(amount); 1286 | } 1287 | 1288 | public void move_x(int amount, int start) 1289 | { 1290 | if (solids) 1291 | { 1292 | var step = G.sign(amount); 1293 | for (int i = start; i <= E.abs(amount); i++) 1294 | { 1295 | if (!is_solid(step, 0)) 1296 | x += step; 1297 | else 1298 | { 1299 | spd.X = 0; 1300 | rem.X = 0; 1301 | break; 1302 | } 1303 | } 1304 | } 1305 | else 1306 | x += amount; 1307 | } 1308 | 1309 | public void move_y(int amount) 1310 | { 1311 | if (solids) 1312 | { 1313 | var step = G.sign(amount); 1314 | for (var i = 0; i <= E.abs(amount); i++) 1315 | if (!is_solid(0, step)) 1316 | y += step; 1317 | else 1318 | { 1319 | spd.Y = 0; 1320 | rem.Y = 0; 1321 | break; 1322 | } 1323 | } 1324 | else 1325 | y += amount; 1326 | } 1327 | 1328 | } 1329 | 1330 | private T init_object(T obj, float x, float y, int? tile = null) where T : ClassicObject 1331 | { 1332 | objects.Add(obj); 1333 | if (tile.HasValue) 1334 | obj.spr = tile.Value; 1335 | obj.x = (int)x; 1336 | obj.y = (int)y; 1337 | obj.init(this, E); 1338 | 1339 | return obj; 1340 | } 1341 | 1342 | private void destroy_object(ClassicObject obj) 1343 | { 1344 | var index = objects.IndexOf(obj); 1345 | if (index >= 0) 1346 | objects[index] = null; 1347 | } 1348 | 1349 | private void kill_player(player obj) 1350 | { 1351 | sfx_timer = 12; 1352 | E.sfx(0); 1353 | deaths++; 1354 | shake = 10; 1355 | destroy_object(obj); 1356 | Stats.Increment(Stat.PICO_DEATHS); 1357 | 1358 | dead_particles.Clear(); 1359 | for (var dir = 0; dir <= 7; dir ++) 1360 | { 1361 | var angle = (dir / 8f); 1362 | dead_particles.Add(new DeadParticle() 1363 | { 1364 | x = obj.x + 4, 1365 | y = obj.y + 4, 1366 | t = 10, 1367 | spd = new Vector2(E.cos(angle) * 3, E.sin(angle + 0.5f) * 3) 1368 | }); 1369 | } 1370 | 1371 | restart_room(); 1372 | } 1373 | 1374 | #endregion 1375 | 1376 | #region room functions 1377 | 1378 | private void restart_room() 1379 | { 1380 | will_restart = true; 1381 | delay_restart = 15; 1382 | } 1383 | 1384 | private void next_room() 1385 | { 1386 | if (room.X == 2 && room.Y == 1) 1387 | E.music(30, 500, 7); 1388 | else if (room.X == 3 && room.Y == 1) 1389 | E.music(20, 500, 7); 1390 | else if (room.X == 4 && room.Y == 2) 1391 | E.music(30, 500, 7); 1392 | else if (room.X == 5 && room.Y == 3) 1393 | E.music(30, 500, 7); 1394 | 1395 | if (room.X == 7) 1396 | load_room(0, room.Y + 1); 1397 | else 1398 | load_room(room.X + 1, room.Y); 1399 | } 1400 | 1401 | public void load_room(int x, int y) 1402 | { 1403 | has_dashed = false; 1404 | has_key = false; 1405 | 1406 | // remove existing objects 1407 | for (int i = 0; i < objects.Count; i++) 1408 | objects[i] = null; 1409 | 1410 | // current room 1411 | room.X = x; 1412 | room.Y = y; 1413 | 1414 | // entities 1415 | for (int tx = 0; tx <= 15; tx ++) 1416 | { 1417 | for (int ty = 0; ty <= 15; ty ++) 1418 | { 1419 | var tile = E.mget(room.X * 16 + tx, room.Y * 16 + ty); 1420 | if (tile == 11) 1421 | init_object(new platform(), tx * 8, ty * 8).dir = -1; 1422 | else if (tile == 12) 1423 | init_object(new platform(), tx * 8, ty * 8).dir = 1; 1424 | else 1425 | { 1426 | ClassicObject obj = null; 1427 | 1428 | if (tile == 1) 1429 | obj = new player_spawn(); 1430 | else if (tile == 18) 1431 | obj = new spring(); 1432 | else if (tile == 22) 1433 | obj = new balloon(); 1434 | else if (tile == 23) 1435 | obj = new fall_floor(); 1436 | else if (tile == 86) 1437 | obj = new message(); 1438 | else if (tile == 96) 1439 | obj = new big_chest(); 1440 | else if (tile == 118) 1441 | obj = new flag(); 1442 | else if (!got_fruit.Contains(1 + level_index())) 1443 | { 1444 | if (tile == 26) 1445 | obj = new fruit(); 1446 | else if (tile == 28) 1447 | obj = new fly_fruit(); 1448 | else if (tile == 64) 1449 | obj = new fake_wall(); 1450 | else if (tile == 8) 1451 | obj = new key(); 1452 | else if (tile == 20) 1453 | obj = new chest(); 1454 | } 1455 | 1456 | if (obj != null) 1457 | init_object(obj, tx * 8, ty * 8, tile); 1458 | } 1459 | } 1460 | } 1461 | 1462 | if (!is_title()) 1463 | init_object(new room_title(), 0, 0); 1464 | } 1465 | 1466 | #endregion 1467 | 1468 | #region update 1469 | 1470 | public void Update() 1471 | { 1472 | frames = ((frames + 1) % 30); 1473 | if (frames == 0 && level_index() < 30) 1474 | { 1475 | seconds = ((seconds + 1) % 60); 1476 | if (seconds == 0) 1477 | minutes++; 1478 | } 1479 | 1480 | if (music_timer > 0) 1481 | { 1482 | music_timer--; 1483 | if (music_timer <= 0) 1484 | E.music(10, 0, 7); 1485 | } 1486 | 1487 | if (sfx_timer > 0) 1488 | sfx_timer--; 1489 | 1490 | // cancel if freeze 1491 | if (freeze > 0) 1492 | { 1493 | freeze--; 1494 | return; 1495 | } 1496 | 1497 | // screenshake 1498 | if (shake > 0 && !Settings.Instance.DisableScreenShake) 1499 | { 1500 | shake--; 1501 | E.camera(); 1502 | if (shake > 0) 1503 | E.camera(-2 + E.rnd(5), -2 + E.rnd(5)); 1504 | } 1505 | 1506 | // restart(soon) 1507 | if (will_restart && delay_restart > 0) 1508 | { 1509 | delay_restart--; 1510 | if (delay_restart <= 0) 1511 | { 1512 | will_restart = true; 1513 | load_room(room.X, room.Y); 1514 | } 1515 | } 1516 | 1517 | // update each object 1518 | int length = objects.Count; 1519 | for (var i = 0; i < length; i ++) 1520 | { 1521 | var obj = objects[i]; 1522 | if (obj != null) 1523 | { 1524 | obj.move(obj.spd.X, obj.spd.Y); 1525 | obj.update(); 1526 | } 1527 | } 1528 | 1529 | // C# NEW CODE: 1530 | // clear deleted objects 1531 | while (objects.IndexOf(null) >= 0) 1532 | objects.Remove(null); 1533 | 1534 | // start game 1535 | if (is_title()) 1536 | { 1537 | if (!start_game && (E.btn(k_jump) || E.btn(k_dash))) 1538 | { 1539 | E.music(-1, 0, 0); 1540 | start_game_flash = 50; 1541 | start_game = true; 1542 | E.sfx(38); 1543 | } 1544 | if (start_game) 1545 | { 1546 | start_game_flash--; 1547 | if (start_game_flash <= -30) 1548 | begin_game(); 1549 | } 1550 | } 1551 | } 1552 | 1553 | #endregion 1554 | 1555 | #region drawing 1556 | 1557 | public void Draw() 1558 | { 1559 | // reset all palette values 1560 | E.pal(); 1561 | 1562 | // start game flash 1563 | if (start_game) 1564 | { 1565 | var c = 10; 1566 | if (start_game_flash > 10) 1567 | { 1568 | if (frames % 10 < 5) 1569 | c = 7; 1570 | } 1571 | else if (start_game_flash > 5) 1572 | c = 2; 1573 | else if (start_game_flash > 0) 1574 | c = 1; 1575 | else 1576 | c = 0; 1577 | 1578 | if (c < 10) 1579 | { 1580 | E.pal(6, c); 1581 | E.pal(12, c); 1582 | E.pal(13, c); 1583 | E.pal(5, c); 1584 | E.pal(1, c); 1585 | E.pal(7, c); 1586 | } 1587 | } 1588 | 1589 | // clear screen 1590 | var bg_col = 0; 1591 | if (flash_bg) 1592 | bg_col = frames / 5; 1593 | else if (new_bg) 1594 | bg_col = 2; 1595 | E.rectfill(0, 0, 128, 128, bg_col); 1596 | 1597 | // clouds 1598 | if (!is_title()) 1599 | { 1600 | foreach (var c in clouds) 1601 | { 1602 | c.x += c.spd; 1603 | E.rectfill(c.x, c.y, c.x + c.w, c.y + 4 + (1 - c.w / 64) * 12, new_bg ? 14 : 1); 1604 | if (c.x > 128) 1605 | { 1606 | c.x = -c.w; 1607 | c.y = E.rnd(128 - 8); 1608 | } 1609 | } 1610 | } 1611 | 1612 | // draw bg terrain 1613 | E.map(room.X * 16, room.Y * 16, 0, 0, 16, 16, 2); 1614 | 1615 | // platforms / big chest 1616 | for (var i = 0; i < objects.Count; i++) 1617 | { 1618 | var o = objects[i]; 1619 | if (o != null && (o is platform || o is big_chest)) 1620 | draw_object(o); 1621 | } 1622 | 1623 | // draw terrain 1624 | var off = is_title() ? -4 : 0; 1625 | E.map(room.X * 16, room.Y * 16, off, 0, 16, 16, 1); 1626 | 1627 | // draw objects 1628 | for (var i = 0; i < objects.Count; i++) 1629 | { 1630 | var o = objects[i]; 1631 | if (o != null && !(o is platform) && !(o is big_chest)) 1632 | draw_object(o); 1633 | } 1634 | 1635 | // draw fg terrain 1636 | E.map(room.X * 16, room.Y * 16, 0, 0, 16, 16, 3); 1637 | 1638 | // particles 1639 | foreach (var p in particles) 1640 | { 1641 | p.x += p.spd; 1642 | p.y += E.sin(p.off); 1643 | p.off += E.min(0.05f, p.spd / 32); 1644 | E.rectfill(p.x, p.y, p.x + p.s, p.y + p.s, p.c); 1645 | if (p.x > 128 + 4) 1646 | { 1647 | p.x = -4; 1648 | p.y = E.rnd(128); 1649 | } 1650 | } 1651 | 1652 | // dead particles 1653 | for (int i = dead_particles.Count - 1; i >= 0; i--) 1654 | { 1655 | var p = dead_particles[i]; 1656 | p.x += p.spd.X; 1657 | p.y += p.spd.Y; 1658 | p.t--; 1659 | if (p.t <= 0) 1660 | dead_particles.RemoveAt(i); 1661 | E.rectfill(p.x - p.t / 5, p.y - p.t / 5, p.x + p.t / 5, p.y + p.t / 5, 14 + p.t % 2); 1662 | } 1663 | 1664 | // draw outside of the screen for screenshake 1665 | E.rectfill(-5, -5, -1, 133, 0); 1666 | E.rectfill(-5, -5, 133, -1, 0); 1667 | E.rectfill(-5, 128, 133, 133, 0); 1668 | E.rectfill(128, -5, 133, 133, 0); 1669 | 1670 | // C# Change: "press button" instead to fit consoles 1671 | // no need for credits here 1672 | if (is_title()) 1673 | { 1674 | E.print("press button", 42, 96, 5); 1675 | //E.print("matt thorson", 42, 96, 5); 1676 | //E.print("noel berry", 46, 102, 5); 1677 | } 1678 | 1679 | if (level_index() == 30) 1680 | { 1681 | ClassicObject p = null; 1682 | 1683 | foreach (var o in objects) 1684 | if (o is player) 1685 | { 1686 | p = o; 1687 | break; 1688 | } 1689 | 1690 | if (p != null) 1691 | { 1692 | var diff = E.min(24, 40 - E.abs(p.x + 4 - 64)); 1693 | E.rectfill(0, 0, diff, 128, 0); 1694 | E.rectfill(128 - diff, 0, 128, 128, 0); 1695 | } 1696 | } 1697 | } 1698 | 1699 | private void draw_object(ClassicObject obj) 1700 | { 1701 | obj.draw(); 1702 | } 1703 | 1704 | private void draw_time(int x, int y) 1705 | { 1706 | var s = seconds; 1707 | var m = minutes % 60; 1708 | var h = E.flr(minutes / 60); 1709 | 1710 | E.rectfill(x, y, x + 32, y + 6, 0); 1711 | E.print((h < 10 ? "0" : "") + h + ":" + (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s, x + 1, y + 1,7); 1712 | } 1713 | 1714 | #endregion 1715 | 1716 | #region util 1717 | 1718 | private float clamp(float val, float a, float b) 1719 | { 1720 | return E.max(a, E.min(b, val)); 1721 | } 1722 | 1723 | private float appr(float val, float target, float amount) 1724 | { 1725 | return (val > target ? E.max(val - amount, target) : E.min(val + amount, target)); 1726 | } 1727 | 1728 | private int sign(float v) 1729 | { 1730 | return (v > 0 ? 1 : (v < 0 ? -1 : 0)); 1731 | } 1732 | 1733 | private bool maybe() 1734 | { 1735 | return E.rnd(1) < 0.5f; 1736 | } 1737 | 1738 | private bool solid_at(float x, float y, float w, float h) 1739 | { 1740 | return tile_flag_at(x, y, w, h, 0); 1741 | } 1742 | 1743 | private bool ice_at(float x, float y, float w, float h) 1744 | { 1745 | return tile_flag_at(x, y, w, h, 4); 1746 | } 1747 | 1748 | private bool tile_flag_at(float x, float y, float w, float h, int flag) 1749 | { 1750 | for (var i = (int)E.max(0, E.flr(x / 8f)); i <= E.min(15, (x + w - 1) / 8); i++) 1751 | for (var j = (int)E.max(0, E.flr(y / 8f)); j <= E.min(15, (y + h - 1) / 8); j ++) 1752 | if (E.fget(tile_at(i, j), flag)) 1753 | return true; 1754 | return false; 1755 | } 1756 | 1757 | private int tile_at(int x, int y) 1758 | { 1759 | return E.mget(room.X * 16 + x, room.Y * 16 + y); 1760 | } 1761 | 1762 | private bool spikes_at(float x, float y, int w, int h, float xspd, float yspd) 1763 | { 1764 | for (var i = (int)E.max(0, E.flr(x / 8f)); i <= E.min(15, (x + w - 1) / 8); i++) 1765 | for (var j = (int)E.max(0, E.flr(y / 8f)); j <= E.min(15, (y + h - 1) / 8); j++) 1766 | { 1767 | var tile = tile_at(i, j); 1768 | if (tile == 17 && ((y + h - 1) % 8 >= 6 || y + h == j * 8 + 8) && yspd >= 0) 1769 | return true; 1770 | else if (tile == 27 && y % 8 <= 2 && yspd <= 0) 1771 | return true; 1772 | else if (tile == 43 && x % 8 <= 2 && xspd <= 0) 1773 | return true; 1774 | else if (tile == 59 && ((x + w - 1) % 8 >= 6 || x + w == i * 8 + 8) && xspd >= 0) 1775 | return true; 1776 | } 1777 | return false; 1778 | } 1779 | 1780 | #endregion 1781 | } 1782 | } 1783 | -------------------------------------------------------------------------------- /Source/PICO-8/Emulator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Graphics; 3 | using Monocle; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace Celeste.Pico8 9 | { 10 | /// 11 | /// This scene runs the C# version of the PICO-8 version of Celeste 12 | /// It recreates various methods that PICO-8 has built in 13 | /// 14 | public class Emulator : Scene 15 | { 16 | #region Map 17 | 18 | private const string MapData = @"2331252548252532323232323300002425262425252631323232252628282824252525252525323328382828312525253232323233000000313232323232323232330000002432323233313232322525252525482525252525252526282824252548252525262828282824254825252526282828283132323225482525252525 19 | 252331323232332900002829000000242526313232332828002824262a102824254825252526002a2828292810244825282828290000000028282900000000002810000000372829000000002a2831482525252525482525323232332828242525254825323338282a283132252548252628382828282a2a2831323232322525 20 | 252523201028380000002a0000003d24252523201028292900282426003a382425252548253300002900002a0031252528382900003a676838280000000000003828393e003a2800000000000028002425253232323232332122222328282425252532332828282900002a283132252526282828282900002a28282838282448 21 | 3232332828282900000000003f2020244825262828290000002a243300002a2425322525260000000000000000003125290000000021222328280000000000002a2828343536290000000000002839242526212223202123313232332828242548262b000000000000001c00003b242526282828000000000028282828282425 22 | 2340283828293a2839000000343522252548262900000000000030000000002433003125333d3f00000000000000003100001c3a3a31252620283900000000000010282828290000000011113a2828313233242526103133202828282838242525262b000000000000000000003b2425262a2828670016002a28283828282425 23 | 263a282828102829000000000000312525323300000000110000370000003e2400000037212223000000000000000000395868282828242628290000000000002a2828290000000000002123283828292828313233282829002a002a2828242525332b0c00000011110000000c3b314826112810000000006828282828282425 24 | 252235353628280000000000003a282426003d003a3900270000000000002125001a000024252611111111000000002c28382828283831332800000017170000002a000000001111000024261028290028281b1b1b282800000000002a2125482628390000003b34362b000000002824252328283a67003a28282829002a3132 25 | 25333828282900000000000000283824252320201029003039000000005824480000003a31323235353536675800003c282828281028212329000000000000000000000000003436003a2426282800003828390000002a29000000000031323226101000000000282839000000002a2425332828283800282828390000001700 26 | 2600002a28000000003a283a2828282425252223283900372858390068283132000000282828282820202828283921222829002a28282426000000000000000000000000000020382828312523000000282828290000000000163a67682828003338280b00000010382800000b00003133282828282868282828280000001700 27 | 330000002867580000281028283422252525482628286720282828382828212200003a283828102900002a28382824252a0000002838242600000017170000000000000000002728282a283133390000282900000000000000002a28282829002a2839000000002a282900000000000028282838282828282828290000000000 28 | 0000003a2828383e3a2828283828242548252526002a282729002a28283432250000002a282828000000002810282425000000002a282426000000000000000000000000000037280000002a28283900280000003928390000000000282800000028290000002a2828000000000000002a282828281028282828675800000000 29 | 0000002838282821232800002a28242532322526003a2830000000002a28282400000000002a281111111128282824480000003a28283133000000000000171700013f0000002029000000003828000028013a28281028580000003a28290000002a280c0000003a380c00000000000c00002a2828282828292828290000003a 30 | 00013a2123282a313329001111112425002831263a3829300000000000002a310000000000002834222236292a0024253e013a3828292a00000000000000000035353536000020000000003d2a28671422222328282828283900582838283d00003a290000000028280000000000000000002a28282a29000058100012002a28 31 | 22222225262900212311112122222525002a3837282900301111110000003a2800013f0000002a282426290000002425222222232900000000000000171700002a282039003a2000003a003435353535252525222222232828282810282821220b10000000000b28100000000b0000002c00002838000000002a283917000028 32 | 2548252526111124252222252525482500012a2828673f242222230000003828222223000012002a24260000001224252525252600000000171700000000000000382028392827080028676820282828254825252525262a28282122222225253a28013d0000006828390000000000003c0168282800171717003a2800003a28 33 | 25252525252222252525252525252525222222222222222525482667586828282548260000270000242600000021252525254826171700000000000000000000002a2028102830003a282828202828282525252548252600002a2425252548252821222300000028282800000000000022222223286700000000282839002838 34 | 2532330000002432323232323232252525252628282828242532323232254825253232323232323225262828282448252525253300000000000000000000005225253232323233313232323233282900262829286700000000002828313232322525253233282800312525482525254825254826283828313232323232322548 35 | 26282800000030402a282828282824252548262838282831333828290031322526280000163a28283133282838242525482526000000000000000000000000522526000016000000002a10282838390026281a3820393d000000002a3828282825252628282829003b2425323232323232323233282828282828102828203125 36 | 3328390000003700002a3828002a2425252526282828282028292a0000002a313328111111282828000028002a312525252526000000000000000000000000522526000000001111000000292a28290026283a2820102011111121222328281025252628382800003b24262b002a2a38282828282829002a2800282838282831 37 | 28281029000000000000282839002448252526282900282067000000000000003810212223283829003a1029002a242532323367000000000000000000004200252639000000212300000000002122222522222321222321222324482628282832323328282800003b31332b00000028102829000000000029002a2828282900 38 | 2828280016000000162a2828280024252525262700002a2029000000000000002834252533292a0000002a00111124252223282800002c46472c00000042535325262800003a242600001600002425252525482631323331323324252620283822222328292867000028290000000000283800111100001200000028292a1600 39 | 283828000000000000003a28290024254825263700000029000000000000003a293b2426283900000000003b212225252526382867003c56573c4243435363633233283900282426111111111124252525482526201b1b1b1b1b24252628282825252600002a28143a2900000000000028293b21230000170000112867000000 40 | 2828286758000000586828380000313232323320000000000000000000272828003b2426290000000000003b312548252533282828392122222352535364000029002a28382831323535353522254825252525252300000000003132332810284825261111113435361111111100000000003b3133111111111127282900003b 41 | 2828282810290000002a28286700002835353536111100000000000011302838003b3133000000000000002a28313225262a282810282425252662636400000000160028282829000000000031322525252525252667580000002000002a28282525323535352222222222353639000000003b34353535353536303800000017 42 | 282900002a0000000000382a29003a282828283436200000000000002030282800002a29000011110000000028282831260029002a282448252523000000000039003a282900000000000000002831322525482526382900000017000058682832331028293b2448252526282828000000003b201b1b1b1b1b1b302800000017 43 | 283a0000000000000000280000002828283810292a000000000000002a3710281111111111112136000000002a28380b2600000000212525252526001c0000002828281000000000001100002a382829252525252628000000001700002a212228282908003b242525482628282912000000001b00000000000030290000003b 44 | 3829000000000000003a102900002838282828000000000000000000002a2828223535353535330000000000002828393300000000313225252533000000000028382829000000003b202b00682828003232323233290000000000000000312528280000003b3132322526382800170000000000000000110000370000000000 45 | 290000000000000000002a000000282928292a0000000000000000000000282a332838282829000000000000001028280000000042434424252628390000000028002a0000110000001b002a2010292c1b1b1b1b0000000000000000000010312829160000001b1b1b313328106700000000001100003a2700001b0000000000 46 | 00000100000011111100000000002a3a2a0000000000000000000000002a2800282829002a000000000000000028282800000000525354244826282800000000290000003b202b39000000002900003c000000000000000000000000000028282800000000000000001b1b2a2829000001000027390038300000000000000000 47 | 1111201111112122230000001212002a00010000000000000000000000002900290000000000000000002a6768282900003f01005253542425262810673a3900013f0000002a3829001100000000002101000000000000003a67000000002a382867586800000100000000682800000021230037282928300000000000000000 48 | 22222222222324482611111120201111002739000017170000001717000000000001000000001717000000282838393a0021222352535424253328282838290022232b00000828393b27000000001424230000001200000028290000000000282828102867001717171717282839000031333927101228370000000000000000 49 | 254825252526242526212222222222223a303800000000000000000000000000001717000000000000003a28282828280024252652535424262828282828283925262b00003a28103b30000000212225260000002700003a28000000000000282838282828390000005868283828000022233830281728270000000000000000 50 | 00000000000000008242525252528452339200001323232352232323232352230000000000000000b302000013232352526200a2828342525223232323232323 51 | 00000000000000a20182920013232352363636462535353545550000005525355284525262b20000000000004252525262828282425284525252845252525252 52 | 00000000000085868242845252525252b1006100b1b1b1b103b1b1b1b1b103b100000000000000111102000000a282425233000000a213233300009200008392 53 | 000000000000110000a2000000a28213000000002636363646550000005525355252528462b2a300000000004252845262828382132323232323232352528452 54 | 000000000000a201821323525284525200000000000000007300000000007300000000000000b343536300410000011362b2000000000000000000000000a200 55 | 0000000000b302b2002100000000a282000000000000000000560000005526365252522333b28292001111024252525262019200829200000000a28213525252 56 | 0000000000000000a2828242525252840000000000000000b10000000000b1000000000000000000b3435363930000b162273737373737373737374711000061 57 | 000000110000b100b302b20000006182000000000000000000000000005600005252338282828201a31222225252525262820000a20011111100008283425252 58 | 0000000000000093a382824252525252000061000011000000000011000000001100000000000000000000020182001152222222222222222222222232b20000 59 | 0000b302b200000000b10000000000a200000000000000009300000000000000846282828283828282132323528452526292000000112434440000a282425284 60 | 00000000000000a2828382428452525200000000b302b2936100b302b20061007293a30000000000000000b1a282931252845252525252232323232362b20000 61 | 000000b10000001100000000000000000000000093000086820000a3000000005262828201a200a282829200132323236211111111243535450000b312525252 62 | 00000000000000008282821323232323820000a300b1a382930000b100000000738283931100000000000011a382821323232323528462829200a20173b20061 63 | 000000000000b302b2000061000000000000a385828286828282828293000000526283829200000000a20000000000005222222232263636460000b342525252 64 | 00000011111111a3828201b1b1b1b1b182938282930082820000000000000000b100a282721100000000b372828283b122222232132333610000869200000000 65 | 00100000000000b1000000000000000086938282828201920000a20182a37686526282829300000000000000000000005252845252328283920000b342845252 66 | 00008612222232828382829300000000828282828283829200000000000061001100a382737200000000b373a2829211525284628382a2000000a20000000000 67 | 00021111111111111111111111110061828282a28382820000000000828282825262829200000000000000000000000052525252526201a2000000b342525252 68 | 00000113235252225353536300000000828300a282828201939300001100000072828292b1039300000000b100a282125223526292000000000000a300000000 69 | 0043535353535353535353535363b2008282920082829200061600a3828382a28462000000000000000000000000000052845252526292000011111142525252 70 | 0000a28282132362b1b1b1b1000000009200000000a28282828293b372b2000073820100110382a3000000110082821362101333610000000000008293000000 71 | 0002828382828202828282828272b20083820000a282d3000717f38282920000526200000000000093000000000000005252525284620000b312223213528452 72 | 000000828392b30300000000002100000000000000000082828282b303b20000b1a282837203820193000072a38292b162710000000000009300008382000000 73 | 00b1a282820182b1a28283a28273b200828293000082122232122232820000a3233300000000000082920000000000002323232323330000b342525232135252 74 | 000000a28200b37300000000a37200000010000000111111118283b373b200a30000828273039200828300738283001162930000000000008200008282920000 75 | 0000009261a28200008261008282000001920000000213233342846282243434000000000000000082000085860000008382829200000000b342528452321323 76 | 0000100082000082000000a2820300002222321111125353630182829200008300009200b1030000a28200008282001262829200000000a38292008282000000 77 | 00858600008282a3828293008292610082001000001222222252525232253535000000f3100000a3820000a2010000008292000000009300b342525252522222 78 | 0400122232b200839321008683039300528452222262c000a28282820000a38210000000a3738000008293008292001362820000000000828300a38201000000 79 | 00a282828292a2828283828282000000343434344442528452525252622535350000001263000083829300008200c1008210d3e300a38200b342525252845252 80 | 1232425262b28682827282820103820052525252846200000082829200008282320000008382930000a28201820000b162839300000000828200828282930000 81 | 0000008382000000a28201820000000035353535454252525252528462253535000000032444008282820000829300002222223201828393b342525252525252 82 | 525252525262b2b1b1b1132323526200845223232323232352522323233382825252525252525252525284522333b2822323232323526282820000b342525252 83 | 52845252525252848452525262838242528452522333828292425223232352520000000000000000000000000000000000000000000000000000000000000000 84 | 525252845262b2000000b1b1b142620023338276000000824233b2a282018283525252845252232323235262b1b10083921000a382426283920000b342232323 85 | 2323232323232323232323526201821352522333b1b1018241133383828242840000000000000000000000000000000000000000000000000000000000000000 86 | 525252525262b20000000000a242627682828392000011a273b200a382729200525252525233b1b1b1b11333000000825353536382426282410000b30382a2a2 87 | a1829200a2828382820182426200a2835262b1b10000831232b2000080014252000000000000a300000000000000000000000000000000000000000000000000 88 | 528452232333b20000001100824262928201a20000b3720092000000830300002323525262b200000000b3720000a382828283828242522232b200b373928000 89 | 000100110092a2829211a2133300a3825262b2000000a21333b20000868242520000000000000100009300000000000000000000000000000000000000000000 90 | 525262122232b200a37672b2a24262838292000000b30300000000a3820300002232132333b200000000b303829300a2838292019242845262b2000000000000 91 | 00a2b302b2a36182b302b200110000825262b200000000b1b10000a283a2425200000000a30082000083000000000000000000000094a4b4c4d4e4f400000000 92 | 525262428462b200a28303b2214262928300000000b3030000000000a203e3415252222232b200000000b30392000000829200000042525262b2000000000000 93 | 000000b100a2828200b100b302b211a25262b200000000000000000092b3428400000000827682000001009300000000000000000095a5b5c5d5e5f500000000 94 | 232333132362b221008203b2711333008293858693b3031111111111114222225252845262b200001100b303b2000000821111111142528462b2000000000000 95 | 000000000000110176851100b1b3026184621111111100000061000000b3135200000000828382670082768200000000000000000096a6b6c6d6e6f600000000 96 | 82000000a203117200a203b200010193828283824353235353535353535252845252525262b200b37200b303b2000000824353535323235262b2000011000000 97 | 0000000000b30282828372b26100b100525232122232b200000000000000b14200000000a28282123282839200000000000000000097a7b7c7d7e7f700000000 98 | 9200110000135362b2001353535353539200a2000001828282829200b34252522323232362b261b30300b3030000000092b1b1b1b1b1b34262b200b372b20000 99 | 001100000000b1a2828273b200000000232333132333b200001111000000b342000000868382125252328293a300000000000000000000000000000000000000 100 | 00b372b200a28303b2000000a28293b3000000000000a2828382827612525252b1b1b1b173b200b30393b30361000000000000000000b34262b271b303b20000 101 | b302b211000000110092b100000000a3b1b1b1b1b1b10011111232110000b342000000a282125284525232828386000000000000000000000000000000000000 102 | 80b303b20000820311111111008283b311111111110000829200928242528452000000a3820000b30382b37300000000000000000000b3426211111103b20000 103 | 00b1b302b200b372b200000000000082b21000000000b31222522363b200b3138585868292425252525262018282860000000000000000000000000000000000 104 | 00b373b20000a21353535363008292b32222222232111102b20000a21323525200000001839200b3038282820000000011111111930011425222222233b20000 105 | 100000b10000b303b200000000858682b27100000000b3425233b1b1000000b182018283001323525284629200a2820000000000000000000000000000000000 106 | 9300b100000000b1b1b1b1b100a200b323232323235363b100000000b1b1135200000000820000b30382839200000000222222328283432323232333b2000000 107 | 329300000000b373b200000000a20182111111110000b31333b100a30061000000a28293f3123242522333020000820000000000000000000000000000000000 108 | 829200001000410000000000000000b39310d30000a28200000000000000824200000086827600b30300a282760000005252526200828200a30182a2006100a3 109 | 62820000000000b100000093a382838222222232b20000b1b1000083000000860000122222526213331222328293827600000000000000000000000000000000 110 | 017685a31222321111111111002100b322223293000182930000000080a301131000a383829200b373000083920000005284526200a282828283920000000082 111 | 62839321000000000000a3828282820152845262b261000093000082a300a3821000135252845222225252523201838200000000000000000000000000000000 112 | 828382824252522222222232007100b352526282a38283820000000000838282320001828200000083000082010000005252526271718283820000000000a382 113 | 628201729300000000a282828382828252528462b20000a38300a382018283821222324252525252525284525222223200000000000000000000000000000000"; 114 | 115 | #endregion 116 | 117 | public Scene ReturnTo; 118 | public bool CanPause { get { return pauseMenu == null; } } 119 | 120 | private Classic game; 121 | private int gameFrame; 122 | private bool gameActive = true; 123 | private float gameDelay = 0f; 124 | private bool booting { get { return game == null; } } 125 | private Point bootLevel; 126 | 127 | private bool leaving; 128 | private bool skipFrame = true; 129 | private FMOD.Studio.EventInstance bgSfx; 130 | 131 | private VirtualRenderTarget buffer; 132 | private Color[] pixels = new Color[128 * 128]; 133 | private Vector2 offset = Vector2.Zero; 134 | 135 | // puasing 136 | private TextMenu pauseMenu = null; 137 | private float pauseFade = 0f; 138 | private FMOD.Studio.EventInstance snapshot; 139 | 140 | // pico-8 boot 141 | private MTexture picoBootLogo; 142 | 143 | // levels tilemap 144 | private byte[] tilemap; 145 | 146 | // sprites & sprite mask 147 | private MTexture[] sprites; 148 | private byte[] mask = new byte[] { 149 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 150 | 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 151 | 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 2, 2, 0, 0, 0, 152 | 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 2, 2, 2, 2, 2, 153 | 0, 0, 19, 19, 19, 19, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 154 | 0, 0, 19, 19, 19, 19, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 155 | 0, 0, 19, 19, 19, 19, 0, 4, 4, 2, 2, 2, 2, 2, 2, 2, 156 | 0, 0, 19, 19, 19, 19, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2 157 | }; 158 | 159 | // pico-8 colors 160 | private Color[] colors = new Color[] 161 | { 162 | Calc.HexToColor("000000"), 163 | Calc.HexToColor("1d2b53"), 164 | Calc.HexToColor("7e2553"), 165 | Calc.HexToColor("008751"), 166 | Calc.HexToColor("ab5236"), 167 | Calc.HexToColor("5f574f"), 168 | Calc.HexToColor("c2c3c7"), 169 | Calc.HexToColor("fff1e8"), 170 | Calc.HexToColor("ff004d"), 171 | Calc.HexToColor("ffa300"), 172 | Calc.HexToColor("ffec27"), 173 | Calc.HexToColor("00e436"), 174 | Calc.HexToColor("29adff"), 175 | Calc.HexToColor("83769c"), 176 | Calc.HexToColor("ff77a8"), 177 | Calc.HexToColor("ffccaa") 178 | }; 179 | private Dictionary paletteSwap = new Dictionary(); 180 | 181 | // font 182 | private MTexture[] font; 183 | private string fontMap = "abcdefghijklmnopqrstuvwxyz0123456789~!@#4%^&*()_+-=?:."; 184 | 185 | public Emulator(Scene returnTo, int levelX = 0, int levelY = 0) 186 | { 187 | ReturnTo = returnTo; 188 | bootLevel = new Point(levelX, levelY); 189 | buffer = VirtualContent.CreateRenderTarget("pico-8", 128, 128); 190 | 191 | // sprites 192 | var atlas = GFX.Game["pico8/atlas"]; 193 | sprites = new MTexture[(atlas.Width / 8) * (atlas.Height / 8)]; 194 | for (int ty = 0; ty < atlas.Height / 8; ty++) 195 | for (int tx = 0; tx < atlas.Width / 8; tx++) 196 | sprites[tx + ty * (atlas.Width / 8)] = atlas.GetSubtexture(tx * 8, ty * 8, 8, 8); 197 | 198 | // tilemap 199 | var tiledata = MapData; 200 | tiledata = Regex.Replace(tiledata, @"\s+", ""); 201 | tilemap = new byte[tiledata.Length / 2]; 202 | for (int i = 0, len = tiledata.Length, hlen = len / 2; i < len; i += 2) 203 | { 204 | var a = tiledata[i]; 205 | var b = tiledata[i + 1]; 206 | var str = (i < hlen ? (a.ToString() + b.ToString()) : (b.ToString() + a.ToString())); 207 | tilemap[i / 2] = (byte)int.Parse(str, System.Globalization.NumberStyles.HexNumber); 208 | } 209 | 210 | // font 211 | var fontatlas = GFX.Game["pico8/font"]; 212 | font = new MTexture[(fontatlas.Width / 4) * (fontatlas.Height / 6)]; 213 | for (var ty = 0; ty < fontatlas.Height / 6; ty++) 214 | for (var tx = 0; tx < fontatlas.Width / 4; tx++) 215 | font[tx + ty * (fontatlas.Width / 4)] = fontatlas.GetSubtexture(tx * 4, ty * 6, 4, 6); 216 | 217 | // boot stuff 218 | picoBootLogo = GFX.Game["pico8/logo"]; 219 | ResetScreen(); 220 | 221 | Audio.SetMusic(null); 222 | Audio.SetAmbience(null); 223 | new FadeWipe(this, true); 224 | RendererList.UpdateLists(); 225 | } 226 | 227 | public override void Begin() 228 | { 229 | bgSfx = Audio.Play(Sfxs.env_amb_03_pico8_closeup); 230 | base.Begin(); 231 | } 232 | 233 | public override void End() 234 | { 235 | buffer.Dispose(); 236 | Audio.BusStopAll(Buses.GAMEPLAY); 237 | Audio.Stop(bgSfx); 238 | if (snapshot != null) 239 | Audio.EndSnapshot(snapshot); 240 | snapshot = null; 241 | Stats.Store(); 242 | 243 | base.End(); 244 | } 245 | 246 | private void ResetScreen() 247 | { 248 | Engine.Graphics.GraphicsDevice.Textures[0] = null; 249 | Engine.Graphics.GraphicsDevice.Textures[1] = null; 250 | 251 | for (var x = 0; x < 128; x++) 252 | for (var y = 0; y < 128; y++) 253 | pixels[x + y * 128] = Color.Black; 254 | buffer.Target.SetData(pixels); 255 | } 256 | 257 | public override void Update() 258 | { 259 | base.Update(); 260 | 261 | // pause menu 262 | if (pauseMenu != null) 263 | pauseMenu.Update(); 264 | else if (!leaving && (Input.Pause.Pressed || Input.ESC.Pressed)) 265 | CreatePauseMenu(); 266 | pauseFade = Calc.Approach(pauseFade, pauseMenu != null ? 0.75f : 0f, Engine.DeltaTime * 6f); 267 | 268 | // this is a pretty dumb hack but because Celeste is locked to 60fps 269 | // and PICO-8 runs at 30 ... we just skip every 2nd frame 270 | // the game buffers inputs so they wont get eaten 271 | skipFrame = !skipFrame; 272 | if (skipFrame) 273 | return; 274 | 275 | // don't update the game 276 | gameDelay -= Engine.DeltaTime; 277 | if (!gameActive || gameDelay > 0) 278 | return; 279 | 280 | // recreating the PICO-8 Boot Sequence 281 | if (booting) 282 | { 283 | Engine.Graphics.GraphicsDevice.Textures[0] = null; 284 | Engine.Graphics.GraphicsDevice.Textures[1] = null; 285 | 286 | gameFrame++; 287 | var t = gameFrame - 20; 288 | 289 | if (t == 1) 290 | { 291 | for (var y = 0; y < 128; y++) 292 | for (var x = 2; x < 128; x += 8) 293 | pixels[x + y * 128] = colors[Calc.Random.Next(4) + (y / 32)]; 294 | buffer.Target.SetData(pixels); 295 | } 296 | if (t == 4) 297 | { 298 | for (var y = 0; y < 128; y += 2) 299 | for (var x = 0; x < 128; x += 4) 300 | pixels[x + y * 128] = colors[6 + (((x + y) / 8) & 7)]; 301 | buffer.Target.SetData(pixels); 302 | } 303 | if (t == 7) 304 | { 305 | for (var y = 0; y < 128; y += 3) 306 | for (var x = 2; x < 128; x += 4) 307 | pixels[x + y * 128] = colors[10 + Calc.Random.Next(4)]; 308 | buffer.Target.SetData(pixels); 309 | } 310 | 311 | // wide 312 | if (t == 9) 313 | { 314 | for (var y = 0; y < 128; y++) 315 | for (var x = 1; x < 127; x += 2) 316 | pixels[x + y * 128] = pixels[x + 1 + y * 128]; 317 | buffer.Target.SetData(pixels); 318 | } 319 | 320 | // stripe blank 321 | if (t == 12) 322 | { 323 | for (var y = 0; y < 128; y++) 324 | if ((y & 3) > 0) 325 | for (var x = 0; x < 128; x++) 326 | pixels[x + y * 128] = colors[0]; 327 | buffer.Target.SetData(pixels); 328 | } 329 | 330 | // clear 331 | if (t == 15) 332 | { 333 | for (var y = 0; y < 128; y++) 334 | for (var x = 0; x < 128; x++) 335 | pixels[x + y * 128] = colors[0]; 336 | buffer.Target.SetData(pixels); 337 | } 338 | 339 | if (t == 30) 340 | Audio.Play(Sfxs.music_pico8_boot); 341 | 342 | // logo 343 | if (t == 30 || t == 35 || t == 40) 344 | { 345 | Engine.Graphics.GraphicsDevice.SetRenderTarget(buffer); 346 | Engine.Graphics.GraphicsDevice.Clear(colors[0]); 347 | Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, RasterizerState.CullNone); 348 | picoBootLogo.Draw(new Vector2(1, 1)); 349 | if (t >= 35) 350 | print("pico-8 0.1.9B", 1, 18, 6); 351 | if (t >= 40) 352 | { 353 | print("(c) 2014-16 lexaloffle games llp", 1, 24, 6); 354 | print("booting cartridge..", 1, 36, 6); 355 | } 356 | Draw.SpriteBatch.End(); 357 | Engine.Graphics.GraphicsDevice.SetRenderTarget(null); 358 | } 359 | 360 | // start it up 361 | if (t == 90) 362 | { 363 | gameFrame = 0; 364 | game = new Classic(); 365 | game.Init(this); 366 | if (bootLevel.X != 0 || bootLevel.Y != 0) 367 | game.load_room(bootLevel.X, bootLevel.Y); 368 | } 369 | } 370 | else 371 | { 372 | gameFrame++; 373 | game.Update(); 374 | 375 | if (game.freeze <= 0) 376 | { 377 | // draw 378 | { 379 | Engine.Graphics.GraphicsDevice.SetRenderTarget(buffer); 380 | Engine.Graphics.GraphicsDevice.Clear(colors[0]); 381 | Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, RasterizerState.CullNone, null, Matrix.CreateTranslation(-offset.X, -offset.Y, 0)); 382 | game.Draw(); 383 | Draw.SpriteBatch.End(); 384 | 385 | // unset in case we do a palette swap 386 | Engine.Graphics.GraphicsDevice.SetRenderTarget(null); 387 | } 388 | 389 | // do a palette swap 390 | // this could be done with a shader but on a 128x128 screen ... I don't really care 391 | if (paletteSwap.Count > 0) 392 | { 393 | buffer.Target.GetData(pixels); 394 | 395 | for (var i = 0; i < pixels.Length; i++) 396 | { 397 | var index = 0; 398 | if (paletteSwap.TryGetValue(pixels[i], out index)) 399 | pixels[i] = colors[index]; 400 | } 401 | 402 | buffer.Target.SetData(pixels); 403 | } 404 | } 405 | } 406 | 407 | } 408 | 409 | public override void Render() 410 | { 411 | Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, RasterizerState.CullNone, null, Engine.ScreenMatrix); 412 | { 413 | var scale = 6; 414 | var size = new Vector2(buffer.Width * scale, buffer.Height * scale); 415 | var pos = new Vector2(Celeste.TargetWidth - size.X, Celeste.TargetHeight - size.Y) / 2f; 416 | var flip = (SaveData.Instance != null && SaveData.Instance.Assists.MirrorMode); 417 | 418 | // surroundings 419 | GFX.Game["pico8/consoleBG"].Draw(Vector2.Zero, Vector2.Zero, Color.White, scale); 420 | 421 | // the buffer / emulator 422 | Draw.SpriteBatch.Draw(buffer, pos, buffer.Bounds, Color.White, 0f, Vector2.Zero, scale, (flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None), 0); 423 | } 424 | Draw.SpriteBatch.End(); 425 | 426 | // Pause Menu 427 | if (pauseMenu != null || pauseFade > 0) 428 | { 429 | Draw.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, null, RasterizerState.CullNone, null, Engine.ScreenMatrix); 430 | 431 | Draw.Rect(-1, -1, Celeste.TargetWidth + 2, Celeste.TargetHeight + 2, Color.Black * pauseFade); 432 | 433 | if (pauseMenu != null) 434 | pauseMenu.Render(); 435 | 436 | Draw.SpriteBatch.End(); 437 | } 438 | 439 | base.Render(); 440 | } 441 | 442 | public void CreatePauseMenu() 443 | { 444 | Audio.Play(Sfxs.ui_game_pause); 445 | Audio.PauseGameplaySfx = true; 446 | snapshot = Audio.CreateSnapshot(Snapshots.PAUSE_MENU); 447 | 448 | var menu = new TextMenu(); 449 | 450 | // Resume Button 451 | menu.Add(new TextMenu.Button(Dialog.Clean("pico8_pause_continue")).Pressed(() => menu.OnCancel())); 452 | 453 | // Restart Button 454 | menu.Add(new TextMenu.Button(Dialog.Clean("pico8_pause_restart")).Pressed(() => 455 | { 456 | pauseMenu = null; 457 | music(-1, 0, 0); 458 | 459 | new FadeWipe(this, false, () => 460 | { 461 | Audio.BusStopAll(Buses.GAMEPLAY, false); 462 | Audio.PauseGameplaySfx = false; 463 | Audio.EndSnapshot(snapshot); 464 | snapshot = null; 465 | 466 | ResetScreen(); 467 | game = null; 468 | gameFrame = 0; 469 | gameActive = true; 470 | new FadeWipe(this, true); 471 | }); 472 | })); 473 | 474 | // Quit Button 475 | menu.Add(new TextMenu.Button(Dialog.Clean("pico8_pause_quit")).Pressed(() => 476 | { 477 | leaving = true; 478 | gameActive = false; 479 | pauseMenu = null; 480 | music(-1, 0, 0); 481 | 482 | new FadeWipe(this, false, () => 483 | { 484 | Audio.BusStopAll(Buses.GAMEPLAY, false); 485 | Audio.PauseGameplaySfx = false; 486 | Audio.EndSnapshot(snapshot); 487 | Audio.Stop(bgSfx); 488 | snapshot = null; 489 | 490 | if (ReturnTo != null) 491 | { 492 | if (ReturnTo is Level) 493 | { 494 | (ReturnTo as Level).Session.Audio.Apply(); 495 | new FadeWipe(ReturnTo, true); 496 | } 497 | 498 | Engine.Scene = ReturnTo; 499 | } 500 | else 501 | { 502 | Engine.Scene = new OverworldLoader(Overworld.StartMode.Titlescreen); 503 | } 504 | }); 505 | })); 506 | 507 | menu.OnCancel = menu.OnESC = menu.OnPause = () => 508 | { 509 | Audio.PauseGameplaySfx = false; 510 | Audio.EndSnapshot(snapshot); 511 | snapshot = null; 512 | 513 | gameDelay = 0.1f; 514 | pauseMenu = null; 515 | gameActive = true; 516 | menu.RemoveSelf(); 517 | }; 518 | 519 | gameActive = false; 520 | pauseMenu = menu; 521 | } 522 | 523 | #region Emulator Methods 524 | 525 | public void music(int index, int fade, int mask) 526 | { 527 | if (index == -1) 528 | { 529 | Audio.SetMusic(null); 530 | } 531 | else if (index == 0) 532 | { 533 | Audio.SetMusic(Sfxs.music_pico8_area1); 534 | } 535 | else if (index == 10) 536 | { 537 | Audio.SetMusic(Sfxs.music_pico8_area3); 538 | } 539 | else if (index == 20) 540 | { 541 | Audio.SetMusic(Sfxs.music_pico8_area2); 542 | } 543 | else if (index == 30) 544 | { 545 | Audio.SetMusic(Sfxs.music_pico8_wind); 546 | } 547 | else if (index == 40) 548 | { 549 | Audio.SetMusic(Sfxs.music_pico8_title); 550 | } 551 | } 552 | 553 | public void sfx(int sfx) 554 | { 555 | Audio.Play("event:/classic/sfx" + sfx); 556 | } 557 | 558 | public float rnd(float max) 559 | { 560 | return Calc.Random.NextFloat(max); 561 | } 562 | 563 | public int flr(float value) 564 | { 565 | return (int)Math.Floor(value); 566 | } 567 | 568 | public int sign(float value) 569 | { 570 | return Math.Sign(value); 571 | } 572 | 573 | public float abs(float value) 574 | { 575 | return Math.Abs(value); 576 | } 577 | 578 | public float min(float a, float b) 579 | { 580 | return Math.Min(a, b); 581 | } 582 | 583 | public float max(float a, float b) 584 | { 585 | return Math.Max(a, b); 586 | } 587 | 588 | public float sin(float a) 589 | { 590 | return (float)Math.Sin((1 - a) * MathHelper.TwoPi); 591 | } 592 | 593 | public float cos(float a) 594 | { 595 | return (float)Math.Cos((1 - a) * MathHelper.TwoPi); 596 | } 597 | 598 | public bool btn(int index) 599 | { 600 | var aim = new Vector2(Input.MoveX, Input.MoveY); 601 | 602 | if (index == 0) 603 | return aim.X < 0; 604 | else if (index == 1) 605 | return aim.X > 0; 606 | else if (index == 2) 607 | return aim.Y < 0; 608 | else if (index == 3) 609 | return aim.Y > 0; 610 | else if (index == 4) 611 | return Input.Jump.Check; 612 | else if (index == 5) 613 | return Input.Dash.Check; 614 | 615 | return false; 616 | } 617 | 618 | public int dashDirectionX(int facing) 619 | { 620 | return Math.Sign(Input.GetAimVector((Facings)facing).X); 621 | } 622 | 623 | public int dashDirectionY(int facing) 624 | { 625 | return Math.Sign(Input.GetAimVector((Facings)facing).Y); 626 | } 627 | 628 | public int mget(int tx, int ty) 629 | { 630 | return tilemap[tx + ty * 128]; 631 | } 632 | 633 | public bool fget(int tile, int flag) 634 | { 635 | return tile < mask.Length && (mask[tile] & (1 << flag)) != 0; 636 | } 637 | 638 | public void camera() 639 | { 640 | offset = Vector2.Zero; 641 | } 642 | 643 | public void camera(float x, float y) 644 | { 645 | offset = new Vector2((int)Math.Round(x), (int)Math.Round(y)); 646 | } 647 | 648 | public void pal() 649 | { 650 | paletteSwap.Clear(); 651 | } 652 | 653 | public void pal(int a, int b) 654 | { 655 | var from = colors[a]; 656 | if (paletteSwap.ContainsKey(from)) 657 | paletteSwap[from] = b; 658 | else 659 | paletteSwap.Add(from, b); 660 | } 661 | 662 | public void rectfill(float x, float y, float x2, float y2, float c) 663 | { 664 | var left = Math.Min(x, x2); 665 | var top = Math.Min(y, y2); 666 | var width = Math.Max(x, x2) - left + 1; 667 | var height = Math.Max(y, y2) - top + 1; 668 | Draw.Rect(left, top, width, height, colors[((int)c) % 16]); 669 | } 670 | 671 | public void circfill(float x, float y, float r, float c) 672 | { 673 | var color = colors[((int)c) % 16]; 674 | if (r <= 1) 675 | { 676 | Draw.Rect(x - 1, y, 3, 1, color); 677 | Draw.Rect(x, y - 1, 1, 3, color); 678 | } 679 | else if (r <= 2) 680 | { 681 | Draw.Rect(x - 2, y - 1, 5, 3, color); 682 | Draw.Rect(x - 1, y - 2, 3, 5, color); 683 | } 684 | else if (r <= 3) 685 | { 686 | Draw.Rect(x - 3, y - 1, 7, 3, color); 687 | Draw.Rect(x - 1, y - 3, 3, 7, color); 688 | Draw.Rect(x - 2, y - 2, 5, 5, color); 689 | } 690 | } 691 | 692 | public void print(string str, float x, float y, float c) 693 | { 694 | var left = x; 695 | var color = colors[((int)c) % 16]; 696 | for (var i = 0; i < str.Length; i ++) 697 | { 698 | var character = str[i]; 699 | var index = -1; 700 | for (var j = 0; j < fontMap.Length;j ++) 701 | if (fontMap[j] == character) 702 | { 703 | index = j; 704 | break; 705 | } 706 | if (index >= 0) 707 | font[index].Draw(new Vector2(left, y), Vector2.Zero, color); 708 | left += 4; 709 | } 710 | } 711 | 712 | public void map(int mx, int my, int tx, int ty, int mw, int mh, int mask = 0) 713 | { 714 | for (int x = 0; x < mw; x++) 715 | { 716 | for (int y = 0; y < mh; y++) 717 | { 718 | var tile = tilemap[x + mx + (y + my) * 128]; 719 | if (tile < sprites.Length) 720 | if (mask == 0 || fget(tile, mask)) 721 | sprites[tile].Draw(new Vector2(tx + x * 8, ty + y * 8)); 722 | } 723 | } 724 | } 725 | 726 | public void spr(float sprite, float x, float y, int columns = 1, int rows = 1, bool flipX = false, bool flipY = false) 727 | { 728 | var flip = SpriteEffects.None; 729 | if (flipX) 730 | flip |= SpriteEffects.FlipHorizontally; 731 | if (flipY) 732 | flip |= SpriteEffects.FlipVertically; 733 | 734 | for (int sx = 0; sx < columns; sx++) 735 | for (int sy = 0; sy < rows; sy++) 736 | sprites[(int)sprite + sx + sy * 16].Draw(new Vector2((int)Math.Floor(x + sx * 8), (int)Math.Floor(y + sy * 8)), Vector2.Zero, Color.White, 1f, 0f, flip); 737 | } 738 | 739 | #endregion 740 | } 741 | 742 | } 743 | -------------------------------------------------------------------------------- /Source/PICO-8/Graphics/atlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelFB/Celeste/1b0ce45c75e05649ae91b44a8bb6b196684e4352/Source/PICO-8/Graphics/atlas.png -------------------------------------------------------------------------------- /Source/PICO-8/Graphics/consolebg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelFB/Celeste/1b0ce45c75e05649ae91b44a8bb6b196684e4352/Source/PICO-8/Graphics/consolebg.png -------------------------------------------------------------------------------- /Source/PICO-8/Graphics/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelFB/Celeste/1b0ce45c75e05649ae91b44a8bb6b196684e4352/Source/PICO-8/Graphics/font.png -------------------------------------------------------------------------------- /Source/PICO-8/Graphics/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelFB/Celeste/1b0ce45c75e05649ae91b44a8bb6b196684e4352/Source/PICO-8/Graphics/logo.png -------------------------------------------------------------------------------- /Source/PICO-8/Readme.md: -------------------------------------------------------------------------------- 1 | This is Celeste Classic written in C# that runs in the new full game. 2 | - The actual C# port is located in `Classic.cs` and tries to be as close to 1-1 with the original LUA code as it can. 3 | - The `Emulator.cs` runs the game inside of a Celeste Scene. It references various Celeste & XNA APIs. 4 | - The Emulator references some art assets which exist in the Graphics folder. Note their paths wont match since in the full game these are stored elsewhere. 5 | - The PICO-8 Name and Logo is owned by Lexaloffle Games LLP 6 | 7 | You can play Celeste Classic and view its LUA code here: 8 | https://www.lexaloffle.com/bbs/?tid=2145 -------------------------------------------------------------------------------- /Source/Player/Readme.md: -------------------------------------------------------------------------------- 1 | Having read comments and questions, we thought it would be fun to talk about the things we feel like we would change, and the things we're happy with. 2 | 3 | Obviously much of the code could simply be cleaner had we known exactly what everything was going to do from the beginning, or had time to do a major refactor (which wasn't ever a big priority). 4 | 5 | ### Animation code 6 | We would have liked all Animation related code to be its own system that was more data driven. It's the way it is because we never implemented more than a simple frame-by-frame sprite component. We completely agree that having `if frame == whatever` inside the player class is ugly. 7 | 8 | ### States that shouldn't be states 9 | There are a large number of states that simply shouldn't exist within the player. Everything regarding the `Intro` states and the `Dummy` state should be an entirely different entity used for cutscenes that get swapped with the real player during gameplay. 10 | 11 | This also goes for the `ChaserState`. This could likely be abstracted into a Component and removed entirely from the player class. 12 | 13 | ### One big file vs. A bunch of files 14 | We wouldn't have moved states into their own classes. To us, due to how much interaction there is between states and the nuance in how the player moves, this would turn into a giant messy web of references between classes. If we were to make a tactics game then yes - a more modular system makes sense. 15 | 16 | One reason we like having one big file with some huge methods is because we like to keep the code sequential for maintainability. If the player behavior was split across several files needlessly, or methods like Update() were split up into many smaller methods needlessly, this will often just make it harder to parse the order of operations. In a platformer like Celeste, the player behavior code needs to be very tightly ordered and tuned, and this style of code was a conscious choice to fit the project and team. 17 | 18 | ### How do you Unit Test this class? 19 | We don't. We wrote unit tests for various other parts of the game (ex. making sure the dialog files across languages all match, trigger the correct events, and that their font files have all the appropriate characters). Writing unit tests for the player in an action game with highly nuanced movement that is constantly being refined feels pointless, and would cause more trouble than they're worth. Unit Tests are great tools, but like every tool they have advantages and disadvantages. Unit tests could be useful for making sure input is still triggering, collision functionality behaves as expected, and so on - but none of that should exist in the Player class. 20 | 21 | ### Is there an Entity / Component structure? 22 | We do use a Scene->Entity->Component system, which may not entirely be clear from the player class alone. She inherits "Actor" which inherits "Entity". Actor has generic code for movement and collisions. Anything that was re-used was put into a component (player sprite, player hair, state machine, mirror reflections, and so on). Things that the player only ever did were left in the player. 23 | 24 | ### Why no array in that `ChaserState` struct at the bottom? 25 | The reason that "ChaserState" struct has a switch statement instead of an array is to save on creating garbage. There's no way to have an array in a struct in C# with a predefined size, (ex. `int[4] fourValues;`) so every struct would be creating a new array instance. In the end this probably didn't matter, but we were trying to save on creating garbage during levels as we weren't sure how the GC would perform cross-platform. 26 | 27 | ### Isn't XNA Deprecated? 28 | Yes, it is! We use XNA because we're comfortable in it, like C#, it's very stable on Windows, and is easy to make cross platforms with open source ports such as FNA and MonoGame. If you're playing on macOS or Linux you're on FNA, and on consoles you're on MonoGame. Will we use it for future projects? Maybe, maybe not. 29 | 30 | ### Do you have any tutorials on how the basic physics of Celeste work? 31 | Not right now, but Maddy wrote this overview of the TowerFall physics a few years back: https://medium.com/@MattThorson/celeste-and-towerfall-physics-d24bd2ae0fc5 32 | 33 | Celeste's physics system is very similar to TowerFall's. 34 | 35 | ### How big was the programming team? 36 | We had 2 programmers working on Celeste (Noel and Maddy). 37 | 38 | --------------------------------------------------------------------------------