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