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