Listing: one-two-one
Het verradelijk verslavende blok-kantelspel, naar voorbeeld van het oudere Flash-spel Bloxorz (vroeger waren veel online spellen gemaakt in Flash), gemaakt door Wiktor Toporek. De uitgeklede versie hieronder is aangepast zodat het in één geheel in Try Elm kan worden gekopieerd en worden gespeeld.
Packages
Pas op: Omdat dit spel gebruikt maakt van 3D, moet je een aantal "packages" installeren. Dat installeren kan niet in de CompuRob workshop, maar wel bij Try Elm.
Je kunt packages installeren door op "packages" te klikken en in het zoekveld steeds de naam van een package te typen, en dan op het plusje naast de juiste te klikken. De benodigde packages zijn:
- elm/json
- avh4/elm-color
- ianmackenzie/elm-units (je kunt
elm-units
typen, maar pas op dat je de juiste hebt, met ianmackenzie) - ianmackenzie/elm-geometry (je kunt
elm-geometry
typen) - ianmackenzie/elm-3d-scene
- ianmackenzie/elm-3d-camera
Als je het blok wilt bewegen met de pijltjestoetsen op een toetsenbord, dan moet je eerst even in het spel klikken; daarna luistert het spel naar de toetsen.
Challenges (vragen en uitdagingen)
- Kun je de kleur van het blok aanpassen?
- Maak eens wat extra gaten in de vloer van bijvoorbeeld level 1..
- Hoe zou je het blok op een andere tegel kunnen laten vallen in het begin van het level?
- Snap je hoe je een brug kan laten open- of dichtklappen?
- Hoe zou je een nieuwe soort tegel aan de vloeren kunnen toevoegen? In welke gedeeltes van de code moet dan wat veranderd worden?
De code
module Main exposing (main)
import Angle
import Array exposing (Array)
import Axis3d
import Block3d exposing (Block3d)
import Browser
import Browser.Dom
import Browser.Events
import Camera3d
import Color exposing (Color)
import Dict exposing (Dict)
import Direction3d
import Html exposing (Html)
import Html.Attributes as Html
import Html.Events as Event
import Json.Decode as Decode
import Length exposing (Length)
import Pixels
import Point3d
import Scene3d
import Scene3d.Material as Material
import Task
import Vector3d
import Viewpoint3d
-- SETTINGS --
tileSize =
Length.centimeters 1
tileSizeCm =
Length.inCentimeters tileSize
playerWidthCm =
1 * tileSizeCm
playerHeightCm =
2 * tileSizeCm
animationSpeed =
1 / 250
-- THE GAME --
type alias Model =
{ screen : { width : Int, height : Int }
, player : Player
, level : Level
, nextLevels : List Level
, control : Maybe Direction
}
type Direction
= Up
| Right
| Left
| Down
type Msg
= ViewportSize ( Int, Int )
| AnimationTick Float
| KeyDown String
| KeyUp String
init : () -> ( Model, Cmd Msg )
init () =
( { screen = { width = 0, height = 0 }
, player = playerInit (getStartingPosition level1)
, level = level1
, nextLevels = List.drop 1 levels
, control = Nothing
}
, Browser.Dom.getViewport
|> Task.perform (\{ viewport } -> ViewportSize ( floor viewport.width, floor viewport.height ))
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ViewportSize ( width, height ) ->
( { model | screen = { width = width, height = height } }, Cmd.none )
AnimationTick delta ->
let
controlledPlayer =
controlPlayer model.control model.player
( animatedPlayer, playerCmd ) =
controlledPlayer
|> playerUpdate delta model.level
( updatedPlayer, interactionMsg ) =
animatedPlayer
|> interact model.level
updatedLevel =
model.level
|> levelUpdate delta
in
case interactionMsg of
InternalUpdate ->
( { model | player = updatedPlayer, level = updatedLevel }, playerCmd )
FinishedLevel ->
case model.nextLevels of
[] ->
( { model
| player = playerInit (getStartingPosition level1)
, level = level1
, nextLevels = List.drop 1 levels
}
, playerCmd
)
nextLevel :: otherLevels ->
( { model
| player = playerInit (getStartingPosition nextLevel)
, level = nextLevel
, nextLevels = otherLevels
}
, playerCmd
)
PushDownTile zOffset ->
( { model
| player = updatedPlayer
, level = shiftTile (getPosition updatedPlayer) zOffset model.level
}
, playerCmd
)
TriggerActions actions ->
let
previousInteractionMsg =
model.player
|> interact model.level
|> Tuple.second
( actionUpdatedLevel, actionCommand ) =
case previousInteractionMsg of
TriggerActions _ ->
( updatedLevel, playerCmd )
_ ->
-- Trigger only if not trigged in last update already
triggerActions actions updatedLevel
in
( { model
| player = updatedPlayer
, level = actionUpdatedLevel
}
, actionCommand
)
RestartedLevel ->
( { model
| player = updatedPlayer
, level = restart model.level
}
, playerCmd
)
_ ->
( { model
| player = updatedPlayer
, level = updatedLevel
}
, playerCmd
)
KeyDown key ->
( key
|> keyToDirection
|> Maybe.map (\direction -> { model | control = Just direction })
|> Maybe.withDefault model
, Cmd.none
)
KeyUp key ->
( if keyToDirection key == model.control then
{ model | control = Nothing }
else
model
, Cmd.none
)
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ case ( model.player, model.control, levelIsBusy model.level ) of
( Cuboid Standing _, Nothing, False ) ->
Sub.none
( Cuboid (Lying _) _, Nothing, False ) ->
Sub.none
_ ->
Browser.Events.onAnimationFrameDelta AnimationTick
, Browser.Events.onKeyDown (Decode.map KeyDown keyDecoder)
, Browser.Events.onKeyUp (Decode.map KeyUp keyDecoder)
]
keyDecoder : Decode.Decoder String
keyDecoder =
Decode.field "key" Decode.string
keyToDirection : String -> Maybe Direction
keyToDirection string =
case string of
"ArrowLeft" ->
Just Left
"ArrowRight" ->
Just Right
"ArrowUp" ->
Just Up
"ArrowDown" ->
Just Down
_ ->
Nothing
controlPlayer : Maybe Direction -> Player -> Player
controlPlayer control player =
case control of
Just direction ->
move direction player
Nothing ->
player
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
view : Model -> Html Msg
view { screen, player, level, control } =
let
zoomOut =
max (800 / toFloat screen.width) 1
camera =
Camera3d.perspective
{ viewpoint =
Viewpoint3d.orbitZ
{ focalPoint = Point3d.centimeters 5 5 0
, azimuth = Angle.degrees 35
, elevation = Angle.degrees 25
, distance = Length.centimeters (15 * zoomOut)
}
, verticalFieldOfView = Angle.degrees 30
}
info =
String.join "; "
[ playerInfo, controlInfo, levelInfo ]
playerInfo =
"player = "
++ (case player of
Cuboid Standing _ ->
"standing"
Cuboid (Lying _) _ ->
"lying"
Cuboid (KnockingOver _ _) _ ->
"being knocked over"
Cuboid (GettingUp _ _) _ ->
"getting up"
Cuboid (Rolling _ _) _ ->
"rolling"
Cuboid (SlideIn _) _ ->
"sliding in"
Cuboid (FallingUnbalanced _ _) _ ->
"falling unbalanced"
Cuboid (FallingInHorizontalOrientation _ _) _ ->
"falling horizontally"
Cuboid (FallingInVerticalOrientation _) _ ->
"falling vertically"
Cuboid (FallingFromTheSky _) _ ->
"falling from the sky"
Cuboid (FallingWithTheFloor _) _ ->
"falling with the floor"
)
controlInfo =
"control = "
++ (case control of
Nothing ->
"stay"
Just Left ->
"left"
Just Right ->
"right"
Just Up ->
"up"
Just Down ->
"down"
)
levelInfo =
"level = "
++ (if levelIsBusy level then
"in action"
else
"idle"
)
in
Html.div []
[ Html.div
[ Html.style "position" "absolute"
, Html.style "top" "0"
, Html.style "left" "0"
, Html.style "right" "0"
, Html.style "font-size" "25px"
, Html.style "text-align" "center"
, Html.style "white-space" "pre"
]
[ Html.text info ]
, Scene3d.sunny
{ entities = [ playerView player, levelView level ]
, camera = camera
, upDirection = Direction3d.z
, sunlightDirection = Direction3d.xz (Angle.degrees -120)
, background = Scene3d.transparentBackground
, clipDepth = Length.centimeters 1
, shadows = True
, dimensions = ( Pixels.int screen.width, Pixels.int screen.height )
}
, mobileControls player
]
onTouchStart msg =
Event.on "touchstart" (Decode.succeed msg)
onTouchEnd msg =
Event.on "touchend" (Decode.succeed msg)
mobileControls : Player -> Html Msg
mobileControls player =
Html.div
[ Html.style "position" "absolute"
, Html.style "right" "0"
, Html.style "bottom" "0"
, Html.style "width" "30vw"
, Html.style "height" "30vw"
, Html.style "font-size" "10vw"
]
[ Html.div
[ onTouchStart (KeyDown "ArrowUp")
, onTouchEnd (KeyUp "ArrowUp")
, Html.style "position" "absolute"
, Html.style "top" "0"
, Html.style "left" "10vw"
]
[ Html.text "⬆️️" ]
, Html.div
[ onTouchStart (KeyDown "ArrowLeft")
, onTouchEnd (KeyUp "ArrowLeft")
, Html.style "position" "absolute"
, Html.style "top" "9vw"
, Html.style "left" "0"
]
[ Html.text "⬅️" ]
, Html.div
[ onTouchStart (KeyDown "ArrowRight")
, onTouchEnd (KeyUp "ArrowRight")
, Html.style "position" "absolute"
, Html.style "top" "9vw"
, Html.style "right" "0"
]
[ Html.text "➡️️" ]
, Html.div
[ onTouchStart (KeyDown "ArrowDown")
, onTouchEnd (KeyUp "ArrowDown")
, Html.style "position" "absolute"
, Html.style "bottom" "0"
, Html.style "left" "10vw"
]
[ Html.text "⬇️️️" ]
]
-- PLAYER --
type Player
= Cuboid BlockAnimationState ( Int, Int )
type BlockAnimationState
= Standing
| Lying Direction
| KnockingOver Direction Float
| GettingUp Direction Float
| Rolling Direction Float
| SlideIn Float
| FallingUnbalanced Direction Float
| FallingInHorizontalOrientation Direction Float
| FallingInVerticalOrientation { zOffset : Length, progress : Float }
| FallingFromTheSky Float
| FallingWithTheFloor Float
type Cube
= Cube CubeAnimationState ( Int, Int )
type CubeAnimationState
= Stable
| Rotating Direction Float
| Falling Float
type InteractionMsg
= InternalUpdate
| FinishedLevel
| PushDownTile Length
| RestartedLevel
| TriggerActions (List TriggerAction)
| EmitSound String
playerInit : ( Int, Int ) -> Player
playerInit ( x, y ) =
Cuboid (FallingFromTheSky 0) ( x, y )
move : Direction -> Player -> Player
move direction player =
case player of
Cuboid orientation ( x, y ) ->
case ( orientation, direction ) of
( Standing, fallDirection ) ->
Cuboid (KnockingOver fallDirection 0) ( x, y )
-- Lying Up
( Lying Up, Left ) ->
Cuboid (Rolling Left 0) ( x, y )
( Lying Up, Right ) ->
Cuboid (Rolling Right 0) ( x, y )
( Lying Up, Up ) ->
Cuboid (GettingUp Up 0) ( x - 2, y )
( Lying Up, Down ) ->
Cuboid (GettingUp Down 0) ( x + 1, y )
-- Lying Right
( Lying Right, Up ) ->
Cuboid (Rolling Up 0) ( x, y )
( Lying Right, Down ) ->
Cuboid (Rolling Down 0) ( x, y )
( Lying Right, Left ) ->
Cuboid (GettingUp Left 0) ( x, y - 1 )
( Lying Right, Right ) ->
Cuboid (GettingUp Right 0) ( x, y + 2 )
-- Lying Down
( Lying Down, Up ) ->
Cuboid (GettingUp Up 0) ( x - 1, y )
( Lying Down, Down ) ->
Cuboid (GettingUp Down 0) ( x + 2, y )
( Lying Down, Left ) ->
Cuboid (Rolling Left 0) ( x + 1, y )
( Lying Down, Right ) ->
Cuboid (Rolling Right 0) ( x + 1, y )
-- Lying Left
( Lying Left, Up ) ->
Cuboid (Rolling Up 0) ( x, y - 1 )
( Lying Left, Down ) ->
Cuboid (Rolling Down 0) ( x, y - 1 )
( Lying Left, Left ) ->
Cuboid (GettingUp Left 0) ( x, y - 2 )
( Lying Left, Right ) ->
Cuboid (GettingUp Right 0) ( x, y + 1 )
-- Player already in motion (ignore)
_ ->
Cuboid orientation ( x, y )
playerView : Player -> Scene3d.Entity coordinates
playerView player =
player
|> playerBlock
|> Scene3d.blockWithShadow (Material.metal { baseColor = Color.orange, roughness = 2.5 })
playerBlock : Player -> Block3d Length.Meters coordinates
playerBlock player =
case player of
Cuboid orientation ( x, y ) ->
let
positionX =
toFloat x * tileSizeCm
positionY =
toFloat y * tileSizeCm
block =
Block3d.with
{ x1 = Length.centimeters 0
, x2 = Length.centimeters playerWidthCm
, y1 = Length.centimeters 0
, y2 = Length.centimeters playerWidthCm
, z1 = Length.centimeters 0
, z2 = Length.centimeters playerHeightCm
}
in
(case orientation of
-- +
Standing ->
block
-- |
-- 0
Lying Up ->
block
|> Block3d.rotateAround topAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.x tileSize
-- 0-
Lying Right ->
block
|> Block3d.rotateAround rightAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.negativeY tileSize
-- 0
-- |
Lying Down ->
block
|> Block3d.rotateAround bottomAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.negativeX tileSize
-- -0
Lying Left ->
block
|> Block3d.rotateAround leftAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.y tileSize
KnockingOver Up progress ->
block
|> Block3d.rotateAround topAxis (Angle.degrees (progress * 90))
KnockingOver Right progress ->
block
|> Block3d.rotateAround rightAxis (Angle.degrees (progress * 90))
KnockingOver Down progress ->
block
|> Block3d.rotateAround bottomAxis (Angle.degrees (progress * 90))
KnockingOver Left progress ->
block
|> Block3d.rotateAround leftAxis (Angle.degrees (progress * 90))
GettingUp Up progress ->
block
|> Block3d.rotateAround bottomAxis (Angle.degrees ((1 - progress) * 90))
GettingUp Right progress ->
block
|> Block3d.rotateAround leftAxis (Angle.degrees ((1 - progress) * 90))
GettingUp Down progress ->
block
|> Block3d.rotateAround topAxis (Angle.degrees ((1 - progress) * 90))
GettingUp Left progress ->
block
|> Block3d.rotateAround rightAxis (Angle.degrees ((1 - progress) * 90))
Rolling Left progress ->
block
|> Block3d.rotateAround topAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.x tileSize
|> Block3d.rotateAround leftAxis (Angle.degrees (progress * 90))
Rolling Right progress ->
block
|> Block3d.rotateAround topAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.x tileSize
|> Block3d.rotateAround rightAxis (Angle.degrees (progress * 90))
Rolling Up progress ->
block
|> Block3d.rotateAround rightAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.negativeY tileSize
|> Block3d.rotateAround topAxis (Angle.degrees (progress * 90))
Rolling Down progress ->
block
|> Block3d.rotateAround rightAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.negativeY tileSize
|> Block3d.rotateAround bottomAxis (Angle.degrees (progress * 90))
SlideIn progress ->
Block3d.with
{ x1 = Length.centimeters 0
, x2 = Length.centimeters playerWidthCm
, y1 = Length.centimeters 0
, y2 = Length.centimeters playerWidthCm
, z1 = Length.centimeters 0
, z2 = Length.centimeters (playerHeightCm - (progress * progress * 2))
}
FallingUnbalanced Left progress ->
block
|> Block3d.rotateAround leftAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.y tileSize
|> Block3d.rotateAround leftAxis (Angle.degrees (90 * progress))
FallingUnbalanced Right progress ->
block
|> Block3d.rotateAround rightAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.negativeY tileSize
|> Block3d.rotateAround rightAxis (Angle.degrees (90 * progress))
FallingUnbalanced Up progress ->
block
|> Block3d.rotateAround topAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.x tileSize
|> Block3d.rotateAround topAxis (Angle.degrees (90 * progress))
FallingUnbalanced Down progress ->
block
|> Block3d.rotateAround bottomAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.negativeX tileSize
|> Block3d.rotateAround bottomAxis (Angle.degrees (90 * progress))
FallingInVerticalOrientation { zOffset, progress } ->
block
|> Block3d.translateIn Direction3d.negativeZ zOffset
|> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)
FallingInHorizontalOrientation Left progress ->
block
|> Block3d.rotateAround leftAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.y tileSize
|> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)
FallingInHorizontalOrientation Up progress ->
block
|> Block3d.rotateAround topAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.x tileSize
|> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)
FallingInHorizontalOrientation Right progress ->
block
|> Block3d.rotateAround rightAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.negativeY tileSize
|> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)
FallingInHorizontalOrientation Down progress ->
block
|> Block3d.rotateAround bottomAxis (Angle.degrees 90)
|> Block3d.translateIn Direction3d.negativeX tileSize
|> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)
FallingFromTheSky progress ->
block
|> Block3d.translateIn Direction3d.z (Length.centimeters ((1 - progress) * 10))
FallingWithTheFloor progress ->
block
|> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)
)
|> Block3d.translateBy
(Vector3d.centimeters positionX positionY 0)
topAxis =
Axis3d.through (Point3d.centimeters 0 0 0) Direction3d.negativeY
rightAxis =
Axis3d.through (Point3d.centimeters 0 playerWidthCm 0) Direction3d.negativeX
bottomAxis =
Axis3d.through (Point3d.centimeters playerWidthCm 0 0) Direction3d.y
leftAxis =
Axis3d.through (Point3d.centimeters 0 0 0) Direction3d.x
playerUpdate : Float -> Level -> Player -> ( Player, Cmd msg )
playerUpdate delta level player =
case player of
Cuboid (KnockingOver direction progress) ( x, y ) ->
let
newProgress =
progress + delta * animationSpeed
newPlayer =
if newProgress >= 1 then
Cuboid
(Lying direction)
(case direction of
Up ->
( x - 1, y )
Right ->
( x, y + 1 )
Left ->
( x, y - 1 )
Down ->
( x + 1, y )
)
else
Cuboid (KnockingOver direction newProgress) ( x, y )
in
( newPlayer, Cmd.none )
Cuboid (GettingUp direction progress) ( x, y ) ->
let
newProgress =
progress + delta * animationSpeed
newPlayer =
if newProgress >= 1 then
Cuboid Standing ( x, y )
else
Cuboid (GettingUp direction newProgress) ( x, y )
in
( newPlayer, Cmd.none )
Cuboid (Rolling direction progress) ( x, y ) ->
let
newProgress =
progress + delta * animationSpeed
newPlayer =
if newProgress >= 1 then
case direction of
Left ->
Cuboid (Lying Up) ( x, y - 1 )
Right ->
Cuboid (Lying Up) ( x, y + 1 )
Up ->
Cuboid (Lying Right) ( x - 1, y )
Down ->
Cuboid (Lying Right) ( x + 1, y )
else
Cuboid (Rolling direction newProgress) ( x, y )
in
( newPlayer, Cmd.none )
Cuboid (SlideIn progress) ( x, y ) ->
let
newProgress =
min (progress + delta * animationSpeed * 0.5) 1
in
( Cuboid (SlideIn newProgress) ( x, y ), Cmd.none )
Cuboid (FallingUnbalanced direction progress) ( x, y ) ->
let
newProgress =
min (progress + delta * animationSpeed) 1
in
if newProgress == 1 then
( Cuboid (FallingInVerticalOrientation { zOffset = Length.centimeters (0.5 * playerHeightCm), progress = 0 })
(case direction of
Left ->
( x, y - 1 )
Right ->
( x, y + 1 )
Down ->
( x + 1, y )
Up ->
( x - 1, y )
)
, Cmd.none
)
else
( Cuboid (FallingUnbalanced direction newProgress) ( x, y ), Cmd.none )
Cuboid (FallingInVerticalOrientation { zOffset, progress }) ( x, y ) ->
let
newProgress =
progress + delta * animationSpeed * min (progress + 1) 5
in
( Cuboid (FallingInVerticalOrientation { zOffset = zOffset, progress = newProgress }) ( x, y ), Cmd.none )
Cuboid (FallingInHorizontalOrientation direction progress) ( x, y ) ->
let
newProgress =
progress + delta * animationSpeed * min (progress + 1) 5
in
( Cuboid (FallingInHorizontalOrientation direction newProgress) ( x, y ), Cmd.none )
Cuboid (FallingFromTheSky progress) ( x, y ) ->
let
newProgress =
min (progress + delta * animationSpeed * 0.3) 1
in
if newProgress == 1 then
( Cuboid Standing ( x, y ), Cmd.none )
else
( Cuboid (FallingFromTheSky newProgress) ( x, y ), Cmd.none )
Cuboid (FallingWithTheFloor progress) ( x, y ) ->
let
newProgress =
progress + delta * animationSpeed * min (progress + 1) 5
in
( Cuboid (FallingWithTheFloor newProgress) ( x, y ), Cmd.none )
plr ->
( plr, Cmd.none )
getPosition : Player -> ( Int, Int )
getPosition (Cuboid _ position) =
position
fall : Maybe Direction -> Player -> Player
fall unbalancedDirection player =
case player of
Cuboid state ( x, y ) ->
case ( unbalancedDirection, state ) of
( _, Standing ) ->
Cuboid (FallingInVerticalOrientation { zOffset = Length.centimeters 0, progress = 0 }) ( x, y )
( Just Left, Lying Left ) ->
Cuboid (FallingUnbalanced Left 0) ( x, y )
( Just Left, Lying Right ) ->
Cuboid (FallingUnbalanced Left 0) ( x, y + 1 )
( Just Right, Lying Left ) ->
Cuboid (FallingUnbalanced Right 0) ( x, y + 1 )
( Just Right, Lying Right ) ->
Cuboid (FallingUnbalanced Right 0) ( x, y )
( Just Up, Lying Up ) ->
Cuboid (FallingUnbalanced Up 0) ( x, y )
( Just Up, Lying Down ) ->
Cuboid (FallingUnbalanced Up 0) ( x + 1, y )
( Just Down, Lying Up ) ->
Cuboid (FallingUnbalanced Down 0) ( x - 1, y )
( Just Down, Lying Down ) ->
Cuboid (FallingUnbalanced Down 0) ( x, y )
( Nothing, Lying direction ) ->
Cuboid (FallingInHorizontalOrientation direction 0) ( x, y )
_ ->
Cuboid state ( x, y )
occupiedPositions : Player -> List ( Int, Int )
occupiedPositions player =
case player of
Cuboid Standing ( x, y ) ->
[ ( x, y ) ]
Cuboid (Lying Left) ( x, y ) ->
[ ( x, y - 1 ), ( x, y ) ]
Cuboid (Lying Up) ( x, y ) ->
[ ( x - 1, y ), ( x, y ) ]
Cuboid (Lying Right) ( x, y ) ->
[ ( x, y ), ( x, y + 1 ) ]
Cuboid (Lying Down) ( x, y ) ->
[ ( x, y ), ( x + 1, y ) ]
_ ->
[]
occupiedTiles : Player -> Level -> List LevelTile
occupiedTiles player level =
player
|> occupiedPositions
|> List.map (getTileAt level)
unstablePosition : Player -> Level -> Bool
unstablePosition player level =
occupiedTiles player level
|> List.any ((==) Empty)
interact : Level -> Player -> ( Player, InteractionMsg )
interact level player =
let
playerOccupiedTiles =
occupiedTiles player level
in
case player of
Cuboid state ( x, y ) ->
case ( playerOccupiedTiles, state ) of
-- Falling off the stage
( [ Empty ], _ ) ->
( fall Nothing player, EmitSound "fall" )
( [ Empty, Empty ], _ ) ->
( fall Nothing player, EmitSound "fall" )
( [ Empty, _ ], Lying Left ) ->
( fall (Just Left) player, EmitSound "fall" )
( [ Empty, _ ], Lying Right ) ->
( fall (Just Left) player, EmitSound "fall" )
( [ Empty, _ ], Lying Up ) ->
( fall (Just Up) player, EmitSound "fall" )
( [ Empty, _ ], Lying Down ) ->
( fall (Just Up) player, EmitSound "fall" )
( [ _, Empty ], Lying Left ) ->
( fall (Just Right) player, EmitSound "fall" )
( [ _, Empty ], Lying Right ) ->
( fall (Just Right) player, EmitSound "fall" )
( [ _, Empty ], Lying Up ) ->
( fall (Just Down) player, EmitSound "fall" )
( [ _, Empty ], Lying Down ) ->
( fall (Just Down) player, EmitSound "fall" )
-- Stomp on rusty tile
( [ RustyFloor ], Standing ) ->
( Cuboid (FallingWithTheFloor 0) ( x, y ), EmitSound "break-tile" )
( [], FallingWithTheFloor progress ) ->
if progress >= 30 then
( playerInit (getStartingPosition level), RestartedLevel )
else
( player, PushDownTile (Length.centimeters progress) )
-- Success
( [ Finish ], _ ) ->
( Cuboid (SlideIn 0) ( x, y ), EmitSound "slide-in" )
( [], SlideIn progress ) ->
( player
, if progress >= 1 then
FinishedLevel
else
InternalUpdate
)
-- Restart
( [], FallingInHorizontalOrientation _ progress ) ->
if progress >= 30 then
( playerInit (getStartingPosition level), RestartedLevel )
else
( player, InternalUpdate )
( [], FallingInVerticalOrientation { progress } ) ->
if progress >= 30 then
( playerInit (getStartingPosition level), RestartedLevel )
else
( player, InternalUpdate )
-- Trigger activation
( [ Trigger _ actions, _ ], Lying _ ) ->
( player, TriggerActions actions )
( [ _, Trigger _ actions ], Lying _ ) ->
( player, TriggerActions actions )
( [ Trigger _ actions ], Standing ) ->
( player, TriggerActions actions )
-- Nothing to be done
_ ->
( player, InternalUpdate )
-- LEVELS --
levels =
[ level1, level2, level3 ]
level1 =
levelFromData
[ [ Floor, Floor, Floor, Empty, Empty, Empty, Floor, Floor, Floor ]
, [ Floor, Floor, Floor, Floor, Floor, Floor, Floor, Finish, Floor ]
, [ Empty, Empty, Empty, Floor, Floor, Floor, Floor, Floor, Floor ]
, []
]
( 1, 1 )
level2 =
levelFromData
[ [ Floor, Floor, Floor ]
, [ Floor, Floor, Floor, Floor, Floor, Floor ]
, [ Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor ]
, [ Empty, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor ]
, [ Empty, Empty, Empty, Empty, Empty, Floor, Floor, Finish, Floor, Floor ]
, [ Empty, Empty, Empty, Empty, Empty, Empty, Floor, Floor, Floor ]
, []
]
( 1, 1 )
level3 =
levelFromData
[ [ Floor, Floor, Floor, Floor, Empty, Empty, Floor, Floor, Floor ]
, [ Floor
, Floor
, Trigger Color.red
[ ToggleTriggerColor ( 1, 2 ) Color.green
, ToggleBridge ( 3, 4 )
, ToggleBridge ( 3, 5 )
]
, Floor
, Empty
, Empty
, Floor
, Finish
, Floor
]
, [ Floor, Floor, Floor, Floor, Empty, Empty, Floor, Floor, Floor ]
, [ Floor, Floor, Floor, Floor, Bridge Left False, Bridge Right False, Floor, Floor, Floor ]
, []
]
( 3, 1 )
type LevelTile
= Empty
| Floor
| RustyFloor
| Finish
| Bridge Direction Bool
| Trigger Color (List TriggerAction)
type TriggerAction
= ToggleBridge ( Int, Int )
| CloseBridge ( Int, Int )
| OpenBridge ( Int, Int )
| SetTriggerColor ( Int, Int ) Color
| ToggleTriggerColor ( Int, Int ) Color
type TileState
= PushDown Length
| BridgeState Bool Float
| TriggerState Color
type Level
= Level
{ tiles : Array (Array LevelTile)
, tileStates : Dict ( Int, Int ) TileState
, startingPosition : ( Int, Int )
, big : Bool
}
levelFromData : List (List LevelTile) -> ( Int, Int ) -> Level
levelFromData tiles start =
let
width =
List.map List.length tiles |> List.foldl max 0
height =
List.length tiles
in
Level
{ tiles =
tiles
|> List.map Array.fromList
|> Array.fromList
, tileStates = Dict.empty
, startingPosition = start
, big = width >= 15 || height >= 15
}
getStartingPosition : Level -> ( Int, Int )
getStartingPosition (Level { startingPosition }) =
startingPosition
getTileAt : Level -> ( Int, Int ) -> LevelTile
getTileAt ((Level { tileStates }) as level) location =
let
originalTile =
getTileAtInternal level location
in
case originalTile of
Bridge _ initiallyClosed ->
case Dict.get location tileStates of
Just (BridgeState closed _) ->
if closed then
originalTile
else
Empty
_ ->
if initiallyClosed then
originalTile
else
Empty
a ->
a
getTileAtInternal : Level -> ( Int, Int ) -> LevelTile
getTileAtInternal (Level { tiles }) ( x, y ) =
Array.get x tiles
|> Maybe.map (\row -> Array.get y row |> Maybe.withDefault Empty)
|> Maybe.withDefault Empty
shiftTile : ( Int, Int ) -> Length -> Level -> Level
shiftTile location zOffset (Level level) =
Level { level | tileStates = Dict.insert location (PushDown zOffset) level.tileStates }
triggerActions : List TriggerAction -> Level -> ( Level, Cmd a )
triggerActions actions ((Level levelData) as level) =
List.foldl
(\action ( levelAcc, cmdAcc ) ->
(case action of
ToggleBridge ( x, y ) ->
toggleBridge not ( x, y ) levelAcc
CloseBridge ( x, y ) ->
toggleBridge (always True) ( x, y ) levelAcc
OpenBridge ( x, y ) ->
toggleBridge (always False) ( x, y ) levelAcc
SetTriggerColor ( x, y ) newColor ->
( Level { levelData | tileStates = Dict.insert ( x, y ) (TriggerState newColor) levelData.tileStates }
, Cmd.none
)
ToggleTriggerColor ( x, y ) secondColor ->
( Level
{ levelData
| tileStates =
case Dict.get ( x, y ) levelData.tileStates of
Just _ ->
Dict.remove ( x, y ) levelData.tileStates
Nothing ->
Dict.insert ( x, y ) (TriggerState secondColor) levelData.tileStates
}
, Cmd.none
)
)
|> Tuple.mapSecond (\cmd -> Cmd.batch [ cmdAcc, cmd ])
)
( level, Cmd.none )
actions
toggleBridge : (Bool -> Bool) -> ( Int, Int ) -> Level -> ( Level, Cmd a )
toggleBridge mapPreviousState ( x, y ) (Level level) =
case Dict.get ( x, y ) level.tileStates of
Just (BridgeState closed progress) ->
( Level { level | tileStates = Dict.insert ( x, y ) (BridgeState (mapPreviousState closed) progress) level.tileStates }
, Cmd.none
)
_ ->
let
initiallyClosed =
case getTileAtInternal (Level level) ( x, y ) of
Bridge _ initValue ->
initValue
_ ->
False
newClosed =
mapPreviousState initiallyClosed
progress =
if newClosed then
0
else
1
in
( Level { level | tileStates = Dict.insert ( x, y ) (BridgeState newClosed progress) level.tileStates }
, Cmd.none
)
restart : Level -> Level
restart (Level level) =
Level { level | tileStates = Dict.empty }
levelUpdate : Float -> Level -> Level
levelUpdate delta (Level level) =
Level { level | tileStates = Dict.map (always (updateTileState delta)) level.tileStates }
updateTileState : Float -> TileState -> TileState
updateTileState delta tile =
case tile of
BridgeState True progress ->
BridgeState True (min (progress + animationSpeed * delta) 1)
BridgeState False progress ->
BridgeState False (max (progress - animationSpeed * delta) 0)
a ->
a
levelView : Level -> Scene3d.Entity coordinates
levelView (Level { tiles, tileStates }) =
tiles
|> Array.indexedMap
(\x row ->
Array.indexedMap
(\y tile ->
case tile of
Floor ->
floorEntity Color.gray ( x, y )
Empty ->
Scene3d.nothing
Finish ->
floorEntity Color.black ( x, y )
RustyFloor ->
floorEntity Color.lightOrange ( x, y )
|> (case Dict.get ( x, y ) tileStates of
Just (PushDown zOffset) ->
Scene3d.translateIn Direction3d.negativeZ zOffset
_ ->
identity
)
Trigger initialColor _ ->
triggerEntity
(case Dict.get ( x, y ) tileStates of
Just (TriggerState color) ->
color
_ ->
initialColor
)
( x, y )
Bridge openingDirection initiallyClosed ->
floorEntity Color.darkYellow ( x, y )
|> (case
Maybe.withDefault
(BridgeState initiallyClosed
(if initiallyClosed then
1
else
0
)
)
(Dict.get ( x, y ) tileStates)
of
BridgeState _ progress ->
let
axis =
case openingDirection of
Up ->
Axis3d.through (Point3d.centimeters (toFloat x * tileSizeCm) (toFloat y * tileSizeCm) -0.1) Direction3d.negativeY
Right ->
Axis3d.through (Point3d.centimeters (toFloat x * tileSizeCm) (toFloat (y + 1) * tileSizeCm) -0.1) Direction3d.negativeX
Down ->
Axis3d.through (Point3d.centimeters (toFloat (x + 1) * tileSizeCm) (toFloat y * tileSizeCm) -0.1) Direction3d.y
Left ->
Axis3d.through (Point3d.centimeters (toFloat x * tileSizeCm) (toFloat y * tileSizeCm) -0.1) Direction3d.x
in
Scene3d.rotateAround axis (Angle.degrees (-180 * (1 - progress)))
_ ->
identity
)
)
row
|> Array.toList
)
|> Array.toList
|> List.concatMap identity
|> Scene3d.group
floorEntity color ( x, y ) =
let
tileBorderSizeCm =
tileSizeCm * 0.02
in
Scene3d.block
(Material.matte color)
(Block3d.with
{ x1 = Length.centimeters (tileSizeCm * toFloat x + tileBorderSizeCm)
, x2 = Length.centimeters (tileSizeCm * toFloat (x + 1) - tileBorderSizeCm)
, y1 = Length.centimeters (tileSizeCm * toFloat y + tileBorderSizeCm)
, y2 = Length.centimeters (tileSizeCm * toFloat (y + 1) - tileBorderSizeCm)
, z1 = Length.centimeters (tileSizeCm * -0.1)
, z2 = Length.centimeters 0
}
)
triggerEntity color ( x, y ) =
let
buttonPaddingCm =
tileSizeCm * 0.2
in
Scene3d.group
[ floorEntity Color.gray ( x, y )
, Scene3d.block
(Material.matte color)
(Block3d.with
{ x1 = Length.centimeters (tileSizeCm * toFloat x + buttonPaddingCm)
, x2 = Length.centimeters (tileSizeCm * toFloat (x + 1) - buttonPaddingCm)
, y1 = Length.centimeters (tileSizeCm * toFloat y + buttonPaddingCm)
, y2 = Length.centimeters (tileSizeCm * toFloat (y + 1) - buttonPaddingCm)
, z1 = Length.centimeters 0
, z2 = Length.centimeters (tileSizeCm * 0.1)
}
)
]
isBigLevel : Level -> Bool
isBigLevel (Level { big }) =
big
levelIsBusy : Level -> Bool
levelIsBusy (Level { tileStates }) =
tileStates
|> Dict.values
|> List.any
(\state ->
case state of
BridgeState True x ->
x < 1
BridgeState False x ->
x > 0
_ ->
False
)