├── 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 |
--------------------------------------------------------------------------------