f79205dc952ec1e8763002ba8fdbaf57226d41f4
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
28 # higher means more dense grid
31 # Various grid sizes from large to small (16x16, 12x12, 8x8, 6x6)
32 var ratios
: Array[Int] = [200, 150, 100, 75]
38 if w
*100/r
<= 8 and h
*100/r
<= 8 then self.ratio
= r
43 # * ENTITIES ****************************************************************
45 # A game entity is something that is displayed and may interact with the player.
50 # X left coordinate (in pixel).
53 # Y top coordinate (in pixel).
56 # X right coordinate (in pixel).
57 fun x2
: Int do return x
+ width
59 # Y bottom coordinate (in pixel).
60 fun y2
: Int do return y
+ height
68 # Tool tip text (if any)
69 var over
: nullable String = null
71 # can the entity intercepts drag ang drop events?
74 # Draw function. To implement
75 fun draw
(ctx
: Display) do end
77 # Update function. Called each loop. To implement
80 # Enter function. Called when the cursor enter in the element. To implement
81 fun enter
(ev
: Event) do end
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
87 # keyboard shortcut do activate the entity, if any
88 var shortcut
: nullable String = null
90 # Are events received?
93 fun bw
: Int do return game
.bw
94 fun bh
: Int do return game
.bh
96 # Should the entity be redrawn
100 # TEXT BUTTONS ***********************************************************/
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
109 init(game
: Game, str
: String, x
,y
: Int, color
: nullable String, over
, over2
: nullable String)
112 super(game
, x
,y
,w
,24)
114 self.color
= color
or else "purple"
119 if self.toggleable
then
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
131 # is the button a two-state button
132 fun toggleable
: Bool do return over2
!= null
134 # is the toggleable button in its second state?
137 # ttl for highlighting
140 # position of the start of the text
141 # in a toggleable button, there is space for the mark between `x` and `textx`
144 redef fun draw
(ctx
) do
145 if self.toggleable
then
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
)
151 if self.enabled
then c
= self.color
else c
= "gray"
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
161 if game
.statusbar
.over_what
!= self and self.ttl
> 0 then
169 if over1
== null then return
170 if not self.enabled
then return
177 # Called by `enter` do perform additionnal work if the button is active
178 # Specific button should implement this instead of `enter`
183 if not self.enabled
then
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
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
202 # LEVEL BUTTONS ***********************************************************/
204 # button to play a level in the menu screen
208 # The associated level to play
215 super(l
.game
, (i
%5)*56 + 54, (i
/5)*56 + 55, l
.game
.bw
, l
.game
.bh
)
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}"
223 self.enabled
= l
.get_state
>= l
.l_open
or game
.cheated
229 var s
= self.level
.get_state
230 var ix
= 5 + l
.number
% 2
232 if s
== l
.l_disabled
then
235 else if s
== l
.l_open
then
238 ctx
.blit
(game
.img
[ix
,iy
], self.x
, self.y
)
242 ctx
.blit
(game
.img
[ix
,iy
], self.x
, self.y
)
244 if s
== l
.l_gold
then
245 ctx
.blit
(game
.img2
[7,0], self.x
+ bw
*5/8, self.y-bh
*1/8)
247 ctx
.textx
(self.level
.name
, self.x
+5, self.y
+5, 24, null, null)
254 game
.play
(self.level
)
257 game
.statusbar
.set_tmp
("Locked level", "red")
263 # ACHIEVEMENTS ************************************************************/
265 # Achievement (monster-like) button in the menu screen
269 # The number of the achievement (0 is first)
272 # The name of the achievement
275 init(game
: Game, i
: Int, name
: String)
277 super(game
, 5*56 + 54, i
*56 + 55, game
.bw
, game
.bh
)
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)"
289 if self.enabled
then w
= 5 else w
= 3
290 ctx
.blit
(game
.img
[w
,self.number
+5], self.x
, self.y
)
295 if not self.enabled
then
297 game
.statusbar
.set_tmp
("Locked achievement!", "red")
304 fun click2
(ev
: Event) do
310 # BOARD (THE GRID) *******************************************************/
312 # The board game element.
317 super(game
, game
.xpad
, game
.ypad
, 8*game
.bw
, 8*game
.bh
)
324 var bwr
= bw
*100/grid
.ratio
325 var bhr
= bh
*100/grid
.ratio
328 if game
.selected_button
== game
.button_size
then
334 self.x
= game
.xpad
+(48*8/2)-w
*bwr
/2
335 self.y
= game
.ypad
+(48*8/2)-h
*bhr
/2
340 var t
= grid
.grid
[i
][j
]
341 var dx
= i
* bwr
+ self.x
342 var dy
= j
* bhr
+ self.y
344 ctx
.blit_scaled
(game
.img
[5,0], dx
, dy
, bwr
, bhr
)
346 ctx
.blit_scaled
(game
.img
[6,0], dx
, dy
, bwr
, bhr
)
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
)
353 ctx
.blit_scaled
(game
.img
[3,3], dx
, dy
, bwr
, bhr
)
357 var m
= grid
.monsters
[t
.kind
]
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
)
366 #ctx.textx(t.chain_mark.to_s, dx, dy, 20, "", null)
369 if game
.selected_button
== game
.button_size
then
371 var x1
= (grid
.width
) * bwr
- bwr
/2 + self.x
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)
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
393 if t
.shocked
> 0 then
396 else if 100.rand
== 0 then
405 # Uded to filter drag events
406 private var last
: nullable Tile = null
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
]
417 if ev
.drag
and last
== t
then return
420 if game
.selected_button
!= game
.button_size
and (x
>=grid
.width
or y
>=grid
.height
) then return
422 # print "{ev.game_x},{ev.game_y},{ev.drag} -> {x},{y}:{t}"
424 if game
.selected_button
!= null then
425 game
.selected_button
.click_board
(ev
, t
)
430 # BUTTONS *****************************************************************/
432 # A in-game selectable button for monsters or tools
442 # The associated monster tile
443 # >0 for monsters, <=0 for tools
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
)
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
461 # Current inputed chain
463 private var chain
= new Array[Tile]
465 # Board click. Called when the player click on the board with the button selected.
466 fun click_board
(ev
: Event, t
: Tile)
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
473 if t2
.fixed
and not game
.editing
then return
477 if t
.fixed
and t
.kind
== self.kind
then
478 self.chain
.unshift
(t
)
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
487 if t
.kind
!= 0 and t
.kind
!= self.kind
then
492 self.chain
.unshift
(t
)
493 if t
.kind
== self.kind
then return
499 if t
.fixed
and not game
.editing
then
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
]
513 if t
.fixed
and game
.editing
and self == game
.button_erase
and t
.kind
== 0 then
519 var nkind
= 0 # the new kind
522 if t
.kind
== 0 then return
523 if self.kind
!= 0 and t
.kind
!= self.kind
then
529 else if t
.kind
!= self.kind
then
532 else if t
.kind
!= 0 then
536 if nkind
== t
.kind
then return
546 # TTL for the monster being angry
548 # TTL for the monster being happy
550 # TTL for the monster being shocked
552 # TTL for the monster blinking
555 init(game
: Game, i
: Int)
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
565 over
= game
.colors
[i
] + " monster ({i})"
566 shortcut
= i
.to_s
# code for 1 trough 9
577 if self.happy
> 0 then
581 if self.shocked
> 0 then
585 if self.blink
> 0 then
588 else if 100.rand
== 0 then
597 if self.angries
>0 then
599 else if self.happy
> 5 then
601 else if self.shocked
> 0 then
603 else if self.blink
> 0 then
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
)
615 super(game
, 440, 92, game
.bh
, 22+game
.bh
)
624 # Metal (fixed) button.
629 super(game
, 498, 92, game
.bh
, 20+game
.bh
)
633 over
= "Metal block (q)"
637 private var fixed
= false
639 redef fun click_board
(ev
,t
)
641 if not ev
.drag
then self.fixed
= not t
.fixed
642 if t
.fixed
== self.fixed
then return
654 super(game
,556, 92, game
.bh
, 20+game
.bh
)
656 over
= "Resize the grid"
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)
668 if game
.selected_button
== self then ctx
.blit
(game
.img
[0, 0], self.x
, self.y
)
673 if game
.selected_button
!= game
.button_size
then
676 game
.selected_button
= null
677 game
.board
.dirty
=true
681 redef fun click_board
(evt
, t
)
686 if w
< 3 or h
< 3 then
688 game
.statusbar
.set_tmp
("Too small!", "red")
692 for i
in [0..grid
.width
[ do
693 for j
in [0..grid
.height
[ do
695 var t2
= grid
.grid
[i
][j
]
705 game
.statusbar
.set_tmp
("Monsters on the way!", "red")
713 # Inactive area used to display the score
718 super(game
,440,310,199,62)
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)
728 ctx
.textx
("BEST: -",self.x
,self.y
+22,21,"pink", null)
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)
734 ctx
.textx
("GOLD: {level.gold}",self.x
,self.y
+44,21,"yellow",null)
740 # Status bar element.
745 super(game
,24, 440, 418-24, 30)
748 # Permanant text, if any
749 var main_txt
: nullable String = null
751 # Text to display when the cursor if over an entity (`over_what`), if any
752 var over_txt
: nullable String = null
754 # What is the entity for `over_txt`
755 var over_what
: nullable Entity
757 # Text to temporally display, for some game event, if any
758 var tmp_txt
: nullable String = null
760 # time-to-live for the `tmp_txt`
763 # Color used to display `tmp_txt`
764 var tmp_txt_color
: nullable String = null
774 # set a temporary text
775 fun set_tmp
(txt
, color
: String)
777 print
"***STATUS** {txt}"
779 self.tmp_txt_ttl
= 60
780 self.tmp_txt_color
= color
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)
799 if self.tmp_txt_ttl
>0 then
806 # ************************************************************************/
810 fun textx
(str
: String, x
, y
, height
: Int, color
, color2
: nullable String)
812 #var w = measureText(str, height)
814 text
(str
.to_upper
, app
.game
.font
, x
, y
)
817 # give the width for a giver text
818 fun measureText
(str
: String, height
: Int): Int
820 var font
= app
.game
.font
821 return str
.length
* (font
.width
+ font
.hspace
.to_i
)
824 # displays a debug rectangle
825 fun rect
(x
,y
,w
,h
:Int)
827 var image
= once app
.load_image
("hitbox.png")
828 blit_scaled
(image
, x
, y
, w
, h
)
832 # Simple basic class for event
845 var char_code
: String
849 # width of a tile, used for most width reference in the game
851 # height a tile, used for most width reference in the game
853 # x-coordinate of the board (padding)
855 # y-coordinate of the board (padding)
861 var img
= new TileSet(app
.load_image
("tiles2.png"),48,48)
863 # Sub tileset (for marks or other)
864 var img2
= new TileSet(app
.load_image
("tiles2.png"),24,24)
867 var back
: Image = app
.load_image
("background.png")
870 var logo
: Image = app
.load_image
("logo.png")
873 var font
= new TileSetFont(app
.load_image
("deltaforce_font.png"), 16, 17, "ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789.:;!?\"'() -,/")
875 # DISPLAY *****************************************************************
877 # Is the game in editing mode
880 # The selected button, if any
881 var selected_button: nullable Button = null
885 # Is the music muted?
886 var music_muted: Bool = app.data_store["music_muted"] == true
888 # Is the sound effects muted?
889 var sfx_muted: Bool = app.data_store["sfx_muted"] == true
891 # The background music resource. */
892 var music = new Music("music.ogg")
895 var snd_click = new Sound("click.wav")
898 var snd_win = new Sound("level.wav")
901 var snd_duh = new Sound("duh.wav")
904 var snd_bing = new Sound("bing.wav")
907 var snd_whip = new Sound("whip.wav")
910 # INPUT ******************************************************************
912 # Current grid edited (if any).
913 var grid_edit: nullable Grid = null
915 # Sequence of current entities
916 var entities = new Array[Entity]
918 # The current statusbar
919 var statusbar = new StatusBar(self)
922 var board = new Board(self)
924 # The current score board
925 var score = new Score(self)
927 # Monster button game elements.
928 var buttons = new Array[MonsterButton]
931 var button_wall = new MetalButton(self)
934 var button_erase = new EraseButton(self)
937 var button_size = new ResizeButton(self)
939 # Cheat mode enabled?
949 if not music_muted then music.play
956 buttons[i] = new MonsterButton(self, i)
960 # Play a level in player mode.
963 save # save the previous level grid
965 grid.load(level.saved_str or else level.str)
966 init_play_menu(false)
967 if level.status != "" then
968 statusbar.main_txt = level.status
970 statusbar.main_txt = level.fullname
972 var t = new NextLevelButton(self)
977 # Play the next level.
980 play(levels[level.number+1])
984 # Helper function to initialize all states.
985 # Set up buttons for music and SFX.
991 entities.push(new MusicButton(self))
992 entities.push(new SFXButton(self))
993 entities.push(new MenuButton(self))
995 entities.push(statusbar)
998 # Helper function to initialize monster menu entries.
999 fun init_play_menu(full: Bool)
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
1012 entities.push(buttons[i])
1015 selected_button = sel
1016 entities.push(score)
1019 # Play a arbitrary grid in try mode.
1020 fun play_grid(g: Grid)
1023 init_play_menu(false)
1024 statusbar.main_txt = "User level"
1025 if grid_edit != null then
1026 entities.push(new EditButton(self))
1028 entities.push(new WonButton(self))
1032 # Launch the editor starting with a grid.
1033 fun edit_grid(g: Grid)
1036 init_play_menu(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))
1048 # Launch the title screen
1052 entities.push(new Splash(self))
1056 # Helper function to initialize the menu (and tile) screen
1059 save # save the previous level grid
1062 var i = levels.first
1065 if l.get_state == l.l_open then break
1067 entities.push(new StartButton(self, i))
1075 t = new TextButton(self,"LEVEL SELECT", 120, ypad, "white", null, null)
1077 for i in [0..levels.length[ do
1078 var b = new LevelButton(levels[i])
1081 t = new Achievement(self, 0, "Training")
1083 t = new Achievement(self, 1, "Gold")
1085 t = new Achievement(self, 2, "Editor")
1087 t = new Achievement(self, 3, "Challenge")
1089 t = new Achievement(self, 4, "Congraturation")
1091 t = new Achievement(self, 5, "Awesome")
1096 # Last function called when the lauch state is ready
1101 # Should all entity redrawn?
1102 var dirty_all = true
1104 # Draw all game entities.
1105 fun draw(display: Display) do
1107 if dirty_all then display.blit(back, 0, 0)
1108 for g in entities do
1109 if g.dirty or dirty_all then
1111 #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)
1113 #ctx.rect(g.x, g.y, g.width, g.height)
1117 if ev isa Event then
1118 # Cursor, kept for debugging
1119 #display.blit(img[4,0],ev.offset_x-42,ev.offset_y-6)
1124 # Update all game entities.
1126 if solver != null and not solver_pause then
1127 if solver.run_steps(solver_steps) != null then solver_pause = true
1129 if not solver.is_running then solver = null
1131 for g in entities do
1136 # Return the game entity located at a mouse event.
1137 fun get_game_element(ev: Event): nullable Entity
1141 for g in entities do
1142 if x>=g.x and x<g.x2 and y>g.y and y<g.y2 then
1152 # The game entlty the mouse went down on
1153 var drag: nullable Entity = null
1155 # Last mouse event. Used to dray the cursor
1156 var lastev: nullable Event = null
1158 # Callback when the mouse is pressed
1159 fun onMouseDown(ev: Event) do
1161 var g = get_game_element(ev)
1169 # Callback when the mouse is releassed
1170 fun onMouseUp(ev: Event) do
1174 # Callback when the mouse if moved while pressed
1175 fun onMouseMove(ev: Event) do
1177 var g = get_game_element(ev)
1179 statusbar.dirty = true
1180 statusbar.over_txt = null
1181 statusbar.over_what = null
1184 if statusbar.over_what != g then
1185 statusbar.dirty = true
1187 statusbar.over_txt = go
1188 statusbar.over_what = g
1190 if go != null then print "***OVER*** {go}"
1192 # We moved abode a element that accepts drag event
1193 if drag == g and g.draggable then
1201 # Current solver, if any
1202 var solver: nullable BacktrackSolver[Grid, Action] = null
1204 # Is the solver paused?
1205 var solver_pause = false
1207 # Number of solver steps played in a single game `update`
1208 var solver_steps = 20000
1210 # Callback when a keyboard event is recieved
1211 fun onKeyDown(ev: Event) do
1212 var kc = ev.char_code
1214 set_tmp("RUN EDITOR")
1215 grid_edit = grid.copy(true)
1217 else if kc == "c" then
1219 set_tmp("CHEAT: OFF")
1223 set_tmp("CHEAT: ON")
1227 else if kc == "s" then
1228 if solver == null then
1229 solver = (new FriendzProblem(grid)).solve
1230 solver_pause = false
1232 solver_pause = not solver_pause
1234 if solver_pause then
1235 set_tmp("SOLVER: PAUSED")
1237 set_tmp("SOLVER: ON")
1240 else if kc == "d" then
1241 if solver == null then
1242 solver = (new FriendzProblem(grid)).solve
1244 set_tmp("SOLVER: ON")
1248 set_tmp("SOLVER: ONE STEP")
1250 else if kc == "+" then
1252 set_tmp("SOLVER: {solver_steps} STEPS")
1253 else if kc == "-" then
1255 set_tmp("SOLVER: {solver_steps} STEPS")
1256 else for g in entities do
1257 if kc == g.shortcut then
1264 fun set_tmp(s: String)
1266 statusbar.set_tmp(s, "cyan")
1269 redef fun load_levels
1273 for level in levels do
1274 var score = app.data_store["s{level.str}"]
1275 if score isa Int then
1278 var saved_str = app.data_store["g{level.str}"]
1279 if saved_str isa String then
1280 print "LOAD {level.name}: {saved_str}"
1281 level.saved_str = saved_str
1295 # The spash title image
1300 super(game,game.xpad,game.ypad,380,350)
1304 ctx.blit(game.logo, game.xpad, game.ypad)
1313 class NextLevelButton
1317 super(game, "NEXT LEVEL", 440, 24, "cyan", "Play next level", null)
1323 var w = game.level.check_won(game.grid)
1324 if self.enabled != w then
1329 game.statusbar.set_tmp("Level solved!", "cyan")
1336 if not self.enabled then
1338 var grid = game.grid
1339 var monsters = grid.monsters
1340 var angry = new Array[Tile]
1341 var lonely = new Array[Tile]
1342 var edges = new Array[Tile]
1343 for i in [0..grid.width[ do
1344 for j in [0..grid.height[ do
1345 var t = grid.grid[i][j]
1346 if t.kind == 0 then continue
1347 if t.nexts == 0 then lonely.push(t)
1348 if t.nexts == 1 and not monsters[t.kind].chain then edges.push(t)
1349 if t.nexts > 2 then angry.push(t)
1354 if angry.length>0 then
1356 else if lonely.length>0 then
1361 for i in l do i.shocked=5
1363 if angry.length>0 then
1364 game.statusbar.set_tmp("Angry monsters!", "red")
1365 else if lonely.length>0 then
1366 game.statusbar.set_tmp("Lonely monsters!", "red")
1367 else if not grid.won then
1368 game.statusbar.set_tmp("Unconnected monsters!", "red")
1370 game.statusbar.set_tmp("Not enough monsters!", "red")
1384 super(game, "MUSIC", 470, 412, "purple", "Mute the music", "Unmute the music")
1385 toggled = game.music_muted
1387 redef fun click2(ev)
1389 game.music_muted = self.toggled
1390 if game.music_muted then game.music.pause else game.music.play
1391 app.data_store["music_muted"] = game.music_muted
1399 super(game, "SOUND FX", 470, 382, "purple", "Mute the sound effects", "Unmute the sound effects")
1400 toggled = game.sfx_muted
1403 redef fun click2(ev)
1405 game.sfx_muted = self.toggled
1406 if not game.sfx_muted then game.snd_whip.play # Because the automatic one was muted
1407 app.data_store["sfx_muted"] = game.sfx_muted
1415 super(game, "MENU", 470, 442, "purple", "Exit to the main menu", null)
1419 redef fun click2(ev)
1429 super(game,"RESET", 440, 61, "purple", "Clear the grid",null)
1434 redef fun click2(ev)
1437 if self.count==1 then
1438 game.statusbar.set_tmp("Click again to reset","white")
1439 else if self.count==2 then
1440 game.grid.reset(false)
1441 if game.editing then
1442 game.statusbar.set_tmp("Click again to clear all","white")
1444 else if game.editing then
1445 game.grid.reset(true)
1447 game.dirty_all = true
1460 super(game,"EDIT", 540, 24, "purple", "Return to the editor",null)
1463 redef fun click2(ev)
1465 var ge = game.grid_edit
1475 super(game,"WON", 440, 24, "cyan", "", null)
1478 redef fun click2(ev)
1480 var ge = game.grid_edit
1481 if not self.enabled then
1482 game.statusbar.set_tmp("Solve the level first!", "red")
1483 else if ge != null then
1494 var w = game.grid.won
1495 if self.enabled != w then
1500 game.statusbar.set_tmp("Level solved!", "cyan")
1510 super(game,"TEST", 440, 24, "cyan", "Try to play the level", null)
1513 redef fun click2(ev)
1515 game.grid_edit = game.grid
1516 game.play_grid(game.grid.copy(false))
1524 super(game, "SAVE", 540, 24, "purple", "Save the level", null)
1527 redef fun click2(ev)
1529 var res = game.grid.save
1538 super(game,"LOAD", 540, 61, "purple", "Load an user level", null)
1541 redef fun click2(ev)
1543 var grid2 = new Grid(game.gw,game.gh,game.monsters)
1544 if grid2.load("") then
1547 game.dirty_all = true
1551 class ContinueButton
1555 super(game,"CONTINUE", 440, 24, "purple", "Continue playing", null)
1558 redef fun click2(ev)
1567 init(game: Game, level: Level)
1570 if level.number == 0 then
1571 super(game,"START", 440, 24, "purple", "Play the first level", null)
1573 super(game,"CONTINUE", 440, 24, "purple", "Continue from level "+level.name, null)
1577 redef fun click2(ev)
1590 # Wanted screen width
1591 var screen_width = 640
1593 # Wanted screen height
1594 var screen_height = 480
1600 game.font.hspace = -2
1601 if args.length > 0 then
1602 game.play(game.levels[args.first.to_i])
1613 # Maximum wanted frame per second
1616 # clock used to track FPS
1617 private var clock = new Clock
1619 redef fun frame_core(display)
1623 var dt = clock.lapse
1624 var target_dt = 1000000000 / max_fps
1625 if dt.sec == 0 and dt.nanosec < target_dt then
1626 var sleep_t = target_dt - dt.nanosec
1627 sys.nanosleep(0, sleep_t)
1631 redef fun input(input_event)
1634 if input_event isa QuitEvent then # close window button
1635 quit = true # orders system to quit
1636 else if input_event isa PointerEvent then
1637 var ev = new Event(input_event.x.to_i, input_event.y.to_i, "")
1638 if input_event.is_motion then
1639 game.onMouseMove(ev)
1640 else if input_event.pressed then
1641 game.onMouseDown(ev)
1646 else if input_event isa KeyEvent and input_event.is_down then
1647 var ev = new Event(0, 0, input_event.key_name)
1656 redef class PointerEvent
1657 fun is_motion: Bool do return false
1660 redef class KeyEvent
1661 fun key_name: String
1664 if c != null then return c.to_s
1670 # Save the score and grid of the level
1673 app.data_store["s{str}"] = if score > 0 then score else null
1674 var saved = game.grid.save
1676 app.data_store["g{str}"] = saved
1677 print "SAVE: {name}: {saved}"
1680 # The saved player grid (to continue games)
1681 var saved_str: nullable String = null