contrib/friendz: use the correct next level on "continue"
[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 mnit
17 import realtime
18 import solver
19 import mnit::tileset
20 import app::data_store
21 import md5
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.replay
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.replay
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.replay
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.par}"
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.par}"
222 end
223 #self.enabled = l.get_state >= l.l_open
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_par 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.replay
254 game.play(self.level)
255 else
256 game.snd_bing.replay
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.replay
297 game.statusbar.set_tmp("Locked achievement!", "red")
298 else
299 game.snd_whip.replay
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.replay
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.replay
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.replay
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.replay
485 return
486 end
487 if t.kind != 0 and t.kind != self.kind then
488 t.shocked = 5
489 game.snd_duh.replay
490 return
491 end
492 self.chain.unshift(t)
493 if t.kind == self.kind then return
494 game.snd_click.replay
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.replay
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.replay
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.replay
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.replay
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.replay
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.replay
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.replay
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.replay
705 game.statusbar.set_tmp("Monsters on the way!", "red")
706 return
707 end
708 game.snd_click.replay
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.par}",self.x,self.y+44,21,"yellow",null)
733 else
734 ctx.textx("PAR: {level.par}",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 = 20
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 # Simple audio asset
809 class Audio
810 var path: String
811
812 # placebo
813 fun play do end
814
815 # placebo
816 fun pause do end
817
818 # Play a sound.
819 fun replay
820 do
821 sys.system("aplay assets/{path} &")
822 end
823 end
824
825 redef class Display
826 # Display a text
827 fun textx(str: String, x, y, height: Int, color, color2: nullable String)
828 do
829 #var w = measureText(str, height)
830 #rect(x,y,w,height)
831 text(str.to_upper, app.game.font, x, y)
832 end
833
834 # give the width for a giver text
835 fun measureText(str: String, height: Int): Int
836 do
837 var font = app.game.font
838 return str.length * (font.width + font.hspace.to_i)
839 end
840
841 # displays a debug rectangle
842 fun rect(x,y,w,h:Int)
843 do
844 var image = once app.load_image("hitbox.png")
845 blit_scaled(image, x, y, w, h)
846 end
847 end
848
849 # Simple basic class for event
850 class Event
851 # Is a drag event?
852 var drag = false
853 # screen x
854 var offset_x: Int
855 # screen y
856 var offset_y: Int
857 # entity x
858 var game_x = 0
859 # entity y
860 var game_y = 0
861 # key pressed
862 var char_code: String
863 end
864
865 redef class Game
866 # width of a tile, used for most width reference in the game
867 var bw = 48
868 # height a tile, used for most width reference in the game
869 var bh = 48
870 # x-coordinate of the board (padding)
871 var xpad = 24
872 # y-coordinate of the board (padding)
873 var ypad = 24
874
875 # Load tiles
876
877 # Basic tileset
878 var img = new TileSet(app.load_image("tiles2.png"),48,48)
879
880 # Sub tileset (for marks or other)
881 var img2 = new TileSet(app.load_image("tiles2.png"),24,24)
882
883 # background image
884 var back: Image = app.load_image("background.png")
885
886 # Logo image
887 var logo: Image = app.load_image("logo.png")
888
889 # Font
890 var font = new TileSetFont(app.load_image("deltaforce_font.png"), 16, 17, "ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789.:;!?\"'() -,/")
891
892 var xxx = """
893 fun save_cookie(name, val:String) do
894 var days = 365
895 var date = new Date()
896 date.setTime(date.getTime()+(days*24*60*60*1000))
897 document.cookie = name+"="+val+"; expires="+date.toGMTString()+"; path=/"
898 end
899
900 fun read_cookie(name:String):String do
901 var key = name + "="
902 var ca = document.cookie.split(';')
903 for(var i=0; i<ca.length; i++) do
904 var c = ca[i]
905 while (c[0]==' ') c = c.substring(1, c.length)
906 if (c.indexOf(key) == 0) return c.substring(key.length)
907 end
908 return null
909 end
910 """
911
912 # DISPLAY *****************************************************************
913
914 # Is the game in editing mode
915 var editing = false
916
917 # The selected button, if any
918 var selected_button: nullable Button = null
919
920 # SOUND
921
922 # Is the music muted?
923 var music_muted: Bool = true #read_cookie("music_muted")
924
925 # Is the sound effects muted?
926 var sfx_muted: Bool = true #read_cookie("sfx_muted")
927
928 # The background music resource. */
929 var music = new Audio("music.ogg")
930
931 # Click sound
932 var snd_click = new Audio("click.wav")
933
934 # Wining soulf
935 var snd_win = new Audio("level.wav")
936
937 # Shocked sound
938 var snd_duh = new Audio("duh.wav")
939
940 # metal sound
941 var snd_bing = new Audio("bing.wav")
942
943 # transition sound
944 var snd_whip = new Audio("whip.wav")
945
946 # INPUT ******************************************************************
947
948 # Current grid edited (if any).
949 var grid_edit: nullable Grid = null
950
951 # Sequence of current entities
952 var entities = new Array[Entity]
953
954 # The current statusbar
955 var statusbar = new StatusBar(self)
956
957 # The grid board
958 var board = new Board(self)
959
960 # The current score board
961 var score = new Score(self)
962
963 # Monster button game elements.
964 var buttons = new Array[MonsterButton]
965
966 # MetalButton
967 var button_wall = new MetalButton(self)
968
969 # EraseButton
970 var button_erase = new EraseButton(self)
971
972 # ResizeButton
973 var button_size = new ResizeButton(self)
974
975 # fill `buttons`
976 fun init_buttons
977 do
978 for i in [0..9] do
979 buttons[i] = new MonsterButton(self, i)
980 end
981 end
982
983 # Play a level in player mode.
984 fun play(l: Level)
985 do
986 level = l
987 grid.load(level.str)
988 init_play_menu(false)
989 if level.status != "" then
990 statusbar.main_txt = level.status
991 else
992 statusbar.main_txt = level.fullname
993 end
994 var t = new NextLevelButton(self)
995 entities.push(t)
996 run
997 end
998
999 # Play the next level.
1000 fun play_next
1001 do
1002 play(levels[level.number+1])
1003 end
1004
1005
1006 # Helper function to initialize all states.
1007 # Set up buttons for music and SFX.
1008 fun init_game
1009 do
1010 editing = false
1011 solver = null
1012 entities.clear
1013 entities.push(new MusicButton(self))
1014 entities.push(new SFXButton(self))
1015 entities.push(new MenuButton(self))
1016 statusbar.clear
1017 entities.push(statusbar)
1018 end
1019
1020 # Helper function to initialize monster menu entries.
1021 fun init_play_menu(full: Bool)
1022 do
1023 init_game
1024 entities.push(board)
1025 entities.push(new ResetButton(self))
1026 entities.push(button_erase)
1027 # Push monster buttons and determine the selected one
1028 var sel: nullable Button = null
1029 for i in [1..monsters] do
1030 if grid.monsters[i].number > 0 or full then
1031 if selected_button == buttons[i] or sel == null then
1032 sel = buttons[i]
1033 end
1034 entities.push(buttons[i])
1035 end
1036 end
1037 selected_button = sel
1038 entities.push(score)
1039 end
1040
1041 # Play a arbitrary grid in try mode.
1042 fun play_grid(g: Grid)
1043 do
1044 grid = g
1045 init_play_menu(false)
1046 statusbar.main_txt = "User level"
1047 if grid_edit != null then
1048 entities.push(new EditButton(self))
1049 end
1050 entities.push(new WonButton(self))
1051 run
1052 end
1053
1054 # Launch the editor starting with a grid.
1055 fun edit_grid(g: Grid)
1056 do
1057 grid = g
1058 init_play_menu(true)
1059 editing = true
1060 statusbar.main_txt = "Level editor"
1061 if level != null then statusbar.main_txt += ": level "+level.name
1062 entities.push(button_wall)
1063 entities.push(button_size)
1064 entities.push(new TestButton(self))
1065 entities.push(new SaveButton(self))
1066 entities.push(new LoadButton(self))
1067 run
1068 end
1069
1070 # Launch the title screen
1071 fun title
1072 do
1073 init_menu
1074 entities.push(new Splash(self))
1075 run
1076 end
1077
1078 # Helper function to initialize the menu (and tile) screen
1079 fun init_menu
1080 do
1081 init_game
1082 level = null
1083 var i = levels.first
1084 for l in levels do
1085 i = l
1086 if l.get_state == l.l_open then break
1087 end
1088 entities.push(new StartButton(self, i))
1089 end
1090
1091 # Launch the menu.
1092 fun menu
1093 do
1094 init_menu
1095 var t
1096 t = new TextButton(self,"LEVEL SELECT", 120, ypad, "white", null, null)
1097 entities.push(t)
1098 for i in [0..levels.length[ do
1099 var b = new LevelButton(levels[i])
1100 entities.push(b)
1101 end
1102 t = new Achievement(self, 0, "Training")
1103 entities.push(t)
1104 t = new Achievement(self, 1, "Par")
1105 entities.push(t)
1106 t = new Achievement(self, 2, "Editor")
1107 entities.push(t)
1108 t = new Achievement(self, 3, "Challenge")
1109 entities.push(t)
1110 t = new Achievement(self, 4, "Congraturation")
1111 entities.push(t)
1112 t = new Achievement(self, 5, "Awesome")
1113 entities.push(t)
1114 run
1115 end
1116
1117 # Last function called when the lauch state is ready
1118 fun run do
1119 dirty_all = true
1120 end
1121
1122 init
1123 do
1124 load_levels
1125 init_buttons
1126 entities.clear
1127 title
1128 end
1129
1130 # Should all entity redrawn?
1131 var dirty_all = true
1132
1133 # Draw all game entities.
1134 fun draw(display: Display) do
1135 dirty_all = true
1136 if dirty_all then display.blit(back, 0, 0)
1137 for g in entities do
1138 if g.dirty or dirty_all then
1139 g.dirty = false
1140 #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)
1141 g.draw(display)
1142 #ctx.rect(g.x, g.y, g.width, g.height)
1143 end
1144 end
1145 var ev = lastev
1146 if ev isa Event then
1147 display.blit(img[4,0],ev.offset_x-42,ev.offset_y-6)
1148 end
1149 dirty_all = false
1150 end
1151
1152 # Update all game entities.
1153 fun step do
1154 if solver != null and not solver_pause then
1155 if solver.run_steps(solver_steps) != null then solver_pause = true
1156 print solver.to_s
1157 if not solver.is_running then solver = null
1158 end
1159 for g in entities do
1160 g.update
1161 end
1162 end
1163
1164 # Return the game entity located at a mouse event.
1165 fun get_game_element(ev: Event): nullable Entity
1166 do
1167 var x = ev.offset_x
1168 var y = ev.offset_y
1169 for g in entities do
1170 if x>=g.x and x<g.x2 and y>g.y and y<g.y2 then
1171 ev.game_x = x-g.x
1172 ev.game_y = y-g.y
1173 #print "get {g}"
1174 return g
1175 end
1176 end
1177 return null
1178 end
1179
1180 # The game entlty the mouse went down on
1181 var drag: nullable Entity = null
1182
1183 # Last mouse event. Used to dray the cursor
1184 var lastev: nullable Event = null
1185
1186 # Callback when the mouse is pressed
1187 fun onMouseDown(ev: Event) do
1188 lastev = ev
1189 var g = get_game_element(ev)
1190 if g != null then
1191 g.click(ev)
1192 g.dirty = true
1193 end
1194 drag = g
1195 end
1196
1197 # Callback when the mouse is releassed
1198 fun onMouseUp(ev: Event) do
1199 drag = null
1200 end
1201
1202 # Callback when the mouse if moved while pressed
1203 fun onMouseMove(ev: Event) do
1204 lastev = ev
1205 var g = get_game_element(ev)
1206 if g == null then
1207 statusbar.dirty = true
1208 statusbar.over_txt = null
1209 statusbar.over_what = null
1210 return
1211 end
1212 if statusbar.over_what != g then
1213 statusbar.dirty = true
1214 var go = g.over
1215 statusbar.over_txt = go
1216 statusbar.over_what = g
1217 g.enter(ev)
1218 if go != null then print "***OVER*** {go}"
1219 end
1220 # We moved abode a element that accepts drag event
1221 if drag == g and g.draggable then
1222 # print "DRAG {g}"
1223 ev.drag = true
1224 g.click(ev)
1225 g.dirty = true
1226 end
1227 end
1228
1229 # Current solver, if any
1230 var solver: nullable BacktrackSolver[Grid, Action] = null
1231
1232 # Is the solver paused?
1233 var solver_pause = false
1234
1235 # Number of solver steps played in a single game `update`
1236 var solver_steps = 20000
1237
1238 # Callback when a keyboard event is recieved
1239 fun onKeyDown(ev: Event) do
1240 var kc = ev.char_code
1241 if kc == "e" then
1242 grid_edit = grid.copy(true)
1243 edit_grid(grid)
1244 else if kc == "s" then
1245 if solver == null then
1246 solver = (new FriendzProblem(grid)).solve
1247 solver_pause = false
1248 else
1249 solver_pause = not solver_pause
1250 end
1251 #solver.step
1252 else if kc == "d" then
1253 if solver == null then
1254 solver = (new FriendzProblem(grid)).solve
1255 solver_pause = true
1256 else
1257 solver.run_steps(1)
1258 end
1259 else if kc == "+" then
1260 solver_steps += 100
1261 print solver_steps
1262 else if kc == "-" then
1263 solver_steps -= 100
1264 print solver_steps
1265 else for g in entities do
1266 if kc == g.shortcut then
1267 g.click(ev)
1268 g.dirty = true
1269 end
1270 end
1271 end
1272
1273 redef fun load_levels
1274 do
1275 super
1276
1277 for level in levels do
1278 var score = app.data_store["s{level.str.md5}"]
1279 if score isa Int then
1280 level.score = score
1281 end
1282 end
1283 end
1284 end
1285
1286 # The spash title image
1287 class Splash
1288 super Entity
1289 init(game: Game)
1290 do
1291 super(game,game.xpad,game.ypad,380,350)
1292 end
1293 redef fun draw(ctx)
1294 do
1295 ctx.blit(game.logo, game.xpad, game.ypad)
1296 end
1297 redef fun click(ev)
1298 do
1299 game.snd_whip.replay
1300 game.menu
1301 end
1302 end
1303
1304 class NextLevelButton
1305 super TextButton
1306 init(game: Game)
1307 do
1308 super(game, "NEXT LEVEL", 440, 24, "cyan", "Play next level", null)
1309 enabled = false
1310 end
1311
1312 redef fun update
1313 do
1314 var w = game.level.check_won(game.grid)
1315 if self.enabled != w then
1316 self.dirty = true
1317 self.enabled = w
1318 if w then
1319 game.snd_win.replay
1320 game.statusbar.set_tmp("Level solved!", "cyan")
1321 end
1322 end
1323 end
1324
1325 redef fun click(ev)
1326 do
1327 if not self.enabled then
1328 game.snd_duh.replay
1329 var grid = game.grid
1330 var monsters = grid.monsters
1331 var angry = new Array[Tile]
1332 var lonely = new Array[Tile]
1333 var edges = new Array[Tile]
1334 for i in [0..grid.width[ do
1335 for j in [0..grid.height[ do
1336 var t = grid.grid[i][j]
1337 if t.kind == 0 then continue
1338 if t.nexts == 0 then lonely.push(t)
1339 if t.nexts == 1 and not monsters[t.kind].chain then edges.push(t)
1340 if t.nexts > 2 then angry.push(t)
1341 end
1342 end
1343
1344 var l
1345 if angry.length>0 then
1346 l = angry
1347 else if lonely.length>0 then
1348 l = lonely
1349 else
1350 l = edges
1351 end
1352 for i in l do i.shocked=5
1353
1354 if angry.length>0 then
1355 game.statusbar.set_tmp("Angry monsters!", "red")
1356 else if lonely.length>0 then
1357 game.statusbar.set_tmp("Lonely monsters!", "red")
1358 else if not grid.won then
1359 game.statusbar.set_tmp("Unconnected monsters!", "red")
1360 else
1361 game.statusbar.set_tmp("Not enough monsters!", "red")
1362 end
1363 return
1364 end
1365
1366 game.snd_whip.replay
1367 game.play_next
1368 end
1369 end
1370
1371 class MusicButton
1372 super TextButton
1373 init(game: Game)
1374 do
1375 super(game, "MUSIC", 470, 412, "purple", "Mute the music", "Unmute the music")
1376 end
1377 redef fun click2(ev)
1378 do
1379 game.music_muted = self.toggled
1380 if game.music_muted then game.music.pause else game.music.play
1381 #game.save_cookie("music_muted",music_muted?"true":"")
1382 end
1383 end
1384
1385 class SFXButton
1386 super TextButton
1387 init(game: Game)
1388 do
1389 super(game, "SOUND FX", 470, 382, "purple", "Mute the sound effects", "Unmute the sound effects")
1390 end
1391
1392 redef fun click2(ev)
1393 do
1394 game.sfx_muted = self.toggled
1395 if not game.sfx_muted then game.snd_whip.replay # Because the automatic one was muted
1396 #save_cookie("sfx_muted",sfx_muted?"true":"")
1397 end
1398 end
1399
1400 class MenuButton
1401 super TextButton
1402 init(game: Game)
1403 do
1404 super(game, "MENU", 470, 442, "purple", "Exit to the main menu", null)
1405 shortcut = "back"
1406 end
1407
1408 redef fun click2(ev)
1409 do
1410 game.menu
1411 end
1412 end
1413
1414 class ResetButton
1415 super TextButton
1416 init(game: Game)
1417 do
1418 super(game,"RESET", 440, 61, "purple", "Clear the grid",null)
1419 end
1420
1421 var count = 0
1422
1423 redef fun click2(ev)
1424 do
1425 self.count += 1
1426 if self.count==1 then
1427 game.statusbar.set_tmp("Click again to reset","white")
1428 else if self.count==2 then
1429 game.grid.reset(false)
1430 if game.editing then
1431 game.statusbar.set_tmp("Click again to clear all","white")
1432 end
1433 else if game.editing then
1434 game.grid.reset(true)
1435 end
1436 game.dirty_all = true
1437 end
1438
1439 redef fun enter2
1440 do
1441 self.count = 0
1442 end
1443 end
1444
1445 class EditButton
1446 super TextButton
1447 init(game: Game)
1448 do
1449 super(game,"EDIT", 540, 24, "purple", "Return to the editor",null)
1450 end
1451
1452 redef fun click2(ev)
1453 do
1454 var ge = game.grid_edit
1455 assert ge != null
1456 game.edit_grid(ge)
1457 end
1458 end
1459
1460 class WonButton
1461 super TextButton
1462 init(game: Game)
1463 do
1464 super(game,"WON", 440, 24, "cyan", "", null)
1465 enabled = false
1466 end
1467 redef fun click2(ev)
1468 do
1469 var ge = game.grid_edit
1470 if not self.enabled then
1471 game.statusbar.set_tmp("Solve the level first!", "red")
1472 else if ge != null then
1473 game.snd_whip.replay
1474 game.edit_grid(ge)
1475 else
1476 game.snd_whip.replay
1477 game.menu
1478 end
1479 end
1480
1481 redef fun update
1482 do
1483 var w = game.grid.won
1484 if self.enabled != w then
1485 self.dirty = true
1486 self.enabled = w
1487 if w then
1488 game.snd_win.replay
1489 game.statusbar.set_tmp("Level solved!", "cyan")
1490 end
1491 end
1492 end
1493 end
1494
1495 class TestButton
1496 super TextButton
1497 init(game: Game)
1498 do
1499 super(game,"TEST", 440, 24, "cyan", "Try to play the level", null)
1500 end
1501
1502 redef fun click2(ev)
1503 do
1504 game.grid_edit = game.grid
1505 game.play_grid(game.grid.copy(false))
1506 end
1507 end
1508
1509 class SaveButton
1510 super TextButton
1511 init(game: Game)
1512 do
1513 super(game, "SAVE", 540, 24, "purple", "Save the level", null)
1514 end
1515
1516 redef fun click2(ev)
1517 do
1518 var res = game.grid.save
1519 print "SAVE: {res}"
1520 end
1521 end
1522
1523 class LoadButton
1524 super TextButton
1525 init(game: Game)
1526 do
1527 super(game,"LOAD", 540, 61, "purple", "Load an user level", null)
1528 end
1529
1530 redef fun click2(ev)
1531 do
1532 var grid2 = new Grid(game.gw,game.gh,game.monsters)
1533 if grid2.load("") then
1534 game.grid = grid2
1535 end
1536 game.dirty_all = true
1537 end
1538 end
1539
1540 class ContinueButton
1541 super TextButton
1542 init(game: Game)
1543 do
1544 super(game,"CONTINUE", 440, 24, "purple", "Continue playing", null)
1545 end
1546
1547 redef fun click2(ev)
1548 do
1549 game.play_next
1550 end
1551 end
1552
1553 class StartButton
1554 super TextButton
1555 var level: Level
1556 init(game: Game, level: Level)
1557 do
1558 self.level = level
1559 if level.number == 0 then
1560 super(game,"START", 440, 24, "purple", "Play the first level", null)
1561 else
1562 super(game,"CONTINUE", 440, 24, "purple", "Continue from level "+level.name, null)
1563 end
1564 end
1565
1566 redef fun click2(ev)
1567 do
1568 game.play(level)
1569 end
1570 end
1571
1572 #
1573
1574 redef class App
1575
1576 # The game
1577 var game: Game
1578
1579 # Wanted screen width
1580 var screen_width = 640
1581
1582 # Wanted screen height
1583 var screen_height = 480
1584
1585 redef fun window_created
1586 do
1587 super
1588 game = new Game
1589 game.font.hspace = -2
1590 if args.length > 0 then
1591 game.play(game.levels[args.first.to_i])
1592 end
1593 # img loading?
1594 end
1595
1596 # Maximum wanted frame per second
1597 var max_fps = 30
1598
1599 # clock used to track FPS
1600 private var clock = new Clock
1601
1602 redef fun frame_core(display)
1603 do
1604 game.step
1605 game.draw(display)
1606 var dt = clock.lapse
1607 var target_dt = 1000000000 / max_fps
1608 if dt.sec == 0 and dt.nanosec < target_dt then
1609 var sleep_t = target_dt - dt.nanosec
1610 sys.nanosleep(0, sleep_t)
1611 end
1612 end
1613
1614 redef fun input(input_event)
1615 do
1616 #print input_event
1617 if input_event isa QuitEvent then # close window button
1618 quit = true # orders system to quit
1619 else if input_event isa PointerEvent then
1620 var ev = new Event(input_event.x.to_i, input_event.y.to_i, "")
1621 if input_event.is_motion then
1622 game.onMouseMove(ev)
1623 else if input_event.pressed then
1624 game.onMouseDown(ev)
1625 else
1626 game.onMouseUp(ev)
1627 end
1628 return true
1629 else if input_event isa KeyEvent and input_event.is_down then
1630 var ev = new Event(0, 0, input_event.key_name)
1631 game.onKeyDown(ev)
1632 return true
1633 end
1634
1635 return false
1636 end
1637 end
1638
1639 redef class PointerEvent
1640 fun is_motion: Bool do return false
1641 end
1642
1643 redef class KeyEvent
1644 fun key_name: String
1645 do
1646 var c = to_c
1647 if c != null then return c.to_s
1648 return "unknown"
1649 end
1650 end
1651
1652 redef class Level
1653 redef fun save
1654 do
1655 app.data_store["s{str.md5}"] = if score > 0 then score else null
1656 end
1657 end