contrib/friendz: do not show a cursor
[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 # Cursor, kept for debugging
1148 #display.blit(img[4,0],ev.offset_x-42,ev.offset_y-6)
1149 end
1150 dirty_all = false
1151 end
1152
1153 # Update all game entities.
1154 fun step do
1155 if solver != null and not solver_pause then
1156 if solver.run_steps(solver_steps) != null then solver_pause = true
1157 print solver.to_s
1158 if not solver.is_running then solver = null
1159 end
1160 for g in entities do
1161 g.update
1162 end
1163 end
1164
1165 # Return the game entity located at a mouse event.
1166 fun get_game_element(ev: Event): nullable Entity
1167 do
1168 var x = ev.offset_x
1169 var y = ev.offset_y
1170 for g in entities do
1171 if x>=g.x and x<g.x2 and y>g.y and y<g.y2 then
1172 ev.game_x = x-g.x
1173 ev.game_y = y-g.y
1174 #print "get {g}"
1175 return g
1176 end
1177 end
1178 return null
1179 end
1180
1181 # The game entlty the mouse went down on
1182 var drag: nullable Entity = null
1183
1184 # Last mouse event. Used to dray the cursor
1185 var lastev: nullable Event = null
1186
1187 # Callback when the mouse is pressed
1188 fun onMouseDown(ev: Event) do
1189 lastev = ev
1190 var g = get_game_element(ev)
1191 if g != null then
1192 g.click(ev)
1193 g.dirty = true
1194 end
1195 drag = g
1196 end
1197
1198 # Callback when the mouse is releassed
1199 fun onMouseUp(ev: Event) do
1200 drag = null
1201 end
1202
1203 # Callback when the mouse if moved while pressed
1204 fun onMouseMove(ev: Event) do
1205 lastev = ev
1206 var g = get_game_element(ev)
1207 if g == null then
1208 statusbar.dirty = true
1209 statusbar.over_txt = null
1210 statusbar.over_what = null
1211 return
1212 end
1213 if statusbar.over_what != g then
1214 statusbar.dirty = true
1215 var go = g.over
1216 statusbar.over_txt = go
1217 statusbar.over_what = g
1218 g.enter(ev)
1219 if go != null then print "***OVER*** {go}"
1220 end
1221 # We moved abode a element that accepts drag event
1222 if drag == g and g.draggable then
1223 # print "DRAG {g}"
1224 ev.drag = true
1225 g.click(ev)
1226 g.dirty = true
1227 end
1228 end
1229
1230 # Current solver, if any
1231 var solver: nullable BacktrackSolver[Grid, Action] = null
1232
1233 # Is the solver paused?
1234 var solver_pause = false
1235
1236 # Number of solver steps played in a single game `update`
1237 var solver_steps = 20000
1238
1239 # Callback when a keyboard event is recieved
1240 fun onKeyDown(ev: Event) do
1241 var kc = ev.char_code
1242 if kc == "e" then
1243 grid_edit = grid.copy(true)
1244 edit_grid(grid)
1245 else if kc == "s" then
1246 if solver == null then
1247 solver = (new FriendzProblem(grid)).solve
1248 solver_pause = false
1249 else
1250 solver_pause = not solver_pause
1251 end
1252 #solver.step
1253 else if kc == "d" then
1254 if solver == null then
1255 solver = (new FriendzProblem(grid)).solve
1256 solver_pause = true
1257 else
1258 solver.run_steps(1)
1259 end
1260 else if kc == "+" then
1261 solver_steps += 100
1262 print solver_steps
1263 else if kc == "-" then
1264 solver_steps -= 100
1265 print solver_steps
1266 else for g in entities do
1267 if kc == g.shortcut then
1268 g.click(ev)
1269 g.dirty = true
1270 end
1271 end
1272 end
1273
1274 redef fun load_levels
1275 do
1276 super
1277
1278 for level in levels do
1279 var score = app.data_store["s{level.str.md5}"]
1280 if score isa Int then
1281 level.score = score
1282 end
1283 end
1284 end
1285 end
1286
1287 # The spash title image
1288 class Splash
1289 super Entity
1290 init(game: Game)
1291 do
1292 super(game,game.xpad,game.ypad,380,350)
1293 end
1294 redef fun draw(ctx)
1295 do
1296 ctx.blit(game.logo, game.xpad, game.ypad)
1297 end
1298 redef fun click(ev)
1299 do
1300 game.snd_whip.replay
1301 game.menu
1302 end
1303 end
1304
1305 class NextLevelButton
1306 super TextButton
1307 init(game: Game)
1308 do
1309 super(game, "NEXT LEVEL", 440, 24, "cyan", "Play next level", null)
1310 enabled = false
1311 end
1312
1313 redef fun update
1314 do
1315 var w = game.level.check_won(game.grid)
1316 if self.enabled != w then
1317 self.dirty = true
1318 self.enabled = w
1319 if w then
1320 game.snd_win.replay
1321 game.statusbar.set_tmp("Level solved!", "cyan")
1322 end
1323 end
1324 end
1325
1326 redef fun click(ev)
1327 do
1328 if not self.enabled then
1329 game.snd_duh.replay
1330 var grid = game.grid
1331 var monsters = grid.monsters
1332 var angry = new Array[Tile]
1333 var lonely = new Array[Tile]
1334 var edges = new Array[Tile]
1335 for i in [0..grid.width[ do
1336 for j in [0..grid.height[ do
1337 var t = grid.grid[i][j]
1338 if t.kind == 0 then continue
1339 if t.nexts == 0 then lonely.push(t)
1340 if t.nexts == 1 and not monsters[t.kind].chain then edges.push(t)
1341 if t.nexts > 2 then angry.push(t)
1342 end
1343 end
1344
1345 var l
1346 if angry.length>0 then
1347 l = angry
1348 else if lonely.length>0 then
1349 l = lonely
1350 else
1351 l = edges
1352 end
1353 for i in l do i.shocked=5
1354
1355 if angry.length>0 then
1356 game.statusbar.set_tmp("Angry monsters!", "red")
1357 else if lonely.length>0 then
1358 game.statusbar.set_tmp("Lonely monsters!", "red")
1359 else if not grid.won then
1360 game.statusbar.set_tmp("Unconnected monsters!", "red")
1361 else
1362 game.statusbar.set_tmp("Not enough monsters!", "red")
1363 end
1364 return
1365 end
1366
1367 game.snd_whip.replay
1368 game.play_next
1369 end
1370 end
1371
1372 class MusicButton
1373 super TextButton
1374 init(game: Game)
1375 do
1376 super(game, "MUSIC", 470, 412, "purple", "Mute the music", "Unmute the music")
1377 end
1378 redef fun click2(ev)
1379 do
1380 game.music_muted = self.toggled
1381 if game.music_muted then game.music.pause else game.music.play
1382 #game.save_cookie("music_muted",music_muted?"true":"")
1383 end
1384 end
1385
1386 class SFXButton
1387 super TextButton
1388 init(game: Game)
1389 do
1390 super(game, "SOUND FX", 470, 382, "purple", "Mute the sound effects", "Unmute the sound effects")
1391 end
1392
1393 redef fun click2(ev)
1394 do
1395 game.sfx_muted = self.toggled
1396 if not game.sfx_muted then game.snd_whip.replay # Because the automatic one was muted
1397 #save_cookie("sfx_muted",sfx_muted?"true":"")
1398 end
1399 end
1400
1401 class MenuButton
1402 super TextButton
1403 init(game: Game)
1404 do
1405 super(game, "MENU", 470, 442, "purple", "Exit to the main menu", null)
1406 shortcut = "back"
1407 end
1408
1409 redef fun click2(ev)
1410 do
1411 game.menu
1412 end
1413 end
1414
1415 class ResetButton
1416 super TextButton
1417 init(game: Game)
1418 do
1419 super(game,"RESET", 440, 61, "purple", "Clear the grid",null)
1420 end
1421
1422 var count = 0
1423
1424 redef fun click2(ev)
1425 do
1426 self.count += 1
1427 if self.count==1 then
1428 game.statusbar.set_tmp("Click again to reset","white")
1429 else if self.count==2 then
1430 game.grid.reset(false)
1431 if game.editing then
1432 game.statusbar.set_tmp("Click again to clear all","white")
1433 end
1434 else if game.editing then
1435 game.grid.reset(true)
1436 end
1437 game.dirty_all = true
1438 end
1439
1440 redef fun enter2
1441 do
1442 self.count = 0
1443 end
1444 end
1445
1446 class EditButton
1447 super TextButton
1448 init(game: Game)
1449 do
1450 super(game,"EDIT", 540, 24, "purple", "Return to the editor",null)
1451 end
1452
1453 redef fun click2(ev)
1454 do
1455 var ge = game.grid_edit
1456 assert ge != null
1457 game.edit_grid(ge)
1458 end
1459 end
1460
1461 class WonButton
1462 super TextButton
1463 init(game: Game)
1464 do
1465 super(game,"WON", 440, 24, "cyan", "", null)
1466 enabled = false
1467 end
1468 redef fun click2(ev)
1469 do
1470 var ge = game.grid_edit
1471 if not self.enabled then
1472 game.statusbar.set_tmp("Solve the level first!", "red")
1473 else if ge != null then
1474 game.snd_whip.replay
1475 game.edit_grid(ge)
1476 else
1477 game.snd_whip.replay
1478 game.menu
1479 end
1480 end
1481
1482 redef fun update
1483 do
1484 var w = game.grid.won
1485 if self.enabled != w then
1486 self.dirty = true
1487 self.enabled = w
1488 if w then
1489 game.snd_win.replay
1490 game.statusbar.set_tmp("Level solved!", "cyan")
1491 end
1492 end
1493 end
1494 end
1495
1496 class TestButton
1497 super TextButton
1498 init(game: Game)
1499 do
1500 super(game,"TEST", 440, 24, "cyan", "Try to play the level", null)
1501 end
1502
1503 redef fun click2(ev)
1504 do
1505 game.grid_edit = game.grid
1506 game.play_grid(game.grid.copy(false))
1507 end
1508 end
1509
1510 class SaveButton
1511 super TextButton
1512 init(game: Game)
1513 do
1514 super(game, "SAVE", 540, 24, "purple", "Save the level", null)
1515 end
1516
1517 redef fun click2(ev)
1518 do
1519 var res = game.grid.save
1520 print "SAVE: {res}"
1521 end
1522 end
1523
1524 class LoadButton
1525 super TextButton
1526 init(game: Game)
1527 do
1528 super(game,"LOAD", 540, 61, "purple", "Load an user level", null)
1529 end
1530
1531 redef fun click2(ev)
1532 do
1533 var grid2 = new Grid(game.gw,game.gh,game.monsters)
1534 if grid2.load("") then
1535 game.grid = grid2
1536 end
1537 game.dirty_all = true
1538 end
1539 end
1540
1541 class ContinueButton
1542 super TextButton
1543 init(game: Game)
1544 do
1545 super(game,"CONTINUE", 440, 24, "purple", "Continue playing", null)
1546 end
1547
1548 redef fun click2(ev)
1549 do
1550 game.play_next
1551 end
1552 end
1553
1554 class StartButton
1555 super TextButton
1556 var level: Level
1557 init(game: Game, level: Level)
1558 do
1559 self.level = level
1560 if level.number == 0 then
1561 super(game,"START", 440, 24, "purple", "Play the first level", null)
1562 else
1563 super(game,"CONTINUE", 440, 24, "purple", "Continue from level "+level.name, null)
1564 end
1565 end
1566
1567 redef fun click2(ev)
1568 do
1569 game.play(level)
1570 end
1571 end
1572
1573 #
1574
1575 redef class App
1576
1577 # The game
1578 var game: Game
1579
1580 # Wanted screen width
1581 var screen_width = 640
1582
1583 # Wanted screen height
1584 var screen_height = 480
1585
1586 redef fun window_created
1587 do
1588 super
1589 game = new Game
1590 game.font.hspace = -2
1591 if args.length > 0 then
1592 game.play(game.levels[args.first.to_i])
1593 end
1594 # img loading?
1595 end
1596
1597 # Maximum wanted frame per second
1598 var max_fps = 30
1599
1600 # clock used to track FPS
1601 private var clock = new Clock
1602
1603 redef fun frame_core(display)
1604 do
1605 game.step
1606 game.draw(display)
1607 var dt = clock.lapse
1608 var target_dt = 1000000000 / max_fps
1609 if dt.sec == 0 and dt.nanosec < target_dt then
1610 var sleep_t = target_dt - dt.nanosec
1611 sys.nanosleep(0, sleep_t)
1612 end
1613 end
1614
1615 redef fun input(input_event)
1616 do
1617 #print input_event
1618 if input_event isa QuitEvent then # close window button
1619 quit = true # orders system to quit
1620 else if input_event isa PointerEvent then
1621 var ev = new Event(input_event.x.to_i, input_event.y.to_i, "")
1622 if input_event.is_motion then
1623 game.onMouseMove(ev)
1624 else if input_event.pressed then
1625 game.onMouseDown(ev)
1626 else
1627 game.onMouseUp(ev)
1628 end
1629 return true
1630 else if input_event isa KeyEvent and input_event.is_down then
1631 var ev = new Event(0, 0, input_event.key_name)
1632 game.onKeyDown(ev)
1633 return true
1634 end
1635
1636 return false
1637 end
1638 end
1639
1640 redef class PointerEvent
1641 fun is_motion: Bool do return false
1642 end
1643
1644 redef class KeyEvent
1645 fun key_name: String
1646 do
1647 var c = to_c
1648 if c != null then return c.to_s
1649 return "unknown"
1650 end
1651 end
1652
1653 redef class Level
1654 redef fun save
1655 do
1656 app.data_store["s{str.md5}"] = if score > 0 then score else null
1657 end
1658 end