├── .gitignore ├── src ├── site │ ├── style.css │ ├── images │ │ ├── frog.png │ │ ├── leaf.png │ │ ├── next.png │ │ ├── level.png │ │ ├── loading.gif │ │ ├── restart.png │ │ ├── water.jpg │ │ ├── completed.svg │ │ ├── message │ │ │ ├── 90.svg │ │ │ ├── 180.svg │ │ │ ├── 0.svg │ │ │ └── 270.svg │ │ ├── key.svg │ │ └── arrows │ │ │ ├── 1 │ │ │ ├── 180.svg │ │ │ └── 0.svg │ │ │ └── 2 │ │ │ ├── 180.svg │ │ │ ├── 0.svg │ │ │ ├── 90.svg │ │ │ └── 270.svg │ ├── index.html │ └── script.js └── elm │ └── Froggy │ ├── TransitionUtil.elm │ ├── Grid.elm │ ├── Util.elm │ ├── Main.elm │ ├── Commands.elm │ ├── Model.elm │ ├── State.elm │ └── View.elm └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/cache 2 | build/elm_dependencies 3 | build/node_modules 4 | target -------------------------------------------------------------------------------- /src/site/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-image: url("images/water.jpg"); 3 | } -------------------------------------------------------------------------------- /src/site/images/frog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thSoft/froggy/HEAD/src/site/images/frog.png -------------------------------------------------------------------------------- /src/site/images/leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thSoft/froggy/HEAD/src/site/images/leaf.png -------------------------------------------------------------------------------- /src/site/images/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thSoft/froggy/HEAD/src/site/images/next.png -------------------------------------------------------------------------------- /src/site/images/level.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thSoft/froggy/HEAD/src/site/images/level.png -------------------------------------------------------------------------------- /src/site/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thSoft/froggy/HEAD/src/site/images/loading.gif -------------------------------------------------------------------------------- /src/site/images/restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thSoft/froggy/HEAD/src/site/images/restart.png -------------------------------------------------------------------------------- /src/site/images/water.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thSoft/froggy/HEAD/src/site/images/water.jpg -------------------------------------------------------------------------------- /src/elm/Froggy/TransitionUtil.elm: -------------------------------------------------------------------------------- 1 | module Froggy.TransitionUtil where 2 | 3 | type TransitionInfo a = { 4 | oldValue: a, 5 | startTime: Time 6 | } 7 | 8 | time : Signal Time 9 | time = fps 30 |> timestamp |> lift fst -------------------------------------------------------------------------------- /src/elm/Froggy/Grid.elm: -------------------------------------------------------------------------------- 1 | module Froggy.Grid where 2 | 3 | type Position = { 4 | x: Int, 5 | y: Int 6 | } 7 | 8 | equals : Position -> Position -> Bool 9 | equals a b = (a.x == b.x) && (a.y == b.y) 10 | 11 | translate : Position -> Position -> Position 12 | translate a b = 13 | { 14 | x = a.x + b.x, 15 | y = a.y + b.y 16 | } -------------------------------------------------------------------------------- /src/elm/Froggy/Util.elm: -------------------------------------------------------------------------------- 1 | module Froggy.Util where 2 | 3 | import Maybe (..) 4 | 5 | remove : [a] -> a -> [a] 6 | remove xs x = xs |> filter (\element -> element /= x) 7 | 8 | getOrElse : a -> Maybe a -> a 9 | getOrElse defaultValue maybeValue = maybeValue |> maybe defaultValue identity 10 | 11 | distance : Int -> Int -> Int 12 | distance a b = abs(a - b) -------------------------------------------------------------------------------- /src/elm/Froggy/Main.elm: -------------------------------------------------------------------------------- 1 | module Froggy.Main where 2 | 3 | import Window 4 | import Froggy.Model (..) 5 | import Froggy.State (..) 6 | import Froggy.View (..) 7 | import Froggy.TransitionUtil (..) 8 | 9 | main = lift3 (view fontName) Window.dimensions time mainState 10 | 11 | mainState = game loadedGame 12 | 13 | port loadedGame : Signal (Maybe Game) 14 | 15 | port savedGame : Signal Game 16 | port savedGame = mainState 17 | 18 | port fontName : String -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Play 2 | 3 | [Have fun!](http://thsoft.github.io/froggy) 4 | 5 | ## Design 6 | 7 | If you are a graphic designer and this game inspires you, I'd be very grateful if you created the graphics for it. Please [contact me](http://thsoft.hu/en/Contact)! 8 | 9 | ## Develop 10 | 11 | ### Install 12 | * [node.js](http://nodejs.org/download/) 13 | * [Elm 0.13](http://elm-lang.org/Install.elm) 14 | 15 | ### Check out 16 | git clone https://github.com/thSoft/froggy.git 17 | cd froggy 18 | build/init.sh 19 | 20 | ### Build 21 | build/build.js 22 | * Open `target/index.html` 23 | 24 | ### Release 25 | build/publish.js -------------------------------------------------------------------------------- /src/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Froggy 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | -------------------------------------------------------------------------------- /src/elm/Froggy/Commands.elm: -------------------------------------------------------------------------------- 1 | module Froggy.Commands where 2 | 3 | import Keyboard 4 | import Mouse 5 | import Graphics.Input (..) 6 | import Froggy.Grid as Grid 7 | import Froggy.Model (..) 8 | 9 | data Command = 10 | Nop | 11 | MoveBy Grid.Position | 12 | MoveTo Leaf | 13 | Continue | 14 | RestartLevel | 15 | Start (Maybe Game) 16 | 17 | commands : Signal (Maybe Game) -> Signal (Time, Command) 18 | commands loadedGame = 19 | let moveBy = lift2 makeMoveBy Keyboard.shift Keyboard.arrows 20 | continue = lift (makeCommand Continue) (merge Keyboard.enter Mouse.isDown) 21 | restartLevel = lift (makeCommand RestartLevel) (Keyboard.isDown 27) 22 | start = makeStart loadedGame 23 | in merges [moveBy, moveTo.signal, continue, restartLevel, start] |> timestamp 24 | 25 | makeMoveBy : Bool -> Grid.Position -> Command 26 | makeMoveBy shift arrows = 27 | let multiplier = if shift then 2 else 1 28 | in MoveBy { 29 | x = arrows.x * multiplier, 30 | y = -arrows.y * multiplier 31 | } 32 | 33 | moveTo : Input Command 34 | moveTo = input Nop 35 | 36 | makeCommand : Command -> Bool -> Command 37 | makeCommand command pressed = if pressed then command else Nop 38 | 39 | makeStart : Signal (Maybe Game) -> Signal Command 40 | makeStart loadedGame = foldp updateStart Nop loadedGame |> dropRepeats 41 | 42 | updateStart : Maybe Game -> Command -> Command 43 | updateStart loadedGame startGame = 44 | case startGame of 45 | Nop -> Start loadedGame 46 | _ -> startGame -------------------------------------------------------------------------------- /src/elm/Froggy/Model.elm: -------------------------------------------------------------------------------- 1 | module Froggy.Model where 2 | 3 | import Maybe (..) 4 | import Froggy.Util (..) 5 | import Froggy.TransitionUtil (..) 6 | import Froggy.Grid as Grid 7 | import Froggy.Levels (..) 8 | 9 | type Game = { 10 | scene: Scene, 11 | usingKeyboard: Bool, 12 | lastSceneChange: Maybe (TransitionInfo (Maybe Scene)) 13 | } 14 | 15 | type Scene = { 16 | frog: Frog, 17 | leaves: [Leaf], 18 | levelNumber: Int 19 | } 20 | 21 | type Frog = { 22 | leaf: Leaf, 23 | lastMove: Maybe (TransitionInfo Leaf) 24 | } 25 | 26 | type Leaf = { 27 | position: Grid.Position 28 | } 29 | 30 | levelCompleted : Scene -> Bool 31 | levelCompleted scene = (scene.leaves |> length) == 1 32 | 33 | stuck : Scene -> Bool 34 | stuck scene = not (scene.leaves |> any (reachableBy scene.frog)) 35 | 36 | reachableBy : Frog -> Leaf -> Bool 37 | reachableBy frog leaf = (frog.leaf `angleBetween` leaf) |> isJust 38 | 39 | angleBetween : Leaf -> Leaf -> Maybe Int 40 | angleBetween sourceLeaf targetLeaf = 41 | let sourceX = sourceLeaf.position.x 42 | sourceY = sourceLeaf.position.y 43 | targetX = targetLeaf.position.x 44 | targetY = targetLeaf.position.y 45 | in if | (sourceX == targetX) && (sourceY > targetY) && (sourceY `near` targetY) -> Just 90 46 | | (sourceY == targetY) && (sourceX < targetX) && (sourceX `near` targetX) -> Just 0 47 | | (sourceX == targetX) && (sourceY < targetY) && (sourceY `near` targetY) -> Just 270 48 | | (sourceY == targetY) && (sourceX > targetX) && (sourceX `near` targetX) -> Just 180 49 | | otherwise -> Nothing 50 | 51 | near : Int -> Int -> Bool 52 | near a b = (distance a b) <= 2 53 | 54 | onlyDoubleJump : Scene -> Bool 55 | onlyDoubleJump scene = 56 | let distanceX leaf = distance scene.frog.leaf.position.x leaf.position.x 57 | distanceY leaf = distance scene.frog.leaf.position.y leaf.position.y 58 | leafOnlyDoubleJump leaf = ((distanceX leaf) == 2) || ((distanceY leaf) == 2) 59 | in scene.leaves |> filter (reachableBy scene.frog) |> all leafOnlyDoubleJump 60 | 61 | angleOf : Frog -> Int 62 | angleOf frog = 63 | case frog.lastMove of 64 | Just { oldValue } -> oldValue `angleBetween` frog.leaf |> getOrElse 0 65 | Nothing -> 90 -------------------------------------------------------------------------------- /src/site/script.js: -------------------------------------------------------------------------------- 1 | function translateTouchEventsToMouseEvents() { 2 | function touchHandler(event) { 3 | var touches = event.changedTouches; 4 | var first = touches[0]; 5 | var type; 6 | switch (event.type) { 7 | case "touchstart": 8 | type = "mousedown"; 9 | break; 10 | case "touchmove": 11 | type = "mousemove"; 12 | break; 13 | case "touchend": 14 | type = "mouseup"; 15 | break; 16 | default: 17 | return; 18 | } 19 | var simulatedEvent = document.createEvent("MouseEvent"); 20 | simulatedEvent.initMouseEvent(type, true, true, window, 1, first.screenX, first.screenY, first.clientX, first.clientY, false, false, false, false, 0, null); 21 | first.target.dispatchEvent(simulatedEvent); 22 | } 23 | 24 | document.addEventListener("touchstart", touchHandler, true); 25 | document.addEventListener("touchmove", touchHandler, true); 26 | document.addEventListener("touchend", touchHandler, true); 27 | document.addEventListener("touchcancel", touchHandler, true); 28 | } 29 | 30 | function initializeElm() { 31 | var module = Elm.Froggy.Main; 32 | return Elm.fullscreen(module, { 33 | loadedGame: null, 34 | fontName: fontName 35 | }); 36 | } 37 | 38 | var fontName = "Boogaloo"; 39 | 40 | function loadFonts() { 41 | WebFont.load({ 42 | google: { 43 | families: [fontName] 44 | } 45 | }); 46 | } 47 | 48 | function loadImages(callback) { 49 | var loadedImagesCount = 0; 50 | images.forEach(function(imagePath) { 51 | var image = new Image(); 52 | image.src = imagePath; 53 | image.onload = function() { 54 | loadedImagesCount++; 55 | if (loadedImagesCount >= images.length) { 56 | callback(); 57 | } 58 | } 59 | }); 60 | } 61 | 62 | function loadGame(component) { 63 | var storageKey = 'froggy'; 64 | var loadedJson = localStorage.getItem(storageKey); 65 | var loadedState = loadedJson ? JSON.parse(loadedJson) : null; 66 | try { 67 | component.ports.loadedGame.send(loadedState); 68 | } catch (e) { 69 | console.log("The game state can't be loaded because the internal model changed. Trying to start a new game. " + e); 70 | component.ports.loadedGame.send(null); 71 | } 72 | component.ports.savedGame.subscribe(function(state) { 73 | localStorage.setItem(storageKey, JSON.stringify(state)); 74 | }); 75 | } -------------------------------------------------------------------------------- /src/elm/Froggy/State.elm: -------------------------------------------------------------------------------- 1 | module Froggy.State where 2 | 3 | import Array 4 | import Froggy.Util (..) 5 | import Froggy.Grid as Grid 6 | import Froggy.Grid (..) 7 | import Froggy.TransitionUtil (..) 8 | import Froggy.Levels (..) 9 | import Froggy.Model (..) 10 | import Froggy.Commands (..) 11 | 12 | game : Signal (Maybe Game) -> Signal Game 13 | game loadedGame = foldp update initialGame (commands loadedGame) 14 | 15 | update : (Time, Command) -> Game -> Game 16 | update (time, command) game = 17 | case command of 18 | Nop -> game 19 | MoveBy positionDelta -> game |> moveBy positionDelta time 20 | MoveTo leaf -> game |> moveTo leaf time 21 | Continue -> game |> continue time 22 | RestartLevel -> game |> restartLevel time 23 | Start loadedGame -> start loadedGame time 24 | 25 | useKeyboard : Bool -> Game -> Game 26 | useKeyboard keyboard game = { game | usingKeyboard <- keyboard } 27 | 28 | moveBy : Grid.Position -> Time -> Game -> Game 29 | moveBy positionDelta time game = 30 | let newGame = 31 | if (positionDelta.x == 0) && (positionDelta.y == 0) then game 32 | else 33 | let leafPosition = game.scene.frog.leaf.position `translate` positionDelta 34 | maybeLeaf = game.scene.leaves |> findLeaf leafPosition 35 | in case maybeLeaf of 36 | Nothing -> game 37 | Just leaf -> game |> moveTo leaf time 38 | in newGame |> useKeyboard True 39 | 40 | findLeaf : Grid.Position -> [Leaf] -> Maybe Leaf 41 | findLeaf position leaves = 42 | let hasPosition leaf = leaf.position `equals` position 43 | in leaves |> filter hasPosition |> Array.fromList |> Array.get 0 44 | 45 | moveTo : Leaf -> Time -> Game -> Game 46 | moveTo leaf time game = 47 | let reachable = leaf |> reachableBy game.scene.frog 48 | scene = game.scene 49 | newGame = 50 | if reachable then 51 | { game | 52 | scene <- { scene | 53 | frog <- { 54 | leaf = leaf, 55 | lastMove = Just { 56 | oldValue = game.scene.frog.leaf, 57 | startTime = time 58 | } 59 | }, 60 | leaves <- remove game.scene.leaves game.scene.frog.leaf 61 | } 62 | } 63 | else game 64 | in newGame |> useKeyboard False 65 | 66 | loadLevel : Time -> Maybe Scene -> Int -> Game 67 | loadLevel time oldScene levelNumber = 68 | let actualLevelNumber = if (levelNumber >= numberOfLevels) || (levelNumber < 0) then 0 else levelNumber 69 | level = getLevel actualLevelNumber 70 | leaves = loadLeafMatrix level.leafMatrix 71 | maybeLeaf = leaves |> findLeaf level.frogPosition 72 | leaf = maybeLeaf |> getOrElse (leaves |> head) 73 | in { 74 | scene = { 75 | frog = { 76 | leaf = leaf, 77 | lastMove = Nothing 78 | }, 79 | leaves = leaves, 80 | levelNumber = actualLevelNumber 81 | }, 82 | usingKeyboard = False, 83 | lastSceneChange = Just { 84 | oldValue = oldScene, 85 | startTime = time 86 | } 87 | } 88 | 89 | loadLeafMatrix : LeafMatrix -> [Leaf] 90 | loadLeafMatrix leafMatrix = leafMatrix |> indexedMap loadLeafRow |> concat 91 | 92 | loadLeafRow : Int -> [Bool] -> [Leaf] 93 | loadLeafRow y row = 94 | let make x value = { 95 | position = { 96 | x = x, 97 | y = y 98 | }, 99 | value = value 100 | } 101 | isThere { value } = value 102 | removeValue leaf = { leaf - value } 103 | in row |> indexedMap make |> filter isThere |> map removeValue 104 | 105 | continue : Time -> Game -> Game 106 | continue time game = 107 | if | game.scene |> levelCompleted -> game |> nextLevel time 108 | | game.scene |> stuck -> game |> restartLevel time 109 | | otherwise -> game 110 | 111 | nextLevel : Time -> Game -> Game 112 | nextLevel time game = loadLevel time (Just game.scene) (game.scene.levelNumber + 1) 113 | 114 | restartLevel : Time -> Game -> Game 115 | restartLevel time game = loadLevel time (Just game.scene) game.scene.levelNumber 116 | 117 | initialGame : Game 118 | initialGame = newGame 0 Nothing 119 | 120 | newGame : Time -> Maybe (TransitionInfo (Maybe Scene)) -> Game 121 | newGame time lastSceneChange = 122 | let level0 = loadLevel time Nothing 0 123 | in { level0 | 124 | lastSceneChange <- lastSceneChange 125 | } 126 | 127 | start : Maybe Game -> Time -> Game 128 | start loadedGame time = 129 | let lastSceneChange = Just { 130 | oldValue = Nothing, 131 | startTime = time 132 | } 133 | in case loadedGame of 134 | Nothing -> newGame time lastSceneChange 135 | Just startedGame -> { startedGame | 136 | lastSceneChange <- lastSceneChange 137 | } -------------------------------------------------------------------------------- /src/site/images/completed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 21 | 31 | 36 | 41 | 43 | 53 | 58 | 63 | 65 | 67 | 86 | 92 | 99 | 106 | 113 | 115 | 117 | 119 | 121 | image/svg+xml 124 | 127 | 130 | 132 | 135 | Openclipart 138 | 140 | 142 | 144 | 147 | 150 | 153 | 156 | 158 | 160 | 162 | 164 | -------------------------------------------------------------------------------- /src/site/images/message/90.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | bulle 20 | 22 | 29 | 30 | 52 | 54 | 55 | 57 | image/svg+xml 58 | 60 | bulle 61 | 63 | 2011-03-06 64 | 65 | 66 | lmproulx 67 | 68 | 69 | 70 | 72 | 74 | 76 | 78 | 79 | 80 | 81 | 86 | 89 | 94 | 99 | 100 | 103 | 108 | 113 | 114 | 117 | 122 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/site/images/message/180.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | bulle 20 | 22 | 29 | 30 | 52 | 54 | 55 | 57 | image/svg+xml 58 | 60 | bulle 61 | 63 | 2011-03-06 64 | 65 | 66 | lmproulx 67 | 68 | 69 | 70 | 72 | 74 | 76 | 78 | 79 | 80 | 81 | 86 | 89 | 94 | 99 | 100 | 103 | 108 | 113 | 114 | 117 | 122 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/site/images/message/0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | bulle 20 | 22 | 29 | 30 | 52 | 54 | 55 | 57 | image/svg+xml 58 | 60 | bulle 61 | 63 | 2011-03-06 64 | 65 | 66 | lmproulx 67 | 68 | 69 | 70 | 72 | 74 | 76 | 78 | 79 | 80 | 81 | 86 | 89 | 94 | 99 | 100 | 103 | 108 | 113 | 114 | 117 | 122 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/site/images/message/270.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | bulle 20 | 22 | 29 | 30 | 52 | 54 | 55 | 57 | image/svg+xml 58 | 60 | bulle 61 | 63 | 2011-03-06 64 | 65 | 66 | lmproulx 67 | 68 | 69 | 70 | 72 | 74 | 76 | 78 | 79 | 80 | 81 | 86 | 89 | 94 | 99 | 100 | 103 | 108 | 113 | 114 | 117 | 122 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/elm/Froggy/View.elm: -------------------------------------------------------------------------------- 1 | module Froggy.View where 2 | 3 | import Maybe 4 | import Text 5 | import Graphics.Element as Element 6 | import Graphics.Input (..) 7 | import Easing 8 | import Easing (..) 9 | import Froggy.Util (..) 10 | import Froggy.TransitionUtil (..) 11 | import Froggy.Grid as Grid 12 | import Froggy.Grid (..) 13 | import Froggy.Levels (..) 14 | import Froggy.Model (..) 15 | import Froggy.Commands (..) 16 | 17 | view : String -> (Int, Int) -> Time -> Game -> Element 18 | view fontName (windowWidth, windowHeight) time game = 19 | case game.lastSceneChange of 20 | Just lastSceneChange -> 21 | let viewSize = min windowWidth windowHeight 22 | scene = game.scene |> viewScene fontName viewSize time lastSceneChange 23 | keyboardHint = game |> viewKeyboardHint fontName viewSize time |> collage viewSize viewSize 24 | cover = lastSceneChange |> viewCover (windowWidth, windowHeight) time 25 | in [scene, keyboardHint, cover] |> map (container windowWidth windowHeight middle) |> layers 26 | Nothing -> 27 | let blackRectangle = spacer windowWidth windowHeight |> Element.color black 28 | loadingImage = image 64 64 (imagePath "loading.gif") |> container windowWidth windowHeight middle 29 | in layers [blackRectangle, loadingImage] 30 | 31 | viewScene : String -> Int -> Time -> TransitionInfo (Maybe Scene) -> Scene -> Element 32 | viewScene fontName viewSize time lastSceneChange scene = 33 | let actualScene = case lastSceneChange.oldValue of 34 | Just oldScene -> if (time - lastSceneChange.startTime) < (sceneChangeDuration / 2) then oldScene else scene 35 | Nothing -> scene 36 | tileSize = viewSize |> getTileSize 37 | frog = actualScene.frog |> viewFrog tileSize time 38 | leaves = actualScene.leaves |> map (viewLeaf tileSize) 39 | targets = actualScene.leaves |> viewTargets actualScene.frog tileSize 40 | level = actualScene.levelNumber |> viewLevelNumber fontName tileSize 41 | message = actualScene |> viewMessage fontName tileSize time 42 | in (leaves ++ targets ++ frog ++ level ++ message) |> collage viewSize viewSize 43 | 44 | getTileSize : Int -> Float 45 | getTileSize viewSize = (viewSize |> toFloat) / mapSize 46 | 47 | mapSize = 8 48 | 49 | viewFrog : Float -> Time -> Frog -> [Form] 50 | viewFrog tileSize time frog = 51 | let newWorldPosition = frog.leaf.position |> toWorld tileSize 52 | worldPosition = case frog.lastMove of 53 | Just { oldValue, startTime } -> 54 | let oldWorldPosition = oldValue.position |> toWorld tileSize 55 | in ease easeInOutQuint (pair float) oldWorldPosition newWorldPosition moveDuration (time - startTime) 56 | Nothing -> newWorldPosition 57 | lastLeaf = case frog.lastMove of 58 | Nothing -> [] 59 | Just { oldValue, startTime } -> 60 | let alphaValue = ease easeInCubic float 1 0 moveDuration (time - startTime) 61 | in [viewLeaf tileSize oldValue |> alpha alphaValue] 62 | size = case frog.lastMove of 63 | Nothing -> 1 64 | Just { startTime } -> ease (easeInQuad |> retour) float 1 1.2 moveDuration (time - startTime) 65 | frogSprite = sprite worldPosition tileSize (imagePath "frog.png") |> rotate (angleOf frog |> toFloat |> degrees) |> scale size 66 | in lastLeaf ++ [frogSprite] 67 | 68 | moveDuration : Time 69 | moveDuration = 250 * millisecond 70 | 71 | sprite : (Float, Float) -> Float -> String -> Form 72 | sprite = customSprite identity 73 | 74 | customSprite : (Element -> Element) -> (Float, Float) -> Float -> String -> Form 75 | customSprite transform worldPosition tileSize url = 76 | let element = image (round tileSize) (round tileSize) url 77 | in element |> transform |> makeForm worldPosition 78 | 79 | makeForm : (Float, Float) -> Element -> Form 80 | makeForm worldPosition element = element |> toForm |> move worldPosition 81 | 82 | toWorld : Float -> Grid.Position -> (Float, Float) 83 | toWorld tileSize position = 84 | let transform coordinate = ((coordinate |> toFloat) - mapSize / 2 + 0.5) * tileSize 85 | in (transform position.x, -(transform position.y)) 86 | 87 | viewLeaf : Float -> Leaf -> Form 88 | viewLeaf tileSize leaf = 89 | let worldPosition = leaf.position |> toWorld tileSize 90 | in sprite worldPosition tileSize (imagePath "leaf.png") 91 | 92 | viewTargets : Frog -> Float -> [Leaf] -> [Form] 93 | viewTargets frog tileSize leaves = 94 | let targets = leaves |> filter (reachableBy frog) 95 | toClickable target = clickable moveTo.handle (MoveTo target) 96 | distanceOf target = (distance target.position.x frog.leaf.position.x) + (distance target.position.y frog.leaf.position.y) 97 | angleOf target = frog.leaf `angleBetween` target |> getOrElse 0 98 | filename target = "arrows/" ++ (distanceOf target |> show) ++ "/" ++ (angleOf target |> show) ++ ".svg" |> imagePath 99 | worldPosition target = target.position |> toWorld tileSize 100 | viewTarget target = customSprite (toClickable target) (worldPosition target) tileSize (filename target) 101 | in targets |> map viewTarget 102 | 103 | viewLevelNumber : String -> Float -> Int -> [Form] 104 | viewLevelNumber fontName tileSize levelNumber = 105 | let position = (getLevel levelNumber) |> .levelPosition 106 | worldPosition = position |> toWorld tileSize 107 | background = sprite worldPosition tileSize (imagePath "level.png") 108 | indicator = textSprite fontName position tileSize ("Level\n" ++ show levelNumber ++ "/" ++ show (numberOfLevels - 1)) |> rotate (-1 |> degrees) 109 | in [background, indicator] 110 | 111 | textSprite : String -> Grid.Position -> Float -> String -> Form 112 | textSprite fontName position tileSize string = 113 | let textSize = tileSize / 5 114 | worldPosition = position |> toWorld tileSize 115 | in gameText fontName textSize string |> makeForm worldPosition 116 | 117 | gameText : String -> Float -> String -> Element 118 | gameText fontName height string = toText string |> Text.style { 119 | typeface = [fontName], 120 | height = Just height, 121 | color = red, 122 | bold = True, 123 | italic = False, 124 | line = Nothing 125 | } |> centered 126 | 127 | viewMessage : String -> Float -> Time -> Scene -> [Form] 128 | viewMessage fontName tileSize time scene = 129 | let inverseAngle = ((angleOf scene.frog + 180) % 360) |> toFloat 130 | deltaX = cos (inverseAngle |> degrees) |> round 131 | deltaY = (-1 * sin (inverseAngle |> degrees)) |> round 132 | position = scene.frog.leaf.position `translate` { x = deltaX, y = deltaY } 133 | worldPosition = toWorld tileSize position 134 | filename = "message/" ++ (inverseAngle |> show) ++ ".svg" |> imagePath 135 | background = sprite worldPosition tileSize filename 136 | iconSize = tileSize / 2.5 137 | forms = 138 | if | scene |> levelCompleted -> 139 | let lastLevel = scene.levelNumber == numberOfLevels - 1 140 | in if lastLevel then 141 | [background, sprite worldPosition iconSize (imagePath "completed.svg")] 142 | else 143 | [background, sprite worldPosition iconSize (imagePath "next.png")] 144 | | scene |> stuck -> [background, sprite worldPosition iconSize (imagePath "restart.png")] 145 | | otherwise -> [] 146 | scaleFactor = case scene.frog.lastMove of 147 | Just { startTime } -> ease easeInOutBack float 0 1 (moveDuration * 2) (time - startTime) 148 | Nothing -> 1 149 | in forms |> map (scale scaleFactor) 150 | 151 | imagePath : String -> String 152 | imagePath filename = "images/" ++ filename 153 | 154 | viewKeyboardHint : String -> Int -> Time -> Game -> [Form] 155 | viewKeyboardHint fontName viewSize time game = 156 | if game.usingKeyboard then 157 | let tileSize = viewSize |> getTileSize 158 | gridPosition = game.scene.levelNumber |> getLevel |> .keyboardHintPosition 159 | worldPosition = gridPosition |> toWorld tileSize 160 | background = sprite worldPosition tileSize (imagePath "key.svg") 161 | text string = string |> textSprite fontName gridPosition tileSize 162 | forms = if | (game.scene |> levelCompleted) && (game.scene.levelNumber == 0) -> [background, text "Enter"] 163 | | (game.scene |> stuck) && not (game.scene |> levelCompleted) -> [background, text "Esc"] 164 | | (game.scene |> onlyDoubleJump) && (game.scene.levelNumber == 0) -> [background, text "Shift"] 165 | | otherwise -> [] 166 | scaleFactor = case game.scene.frog.lastMove of 167 | Just { startTime } -> ease easeInOutQuad float 0 1 moveDuration (time - startTime) 168 | Nothing -> 1 169 | in forms |> map (scale scaleFactor) 170 | else [] 171 | 172 | viewCover : (Int, Int) -> Time -> TransitionInfo (Maybe Scene) -> Element 173 | viewCover (windowWidth, windowHeight) time lastSceneChange = 174 | let easingFunction = if lastSceneChange.oldValue |> Maybe.isJust then retour Easing.linear else Easing.flip Easing.linear 175 | factor = ease easingFunction float 0 1 sceneChangeDuration (time - lastSceneChange.startTime) 176 | in spacer windowWidth windowHeight |> Element.color black |> opacity factor 177 | 178 | sceneChangeDuration : Time 179 | sceneChangeDuration = 300 * millisecond -------------------------------------------------------------------------------- /src/site/images/key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 42 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | computer 52 | keyboard 53 | key 54 | 55 | 56 | 57 | 59 | Open Clip Art Library 60 | 61 | 62 | 63 | 64 | Yemu 65 | 66 | 67 | 68 | 69 | Yemu 70 | 71 | 72 | 73 | image/svg+xml 74 | 76 | 78 | en 79 | 80 | 82 | 84 | 86 | 88 | 89 | 90 | 91 | 93 | 100 | 102 | 106 | 110 | 111 | 113 | 117 | 121 | 122 | 131 | 140 | 149 | 152 | 161 | 170 | 179 | 188 | 191 | 202 | 213 | 223 | 234 | 245 | 255 | 266 | 277 | 287 | 288 | 291 | 295 | 299 | 300 | 301 | -------------------------------------------------------------------------------- /src/site/images/arrows/2/180.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 24 | 26 | Arrow icon set 27 | icons for toolbar buttons 28 | 29 | 30 | icon 31 | arrow 32 | navigation 33 | green 34 | red 35 | button 36 | 37 | 38 | 39 | 41 | Open Clip Art Library 42 | 43 | 44 | 45 | 46 | Jakub Jankiewicz 47 | 48 | 49 | 50 | 51 | Jakub Jankiewicz 52 | 53 | 54 | 55 | image/svg+xml 56 | 58 | 60 | pl 61 | 62 | 64 | 66 | 68 | 70 | 71 | 72 | 73 | 75 | 82 | 92 | 102 | 112 | 122 | 132 | 142 | 152 | 162 | 172 | 182 | 192 | 202 | 212 | 222 | 232 | 242 | 252 | 262 | 272 | 282 | 292 | 302 | 312 | 322 | 332 | 342 | 352 | 362 | 372 | 382 | 392 | 402 | 412 | 422 | 424 | 428 | 432 | 433 | 443 | 445 | 449 | 453 | 454 | 464 | 474 | 484 | 494 | 496 | 500 | 504 | 505 | 515 | 525 | 535 | 545 | 547 | 551 | 555 | 556 | 566 | 567 | 585 | 587 | 588 | 590 | image/svg+xml 591 | 593 | 594 | 595 | 596 | 601 | 605 | 609 | 613 | 614 | 615 | 616 | -------------------------------------------------------------------------------- /src/site/images/arrows/1/180.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 24 | 26 | 27 | icons for toolbar buttons 28 | 29 | 30 | icon 31 | arrow 32 | navigation 33 | green 34 | red 35 | button 36 | 37 | 38 | 39 | 41 | Open Clip Art Library 42 | 43 | 44 | 45 | 46 | Jakub Jankiewicz 47 | 48 | 49 | 50 | 51 | Jakub Jankiewicz 52 | 53 | 54 | 55 | image/svg+xml 56 | 58 | 60 | pl 61 | 62 | 64 | 66 | 68 | 70 | 71 | 72 | 73 | 75 | 82 | 92 | 102 | 112 | 122 | 132 | 142 | 152 | 162 | 172 | 182 | 192 | 202 | 212 | 222 | 232 | 242 | 252 | 262 | 272 | 282 | 292 | 302 | 312 | 322 | 332 | 342 | 352 | 362 | 372 | 382 | 392 | 402 | 412 | 422 | 424 | 428 | 432 | 433 | 443 | 445 | 449 | 453 | 454 | 464 | 474 | 484 | 494 | 496 | 500 | 504 | 505 | 515 | 525 | 535 | 545 | 547 | 551 | 555 | 556 | 566 | 576 | 586 | 596 | 606 | 607 | 625 | 627 | 628 | 630 | image/svg+xml 631 | 633 | 634 | 635 | 636 | 641 | 645 | 646 | 647 | -------------------------------------------------------------------------------- /src/site/images/arrows/2/0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 26 | 27 | 29 | Arrow icon set 30 | icons for toolbar buttons 31 | 32 | 33 | icon 34 | arrow 35 | navigation 36 | green 37 | red 38 | button 39 | 40 | 41 | 42 | 44 | Open Clip Art Library 45 | 46 | 47 | 48 | 49 | Jakub Jankiewicz 50 | 51 | 52 | 53 | 54 | Jakub Jankiewicz 55 | 56 | 57 | 58 | image/svg+xml 59 | 61 | 63 | pl 64 | 65 | 67 | 69 | 71 | 73 | 74 | 75 | 76 | 78 | 85 | 95 | 105 | 115 | 125 | 135 | 145 | 155 | 165 | 175 | 185 | 195 | 205 | 215 | 225 | 235 | 245 | 255 | 265 | 275 | 285 | 295 | 305 | 315 | 325 | 335 | 345 | 355 | 365 | 375 | 385 | 395 | 405 | 415 | 425 | 427 | 431 | 435 | 436 | 446 | 448 | 452 | 456 | 457 | 467 | 477 | 487 | 497 | 499 | 503 | 507 | 508 | 518 | 528 | 538 | 548 | 550 | 554 | 558 | 559 | 569 | 579 | 589 | 599 | 609 | 619 | 629 | 639 | 649 | 650 | 668 | 670 | 671 | 673 | image/svg+xml 674 | 676 | 677 | 678 | 679 | 684 | 688 | 692 | 696 | 697 | 698 | 699 | -------------------------------------------------------------------------------- /src/site/images/arrows/1/0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 26 | 27 | 29 | 30 | icons for toolbar buttons 31 | 32 | 33 | icon 34 | arrow 35 | navigation 36 | green 37 | red 38 | button 39 | 40 | 41 | 42 | 44 | Open Clip Art Library 45 | 46 | 47 | 48 | 49 | Jakub Jankiewicz 50 | 51 | 52 | 53 | 54 | Jakub Jankiewicz 55 | 56 | 57 | 58 | image/svg+xml 59 | 61 | 63 | pl 64 | 65 | 67 | 69 | 71 | 73 | 74 | 75 | 76 | 78 | 85 | 95 | 105 | 115 | 125 | 135 | 145 | 155 | 165 | 175 | 185 | 195 | 205 | 215 | 225 | 235 | 245 | 255 | 265 | 275 | 285 | 295 | 305 | 315 | 325 | 335 | 345 | 355 | 365 | 375 | 385 | 395 | 405 | 415 | 425 | 427 | 431 | 435 | 436 | 446 | 448 | 452 | 456 | 457 | 467 | 477 | 487 | 497 | 499 | 503 | 507 | 508 | 518 | 528 | 538 | 548 | 550 | 554 | 558 | 559 | 569 | 579 | 589 | 599 | 609 | 619 | 629 | 639 | 649 | 659 | 669 | 670 | 688 | 690 | 691 | 693 | image/svg+xml 694 | 696 | 697 | 698 | 699 | 704 | 708 | 709 | 710 | -------------------------------------------------------------------------------- /src/site/images/arrows/2/90.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 26 | 27 | 29 | Arrow icon set 30 | icons for toolbar buttons 31 | 32 | 33 | icon 34 | arrow 35 | navigation 36 | green 37 | red 38 | button 39 | 40 | 41 | 42 | 44 | Open Clip Art Library 45 | 46 | 47 | 48 | 49 | Jakub Jankiewicz 50 | 51 | 52 | 53 | 54 | Jakub Jankiewicz 55 | 56 | 57 | 58 | image/svg+xml 59 | 61 | 63 | pl 64 | 65 | 67 | 69 | 71 | 73 | 74 | 75 | 76 | 78 | 85 | 95 | 105 | 115 | 125 | 135 | 145 | 155 | 165 | 175 | 185 | 195 | 205 | 215 | 225 | 235 | 245 | 255 | 265 | 275 | 285 | 295 | 305 | 315 | 325 | 335 | 345 | 355 | 365 | 375 | 385 | 395 | 405 | 415 | 425 | 427 | 431 | 435 | 436 | 446 | 448 | 452 | 456 | 457 | 467 | 477 | 487 | 497 | 499 | 503 | 507 | 508 | 518 | 528 | 538 | 548 | 550 | 554 | 558 | 559 | 569 | 579 | 589 | 599 | 609 | 619 | 629 | 639 | 649 | 659 | 669 | 670 | 688 | 690 | 691 | 693 | image/svg+xml 694 | 696 | 697 | 698 | 699 | 704 | 708 | 712 | 716 | 717 | 718 | 719 | -------------------------------------------------------------------------------- /src/site/images/arrows/2/270.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 26 | 27 | 29 | Arrow icon set 30 | icons for toolbar buttons 31 | 32 | 33 | icon 34 | arrow 35 | navigation 36 | green 37 | red 38 | button 39 | 40 | 41 | 42 | 44 | Open Clip Art Library 45 | 46 | 47 | 48 | 49 | Jakub Jankiewicz 50 | 51 | 52 | 53 | 54 | Jakub Jankiewicz 55 | 56 | 57 | 58 | image/svg+xml 59 | 61 | 63 | pl 64 | 65 | 67 | 69 | 71 | 73 | 74 | 75 | 76 | 78 | 85 | 95 | 105 | 115 | 125 | 135 | 145 | 155 | 165 | 175 | 185 | 195 | 205 | 215 | 225 | 235 | 245 | 255 | 265 | 275 | 285 | 295 | 305 | 315 | 325 | 335 | 345 | 355 | 365 | 375 | 385 | 395 | 405 | 415 | 425 | 427 | 431 | 435 | 436 | 446 | 448 | 452 | 456 | 457 | 467 | 477 | 487 | 497 | 499 | 503 | 507 | 508 | 518 | 528 | 538 | 548 | 550 | 554 | 558 | 559 | 569 | 579 | 589 | 599 | 609 | 619 | 629 | 639 | 649 | 659 | 669 | 670 | 688 | 690 | 691 | 693 | image/svg+xml 694 | 696 | 697 | 698 | 699 | 704 | 708 | 712 | 716 | 717 | 718 | 719 | --------------------------------------------------------------------------------