friendz: save level on level change and game quit
[nit.git] / contrib / friendz / src / friendz.nit
1 # Monsterz - Chains of Friends
2 #
3 # 2010-2014 (c) Jean Privat <jean@pryen.org>
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the Do What The Fuck You Want To
7 # Public License, Version 2, as published by Sam Hocevar. See
8 # http://sam.zoy.org/projects/COPYING.WTFPL for more details.
9
10 # Full UI for the game
11 module friendz is
12 app_name("ChainZ of FriendZ")
13 app_version(0, 1, git_revision)
14 end
15
16 import app::audio
17 import mnit
18 import realtime
19 import solver
20 import mnit::tileset
21 import app::data_store
22
23 intrude import grid
24 intrude import level
25
26 redef class Grid
27 # Zoom level in %
28 # higher means more dense grid
29 var ratio = 100
30
31 # Various grid sizes from large to small (16x16, 12x12, 8x8, 6x6)
32 var ratios: Array[Int] = [200, 150, 100, 75]
33
34 redef fun resize(w,h)
35 do
36 super
37 for r in ratios do
38 if w*100/r <= 8 and h*100/r <= 8 then self.ratio = r
39 end
40 end
41 end
42
43 # * ENTITIES ****************************************************************
44
45 # A game entity is something that is displayed and may interact with the player.
46 abstract class Entity
47 # The associated game
48 var game: Game
49
50 # X left coordinate (in pixel).
51 var x: Int
52
53 # Y top coordinate (in pixel).
54 var y: Int
55
56 # X right coordinate (in pixel).
57 fun x2: Int do return x + width
58
59 # Y bottom coordinate (in pixel).
60 fun y2: Int do return y + height
61
62 # Width
63 var width: Int
64
65 # Height
66 var height: Int
67
68 # Tool tip text (if any)
69 var over: nullable String = null
70
71 # can the entity intercepts drag ang drop events?
72 var draggable = false
73
74 # Draw function. To implement
75 fun draw(ctx: Display) do end
76
77 # Update function. Called each loop. To implement
78 fun update do end
79
80 # Enter function. Called when the cursor enter in the element. To implement
81 fun enter(ev: Event) do end
82
83 # Click function. Called when the player click in the element.
84 # (or activate it with a shortcut).
85 fun click(ev: Event) do end
86
87 # keyboard shortcut do activate the entity, if any
88 var shortcut: nullable String = null
89
90 # Are events received?
91 var enabled = true
92
93 fun bw: Int do return game.bw
94 fun bh: Int do return game.bh
95
96 # Should the entity be redrawn
97 var dirty = true
98 end
99
100 # TEXT BUTTONS ***********************************************************/
101
102 # Button entity displayed as a simple text.
103 # if `over1` is null, then the button is a simple pasive label
104 # if `over1` is set but `over2` is null, then the button is a normal button
105 # if both `over1` and `over2` arew set, then the button is a toggleable button with two states
106 class TextButton
107 super Entity
108 var str: String
109 init(game: Game, str: String, x,y: Int, color: nullable String, over, over2: nullable String)
110 do
111 var w = 10 # TODO
112 super(game, x,y,w,24)
113 self.str = str
114 self.color = color or else "purple"
115 self.over = over
116 self.over1 = over
117 self.over2 = over2
118 self.textx = x
119 if self.toggleable then
120 self.x -= bw/2 + 4
121 end
122 end
123
124 var color: String
125
126 # The description of the button action
127 var over1: nullable String
128 # The description of the state2 button action
129 var over2: nullable String
130
131 # is the button a two-state button
132 fun toggleable: Bool do return over2 != null
133
134 # is the toggleable button in its second state?
135 var toggled = false
136
137 # ttl for highlighting
138 var ttl = 0
139
140 # position of the start of the text
141 # in a toggleable button, there is space for the mark between `x` and `textx`
142 var textx: Int
143
144 redef fun draw(ctx) do
145 if self.toggleable then
146 var w
147 if self.toggled or not self.enabled then w = 6 else w = 7
148 ctx.blit(game.img2[w,0], self.x, self.y)
149 end
150 var c
151 if self.enabled then c = self.color else c = "gray"
152 var c2= null
153 if self.ttl > 0 then c2 = "rgba(255,255,255,{self.ttl/10})"
154 ctx.textx(self.str, self.textx, self.y, self.height, c, c2)
155 self.width = ctx.measureText(self.str, self.height)
156 if self.toggleable then self.width += bw/2 + 4
157 end
158
159 redef fun update
160 do
161 if game.statusbar.over_what != self and self.ttl > 0 then
162 self.ttl-=1
163 self.dirty = true
164 end
165 end
166
167 redef fun enter(ev)
168 do
169 if over1 == null then return
170 if not self.enabled then return
171 game.snd_click.play
172 self.ttl = 10
173 self.dirty = true
174 self.enter2
175 end
176
177 # Called by `enter` do perform additionnal work if the button is active
178 # Specific button should implement this instead of `enter`
179 fun enter2 do end
180
181 redef fun click(ev)
182 do
183 if not self.enabled then
184 game.snd_bing.play
185 else
186 if self.toggleable then
187 self.toggled = not self.toggled
188 if self.toggled then self.over = self.over2 else self.over = self.over1
189 game.statusbar.over_txt = self.over
190 end
191 game.snd_whip.play
192 end
193 self.click2(ev)
194 end
195
196 # Called by `click` do perform additionnal work if the button is active
197 # Specific button should implement this instead of `click`
198 fun click2(ev: Event) do end
199
200 end
201
202 # LEVEL BUTTONS ***********************************************************/
203
204 # button to play a level in the menu screen
205 class LevelButton
206 super Entity
207
208 # The associated level to play
209 var level: Level
210
211 init(l: Level)
212 do
213 self.level = l
214 var i = l.number
215 super(l.game, (i%5)*56 + 54, (i/5)*56 + 55, l.game.bw, l.game.bh)
216
217 self.over = self.level.fullname
218 if self.level.get_state >= l.l_won then
219 if game.levels[9].get_state >= l.l_won then self.over += " --- {self.level.score}/{self.level.gold}"
220 else if self.level.get_state >= l.l_open then
221 if game.levels[9].get_state >= l.l_open then self.over += " --- ?/{self.level.gold}"
222 end
223 self.enabled = l.get_state >= l.l_open or game.cheated
224 end
225
226 redef fun draw(ctx)
227 do
228 var l = level
229 var s = self.level.get_state
230 var ix = 5 + l.number % 2
231 var iy = 0
232 if s == l.l_disabled then
233 ix = 3
234 iy = 3
235 else if s == l.l_open then
236 ix = 1
237 iy = 1
238 ctx.blit(game.img[ix,iy], self.x, self.y)
239 ix = 0
240 iy = 0
241 end
242 ctx.blit(game.img[ix,iy], self.x, self.y)
243
244 if s == l.l_gold then
245 ctx.blit(game.img2[7,0], self.x + bw*5/8, self.y-bh*1/8)
246 end
247 ctx.textx(self.level.name, self.x+5, self.y+5, 24, null, null)
248 end
249
250 redef fun click(ev)
251 do
252 if self.enabled then
253 game.snd_whip.play
254 game.play(self.level)
255 else
256 game.snd_bing.play
257 game.statusbar.set_tmp("Locked level", "red")
258 end
259 end
260
261 end
262
263 # ACHIEVEMENTS ************************************************************/
264
265 # Achievement (monster-like) button in the menu screen
266 class Achievement
267 super Button
268
269 # The number of the achievement (0 is first)
270 var number: Int
271
272 # The name of the achievement
273 var name: String
274
275 init(game: Game, i: Int, name: String)
276 do
277 super(game, 5*56 + 54, i*56 + 55, game.bw, game.bh)
278 self.over = name
279 self.number = i
280 self.name = name
281 var l = game.levels[number*5+4]
282 enabled = l.get_state >= l.l_won
283 if self.enabled then self.over = name + " (unlocked)" else self.over = name + " (locked)"
284 end
285
286 redef fun draw(ctx)
287 do
288 var w
289 if self.enabled then w = 5 else w = 3
290 ctx.blit(game.img[w,self.number+5], self.x, self.y)
291 end
292
293 redef fun click(ev)
294 do
295 if not self.enabled then
296 game.snd_bing.play
297 game.statusbar.set_tmp("Locked achievement!", "red")
298 else
299 game.snd_whip.play
300 self.click2(ev)
301 end
302 end
303
304 fun click2(ev: Event) do
305 # TODO
306 end
307 end
308
309
310 # BOARD (THE GRID) *******************************************************/
311
312 # The board game element.
313 class Board
314 super Entity
315 init(game: Game)
316 do
317 super(game, game.xpad, game.ypad, 8*game.bw, 8*game.bh)
318 draggable = true
319 end
320
321 redef fun draw(ctx)
322 do
323 var grid = game.grid
324 var bwr = bw*100/grid.ratio
325 var bhr = bh*100/grid.ratio
326 var w = grid.width
327 var h = grid.height
328 if game.selected_button == game.button_size then
329 bwr = bw/2
330 bhr = bh/2
331 w = game.gw
332 h = game.gh
333 end
334 self.x = game.xpad+(48*8/2)-w*bwr/2
335 self.y = game.ypad+(48*8/2)-h*bhr/2
336 self.width = w*bwr
337 self.height = h*bhr
338 for i in [0..w[ do
339 for j in [0..h[ do
340 var t = grid.grid[i][j]
341 var dx = i * bwr + self.x
342 var dy = j * bhr + self.y
343 if (i+j)%2 == 0 then
344 ctx.blit_scaled(game.img[5,0], dx, dy, bwr, bhr)
345 else
346 ctx.blit_scaled(game.img[6,0], dx, dy, bwr, bhr)
347 end
348 if t.fixed then
349 if t.shape != null and not game.editing then
350 #ctx.drawImage(game.img, t.shape.x*bw, (2+t.shape.y)*bh, bw, bh, i * bwr + self.x, j * bhr + self.y, bwr, bhr)
351 ctx.blit_scaled(game.img[3,3], dx, dy, bwr, bhr)
352 else
353 ctx.blit_scaled(game.img[3,3], dx, dy, bwr, bhr)
354 end
355 end
356 if t.kind>0 then
357 var m = grid.monsters[t.kind]
358 var s = 0
359 if t.blink > 0 then s = 1
360 if t.nexts > 2 then s = 3
361 if t.nexts == 0 then s = 6
362 if m.chain then s = 5
363 if t.shocked>0 then s = 2
364 ctx.blit_scaled(game.img[s,(4+t.kind)], dx, dy, bwr, bhr)
365 end
366 #ctx.textx(t.chain_mark.to_s, dx, dy, 20, "", null)
367 end
368 end
369 if game.selected_button == game.button_size then
370 var x0 = self.x
371 var x1 = (grid.width) * bwr - bwr/2 + self.x
372 var y0 = self.y
373 var y1 = (grid.height) * bhr - bhr/2 + self.y
374 ctx.blit_scaled(game.img2[0,0], x0, y0, bwr/2, bhr/2)
375 ctx.blit_scaled(game.img2[1,0], x1, y0, bwr/2, bhr/2)
376 ctx.blit_scaled(game.img2[1,1], x1, y1, bwr/2, bhr/2)
377 ctx.blit_scaled(game.img2[0,1], x0, y1, bwr/2, bhr/2)
378 ctx.textx("{grid.width}x{grid.height}",self.x + grid.width*bwr/2,self.y+grid.height*bhr/2,20,"orange",null)
379 end
380 end
381
382 redef fun update
383 do
384 var grid = game.grid
385 for i in [0..grid.width[ do
386 for j in [0..grid.height[ do
387 var t = grid.grid[i][j]
388 if t.kind == 0 then continue
389 if t.blink > 0 then
390 t.blink-=1
391 self.dirty=true
392 end
393 if t.shocked > 0 then
394 t.shocked-=1
395 self.dirty=true
396 else if 100.rand == 0 then
397 t.blink = 5
398 self.dirty=true
399 end
400 end
401 end
402 end
403
404 # Last clicked tile
405 # Uded to filter drag events
406 private var last: nullable Tile = null
407
408 redef fun click(ev)
409 do
410 var grid = game.grid
411 var r = grid.ratio
412 if game.selected_button == game.button_size then r = 200
413 var x = ev.game_x * r / bw / 100
414 var y = ev.game_y * r / bh / 100
415 var t = grid.grid[x][y]
416
417 if ev.drag and last == t then return
418 last = t
419
420 if game.selected_button != game.button_size and (x>=grid.width or y>=grid.height) then return
421
422 # print "{ev.game_x},{ev.game_y},{ev.drag} -> {x},{y}:{t}"
423
424 if game.selected_button != null then
425 game.selected_button.click_board(ev, t)
426 end
427 end
428 end
429
430 # BUTTONS *****************************************************************/
431
432 # A in-game selectable button for monsters or tools
433 class Button
434 super Entity
435
436 # The x tile
437 var imgx: Int = 0
438
439 # The y tile
440 var imgy: Int = 0
441
442 # The associated monster tile
443 # >0 for monsters, <=0 for tools
444 var kind = 0
445
446 redef fun draw(ctx)
447 do
448 ctx.blit(game.img[self.imgx, self.imgy], self.x, self.y)
449 if game.selected_button == self then ctx.blit(game.img[0, 0], self.x, self.y)
450 end
451
452 redef fun click(ev)
453 do
454 var sel = game.selected_button
455 if game.selected_button == game.button_size then game.board.dirty=true
456 if sel != null then sel.dirty=true
457 game.selected_button = self
458 game.snd_click.play
459 end
460
461 # Current inputed chain
462 # Used for drag
463 private var chain = new Array[Tile]
464
465 # Board click. Called when the player click on the board with the button selected.
466 fun click_board(ev: Event, t: Tile)
467 do
468 game.score.dirty = true
469 if ev.drag and self.kind>0 and not chain.is_empty then
470 if self.chain.length >= 2 and self.chain[1] == t then
471 var t2 = self.chain.shift
472 game.snd_click.play
473 if t2.fixed and not game.editing then return
474 t2.update(0)
475 return
476 end
477 if t.fixed and t.kind == self.kind then
478 self.chain.unshift(t)
479 game.snd_click.play
480 return
481 end
482 if (self.chain[0].x - t.x).abs + (self.chain[0].y - t.y).abs != 1 then return
483 if t.fixed and not game.editing then
484 game.snd_bing.play
485 return
486 end
487 if t.kind != 0 and t.kind != self.kind then
488 t.shocked = 5
489 game.snd_duh.play
490 return
491 end
492 self.chain.unshift(t)
493 if t.kind == self.kind then return
494 game.snd_click.play
495 t.update(self.kind)
496 return
497 end
498
499 if t.fixed and not game.editing then
500 if t.kind == 0 then
501 game.snd_bing.play
502 return
503 end
504 if t.kind != self.kind and not ev.drag then
505 game.buttons[t.kind].click(ev)
506 game.buttons[t.kind].chain = [t]
507 else
508 self.chain = [t]
509 game.snd_bing.play
510 end
511 return
512 end
513 if t.fixed and game.editing and self == game.button_erase and t.kind == 0 then
514 t.fixed = false
515 game.snd_click.play
516 return
517 end
518
519 var nkind = 0 # the new kind
520 if ev.drag then
521 # Here we clean
522 if t.kind == 0 then return
523 if self.kind != 0 and t.kind != self.kind then
524 t.shocked = 5
525 game.snd_duh.play
526 return
527 end
528 nkind = 0
529 else if t.kind != self.kind then
530 nkind = self.kind
531 self.chain = [t]
532 else if t.kind != 0 then
533 nkind = 0
534 self.chain.clear
535 end
536 if nkind == t.kind then return
537 game.snd_click.play
538 t.update(nkind)
539 end
540 end
541
542 # A monster button
543 class MonsterButton
544 super Button
545
546 # TTL for the monster being angry
547 var angries = 0
548 # TTL for the monster being happy
549 var happy = 0
550 # TTL for the monster being shocked
551 var shocked = 0
552 # TTL for the monster blinking
553 var blink = 0
554
555 init(game: Game, i: Int)
556 do
557 self.game = game
558 var x = 440 + 58 * ((i-1).abs%3)
559 var y = 150 + (bh+5) * ((i-1)/3)
560 super(game, x, y, game.bw, game.bh)
561 if i == 0 then return
562 kind = i
563 imgx = 0
564 imgy = (4+i)
565 over = game.colors[i] + " monster ({i})"
566 shortcut = i.to_s # code for 1 trough 9
567 end
568
569 redef fun click(ev)
570 do
571 super
572 self.shocked = 5
573 end
574
575 redef fun update
576 do
577 if self.happy > 0 then
578 self.happy-=1
579 self.dirty=true
580 end
581 if self.shocked > 0 then
582 self.shocked-=1
583 self.dirty=true
584 end
585 if self.blink > 0 then
586 self.blink-=1
587 self.dirty=true
588 else if 100.rand == 0 then
589 self.blink = 5
590 self.dirty=true
591 end
592 end
593
594 redef fun draw(ctx)
595 do
596 var s = self.imgx
597 if self.angries>0 then
598 s += 3
599 else if self.happy > 5 then
600 s += 5
601 else if self.shocked > 0 then
602 s += 5
603 else if self.blink > 0 then
604 s += 1
605 end
606 ctx.blit(game.img[s, self.imgy], self.x, self.y)
607 if game.selected_button == self then ctx.blit(game.img[0, 0], self.x, self.y)
608 end
609 end
610
611 # Erase button.
612 class EraseButton
613 super Button
614 init(game: Game) do
615 super(game, 440, 92, game.bh, 22+game.bh)
616 imgx = 4
617 imgy = 13
618 kind = 0
619 over = "Eraser (0)"
620 shortcut = "0"
621 end
622 end
623
624 # Metal (fixed) button.
625 class MetalButton
626 super Button
627 init(game: Game)
628 do
629 super(game, 498, 92, game.bh, 20+game.bh)
630 imgx = 3
631 imgy = 3
632 kind = -1
633 over = "Metal block (q)"
634 shortcut = "q"
635 end
636
637 private var fixed = false
638
639 redef fun click_board(ev,t)
640 do
641 if not ev.drag then self.fixed = not t.fixed
642 if t.fixed == self.fixed then return
643 t.fixed = self.fixed
644 game.snd_click.play
645 end
646 end
647
648 # Resize button.
649 class ResizeButton
650 super Button
651
652 init(game: Game)
653 do
654 super(game,556, 92, game.bh, 20+game.bh)
655 kind = -2
656 over = "Resize the grid"
657 end
658
659 redef fun draw(ctx)
660 do
661 for i in [0..3[ do
662 for j in [0..3[ do
663 var x = self.x + i*bw/3
664 var y = self.y + j*bh/3
665 ctx.blit_scaled(game.img[5+(i+j)%2,0], x, y, bw/3, bh/3)
666 end
667 end
668 if game.selected_button == self then ctx.blit(game.img[0, 0], self.x, self.y)
669 end
670
671 redef fun click(ev)
672 do
673 if game.selected_button != game.button_size then
674 super
675 else
676 game.selected_button = null
677 game.board.dirty=true
678 end
679 end
680
681 redef fun click_board(evt, t)
682 do
683 var grid = t.grid
684 var w = t.x+1
685 var h = t.y+1
686 if w < 3 or h < 3 then
687 game.snd_bing.play
688 game.statusbar.set_tmp("Too small!", "red")
689 return
690 end
691 var aborts = false
692 for i in [0..grid.width[ do
693 for j in [0..grid.height[ do
694 if i>=w or j>=h then
695 var t2 = grid.grid[i][j]
696 if t2.kind > 0 then
697 aborts = true
698 t2.shocked = 5
699 end
700 end
701 end
702 end
703 if aborts then
704 game.snd_duh.play
705 game.statusbar.set_tmp("Monsters on the way!", "red")
706 return
707 end
708 game.snd_click.play
709 grid.resize(w,h)
710 end
711 end
712
713 # Inactive area used to display the score
714 class Score
715 super Entity
716 init(game: Game)
717 do
718 super(game,440,310,199,62)
719 end
720 redef fun draw(ctx)
721 do
722 ctx.textx("MONSTERS: {game.grid.number}",self.x,self.y+1,21,"cyan",null)
723 var level = game.level
724 if level == null then return
725 if level.get_state >= level.l_won then
726 ctx.textx("BEST: {level.score}",self.x,self.y+22,21,"pink", null)
727 else
728 ctx.textx("BEST: -",self.x,self.y+22,21,"pink", null)
729 end
730 if game.levels[9].get_state >= level.l_won then
731 if level.is_challenge then
732 ctx.textx("GOAL: {level.gold}",self.x,self.y+44,21,"yellow",null)
733 else
734 ctx.textx("GOLD: {level.gold}",self.x,self.y+44,21,"yellow",null)
735 end
736 end
737 end
738 end
739
740 # Status bar element.
741 class StatusBar
742 super Entity
743 init(game: Game)
744 do
745 super(game,24, 440, 418-24, 30)
746 end
747
748 # Permanant text, if any
749 var main_txt: nullable String = null
750
751 # Text to display when the cursor if over an entity (`over_what`), if any
752 var over_txt: nullable String = null
753
754 # What is the entity for `over_txt`
755 var over_what: nullable Entity
756
757 # Text to temporally display, for some game event, if any
758 var tmp_txt: nullable String = null
759
760 # time-to-live for the `tmp_txt`
761 var tmp_txt_ttl = 0
762
763 # Color used to display `tmp_txt`
764 var tmp_txt_color: nullable String = null
765
766 # reset the status
767 fun clear do
768 self.main_txt = null
769 self.over_txt = null
770 self.tmp_txt = null
771 self.over = null
772 end
773
774 # set a temporary text
775 fun set_tmp(txt, color: String)
776 do
777 print "***STATUS** {txt}"
778 self.tmp_txt = txt
779 self.tmp_txt_ttl = 60
780 self.tmp_txt_color = color
781 end
782
783 redef fun draw(ctx)
784 do
785 var tmp_txt = self.tmp_txt
786 var over_txt = self.over_txt
787 var main_txt = self.main_txt
788 if tmp_txt != null and self.tmp_txt_ttl>0 then
789 ctx.textx(tmp_txt,24,442,24,self.tmp_txt_color,null)
790 else if over_txt != null then
791 ctx.textx(over_txt,24,442,24,"yellow",null)
792 else if main_txt != null then
793 ctx.textx(main_txt,24,442,24,"white",null)
794 end
795 end
796
797 redef fun update
798 do
799 if self.tmp_txt_ttl>0 then
800 self.tmp_txt_ttl-=1
801 self.dirty=true
802 end
803 end
804 end
805
806 # ************************************************************************/
807
808 redef class Display
809 # Display a text
810 fun textx(str: String, x, y, height: Int, color, color2: nullable String)
811 do
812 #var w = measureText(str, height)
813 #rect(x,y,w,height)
814 text(str.to_upper, app.game.font, x, y)
815 end
816
817 # give the width for a giver text
818 fun measureText(str: String, height: Int): Int
819 do
820 var font = app.game.font
821 return str.length * (font.width + font.hspace.to_i)
822 end
823
824 # displays a debug rectangle
825 fun rect(x,y,w,h:Int)
826 do
827 var image = once app.load_image("hitbox.png")
828 blit_scaled(image, x, y, w, h)
829 end
830 end
831
832 # Simple basic class for event
833 class Event
834 # Is a drag event?
835 var drag = false
836 # screen x
837 var offset_x: Int
838 # screen y
839 var offset_y: Int
840 # entity x
841 var game_x = 0
842 # entity y
843 var game_y = 0
844 # key pressed
845 var char_code: String
846 end
847
848 redef class Game
849 # width of a tile, used for most width reference in the game
850 var bw = 48
851 # height a tile, used for most width reference in the game
852 var bh = 48
853 # x-coordinate of the board (padding)
854 var xpad = 24
855 # y-coordinate of the board (padding)
856 var ypad = 24
857
858 # Load tiles
859
860 # Basic tileset
861 var img = new TileSet(app.load_image("tiles2.png"),48,48)
862
863 # Sub tileset (for marks or other)
864 var img2 = new TileSet(app.load_image("tiles2.png"),24,24)
865
866 # background image
867 var back: Image = app.load_image("background.png")
868
869 # Logo image
870 var logo: Image = app.load_image("logo.png")
871
872 # Font
873 var font = new TileSetFont(app.load_image("deltaforce_font.png"), 16, 17, "ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789.:;!?\"'() -,/")
874
875 # DISPLAY *****************************************************************
876
877 # Is the game in editing mode
878 var editing = false
879
880 # The selected button, if any
881 var selected_button: nullable Button = null
882
883 # SOUND
884
885 # Is the music muted?
886 var music_muted: Bool = app.data_store["music_muted"] == true
887
888 # Is the sound effects muted?
889 var sfx_muted: Bool = app.data_store["sfx_muted"] == true
890
891 # The background music resource. */
892 var music = new Music("music.ogg")
893
894 # Click sound
895 var snd_click = new Sound("click.wav")
896
897 # Wining soulf
898 var snd_win = new Sound("level.wav")
899
900 # Shocked sound
901 var snd_duh = new Sound("duh.wav")
902
903 # metal sound
904 var snd_bing = new Sound("bing.wav")
905
906 # transition sound
907 var snd_whip = new Sound("whip.wav")
908
909
910 # INPUT ******************************************************************
911
912 # Current grid edited (if any).
913 var grid_edit: nullable Grid = null
914
915 # Sequence of current entities
916 var entities = new Array[Entity]
917
918 # The current statusbar
919 var statusbar = new StatusBar(self)
920
921 # The grid board
922 var board = new Board(self)
923
924 # The current score board
925 var score = new Score(self)
926
927 # Monster button game elements.
928 var buttons = new Array[MonsterButton]
929
930 # MetalButton
931 var button_wall = new MetalButton(self)
932
933 # EraseButton
934 var button_erase = new EraseButton(self)
935
936 # ResizeButton
937 var button_size = new ResizeButton(self)
938
939 # Cheat mode enabled?
940 var cheated = false
941
942 init
943 do
944 load_levels
945 init_buttons
946 entities.clear
947 title
948
949 if not music_muted then music.play
950 end
951
952 # fill `buttons`
953 fun init_buttons
954 do
955 for i in [0..9] do
956 buttons[i] = new MonsterButton(self, i)
957 end
958 end
959
960 # Play a level in player mode.
961 fun play(l: Level)
962 do
963 save # save the previous level grid
964 level = l
965 grid.load(level.saved_str or else level.str)
966 init_play_menu(false)
967 if level.status != "" then
968 statusbar.main_txt = level.status
969 else
970 statusbar.main_txt = level.fullname
971 end
972 var t = new NextLevelButton(self)
973 entities.push(t)
974 run
975 end
976
977 # Play the next level.
978 fun play_next
979 do
980 play(levels[level.number+1])
981 end
982
983
984 # Helper function to initialize all states.
985 # Set up buttons for music and SFX.
986 fun init_game
987 do
988 editing = false
989 solver = null
990 entities.clear
991 entities.push(new MusicButton(self))
992 entities.push(new SFXButton(self))
993 entities.push(new MenuButton(self))
994 statusbar.clear
995 entities.push(statusbar)
996 end
997
998 # Helper function to initialize monster menu entries.
999 fun init_play_menu(full: Bool)
1000 do
1001 init_game
1002 entities.push(board)
1003 entities.push(new ResetButton(self))
1004 entities.push(button_erase)
1005 # Push monster buttons and determine the selected one
1006 var sel: nullable Button = null
1007 for i in [1..monsters] do
1008 if grid.monsters[i].number > 0 or full then
1009 if selected_button == buttons[i] or sel == null then
1010 sel = buttons[i]
1011 end
1012 entities.push(buttons[i])
1013 end
1014 end
1015 selected_button = sel
1016 entities.push(score)
1017 end
1018
1019 # Play a arbitrary grid in try mode.
1020 fun play_grid(g: Grid)
1021 do
1022 grid = g
1023 init_play_menu(false)
1024 statusbar.main_txt = "User level"
1025 if grid_edit != null then
1026 entities.push(new EditButton(self))
1027 end
1028 entities.push(new WonButton(self))
1029 run
1030 end
1031
1032 # Launch the editor starting with a grid.
1033 fun edit_grid(g: Grid)
1034 do
1035 grid = g
1036 init_play_menu(true)
1037 editing = true
1038 statusbar.main_txt = "Level editor"
1039 if level != null then statusbar.main_txt += ": level "+level.name
1040 entities.push(button_wall)
1041 entities.push(button_size)
1042 entities.push(new TestButton(self))
1043 entities.push(new SaveButton(self))
1044 entities.push(new LoadButton(self))
1045 run
1046 end
1047
1048 # Launch the title screen
1049 fun title
1050 do
1051 init_menu
1052 entities.push(new Splash(self))
1053 run
1054 end
1055
1056 # Helper function to initialize the menu (and tile) screen
1057 fun init_menu
1058 do
1059 save # save the previous level grid
1060 init_game
1061 level = null
1062 var i = levels.first
1063 for l in levels do
1064 i = l
1065 if l.get_state == l.l_open then break
1066 end
1067 entities.push(new StartButton(self, i))
1068 end
1069
1070 # Launch the menu.
1071 fun menu
1072 do
1073 init_menu
1074 var t
1075 t = new TextButton(self,"LEVEL SELECT", 120, ypad, "white", null, null)
1076 entities.push(t)
1077 for i in [0..levels.length[ do
1078 var b = new LevelButton(levels[i])
1079 entities.push(b)
1080 end
1081 t = new Achievement(self, 0, "Training")
1082 entities.push(t)
1083 t = new Achievement(self, 1, "Gold")
1084 entities.push(t)
1085 t = new Achievement(self, 2, "Editor")
1086 entities.push(t)
1087 t = new Achievement(self, 3, "Challenge")
1088 entities.push(t)
1089 t = new Achievement(self, 4, "Congraturation")
1090 entities.push(t)
1091 t = new Achievement(self, 5, "Awesome")
1092 entities.push(t)
1093 run
1094 end
1095
1096 # Last function called when the lauch state is ready
1097 fun run do
1098 dirty_all = true
1099 end
1100
1101 # Should all entity redrawn?
1102 var dirty_all = true
1103
1104 # Draw all game entities.
1105 fun draw(display: Display) do
1106 dirty_all = true
1107 if dirty_all then display.blit(back, 0, 0)
1108 for g in entities do
1109 if g.dirty or dirty_all then
1110 g.dirty = false
1111 #if g.x2-g.x>0 and g.y2-g.y>0 then ctx.drawImage(back, g.x, g.y, g.x2-g.x, g.y2-g.y, g.x, g.y, g.x2-g.x, g.y2-g.y)
1112 g.draw(display)
1113 #ctx.rect(g.x, g.y, g.width, g.height)
1114 end
1115 end
1116 var ev = lastev
1117 if ev isa Event then
1118 # Cursor, kept for debugging
1119 #display.blit(img[4,0],ev.offset_x-42,ev.offset_y-6)
1120 end
1121 dirty_all = false
1122 end
1123
1124 # Update all game entities.
1125 fun step do
1126 if solver != null and not solver_pause then
1127 if solver.run_steps(solver_steps) != null then solver_pause = true
1128 print solver.to_s
1129 if not solver.is_running then solver = null
1130 end
1131 for g in entities do
1132 g.update
1133 end
1134 end
1135
1136 # Return the game entity located at a mouse event.
1137 fun get_game_element(ev: Event): nullable Entity
1138 do
1139 var x = ev.offset_x
1140 var y = ev.offset_y
1141 for g in entities do
1142 if x>=g.x and x<g.x2 and y>g.y and y<g.y2 then
1143 ev.game_x = x-g.x
1144 ev.game_y = y-g.y
1145 #print "get {g}"
1146 return g
1147 end
1148 end
1149 return null
1150 end
1151
1152 # The game entlty the mouse went down on
1153 var drag: nullable Entity = null
1154
1155 # Last mouse event. Used to dray the cursor
1156 var lastev: nullable Event = null
1157
1158 # Callback when the mouse is pressed
1159 fun onMouseDown(ev: Event) do
1160 lastev = ev
1161 var g = get_game_element(ev)
1162 if g != null then
1163 g.click(ev)
1164 g.dirty = true
1165 end
1166 drag = g
1167 end
1168
1169 # Callback when the mouse is releassed
1170 fun onMouseUp(ev: Event) do
1171 drag = null
1172 end
1173
1174 # Callback when the mouse if moved while pressed
1175 fun onMouseMove(ev: Event) do
1176 lastev = ev
1177 var g = get_game_element(ev)
1178 if g == null then
1179 statusbar.dirty = true
1180 statusbar.over_txt = null
1181 statusbar.over_what = null
1182 return
1183 end
1184 if statusbar.over_what != g then
1185 statusbar.dirty = true
1186 var go = g.over
1187 statusbar.over_txt = go
1188 statusbar.over_what = g
1189 g.enter(ev)
1190 if go != null then print "***OVER*** {go}"
1191 end
1192 # We moved abode a element that accepts drag event
1193 if drag == g and g.draggable then
1194 # print "DRAG {g}"
1195 ev.drag = true
1196 g.click(ev)
1197 g.dirty = true
1198 end
1199 end
1200
1201 # Current solver, if any
1202 var solver: nullable BacktrackSolver[Grid, Action] = null
1203
1204 # Is the solver paused?
1205 var solver_pause = false
1206
1207 # Number of solver steps played in a single game `update`
1208 var solver_steps = 20000
1209
1210 # Callback when a keyboard event is recieved
1211 fun onKeyDown(ev: Event) do
1212 var kc = ev.char_code
1213 if kc == "e" then
1214 set_tmp("RUN EDITOR")
1215 grid_edit = grid.copy(true)
1216 edit_grid(grid)
1217 else if kc == "c" then
1218 if cheated then
1219 set_tmp("CHEAT: OFF")
1220 snd_duh.play
1221 cheated = false
1222 else
1223 set_tmp("CHEAT: ON")
1224 snd_win.play
1225 cheated = true
1226 end
1227 else if kc == "s" then
1228 if solver == null then
1229 solver = (new FriendzProblem(grid)).solve
1230 solver_pause = false
1231 else
1232 solver_pause = not solver_pause
1233 end
1234 if solver_pause then
1235 set_tmp("SOLVER: PAUSED")
1236 else
1237 set_tmp("SOLVER: ON")
1238 end
1239 #solver.step
1240 else if kc == "d" then
1241 if solver == null then
1242 solver = (new FriendzProblem(grid)).solve
1243 solver_pause = true
1244 set_tmp("SOLVER: ON")
1245 else
1246 solver_pause = true
1247 solver.run_steps(1)
1248 set_tmp("SOLVER: ONE STEP")
1249 end
1250 else if kc == "+" then
1251 solver_steps += 100
1252 set_tmp("SOLVER: {solver_steps} STEPS")
1253 else if kc == "-" then
1254 solver_steps -= 100
1255 set_tmp("SOLVER: {solver_steps} STEPS")
1256 else for g in entities do
1257 if kc == g.shortcut then
1258 g.click(ev)
1259 g.dirty = true
1260 end
1261 end
1262 end
1263
1264 fun set_tmp(s: String)
1265 do
1266 statusbar.set_tmp(s, "cyan")
1267 end
1268
1269 redef fun load_levels
1270 do
1271 super
1272
1273 for level in levels do
1274 var score = app.data_store["s{level.str}"]
1275 if score isa Int then
1276 level.score = score
1277 end
1278 var saved_str = app.data_store["g{level.str}"]
1279 if saved_str isa String then
1280 print "LOAD {level.name}: {saved_str}"
1281 level.saved_str = saved_str
1282 end
1283 end
1284 end
1285
1286 fun save
1287 do
1288 var l = level
1289 if l != null then
1290 l.save
1291 end
1292 end
1293 end
1294
1295 # The spash title image
1296 class Splash
1297 super Entity
1298 init(game: Game)
1299 do
1300 super(game,game.xpad,game.ypad,380,350)
1301 end
1302 redef fun draw(ctx)
1303 do
1304 ctx.blit(game.logo, game.xpad, game.ypad)
1305 end
1306 redef fun click(ev)
1307 do
1308 game.snd_whip.play
1309 game.menu
1310 end
1311 end
1312
1313 class NextLevelButton
1314 super TextButton
1315 init(game: Game)
1316 do
1317 super(game, "NEXT LEVEL", 440, 24, "cyan", "Play next level", null)
1318 enabled = false
1319 end
1320
1321 redef fun update
1322 do
1323 var w = game.level.check_won(game.grid)
1324 if self.enabled != w then
1325 self.dirty = true
1326 self.enabled = w
1327 if w then
1328 game.snd_win.play
1329 game.statusbar.set_tmp("Level solved!", "cyan")
1330 end
1331 end
1332 end
1333
1334 redef fun click(ev)
1335 do
1336 if not self.enabled then
1337 game.snd_duh.play
1338 var grid = game.grid
1339 var monsters = grid.monsters
1340 var angry = new Array[Tile]
1341 var lonely = new Array[Tile]
1342 var edges = new Array[Tile]
1343 for i in [0..grid.width[ do
1344 for j in [0..grid.height[ do
1345 var t = grid.grid[i][j]
1346 if t.kind == 0 then continue
1347 if t.nexts == 0 then lonely.push(t)
1348 if t.nexts == 1 and not monsters[t.kind].chain then edges.push(t)
1349 if t.nexts > 2 then angry.push(t)
1350 end
1351 end
1352
1353 var l
1354 if angry.length>0 then
1355 l = angry
1356 else if lonely.length>0 then
1357 l = lonely
1358 else
1359 l = edges
1360 end
1361 for i in l do i.shocked=5
1362
1363 if angry.length>0 then
1364 game.statusbar.set_tmp("Angry monsters!", "red")
1365 else if lonely.length>0 then
1366 game.statusbar.set_tmp("Lonely monsters!", "red")
1367 else if not grid.won then
1368 game.statusbar.set_tmp("Unconnected monsters!", "red")
1369 else
1370 game.statusbar.set_tmp("Not enough monsters!", "red")
1371 end
1372 return
1373 end
1374
1375 game.snd_whip.play
1376 game.play_next
1377 end
1378 end
1379
1380 class MusicButton
1381 super TextButton
1382 init(game: Game)
1383 do
1384 super(game, "MUSIC", 470, 412, "purple", "Mute the music", "Unmute the music")
1385 toggled = game.music_muted
1386 end
1387 redef fun click2(ev)
1388 do
1389 game.music_muted = self.toggled
1390 if game.music_muted then game.music.pause else game.music.play
1391 app.data_store["music_muted"] = game.music_muted
1392 end
1393 end
1394
1395 class SFXButton
1396 super TextButton
1397 init(game: Game)
1398 do
1399 super(game, "SOUND FX", 470, 382, "purple", "Mute the sound effects", "Unmute the sound effects")
1400 toggled = game.sfx_muted
1401 end
1402
1403 redef fun click2(ev)
1404 do
1405 game.sfx_muted = self.toggled
1406 if not game.sfx_muted then game.snd_whip.play # Because the automatic one was muted
1407 app.data_store["sfx_muted"] = game.sfx_muted
1408 end
1409 end
1410
1411 class MenuButton
1412 super TextButton
1413 init(game: Game)
1414 do
1415 super(game, "MENU", 470, 442, "purple", "Exit to the main menu", null)
1416 shortcut = "back"
1417 end
1418
1419 redef fun click2(ev)
1420 do
1421 game.menu
1422 end
1423 end
1424
1425 class ResetButton
1426 super TextButton
1427 init(game: Game)
1428 do
1429 super(game,"RESET", 440, 61, "purple", "Clear the grid",null)
1430 end
1431
1432 var count = 0
1433
1434 redef fun click2(ev)
1435 do
1436 self.count += 1
1437 if self.count==1 then
1438 game.statusbar.set_tmp("Click again to reset","white")
1439 else if self.count==2 then
1440 game.grid.reset(false)
1441 if game.editing then
1442 game.statusbar.set_tmp("Click again to clear all","white")
1443 end
1444 else if game.editing then
1445 game.grid.reset(true)
1446 end
1447 game.dirty_all = true
1448 end
1449
1450 redef fun enter2
1451 do
1452 self.count = 0
1453 end
1454 end
1455
1456 class EditButton
1457 super TextButton
1458 init(game: Game)
1459 do
1460 super(game,"EDIT", 540, 24, "purple", "Return to the editor",null)
1461 end
1462
1463 redef fun click2(ev)
1464 do
1465 var ge = game.grid_edit
1466 assert ge != null
1467 game.edit_grid(ge)
1468 end
1469 end
1470
1471 class WonButton
1472 super TextButton
1473 init(game: Game)
1474 do
1475 super(game,"WON", 440, 24, "cyan", "", null)
1476 enabled = false
1477 end
1478 redef fun click2(ev)
1479 do
1480 var ge = game.grid_edit
1481 if not self.enabled then
1482 game.statusbar.set_tmp("Solve the level first!", "red")
1483 else if ge != null then
1484 game.snd_whip.play
1485 game.edit_grid(ge)
1486 else
1487 game.snd_whip.play
1488 game.menu
1489 end
1490 end
1491
1492 redef fun update
1493 do
1494 var w = game.grid.won
1495 if self.enabled != w then
1496 self.dirty = true
1497 self.enabled = w
1498 if w then
1499 game.snd_win.play
1500 game.statusbar.set_tmp("Level solved!", "cyan")
1501 end
1502 end
1503 end
1504 end
1505
1506 class TestButton
1507 super TextButton
1508 init(game: Game)
1509 do
1510 super(game,"TEST", 440, 24, "cyan", "Try to play the level", null)
1511 end
1512
1513 redef fun click2(ev)
1514 do
1515 game.grid_edit = game.grid
1516 game.play_grid(game.grid.copy(false))
1517 end
1518 end
1519
1520 class SaveButton
1521 super TextButton
1522 init(game: Game)
1523 do
1524 super(game, "SAVE", 540, 24, "purple", "Save the level", null)
1525 end
1526
1527 redef fun click2(ev)
1528 do
1529 var res = game.grid.save
1530 print "SAVE: {res}"
1531 end
1532 end
1533
1534 class LoadButton
1535 super TextButton
1536 init(game: Game)
1537 do
1538 super(game,"LOAD", 540, 61, "purple", "Load an user level", null)
1539 end
1540
1541 redef fun click2(ev)
1542 do
1543 var grid2 = new Grid(game.gw,game.gh,game.monsters)
1544 if grid2.load("") then
1545 game.grid = grid2
1546 end
1547 game.dirty_all = true
1548 end
1549 end
1550
1551 class ContinueButton
1552 super TextButton
1553 init(game: Game)
1554 do
1555 super(game,"CONTINUE", 440, 24, "purple", "Continue playing", null)
1556 end
1557
1558 redef fun click2(ev)
1559 do
1560 game.play_next
1561 end
1562 end
1563
1564 class StartButton
1565 super TextButton
1566 var level: Level
1567 init(game: Game, level: Level)
1568 do
1569 self.level = level
1570 if level.number == 0 then
1571 super(game,"START", 440, 24, "purple", "Play the first level", null)
1572 else
1573 super(game,"CONTINUE", 440, 24, "purple", "Continue from level "+level.name, null)
1574 end
1575 end
1576
1577 redef fun click2(ev)
1578 do
1579 game.play(level)
1580 end
1581 end
1582
1583 #
1584
1585 redef class App
1586
1587 # The game
1588 var game: Game
1589
1590 # Wanted screen width
1591 var screen_width = 640
1592
1593 # Wanted screen height
1594 var screen_height = 480
1595
1596 redef fun on_create
1597 do
1598 super
1599 game = new Game
1600 game.font.hspace = -2
1601 if args.length > 0 then
1602 game.play(game.levels[args.first.to_i])
1603 end
1604 # img loading?
1605 end
1606
1607 redef fun on_pause
1608 do
1609 super
1610 game.save
1611 end
1612
1613 # Maximum wanted frame per second
1614 var max_fps = 30
1615
1616 # clock used to track FPS
1617 private var clock = new Clock
1618
1619 redef fun frame_core(display)
1620 do
1621 game.step
1622 game.draw(display)
1623 var dt = clock.lapse
1624 var target_dt = 1000000000 / max_fps
1625 if dt.sec == 0 and dt.nanosec < target_dt then
1626 var sleep_t = target_dt - dt.nanosec
1627 sys.nanosleep(0, sleep_t)
1628 end
1629 end
1630
1631 redef fun input(input_event)
1632 do
1633 #print input_event
1634 if input_event isa QuitEvent then # close window button
1635 quit = true # orders system to quit
1636 else if input_event isa PointerEvent then
1637 var ev = new Event(input_event.x.to_i, input_event.y.to_i, "")
1638 if input_event.is_motion then
1639 game.onMouseMove(ev)
1640 else if input_event.pressed then
1641 game.onMouseDown(ev)
1642 else
1643 game.onMouseUp(ev)
1644 end
1645 return true
1646 else if input_event isa KeyEvent and input_event.is_down then
1647 var ev = new Event(0, 0, input_event.key_name)
1648 game.onKeyDown(ev)
1649 return true
1650 end
1651
1652 return false
1653 end
1654 end
1655
1656 redef class PointerEvent
1657 fun is_motion: Bool do return false
1658 end
1659
1660 redef class KeyEvent
1661 fun key_name: String
1662 do
1663 var c = to_c
1664 if c != null then return c.to_s
1665 return "unknown"
1666 end
1667 end
1668
1669 redef class Level
1670 # Save the score and grid of the level
1671 fun save
1672 do
1673 app.data_store["s{str}"] = if score > 0 then score else null
1674 var saved = game.grid.save
1675 saved_str = saved
1676 app.data_store["g{str}"] = saved
1677 print "SAVE: {name}: {saved}"
1678 end
1679
1680 # The saved player grid (to continue games)
1681 var saved_str: nullable String = null
1682 end