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.
964 grid.load(level.saved_str or else level.str)
965 init_play_menu(false)
966 if level.status != "" then
967 statusbar.main_txt = level.status
969 statusbar.main_txt = level.fullname
971 var t = new NextLevelButton(self)
976 # Play the next level.
979 play(levels[level.number+1])
983 # Helper function to initialize all states.
984 # Set up buttons for music and SFX.
990 entities.push(new MusicButton(self))
991 entities.push(new SFXButton(self))
992 entities.push(new MenuButton(self))
994 entities.push(statusbar)
997 # Helper function to initialize monster menu entries.
998 fun init_play_menu(full: Bool)
1001 entities.push(board)
1002 entities.push(new ResetButton(self))
1003 entities.push(button_erase)
1004 # Push monster buttons and determine the selected one
1005 var sel: nullable Button = null
1006 for i in [1..monsters] do
1007 if grid.monsters[i].number > 0 or full then
1008 if selected_button == buttons[i] or sel == null then
1011 entities.push(buttons[i])
1014 selected_button = sel
1015 entities.push(score)
1018 # Play a arbitrary grid in try mode.
1019 fun play_grid(g: Grid)
1022 init_play_menu(false)
1023 statusbar.main_txt = "User level"
1024 if grid_edit != null then
1025 entities.push(new EditButton(self))
1027 entities.push(new WonButton(self))
1031 # Launch the editor starting with a grid.
1032 fun edit_grid(g: Grid)
1035 init_play_menu(true)
1037 statusbar.main_txt = "Level editor"
1038 if level != null then statusbar.main_txt += ": level "+level.name
1039 entities.push(button_wall)
1040 entities.push(button_size)
1041 entities.push(new TestButton(self))
1042 entities.push(new SaveButton(self))
1043 entities.push(new LoadButton(self))
1047 # Launch the title screen
1051 entities.push(new Splash(self))
1055 # Helper function to initialize the menu (and tile) screen
1060 var i = levels.first
1063 if l.get_state == l.l_open then break
1065 entities.push(new StartButton(self, i))
1073 t = new TextButton(self,"LEVEL SELECT", 120, ypad, "white", null, null)
1075 for i in [0..levels.length[ do
1076 var b = new LevelButton(levels[i])
1079 t = new Achievement(self, 0, "Training")
1081 t = new Achievement(self, 1, "Gold")
1083 t = new Achievement(self, 2, "Editor")
1085 t = new Achievement(self, 3, "Challenge")
1087 t = new Achievement(self, 4, "Congraturation")
1089 t = new Achievement(self, 5, "Awesome")
1094 # Last function called when the lauch state is ready
1099 # Should all entity redrawn?
1100 var dirty_all = true
1102 # Draw all game entities.
1103 fun draw(display: Display) do
1105 if dirty_all then display.blit(back, 0, 0)
1106 for g in entities do
1107 if g.dirty or dirty_all then
1109 #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 #ctx.rect(g.x, g.y, g.width, g.height)
1115 if ev isa Event then
1116 # Cursor, kept for debugging
1117 #display.blit(img[4,0],ev.offset_x-42,ev.offset_y-6)
1122 # Update all game entities.
1124 if solver != null and not solver_pause then
1125 if solver.run_steps(solver_steps) != null then solver_pause = true
1127 if not solver.is_running then solver = null
1129 for g in entities do
1134 # Return the game entity located at a mouse event.
1135 fun get_game_element(ev: Event): nullable Entity
1139 for g in entities do
1140 if x>=g.x and x<g.x2 and y>g.y and y<g.y2 then
1150 # The game entlty the mouse went down on
1151 var drag: nullable Entity = null
1153 # Last mouse event. Used to dray the cursor
1154 var lastev: nullable Event = null
1156 # Callback when the mouse is pressed
1157 fun onMouseDown(ev: Event) do
1159 var g = get_game_element(ev)
1167 # Callback when the mouse is releassed
1168 fun onMouseUp(ev: Event) do
1172 # Callback when the mouse if moved while pressed
1173 fun onMouseMove(ev: Event) do
1175 var g = get_game_element(ev)
1177 statusbar.dirty = true
1178 statusbar.over_txt = null
1179 statusbar.over_what = null
1182 if statusbar.over_what != g then
1183 statusbar.dirty = true
1185 statusbar.over_txt = go
1186 statusbar.over_what = g
1188 if go != null then print "***OVER*** {go}"
1190 # We moved abode a element that accepts drag event
1191 if drag == g and g.draggable then
1199 # Current solver, if any
1200 var solver: nullable BacktrackSolver[Grid, Action] = null
1202 # Is the solver paused?
1203 var solver_pause = false
1205 # Number of solver steps played in a single game `update`
1206 var solver_steps = 20000
1208 # Callback when a keyboard event is recieved
1209 fun onKeyDown(ev: Event) do
1210 var kc = ev.char_code
1212 set_tmp("RUN EDITOR")
1213 grid_edit = grid.copy(true)
1215 else if kc == "c" then
1217 set_tmp("CHEAT: OFF")
1221 set_tmp("CHEAT: ON")
1225 else if kc == "s" then
1226 if solver == null then
1227 solver = (new FriendzProblem(grid)).solve
1228 solver_pause = false
1230 solver_pause = not solver_pause
1232 if solver_pause then
1233 set_tmp("SOLVER: PAUSED")
1235 set_tmp("SOLVER: ON")
1238 else if kc == "d" then
1239 if solver == null then
1240 solver = (new FriendzProblem(grid)).solve
1242 set_tmp("SOLVER: ON")
1246 set_tmp("SOLVER: ONE STEP")
1248 else if kc == "+" then
1250 set_tmp("SOLVER: {solver_steps} STEPS")
1251 else if kc == "-" then
1253 set_tmp("SOLVER: {solver_steps} STEPS")
1254 else for g in entities do
1255 if kc == g.shortcut then
1262 fun set_tmp(s: String)
1264 statusbar.set_tmp(s, "cyan")
1267 redef fun load_levels
1271 for level in levels do
1272 var score = app.data_store["s{level.str}"]
1273 if score isa Int then
1276 var saved_str = app.data_store["g{level.str}"]
1277 if saved_str isa String then
1278 print "LOAD {level.name}: {saved_str}"
1279 level.saved_str = saved_str
1285 # The spash title image
1290 super(game,game.xpad,game.ypad,380,350)
1294 ctx.blit(game.logo, game.xpad, game.ypad)
1303 class NextLevelButton
1307 super(game, "NEXT LEVEL", 440, 24, "cyan", "Play next level", null)
1313 var w = game.level.check_won(game.grid)
1314 if self.enabled != w then
1319 game.statusbar.set_tmp("Level solved!", "cyan")
1326 if not self.enabled then
1328 var grid = game.grid
1329 var monsters = grid.monsters
1330 var angry = new Array[Tile]
1331 var lonely = new Array[Tile]
1332 var edges = new Array[Tile]
1333 for i in [0..grid.width[ do
1334 for j in [0..grid.height[ do
1335 var t = grid.grid[i][j]
1336 if t.kind == 0 then continue
1337 if t.nexts == 0 then lonely.push(t)
1338 if t.nexts == 1 and not monsters[t.kind].chain then edges.push(t)
1339 if t.nexts > 2 then angry.push(t)
1344 if angry.length>0 then
1346 else if lonely.length>0 then
1351 for i in l do i.shocked=5
1353 if angry.length>0 then
1354 game.statusbar.set_tmp("Angry monsters!", "red")
1355 else if lonely.length>0 then
1356 game.statusbar.set_tmp("Lonely monsters!", "red")
1357 else if not grid.won then
1358 game.statusbar.set_tmp("Unconnected monsters!", "red")
1360 game.statusbar.set_tmp("Not enough monsters!", "red")
1374 super(game, "MUSIC", 470, 412, "purple", "Mute the music", "Unmute the music")
1375 toggled = game.music_muted
1377 redef fun click2(ev)
1379 game.music_muted = self.toggled
1380 if game.music_muted then game.music.pause else game.music.play
1381 app.data_store["music_muted"] = game.music_muted
1389 super(game, "SOUND FX", 470, 382, "purple", "Mute the sound effects", "Unmute the sound effects")
1390 toggled = game.sfx_muted
1393 redef fun click2(ev)
1395 game.sfx_muted = self.toggled
1396 if not game.sfx_muted then game.snd_whip.play # Because the automatic one was muted
1397 app.data_store["sfx_muted"] = game.sfx_muted
1405 super(game, "MENU", 470, 442, "purple", "Exit to the main menu", null)
1409 redef fun click2(ev)
1419 super(game,"RESET", 440, 61, "purple", "Clear the grid",null)
1424 redef fun click2(ev)
1427 if self.count==1 then
1428 game.statusbar.set_tmp("Click again to reset","white")
1429 else if self.count==2 then
1430 game.grid.reset(false)
1431 if game.editing then
1432 game.statusbar.set_tmp("Click again to clear all","white")
1434 else if game.editing then
1435 game.grid.reset(true)
1437 game.dirty_all = true
1450 super(game,"EDIT", 540, 24, "purple", "Return to the editor",null)
1453 redef fun click2(ev)
1455 var ge = game.grid_edit
1465 super(game,"WON", 440, 24, "cyan", "", null)
1468 redef fun click2(ev)
1470 var ge = game.grid_edit
1471 if not self.enabled then
1472 game.statusbar.set_tmp("Solve the level first!", "red")
1473 else if ge != null then
1484 var w = game.grid.won
1485 if self.enabled != w then
1490 game.statusbar.set_tmp("Level solved!", "cyan")
1500 super(game,"TEST", 440, 24, "cyan", "Try to play the level", null)
1503 redef fun click2(ev)
1505 game.grid_edit = game.grid
1506 game.play_grid(game.grid.copy(false))
1514 super(game, "SAVE", 540, 24, "purple", "Save the level", null)
1517 redef fun click2(ev)
1519 var res = game.grid.save
1528 super(game,"LOAD", 540, 61, "purple", "Load an user level", null)
1531 redef fun click2(ev)
1533 var grid2 = new Grid(game.gw,game.gh,game.monsters)
1534 if grid2.load("") then
1537 game.dirty_all = true
1541 class ContinueButton
1545 super(game,"CONTINUE", 440, 24, "purple", "Continue playing", null)
1548 redef fun click2(ev)
1557 init(game: Game, level: Level)
1560 if level.number == 0 then
1561 super(game,"START", 440, 24, "purple", "Play the first level", null)
1563 super(game,"CONTINUE", 440, 24, "purple", "Continue from level "+level.name, null)
1567 redef fun click2(ev)
1580 # Wanted screen width
1581 var screen_width = 640
1583 # Wanted screen height
1584 var screen_height = 480
1590 game.font.hspace = -2
1591 if args.length > 0 then
1592 game.play(game.levels[args.first.to_i])
1597 # Maximum wanted frame per second
1600 # clock used to track FPS
1601 private var clock = new Clock
1603 redef fun frame_core(display)
1607 var dt = clock.lapse
1608 var target_dt = 1000000000 / max_fps
1609 if dt.sec == 0 and dt.nanosec < target_dt then
1610 var sleep_t = target_dt - dt.nanosec
1611 sys.nanosleep(0, sleep_t)
1615 redef fun input(input_event)
1618 if input_event isa QuitEvent then # close window button
1619 quit = true # orders system to quit
1620 else if input_event isa PointerEvent then
1621 var ev = new Event(input_event.x.to_i, input_event.y.to_i, "")
1622 if input_event.is_motion then
1623 game.onMouseMove(ev)
1624 else if input_event.pressed then
1625 game.onMouseDown(ev)
1630 else if input_event isa KeyEvent and input_event.is_down then
1631 var ev = new Event(0, 0, input_event.key_name)
1640 redef class PointerEvent
1641 fun is_motion: Bool do return false
1644 redef class KeyEvent
1645 fun key_name: String
1648 if c != null then return c.to_s
1656 app.data_store["s{str}"] = if score > 0 then score else null
1657 var saved = game.grid.save
1659 app.data_store["g{str}"] = saved
1660 print "SAVE: {name}: {saved}"
1663 # The saved player grid (to continue games)
1664 var saved_str: nullable String = null