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