883c39b0a2992b0207352eda7500a4db3443263d
1 # Monsterz - Chains of Friends
3 # 2010-2014 (c) Jean Privat <jean@pryen.org>
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.
10 # Full UI for the game
12 app_name
("ChainZ of FriendZ")
13 app_version
(0, 1, git_revision
)
21 import app
::data_store
29 # higher means more dense grid
32 # Various grid sizes from large to small (16x16, 12x12, 8x8, 6x6)
33 var ratios
: Array[Int] = [200, 150, 100, 75]
39 if w
*100/r
<= 8 and h
*100/r
<= 8 then self.ratio
= r
44 # * ENTITIES ****************************************************************
46 # A game entity is something that is displayed and may interact with the player.
51 # X left coordinate (in pixel).
54 # Y top coordinate (in pixel).
57 # X right coordinate (in pixel).
58 fun x2
: Int do return x
+ width
60 # Y bottom coordinate (in pixel).
61 fun y2
: Int do return y
+ height
69 # Tool tip text (if any)
70 var over
: nullable String = null
72 # can the entity intercepts drag ang drop events?
75 # Draw function. To implement
76 fun draw
(ctx
: Display) do end
78 # Update function. Called each loop. To implement
81 # Enter function. Called when the cursor enter in the element. To implement
82 fun enter
(ev
: Event) do end
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
88 # keyboard shortcut do activate the entity, if any
89 var shortcut
: nullable String = null
91 # Are events received?
94 fun bw
: Int do return game
.bw
95 fun bh
: Int do return game
.bh
97 # Should the entity be redrawn
101 # TEXT BUTTONS ***********************************************************/
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
110 init(game
: Game, str
: String, x
,y
: Int, color
: nullable String, over
, over2
: nullable String)
113 super(game
, x
,y
,w
,24)
115 self.color
= color
or else "purple"
120 if self.toggleable
then
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
132 # is the button a two-state button
133 fun toggleable
: Bool do return over2
!= null
135 # is the toggleable button in its second state?
138 # ttl for highlighting
141 # position of the start of the text
142 # in a toggleable button, there is space for the mark between `x` and `textx`
145 redef fun draw
(ctx
) do
146 if self.toggleable
then
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
)
152 if self.enabled
then c
= self.color
else c
= "gray"
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
162 if game
.statusbar
.over_what
!= self and self.ttl
> 0 then
170 if over1
== null then return
171 if not self.enabled
then return
178 # Called by `enter` do perform additionnal work if the button is active
179 # Specific button should implement this instead of `enter`
184 if not self.enabled
then
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
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
203 # LEVEL BUTTONS ***********************************************************/
205 # button to play a level in the menu screen
209 # The associated level to play
216 super(l
.game
, (i
%5)*56 + 54, (i
/5)*56 + 55, l
.game
.bw
, l
.game
.bh
)
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}"
224 #self.enabled = l.get_state >= l.l_open
230 var s
= self.level
.get_state
231 var ix
= 5 + l
.number
% 2
233 if s
== l
.l_disabled
then
236 else if s
== l
.l_open
then
239 ctx
.blit
(game
.img
[ix
,iy
], self.x
, self.y
)
243 ctx
.blit
(game
.img
[ix
,iy
], self.x
, self.y
)
245 if s
== l
.l_gold
then
246 ctx
.blit
(game
.img2
[7,0], self.x
+ bw
*5/8, self.y-bh
*1/8)
248 ctx
.textx
(self.level
.name
, self.x
+5, self.y
+5, 24, null, null)
255 game
.play
(self.level
)
258 game
.statusbar
.set_tmp
("Locked level", "red")
264 # ACHIEVEMENTS ************************************************************/
266 # Achievement (monster-like) button in the menu screen
270 # The number of the achievement (0 is first)
273 # The name of the achievement
276 init(game
: Game, i
: Int, name
: String)
278 super(game
, 5*56 + 54, i
*56 + 55, game
.bw
, game
.bh
)
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)"
290 if self.enabled
then w
= 5 else w
= 3
291 ctx
.blit
(game
.img
[w
,self.number
+5], self.x
, self.y
)
296 if not self.enabled
then
298 game
.statusbar
.set_tmp
("Locked achievement!", "red")
305 fun click2
(ev
: Event) do
311 # BOARD (THE GRID) *******************************************************/
313 # The board game element.
318 super(game
, game
.xpad
, game
.ypad
, 8*game
.bw
, 8*game
.bh
)
325 var bwr
= bw
*100/grid
.ratio
326 var bhr
= bh
*100/grid
.ratio
329 if game
.selected_button
== game
.button_size
then
335 self.x
= game
.xpad
+(48*8/2)-w
*bwr
/2
336 self.y
= game
.ypad
+(48*8/2)-h
*bhr
/2
341 var t
= grid
.grid
[i
][j
]
342 var dx
= i
* bwr
+ self.x
343 var dy
= j
* bhr
+ self.y
345 ctx
.blit_scaled
(game
.img
[5,0], dx
, dy
, bwr
, bhr
)
347 ctx
.blit_scaled
(game
.img
[6,0], dx
, dy
, bwr
, bhr
)
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
)
354 ctx
.blit_scaled
(game
.img
[3,3], dx
, dy
, bwr
, bhr
)
358 var m
= grid
.monsters
[t
.kind
]
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
)
367 #ctx.textx(t.chain_mark.to_s, dx, dy, 20, "", null)
370 if game
.selected_button
== game
.button_size
then
372 var x1
= (grid
.width
) * bwr
- bwr
/2 + self.x
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)
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
394 if t
.shocked
> 0 then
397 else if 100.rand
== 0 then
406 # Uded to filter drag events
407 private var last
: nullable Tile = null
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
]
418 if ev
.drag
and last
== t
then return
421 if game
.selected_button
!= game
.button_size
and (x
>=grid
.width
or y
>=grid
.height
) then return
423 # print "{ev.game_x},{ev.game_y},{ev.drag} -> {x},{y}:{t}"
425 if game
.selected_button
!= null then
426 game
.selected_button
.click_board
(ev
, t
)
431 # BUTTONS *****************************************************************/
433 # A in-game selectable button for monsters or tools
443 # The associated monster tile
444 # >0 for monsters, <=0 for tools
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
)
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
462 # Current inputed chain
464 private var chain
= new Array[Tile]
466 # Board click. Called when the player click on the board with the button selected.
467 fun click_board
(ev
: Event, t
: Tile)
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
474 if t2
.fixed
and not game
.editing
then return
478 if t
.fixed
and t
.kind
== self.kind
then
479 self.chain
.unshift
(t
)
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
488 if t
.kind
!= 0 and t
.kind
!= self.kind
then
493 self.chain
.unshift
(t
)
494 if t
.kind
== self.kind
then return
500 if t
.fixed
and not game
.editing
then
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
]
514 if t
.fixed
and game
.editing
and self == game
.button_erase
and t
.kind
== 0 then
520 var nkind
= 0 # the new kind
523 if t
.kind
== 0 then return
524 if self.kind
!= 0 and t
.kind
!= self.kind
then
530 else if t
.kind
!= self.kind
then
533 else if t
.kind
!= 0 then
537 if nkind
== t
.kind
then return
547 # TTL for the monster being angry
549 # TTL for the monster being happy
551 # TTL for the monster being shocked
553 # TTL for the monster blinking
556 init(game
: Game, i
: Int)
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
566 over
= game
.colors
[i
] + " monster ({i})"
567 shortcut
= i
.to_s
# code for 1 trough 9
578 if self.happy
> 0 then
582 if self.shocked
> 0 then
586 if self.blink
> 0 then
589 else if 100.rand
== 0 then
598 if self.angries
>0 then
600 else if self.happy
> 5 then
602 else if self.shocked
> 0 then
604 else if self.blink
> 0 then
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
)
616 super(game
, 440, 92, game
.bh
, 22+game
.bh
)
625 # Metal (fixed) button.
630 super(game
, 498, 92, game
.bh
, 20+game
.bh
)
634 over
= "Metal block (q)"
638 private var fixed
= false
640 redef fun click_board
(ev
,t
)
642 if not ev
.drag
then self.fixed
= not t
.fixed
643 if t
.fixed
== self.fixed
then return
655 super(game
,556, 92, game
.bh
, 20+game
.bh
)
657 over
= "Resize the grid"
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)
669 if game
.selected_button
== self then ctx
.blit
(game
.img
[0, 0], self.x
, self.y
)
674 if game
.selected_button
!= game
.button_size
then
677 game
.selected_button
= null
678 game
.board
.dirty
=true
682 redef fun click_board
(evt
, t
)
687 if w
< 3 or h
< 3 then
689 game
.statusbar
.set_tmp
("Too small!", "red")
693 for i
in [0..grid
.width
[ do
694 for j
in [0..grid
.height
[ do
696 var t2
= grid
.grid
[i
][j
]
706 game
.statusbar
.set_tmp
("Monsters on the way!", "red")
714 # Inactive area used to display the score
719 super(game
,440,310,199,62)
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)
729 ctx
.textx
("BEST: -",self.x
,self.y
+22,21,"pink", null)
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)
735 ctx
.textx
("GOLD: {level.gold}",self.x
,self.y
+44,21,"yellow",null)
741 # Status bar element.
746 super(game
,24, 440, 418-24, 30)
749 # Permanant text, if any
750 var main_txt
: nullable String = null
752 # Text to display when the cursor if over an entity (`over_what`), if any
753 var over_txt
: nullable String = null
755 # What is the entity for `over_txt`
756 var over_what
: nullable Entity
758 # Text to temporally display, for some game event, if any
759 var tmp_txt
: nullable String = null
761 # time-to-live for the `tmp_txt`
764 # Color used to display `tmp_txt`
765 var tmp_txt_color
: nullable String = null
775 # set a temporary text
776 fun set_tmp
(txt
, color
: String)
778 print
"***STATUS** {txt}"
780 self.tmp_txt_ttl
= 20
781 self.tmp_txt_color
= color
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)
800 if self.tmp_txt_ttl
>0 then
807 # ************************************************************************/
811 fun textx
(str
: String, x
, y
, height
: Int, color
, color2
: nullable String)
813 #var w = measureText(str, height)
815 text
(str
.to_upper
, app
.game
.font
, x
, y
)
818 # give the width for a giver text
819 fun measureText
(str
: String, height
: Int): Int
821 var font
= app
.game
.font
822 return str
.length
* (font
.width
+ font
.hspace
.to_i
)
825 # displays a debug rectangle
826 fun rect
(x
,y
,w
,h
:Int)
828 var image
= once app
.load_image
("hitbox.png")
829 blit_scaled
(image
, x
, y
, w
, h
)
833 # Simple basic class for event
846 var char_code
: String
850 # width of a tile, used for most width reference in the game
852 # height a tile, used for most width reference in the game
854 # x-coordinate of the board (padding)
856 # y-coordinate of the board (padding)
862 var img
= new TileSet(app
.load_image
("tiles2.png"),48,48)
864 # Sub tileset (for marks or other)
865 var img2
= new TileSet(app
.load_image
("tiles2.png"),24,24)
868 var back
: Image = app
.load_image
("background.png")
871 var logo
: Image = app
.load_image
("logo.png")
874 var font
= new TileSetFont(app
.load_image
("deltaforce_font.png"), 16, 17, "ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789.:;!?\"'() -,/")
877 fun save_cookie(name, val:String) do
879 var date = new Date()
880 date.setTime(date.getTime()+(days*24*60*60*1000))
881 document.cookie = name+"="+val+"; expires="+date.toGMTString()+"; path=/"
884 fun read_cookie(name:String):String do
886 var ca = document.cookie.split(';')
887 for(var i=0; i<ca.length; i++) do
889 while (c[0]==' ') c = c.substring(1, c.length)
890 if (c.indexOf(key) == 0) return c.substring(key.length)
896 # DISPLAY *****************************************************************
898 # Is the game in editing mode
901 # The selected button, if any
902 var selected_button: nullable Button = null
906 # Is the music muted?
907 var music_muted: Bool = app.data_store["music_muted"] == true
909 # Is the sound effects muted?
910 var sfx_muted: Bool = app.data_store["sfx_muted"] == true
912 # The background music resource. */
913 var music = new Music("music.ogg")
916 var snd_click = new Sound("click.wav")
919 var snd_win = new Sound("level.wav")
922 var snd_duh = new Sound("duh.wav")
925 var snd_bing = new Sound("bing.wav")
928 var snd_whip = new Sound("whip.wav")
931 # INPUT ******************************************************************
933 # Current grid edited (if any).
934 var grid_edit: nullable Grid = null
936 # Sequence of current entities
937 var entities = new Array[Entity]
939 # The current statusbar
940 var statusbar = new StatusBar(self)
943 var board = new Board(self)
945 # The current score board
946 var score = new Score(self)
948 # Monster button game elements.
949 var buttons = new Array[MonsterButton]
952 var button_wall = new MetalButton(self)
955 var button_erase = new EraseButton(self)
958 var button_size = new ResizeButton(self)
967 if not music_muted then music.play
974 buttons[i] = new MonsterButton(self, i)
978 # Play a level in player mode.
983 init_play_menu(false)
984 if level.status != "" then
985 statusbar.main_txt = level.status
987 statusbar.main_txt = level.fullname
989 var t = new NextLevelButton(self)
994 # Play the next level.
997 play(levels[level.number+1])
1001 # Helper function to initialize all states.
1002 # Set up buttons for music and SFX.
1008 entities.push(new MusicButton(self))
1009 entities.push(new SFXButton(self))
1010 entities.push(new MenuButton(self))
1012 entities.push(statusbar)
1015 # Helper function to initialize monster menu entries.
1016 fun init_play_menu(full: Bool)
1019 entities.push(board)
1020 entities.push(new ResetButton(self))
1021 entities.push(button_erase)
1022 # Push monster buttons and determine the selected one
1023 var sel: nullable Button = null
1024 for i in [1..monsters] do
1025 if grid.monsters[i].number > 0 or full then
1026 if selected_button == buttons[i] or sel == null then
1029 entities.push(buttons[i])
1032 selected_button = sel
1033 entities.push(score)
1036 # Play a arbitrary grid in try mode.
1037 fun play_grid(g: Grid)
1040 init_play_menu(false)
1041 statusbar.main_txt = "User level"
1042 if grid_edit != null then
1043 entities.push(new EditButton(self))
1045 entities.push(new WonButton(self))
1049 # Launch the editor starting with a grid.
1050 fun edit_grid(g: Grid)
1053 init_play_menu(true)
1055 statusbar.main_txt = "Level editor"
1056 if level != null then statusbar.main_txt += ": level "+level.name
1057 entities.push(button_wall)
1058 entities.push(button_size)
1059 entities.push(new TestButton(self))
1060 entities.push(new SaveButton(self))
1061 entities.push(new LoadButton(self))
1065 # Launch the title screen
1069 entities.push(new Splash(self))
1073 # Helper function to initialize the menu (and tile) screen
1078 var i = levels.first
1081 if l.get_state == l.l_open then break
1083 entities.push(new StartButton(self, i))
1091 t = new TextButton(self,"LEVEL SELECT", 120, ypad, "white", null, null)
1093 for i in [0..levels.length[ do
1094 var b = new LevelButton(levels[i])
1097 t = new Achievement(self, 0, "Training")
1099 t = new Achievement(self, 1, "Gold")
1101 t = new Achievement(self, 2, "Editor")
1103 t = new Achievement(self, 3, "Challenge")
1105 t = new Achievement(self, 4, "Congraturation")
1107 t = new Achievement(self, 5, "Awesome")
1112 # Last function called when the lauch state is ready
1117 # Should all entity redrawn?
1118 var dirty_all = true
1120 # Draw all game entities.
1121 fun draw(display: Display) do
1123 if dirty_all then display.blit(back, 0, 0)
1124 for g in entities do
1125 if g.dirty or dirty_all then
1127 #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)
1129 #ctx.rect(g.x, g.y, g.width, g.height)
1133 if ev isa Event then
1134 # Cursor, kept for debugging
1135 #display.blit(img[4,0],ev.offset_x-42,ev.offset_y-6)
1140 # Update all game entities.
1142 if solver != null and not solver_pause then
1143 if solver.run_steps(solver_steps) != null then solver_pause = true
1145 if not solver.is_running then solver = null
1147 for g in entities do
1152 # Return the game entity located at a mouse event.
1153 fun get_game_element(ev: Event): nullable Entity
1157 for g in entities do
1158 if x>=g.x and x<g.x2 and y>g.y and y<g.y2 then
1168 # The game entlty the mouse went down on
1169 var drag: nullable Entity = null
1171 # Last mouse event. Used to dray the cursor
1172 var lastev: nullable Event = null
1174 # Callback when the mouse is pressed
1175 fun onMouseDown(ev: Event) do
1177 var g = get_game_element(ev)
1185 # Callback when the mouse is releassed
1186 fun onMouseUp(ev: Event) do
1190 # Callback when the mouse if moved while pressed
1191 fun onMouseMove(ev: Event) do
1193 var g = get_game_element(ev)
1195 statusbar.dirty = true
1196 statusbar.over_txt = null
1197 statusbar.over_what = null
1200 if statusbar.over_what != g then
1201 statusbar.dirty = true
1203 statusbar.over_txt = go
1204 statusbar.over_what = g
1206 if go != null then print "***OVER*** {go}"
1208 # We moved abode a element that accepts drag event
1209 if drag == g and g.draggable then
1217 # Current solver, if any
1218 var solver: nullable BacktrackSolver[Grid, Action] = null
1220 # Is the solver paused?
1221 var solver_pause = false
1223 # Number of solver steps played in a single game `update`
1224 var solver_steps = 20000
1226 # Callback when a keyboard event is recieved
1227 fun onKeyDown(ev: Event) do
1228 var kc = ev.char_code
1230 grid_edit = grid.copy(true)
1232 else if kc == "s" then
1233 if solver == null then
1234 solver = (new FriendzProblem(grid)).solve
1235 solver_pause = false
1237 solver_pause = not solver_pause
1240 else if kc == "d" then
1241 if solver == null then
1242 solver = (new FriendzProblem(grid)).solve
1247 else if kc == "+" then
1250 else if kc == "-" then
1253 else for g in entities do
1254 if kc == g.shortcut then
1261 redef fun load_levels
1265 for level in levels do
1266 var score = app.data_store["s{level.str.md5}"]
1267 if score isa Int then
1274 # The spash title image
1279 super(game,game.xpad,game.ypad,380,350)
1283 ctx.blit(game.logo, game.xpad, game.ypad)
1292 class NextLevelButton
1296 super(game, "NEXT LEVEL", 440, 24, "cyan", "Play next level", null)
1302 var w = game.level.check_won(game.grid)
1303 if self.enabled != w then
1308 game.statusbar.set_tmp("Level solved!", "cyan")
1315 if not self.enabled then
1317 var grid = game.grid
1318 var monsters = grid.monsters
1319 var angry = new Array[Tile]
1320 var lonely = new Array[Tile]
1321 var edges = new Array[Tile]
1322 for i in [0..grid.width[ do
1323 for j in [0..grid.height[ do
1324 var t = grid.grid[i][j]
1325 if t.kind == 0 then continue
1326 if t.nexts == 0 then lonely.push(t)
1327 if t.nexts == 1 and not monsters[t.kind].chain then edges.push(t)
1328 if t.nexts > 2 then angry.push(t)
1333 if angry.length>0 then
1335 else if lonely.length>0 then
1340 for i in l do i.shocked=5
1342 if angry.length>0 then
1343 game.statusbar.set_tmp("Angry monsters!", "red")
1344 else if lonely.length>0 then
1345 game.statusbar.set_tmp("Lonely monsters!", "red")
1346 else if not grid.won then
1347 game.statusbar.set_tmp("Unconnected monsters!", "red")
1349 game.statusbar.set_tmp("Not enough monsters!", "red")
1363 super(game, "MUSIC", 470, 412, "purple", "Mute the music", "Unmute the music")
1364 toggled = game.music_muted
1366 redef fun click2(ev)
1368 game.music_muted = self.toggled
1369 if game.music_muted then game.music.pause else game.music.play
1370 app.data_store["music_muted"] = game.music_muted
1378 super(game, "SOUND FX", 470, 382, "purple", "Mute the sound effects", "Unmute the sound effects")
1379 toggled = game.sfx_muted
1382 redef fun click2(ev)
1384 game.sfx_muted = self.toggled
1385 if not game.sfx_muted then game.snd_whip.play # Because the automatic one was muted
1386 app.data_store["sfx_muted"] = game.sfx_muted
1394 super(game, "MENU", 470, 442, "purple", "Exit to the main menu", null)
1398 redef fun click2(ev)
1408 super(game,"RESET", 440, 61, "purple", "Clear the grid",null)
1413 redef fun click2(ev)
1416 if self.count==1 then
1417 game.statusbar.set_tmp("Click again to reset","white")
1418 else if self.count==2 then
1419 game.grid.reset(false)
1420 if game.editing then
1421 game.statusbar.set_tmp("Click again to clear all","white")
1423 else if game.editing then
1424 game.grid.reset(true)
1426 game.dirty_all = true
1439 super(game,"EDIT", 540, 24, "purple", "Return to the editor",null)
1442 redef fun click2(ev)
1444 var ge = game.grid_edit
1454 super(game,"WON", 440, 24, "cyan", "", null)
1457 redef fun click2(ev)
1459 var ge = game.grid_edit
1460 if not self.enabled then
1461 game.statusbar.set_tmp("Solve the level first!", "red")
1462 else if ge != null then
1473 var w = game.grid.won
1474 if self.enabled != w then
1479 game.statusbar.set_tmp("Level solved!", "cyan")
1489 super(game,"TEST", 440, 24, "cyan", "Try to play the level", null)
1492 redef fun click2(ev)
1494 game.grid_edit = game.grid
1495 game.play_grid(game.grid.copy(false))
1503 super(game, "SAVE", 540, 24, "purple", "Save the level", null)
1506 redef fun click2(ev)
1508 var res = game.grid.save
1517 super(game,"LOAD", 540, 61, "purple", "Load an user level", null)
1520 redef fun click2(ev)
1522 var grid2 = new Grid(game.gw,game.gh,game.monsters)
1523 if grid2.load("") then
1526 game.dirty_all = true
1530 class ContinueButton
1534 super(game,"CONTINUE", 440, 24, "purple", "Continue playing", null)
1537 redef fun click2(ev)
1546 init(game: Game, level: Level)
1549 if level.number == 0 then
1550 super(game,"START", 440, 24, "purple", "Play the first level", null)
1552 super(game,"CONTINUE", 440, 24, "purple", "Continue from level "+level.name, null)
1556 redef fun click2(ev)
1569 # Wanted screen width
1570 var screen_width = 640
1572 # Wanted screen height
1573 var screen_height = 480
1579 game.font.hspace = -2
1580 if args.length > 0 then
1581 game.play(game.levels[args.first.to_i])
1586 # Maximum wanted frame per second
1589 # clock used to track FPS
1590 private var clock = new Clock
1592 redef fun frame_core(display)
1596 var dt = clock.lapse
1597 var target_dt = 1000000000 / max_fps
1598 if dt.sec == 0 and dt.nanosec < target_dt then
1599 var sleep_t = target_dt - dt.nanosec
1600 sys.nanosleep(0, sleep_t)
1604 redef fun input(input_event)
1607 if input_event isa QuitEvent then # close window button
1608 quit = true # orders system to quit
1609 else if input_event isa PointerEvent then
1610 var ev = new Event(input_event.x.to_i, input_event.y.to_i, "")
1611 if input_event.is_motion then
1612 game.onMouseMove(ev)
1613 else if input_event.pressed then
1614 game.onMouseDown(ev)
1619 else if input_event isa KeyEvent and input_event.is_down then
1620 var ev = new Event(0, 0, input_event.key_name)
1629 redef class PointerEvent
1630 fun is_motion: Bool do return false
1633 redef class KeyEvent
1634 fun key_name: String
1637 if c != null then return c.to_s
1645 app.data_store["s{str.md5}"] = if score > 0 then score else null