Week 05 of 2023
Development log of Word Snake
19 items
- Fix: The snake stops before hitting a poo of flame
- Do not render a dead snake, but place it's bones
- Let the CI run tests before publishing the website
- Fix glitchy movement
- Remove unused field from Game.Model record
- The camera will pan more gently inside the firetrap
- A password will be displayed in area as it gets collected
- Implement annotations system
- Stop the world when a player reads instructions
- Let the time slow on slow computers
- Tweak the graceful degradation to 12 FPS
- Factor out complete the sentence logic to own module
- Factor area out of game model into CompleteTheSentence
- Extend the "safe zone" in front of the snake to 5
- Make sure the next required letter is always there
- Add the Taal Cafe levels
- Rename the Game.viewArea function to viewWorld
- Fix Android Chrome glitch + un-group SVG elements
- Remove unnecessary transition animation
Fix: The snake stops before hitting a poo of flame
On by
It used to be that the snake visually stops before the head moves on the killing position. It looked weird. Now the snake moves and then dies as it should.
index b69de26..4c89a42 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -505,12 +505,6 @@ viewSnake progress snake =
= let
= distance : Float
= distance =
- -- FIXME: When the snake hits a poop or fire the movement stops before the head moves on the killing position.
- --
- -- The snake should move and then die. Otherwise it looks weird. But
- -- without the condition below, dead snake keeps twitching. I'm not
- -- yet sure what's the fix. It might be here, or in the step
- -- function, or somewhere lese.
= if Snake.isAlive snake then
= 1 - progress
=
@@ -1128,7 +1122,13 @@ step model =
= model.snake
= |> Snake.setDirection direction
= |> applyIf (Snake.isAlive model.snake) Snake.move
- |> updateSnake entities
+ |> feed
+
+ feed : Snake -> Snake
+ feed feeder =
+ model.entities
+ |> Entities.get model.snake.head
+ |> List.foldl Snake.feed feeder
=
= tiles =
= snake.head
@@ -1308,13 +1308,6 @@ burnTheEdge area burn snake entities =
= entities
=
=
-updateSnake : Entities -> Snake -> Snake
-updateSnake entities snake =
- entities
- |> Entities.get snake.head
- |> List.foldr Snake.feed snake
-
-
=updateEntities : Goal -> Snake -> Entities -> Entities
=updateEntities goal snake entities =
= entitiesDo not render a dead snake, but place it's bones
On by
The result is, that when the snake is dead, it looks like it's made of skull and bones (kind of like before the solid body rework, except the tip of the tail is same size as the rest of the bones). Another difference is that the bones won't disappear when the new game is started. Now they are entities, and can even be collected.
index 4c89a42..1c28ce4 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -502,120 +502,125 @@ viewEntities entities =
=
=viewSnake : Float -> Snake -> Svg Msg
=viewSnake progress snake =
- let
- distance : Float
- distance =
- if Snake.isAlive snake then
- 1 - progress
+ if Snake.isDead snake then
+ Svg.text ""
=
- else
- 1
- in
- -- TODO: Multiple stripes
- -- [ viewStripe 0.8 "#133d1a" points
- -- , viewStripe 0.7 "#106023" points
- -- , viewStripe 0.5 "#1e7d35" points
- -- , viewStripe 0.2 "#5bb171" points
- -- ]
- -- |> Svg.g []
- case snake.body of
- [] ->
- -- Just the head, so no direction.
- Svg.circle
- [ Svg.Attributes.cx "0"
- , Svg.Attributes.cy "0"
- , Svg.Attributes.r (String.fromFloat (0.3 * scale))
- , Svg.Attributes.fill "green"
- , snake.head
- |> Coordinates.fromPosition scale
- |> Coordinates.toTransformation ""
- |> Transformations.toString
- |> Svg.Attributes.transform
- ]
- []
+ else
+ let
+ distance : Float
+ distance =
+ 1 - progress
+ in
+ -- TODO: Multiple stripes
+ -- [ viewStripe 0.8 "#133d1a" points
+ -- , viewStripe 0.7 "#106023" points
+ -- , viewStripe 0.5 "#1e7d35" points
+ -- , viewStripe 0.2 "#5bb171" points
+ -- ]
+ -- |> Svg.g []
+ case snake.body of
+ [] ->
+ -- Just the head, so no direction.
+ '💀'
+ |> String.fromChar
+ |> Svg.text
+ |> Html.wrap Svg.text_
+ [ Html.Attributes.style "text-anchor" "middle"
+ , Html.Attributes.style "dominant-baseline" "middle"
+ , Svg.Attributes.width (scale |> String.fromFloat)
+ , Svg.Attributes.height (scale |> String.fromFloat)
+ , Svg.Attributes.fontSize (0.6 * scale |> String.fromFloat)
+ , Svg.Attributes.fontWeight "bold"
+ , Svg.Attributes.fill "hsl(0, 0%, 86%)"
+ , snake.head
+ |> Coordinates.fromPosition scale
+ |> Coordinates.toTransformation ""
+ |> Transformations.toString
+ |> Svg.Attributes.transform
+ ]
=
- [ neck ] ->
- -- Snake only has one segment. Head goes straight from it.
- let
- d =
- [ snake.head
- |> Coordinates.fromPosition scale
- |> Coordinates.shift (distance * scale) neck
- |> Coordinates.toString
- |> String.append "M "
- , snake.head
- |> Coordinates.fromPosition scale
- |> Coordinates.shift scale neck
- |> Coordinates.toString
- |> String.append "L "
+ [ neck ] ->
+ -- Snake only has one segment. Head goes straight from it.
+ let
+ d =
+ [ snake.head
+ |> Coordinates.fromPosition scale
+ |> Coordinates.shift (distance * scale) neck
+ |> Coordinates.toString
+ |> String.append "M "
+ , snake.head
+ |> Coordinates.fromPosition scale
+ |> Coordinates.shift scale neck
+ |> Coordinates.toString
+ |> String.append "L "
+ ]
+ |> String.join "\n"
+ |> Svg.Attributes.d
+ in
+ Svg.path
+ [ d
+ , 0.3
+ * scale
+ |> String.fromFloat
+ |> Svg.Attributes.strokeWidth
+ , Svg.Attributes.fill "none"
+ , Svg.Attributes.color "green"
+ , Svg.Attributes.stroke "currentColor"
+ , Svg.Attributes.strokeLinecap "round"
+ , Svg.Attributes.strokeLinejoin "round"
+ , Svg.Attributes.markerStart "url(#head-marker)"
+ , Svg.Attributes.markerEnd "url(#tail-marker)"
= ]
- |> String.join "\n"
- |> Svg.Attributes.d
- in
- Svg.path
- [ d
- , 0.3
- * scale
- |> String.fromFloat
- |> Svg.Attributes.strokeWidth
- , Svg.Attributes.fill "none"
- , Svg.Attributes.color "green"
- , Svg.Attributes.stroke "currentColor"
- , Svg.Attributes.strokeLinecap "round"
- , Svg.Attributes.strokeLinejoin "round"
- , Svg.Attributes.markerStart "url(#head-marker)"
- , Svg.Attributes.markerEnd "url(#tail-marker)"
- ]
- []
-
- neck :: torso :: tail ->
- -- A proper snake.
- let
- d : String
- d =
- dHead distance neck snake.head
- :: dNeck
- :: dTail tipDistance torsoPosition tail
- |> String.join "\n"
-
- dNeck =
- neckCoordinates
- |> Coordinates.toString
- |> String.append "L "
-
- neckCoordinates =
- snake.head
- |> Position.shift neck
- |> Coordinates.fromPosition scale
- |> Coordinates.shift (scale * distance) torso
-
- torsoPosition =
- snake.head
- |> Position.shift neck
- |> Position.shift torso
-
- tipDistance =
- if Snake.isGrowing snake then
- 1
+ []
=
- else
- distance
- in
- Svg.path
- [ d |> Svg.Attributes.d
- , 0.3
- * scale
- |> String.fromFloat
- |> Svg.Attributes.strokeWidth
- , Svg.Attributes.fill "none"
- , Svg.Attributes.color "green"
- , Svg.Attributes.stroke "currentColor"
- , Svg.Attributes.strokeLinecap "round"
- , Svg.Attributes.strokeLinejoin "round"
- , Svg.Attributes.markerStart "url(#head-marker)"
- , Svg.Attributes.markerEnd "url(#tail-marker)"
- ]
- []
+ neck :: torso :: tail ->
+ -- A proper snake.
+ let
+ d : String
+ d =
+ dHead distance neck snake.head
+ :: dNeck
+ :: dTail tipDistance torsoPosition tail
+ |> String.join "\n"
+
+ dNeck =
+ neckCoordinates
+ |> Coordinates.toString
+ |> String.append "L "
+
+ neckCoordinates =
+ snake.head
+ |> Position.shift neck
+ |> Coordinates.fromPosition scale
+ |> Coordinates.shift (scale * distance) torso
+
+ torsoPosition =
+ snake.head
+ |> Position.shift neck
+ |> Position.shift torso
+
+ tipDistance =
+ if Snake.isGrowing snake then
+ 1
+
+ else
+ distance
+ in
+ Svg.path
+ [ d |> Svg.Attributes.d
+ , 0.3
+ * scale
+ |> String.fromFloat
+ |> Svg.Attributes.strokeWidth
+ , Svg.Attributes.fill "none"
+ , Svg.Attributes.color "green"
+ , Svg.Attributes.stroke "currentColor"
+ , Svg.Attributes.strokeLinecap "round"
+ , Svg.Attributes.strokeLinejoin "round"
+ , Svg.Attributes.markerStart "url(#head-marker)"
+ , Svg.Attributes.markerEnd "url(#tail-marker)"
+ ]
+ []
=
=
=dTail : Float -> Position -> List Direction -> List String
@@ -1310,9 +1315,16 @@ burnTheEdge area burn snake entities =
=
=updateEntities : Goal -> Snake -> Entities -> Entities
=updateEntities goal snake entities =
- entities
- |> Entities.get snake.head
- |> List.foldr (processCollected goal snake) entities
+ if Snake.isAlive snake then
+ entities
+ |> Entities.get snake.head
+ |> List.foldr (processCollected goal snake) entities
+
+ else
+ snake.body
+ |> Position.trace snake.head
+ |> List.foldl (\position -> Dict.insert position (Entities.Present '🦴')) entities
+ |> Dict.insert snake.head (Entities.Present '💀')
=
=
=processCollected : Goal -> Snake -> Entity -> Entities -> EntitiesLet the CI run tests before publishing the website
On by
index c654b4f..5525631 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,7 +31,7 @@ pages:
= stage: deploy
=
= script:
- - nix develop --command make install prefix=./public/
+ - nix develop --command make test install prefix=./public/
=
= artifacts:
= paths:Fix glitchy movement
On by
Whenever the progress clock hit exactly 1.0, the step was skipped. Since the framerate is not fixed, it would sometimes happen randomly.
index 1c28ce4..d5bff5a 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -896,7 +896,7 @@ update msg model =
= | pan = pan
= , progress = overflow
= }
- |> applyIf (progress > 1) step
+ |> applyIf (progress >= 1) step
= , Cmd.none
= )
=Remove unused field from Game.Model record
On by
The "word" field, a leftover from before the introduction of different goal variants.
index d5bff5a..ae29447 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -63,7 +63,6 @@ type alias Model =
= , entities : Entities
= , snake : Snake
= , goal : Goal
- , word : String
= , randomness : Random.Seed
= , swipe : Maybe Swipe
= , countdown : Int
@@ -109,7 +108,6 @@ init flags =
= , entities = Dict.empty
= , snake = Snake.new ( 0, 0 ) East 5
= , goal = flags.goal
- , word = ""
= , randomness = flags.randomness
= , swipe = Nothing
= , countdown = Goal.coundown flags.goal
@@ -132,7 +130,6 @@ continue goal ({ area, snake, entities } as game) =
= , body = []
= , length = 5
= }
- , word = ""
= , entities = clearPath snake.head snake.direction 5 entities
= , countdown = Goal.coundown goal
= }The camera will pan more gently inside the firetrap
On by
It has been reported that when the snake is moving around in the firetrap and the camera is following it, it is difficult to find letters, since parts of the area are out of the viewbox. Now the camera is panning toward a midpoint between the snake's head and the center of the area, making most of it visible all the time. The panning is more gentle and potentially less disorienting this way.
While implementing this I also cleaned up some code by separating panning logic to it's own module and adding some helpers to the Area and Coordinates modules.
index bf65b3d..f511233 100644
--- a/src/Area.elm
+++ b/src/Area.elm
@@ -5,6 +5,7 @@ module Area exposing
= , centerY
= , default
= , edge
+ , isInside
= , isOutside
= , maxX
= , maxY
@@ -14,7 +15,6 @@ module Area exposing
= , viewbox
= )
=
-import Direction exposing (Direction)
=import Position exposing (Position)
=import Random
=
@@ -117,6 +117,11 @@ centerY { center } =
= Tuple.second center
=
=
+isInside : Area -> Position -> Bool
+isInside area position =
+ not <| isOutside area position
+
+
=isOutside : Area -> Position -> Bool
=isOutside area ( x, y ) =
= (x < minX area) || (x > maxX area) || (y < minY area) || (y > maxY area)index e31f86c..7c5f039 100644
--- a/src/Coordinates.elm
+++ b/src/Coordinates.elm
@@ -2,7 +2,10 @@ module Coordinates exposing
= ( Coordinates
= , add
= , fromPosition
+ , midpoint
+ , multiply
= , shift
+ , subtract
= , toString
= , toTransformation
= )
@@ -50,6 +53,45 @@ add displacement origin =
= }
=
=
+subtract : Coordinates -> Coordinates -> Coordinates
+subtract displacement origin =
+ displacement
+ |> multiply -1
+ |> add origin
+
+
+{-| If you think about the coordinates as a 2d vector, you can multiply it
+
+This is useful for inverting the vector or finding a midpoint.
+
+ Coordinates 12.5 -20
+ |> multiply 0.5
+ --> Coordinates 6.25 -10
+
+-}
+multiply : Float -> Coordinates -> Coordinates
+multiply factor { x, y } =
+ Coordinates
+ (x * factor)
+ (y * factor)
+
+
+{-| Find a point in the middle between two coordinates
+
+ midpoint
+ (Coordinates 50 20)
+ (Coordinates 20 -20)
+ --> Coordinates 35 0
+
+-}
+midpoint : Coordinates -> Coordinates -> Coordinates
+midpoint a b =
+ a
+ |> subtract b
+ |> multiply 0.5
+ |> add b
+
+
=shift : Float -> Direction -> Coordinates -> Coordinates
=shift distance direction coordinates =
= case direction ofindex ae29447..dd2cf1a 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -29,11 +29,11 @@ import Json.Decode as Decode
=import Keyboard
=import List.Extra as List
=import Maybe.Extra as Maybe
+import Pan exposing (Pan)
=import Position exposing (Position)
=import Random
=import Set exposing (Set)
=import Snake exposing (Snake)
-import Spring exposing (Spring)
=import Svg exposing (Svg)
=import Svg.Attributes
=import Svg.Keyed
@@ -70,12 +70,6 @@ type alias Model =
= }
=
=
-type alias Pan =
- { x : Spring
- , y : Spring
- }
-
-
=type alias Swipe =
= { identifier : Int
= , from : ( Float, Float )
@@ -102,9 +96,7 @@ init flags =
= |> Position.neighborhood
= |> Set.fromList
= , pan =
- Pan
- (Spring.create { strength = 20, dampness = 3 })
- (Spring.create { strength = 20, dampness = 3 })
+ Pan.new 20 3
= , entities = Dict.empty
= , snake = Snake.new ( 0, 0 ) East 5
= , goal = flags.goal
@@ -434,9 +426,10 @@ viewArea model =
= , viewSnake model.progress model.snake
= ]
= |> Svg.g
- [ Transformations.Translate "px"
- -(Spring.value model.pan.x)
- -(Spring.value model.pan.y)
+ [ model.pan
+ |> Pan.value
+ |> Coordinates.multiply -1
+ |> Coordinates.toTransformation "px"
= |> Transformations.toString
= |> Html.Attributes.style "transform"
= , Html.Attributes.style "transition" "transform 200ms linear"
@@ -518,10 +511,10 @@ viewSnake progress snake =
= case snake.body of
= [] ->
= -- Just the head, so no direction.
- '💀'
- |> String.fromChar
- |> Svg.text
- |> Html.wrap Svg.text_
+ '💀'
+ |> String.fromChar
+ |> Svg.text
+ |> Html.wrap Svg.text_
= [ Html.Attributes.style "text-anchor" "middle"
= , Html.Attributes.style "dominant-baseline" "middle"
= , Svg.Attributes.width (scale |> String.fromFloat)
@@ -879,9 +872,7 @@ update msg model =
= NextFrame delta ->
= let
= pan =
- { x = Spring.animate delta model.pan.x
- , y = Spring.animate delta model.pan.y
- }
+ Pan.animate delta model.pan
=
= progress =
= model.progress + (delta / pace)
@@ -1147,22 +1138,25 @@ step model =
= _ ->
= False
=
- panX =
- snake.head
- |> Tuple.first
- |> toFloat
- |> (*) scale
+ panToMidpoint =
+ -- This is a bit of a hack. Basically the concept of area
+ -- only plays a role when the goal is to complete a sentence
+ -- (with the firetrap and stuff).
+ -- TODO: Move area from Game.Model to the Goal.CompleteSentence variant?
+ case goal of
+ CompleteSentence _ _ ->
+ Area.isInside model.area snake.head
=
- panY =
- snake.head
- |> Tuple.second
- |> toFloat
- |> (*) scale
+ _ ->
+ False
=
= pan =
- { x = Spring.setTarget panX model.pan.x
- , y = Spring.setTarget panY model.pan.y
- }
+ snake.head
+ |> Coordinates.fromPosition scale
+ |> applyIf
+ panToMidpoint
+ (Coordinates.midpoint (Coordinates.fromPosition scale model.area.center))
+ |> (\coordinates -> Pan.setTarget coordinates model.pan)
= in
= { model
= | goal = goal
@@ -1389,7 +1383,7 @@ outcome model =
= case model.goal of
= CompleteSentence collected sentence ->
= if snakeEscaped model then
- -- TODO: Check if sentence is collected?
+ -- TODO: Check if sentence is collected? And if not, then what?
= Won
=
= elsenew file mode 100644
index 0000000..cdf4e13
--- /dev/null
+++ b/src/Pan.elm
@@ -0,0 +1,46 @@
+module Pan exposing
+ ( Pan
+ , animate
+ , new
+ , setTarget
+ , value
+ )
+
+import Coordinates exposing (Coordinates)
+import Spring exposing (Spring)
+
+
+{-| A pan is coordinates on springs
+-}
+type alias Pan =
+ { x : Spring
+ , y : Spring
+ }
+
+
+new : Float -> Float -> Pan
+new strength dampness =
+ Pan
+ (Spring.create { strength = strength, dampness = dampness })
+ (Spring.create { strength = strength, dampness = dampness })
+
+
+setTarget : Coordinates -> Pan -> Pan
+setTarget target pan =
+ Pan
+ (Spring.setTarget target.x pan.x)
+ (Spring.setTarget target.y pan.y)
+
+
+value : Pan -> Coordinates
+value pan =
+ { x = Spring.value pan.x
+ , y = Spring.value pan.y
+ }
+
+
+animate : Float -> Pan -> Pan
+animate delta pan =
+ { x = Spring.animate delta pan.x
+ , y = Spring.animate delta pan.y
+ }A password will be displayed in area as it gets collected
On by
I got a feedback that it's difficult to keep track of letters collected so far because they are only displayed on top of the playing area and a player has to keep track of the snake and it's surroundings. So now the password is displayed in large, but dim letters in the background.
The implementation is a bit crude. I plan to improve it while also preserving previously guessed passwords and displaying hints for other variants of the game (like directions etc).
index dd2cf1a..4f6e238 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -16,7 +16,7 @@ port module Game exposing
=
=import Area exposing (Area)
=import Browser.Events
-import Coordinates
+import Coordinates exposing (Coordinates)
=import Dict
=import Direction exposing (Direction(..))
=import Entities exposing (Entities, Entity)
@@ -422,6 +422,7 @@ viewArea model =
= let
= entities =
= [ viewBackground model.tiles
+ , viewCollected model
= , viewEntities model.entities
= , viewSnake model.progress model.snake
= ]
@@ -451,6 +452,73 @@ viewArea model =
= ]
=
=
+viewCollected : Model -> Svg Msg
+viewCollected model =
+ case model.goal of
+ CompleteSentence sentence collected ->
+ let
+ fontSize =
+ sentence.password
+ |> String.length
+ |> toFloat
+ |> (/) (20 * scale)
+
+ coordinates =
+ model.area.center
+ |> Coordinates.fromPosition scale
+
+ visible =
+ []
+
+ invisible =
+ [ Svg.Attributes.fill "none"
+ , Svg.Attributes.stroke "none"
+ ]
+ in
+ sentence.password
+ |> String.toList
+ |> List.indexedMap
+ (\index character ->
+ character
+ |> String.fromChar
+ |> Svg.text
+ |> Html.wrap Svg.tspan
+ (if index < String.length collected then
+ visible
+
+ else
+ invisible
+ )
+ )
+ |> viewAnnotation fontSize coordinates
+
+ _ ->
+ "" |> Svg.text
+
+
+viewAnnotation : Float -> Coordinates -> List (Svg Msg) -> Svg Msg
+viewAnnotation fontSize coordinates text =
+ text
+ |> Svg.text_
+ [ Html.Attributes.style "text-anchor" "middle"
+ , Html.Attributes.style "dominant-baseline" "middle"
+ , fontSize
+ |> String.fromFloat
+ |> Svg.Attributes.fontSize
+ , Svg.Attributes.fontWeight "bold"
+ , Svg.Attributes.fill "hsl(0, 0%, 13%)"
+ , Svg.Attributes.stroke "hsl(0, 0%, 16%)"
+ , Svg.Attributes.strokeWidth (0.2 * scale |> String.fromFloat)
+ , Html.Attributes.style "paint-order" "stroke"
+ , Svg.Attributes.strokeLinecap "butt"
+ , Svg.Attributes.strokeLinejoin "mitter"
+ , coordinates
+ |> Coordinates.toTransformation ""
+ |> Transformations.toString
+ |> Svg.Attributes.transform
+ ]
+
+
=viewBackground : Set Position -> Svg Msg
=viewBackground tiles =
= tilesImplement annotations system
On by
An annotation is a text visible in the playing world. They are used for giving instructions in the tutorial and leaving previously completed words visible, for decoration and greater immersion. This way player is leaving their mark in the game world.
If the annotations system proves itself, maybe it can be used to display the entire sentence to be completed, making the top bar obsolete.
index 4f6e238..3968a95 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -67,6 +67,7 @@ type alias Model =
= , swipe : Maybe Swipe
= , countdown : Int
= , progress : Float -- TODO: Progress is an amount of time (in ms) since last step. Find a better name.
+ , annotations : List Annotation
= }
=
=
@@ -77,6 +78,13 @@ type alias Swipe =
= }
=
=
+type alias Annotation =
+ { text : String
+ , size : Float
+ , coordinates : Coordinates
+ }
+
+
=
=-- INIT
=
@@ -104,6 +112,7 @@ init flags =
= , swipe = Nothing
= , countdown = Goal.coundown flags.goal
= , progress = 0
+ , annotations = []
= }
=
=
@@ -125,6 +134,7 @@ continue goal ({ area, snake, entities } as game) =
= , entities = clearPath snake.head snake.direction 5 entities
= , countdown = Goal.coundown goal
= }
+ |> insertAnnotations game
=
=
=clearPath : Position -> Direction -> Int -> Entities -> Entities
@@ -422,6 +432,7 @@ viewArea model =
= let
= entities =
= [ viewBackground model.tiles
+ , viewAnnotations model.annotations
= , viewCollected model
= , viewEntities model.entities
= , viewSnake model.progress model.snake
@@ -456,53 +467,88 @@ viewCollected : Model -> Svg Msg
=viewCollected model =
= case model.goal of
= CompleteSentence sentence collected ->
- let
- fontSize =
- sentence.password
- |> String.length
- |> toFloat
- |> (/) (20 * scale)
+ if Goal.sentenceCompleted sentence collected then
+ -- As soon as a sentence is completed it will be added to
+ -- annotations (see the step function), so no need to repeat it
+ -- here.
+ let
+ text =
+ "Right. Now escape the trap!"
=
- coordinates =
- model.area.center
- |> Coordinates.fromPosition scale
+ fontSize =
+ autoFontSize text
=
- visible =
- []
+ coordinates =
+ model.area.center
+ |> Coordinates.fromPosition scale
+ |> Coordinates.shift (1.5 * scale) South
+ in
+ text
+ |> Svg.text
+ |> List.singleton
+ |> viewBigText fontSize coordinates
=
- invisible =
- [ Svg.Attributes.fill "none"
- , Svg.Attributes.stroke "none"
- ]
- in
- sentence.password
- |> String.toList
- |> List.indexedMap
- (\index character ->
- character
- |> String.fromChar
- |> Svg.text
- |> Html.wrap Svg.tspan
- (if index < String.length collected then
- visible
-
- else
- invisible
- )
- )
- |> viewAnnotation fontSize coordinates
+ else
+ let
+ fontSize =
+ autoFontSize sentence.password
+
+ coordinates =
+ model.area.center
+ |> Coordinates.fromPosition scale
+
+ visible =
+ []
+
+ invisible =
+ [ Svg.Attributes.fill "none"
+ , Svg.Attributes.stroke "none"
+ ]
+ in
+ sentence.password
+ |> String.toList
+ |> List.indexedMap
+ (\index character ->
+ character
+ |> String.fromChar
+ |> Svg.text
+ |> Html.wrap Svg.tspan
+ (if index < String.length collected then
+ visible
+
+ else
+ invisible
+ )
+ )
+ |> viewBigText fontSize coordinates
=
= _ ->
= "" |> Svg.text
=
=
-viewAnnotation : Float -> Coordinates -> List (Svg Msg) -> Svg Msg
-viewAnnotation fontSize coordinates text =
+viewAnnotations : List Annotation -> Svg Msg
+viewAnnotations annotations =
+ annotations
+ |> List.reverseMap viewAnnotation
+ |> Svg.g [ Svg.Attributes.id "annotations" ]
+
+
+viewAnnotation : Annotation -> Svg Msg
+viewAnnotation annotation =
+ annotation.text
+ |> Svg.text
+ |> List.singleton
+ |> viewBigText annotation.size annotation.coordinates
+
+
+viewBigText : Float -> Coordinates -> List (Svg Msg) -> Svg Msg
+viewBigText fontSize coordinates text =
= text
= |> Svg.text_
= [ Html.Attributes.style "text-anchor" "middle"
= , Html.Attributes.style "dominant-baseline" "middle"
= , fontSize
+ |> (*) scale
= |> String.fromFloat
= |> Svg.Attributes.fontSize
= , Svg.Attributes.fontWeight "bold"
@@ -1238,6 +1284,90 @@ step model =
= , snake = snake
= , swipe = model.swipe |> Maybe.map (\swipe -> { swipe | from = swipe.to })
= }
+ |> insertAnnotations model
+
+
+{-| Annotations are inserted based on changes in the model
+-}
+insertAnnotations : Model -> Model -> Model
+insertAnnotations old new =
+ case ( old.goal, new.goal ) of
+ ( CompleteSentence _ previouslyCollected, CompleteSentence sentence currentlyCollected ) ->
+ let
+ annotation =
+ new.area.center
+ |> Coordinates.fromPosition scale
+ |> Annotation sentence.password fontSize
+
+ fontSize =
+ autoFontSize sentence.password
+ in
+ applyIf
+ ((previouslyCollected /= currentlyCollected) && (currentlyCollected == sentence.password))
+ (insertAnnotation annotation)
+ new
+
+ ( _, ChangeDirections directions ) ->
+ let
+ annotation =
+ new.snake.head
+ |> Coordinates.fromPosition scale
+ |> Annotation text fontSize
+
+ text =
+ directions
+ |> List.head
+ |> Maybe.map Direction.toString
+ |> Maybe.map (String.append "Go ")
+ |> Maybe.withDefault "Good!"
+
+ fontSize =
+ 2
+ in
+ applyIf
+ (old.goal /= new.goal)
+ (insertAnnotation annotation)
+ new
+
+ ( _, CollectSingle _ collectibles ) ->
+ let
+ annotation =
+ new.snake.head
+ |> Coordinates.fromPosition scale
+ |> Annotation text fontSize
+
+ text =
+ collectibles
+ |> List.head
+ |> Maybe.map .label
+ |> Maybe.map (String.append "Find ")
+ |> Maybe.withDefault "Yummy!"
+
+ fontSize =
+ autoFontSize text
+ in
+ applyIf
+ (old.goal /= new.goal)
+ (insertAnnotation annotation)
+ new
+
+ _ ->
+ new
+
+
+insertAnnotation : Annotation -> Model -> Model
+insertAnnotation annotation model =
+ { model | annotations = annotation :: model.annotations }
+
+
+{-| Find a font size that will make given text fit within the area
+-}
+autoFontSize : String -> Float
+autoFontSize text =
+ text
+ |> String.length
+ |> toFloat
+ |> (/) 20
=
=
=updateGoal : Snake -> Entities -> Goal -> Goalindex 187f7cc..9e650b3 100644
--- a/src/Goal.elm
+++ b/src/Goal.elm
@@ -5,10 +5,11 @@ module Goal exposing
= , Sentence
= , collectLetter
= , collectSingle
- , isCompleted
= , coundown
+ , isCompleted
= , missingLetters
= , nextLetter
+ , sentenceCompleted
= , serialize
= )
=
@@ -105,6 +106,13 @@ type alias Charset =
= List Char
=
=
+sentenceCompleted : Sentence -> String -> Bool
+sentenceCompleted sentence collected =
+ collected
+ |> missingLetters sentence
+ |> List.isEmpty
+
+
=missingLetters : Sentence -> String -> List Char
=missingLetters sentence collected =
= sentence.passwordStop the world when a player reads instructions
On by
Two reasons:
-
The snake goes too far from historical annotations, making them kind of pointless.
-
It's a wasted computation
index 3968a95..c1310b7 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -1416,11 +1416,16 @@ subscriptions model =
= |> KeyPressed
=
= clock =
- if model.countdown > 0 then
- Time.every 1000 CountdownClockTick
+ case model.goal of
+ ReadInstructions _ ->
+ Sub.none
=
- else
- Browser.Events.onAnimationFrameDelta NextFrame
+ _ ->
+ if model.countdown > 0 then
+ Time.every 1000 CountdownClockTick
+
+ else
+ Browser.Events.onAnimationFrameDelta NextFrame
= in
= [ clock
= , keydown decodeKeydownLet the time slow on slow computers
On by
If the performance is poor (as it often is), pretend that the frame duration was 32ms. That way, instead of being jerky, the game slows down.
index c1310b7..d7cd503 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -983,8 +983,11 @@ update msg model =
= , Cmd.none
= )
=
- NextFrame delta ->
+ NextFrame duration ->
= let
+ delta =
+ min 32 duration
+
= pan =
= Pan.animate delta model.pan
=Tweak the graceful degradation to 12 FPS
On by
Previously it was ~30, but on some devices this was too much and the game was running in slow motion.
index d7cd503..db0bc28 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -986,7 +986,8 @@ update msg model =
= NextFrame duration ->
= let
= delta =
- min 32 duration
+ -- If the performance degrades below 12 FPS, slow down
+ min 83 duration
=
= pan =
= Pan.animate delta model.panFactor out complete the sentence logic to own module
On by
I want to remove area and countdown (specific only to some goal variants) from Game.Model to those variants. This work is setting the stage for it.
index db0bc28..deb2a60 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -21,6 +21,7 @@ import Dict
=import Direction exposing (Direction(..))
=import Entities exposing (Entities, Entity)
=import Goal exposing (Goal(..))
+import Goal.CompleteTheSentence as CompleteTheSentence
=import Html exposing (Html)
=import Html.Attributes
=import Html.Events
@@ -180,8 +181,8 @@ view model =
= ReadInstructions instructions ->
= [ viewInstructions instructions ]
=
- CompleteSentence sentence collected ->
- [ viewSentence sentence collected
+ CompleteTheSentence completion ->
+ [ viewSentence completion
= , viewArea model
= , viewCurtain model.countdown model
= ]
@@ -392,25 +393,31 @@ viewInstructions instructions =
= ]
=
=
-viewSentence : Goal.Sentence -> String -> Html Msg
-viewSentence sentence collected =
- viewHeader
- [ Html.text sentence.intro
- , Html.span
- [ Html.Attributes.style "background" <|
- if passwordCollected collected sentence then
- "lightgreen"
+viewSentence : CompleteTheSentence.Model -> Html Msg
+viewSentence ({ sentence, collected } as model) =
+ let
+ background =
+ if CompleteTheSentence.isComplete model then
+ "lightgreen"
=
- else
- "lightyellow"
- , Html.Attributes.style "padding" "0.2em"
- ]
- [ if String.isEmpty collected then
- Html.text "..."
+ else
+ "lightyellow"
=
- else
- Html.text collected
- ]
+ collectedText =
+ if String.isEmpty collected then
+ "..."
+
+ else
+ collected
+ in
+ viewHeader
+ [ Html.text sentence.intro
+ , collectedText
+ |> Html.text
+ |> Html.wrap Html.span
+ [ background |> Html.Attributes.style "background"
+ , Html.Attributes.style "padding" "0.2em"
+ ]
= , Html.text sentence.outro
= ]
=
@@ -466,8 +473,8 @@ viewArea model =
=viewCollected : Model -> Svg Msg
=viewCollected model =
= case model.goal of
- CompleteSentence sentence collected ->
- if Goal.sentenceCompleted sentence collected then
+ CompleteTheSentence completion ->
+ if CompleteTheSentence.isComplete completion then
= -- As soon as a sentence is completed it will be added to
= -- annotations (see the step function), so no need to repeat it
= -- here.
@@ -491,7 +498,7 @@ viewCollected model =
= else
= let
= fontSize =
- autoFontSize sentence.password
+ autoFontSize completion.sentence.password
=
= coordinates =
= model.area.center
@@ -505,7 +512,7 @@ viewCollected model =
= , Svg.Attributes.stroke "none"
= ]
= in
- sentence.password
+ completion.sentence.password
= |> String.toList
= |> List.indexedMap
= (\index character ->
@@ -513,7 +520,7 @@ viewCollected model =
= |> String.fromChar
= |> Svg.text
= |> Html.wrap Svg.tspan
- (if index < String.length collected then
+ (if index < String.length completion.collected then
= visible
=
= else
@@ -1250,8 +1257,8 @@ step model =
= burn : Bool
= burn =
= case model.goal of
- CompleteSentence _ _ ->
- not (Goal.isCompleted goal) && Snake.isAlive snake
+ CompleteTheSentence _ ->
+ not (Goal.isComplete goal) && Snake.isAlive snake
=
= _ ->
= False
@@ -1262,7 +1269,7 @@ step model =
= -- (with the firetrap and stuff).
= -- TODO: Move area from Game.Model to the Goal.CompleteSentence variant?
= case goal of
- CompleteSentence _ _ ->
+ CompleteTheSentence _ ->
= Area.isInside model.area snake.head
=
= _ ->
@@ -1296,18 +1303,22 @@ step model =
=insertAnnotations : Model -> Model -> Model
=insertAnnotations old new =
= case ( old.goal, new.goal ) of
- ( CompleteSentence _ previouslyCollected, CompleteSentence sentence currentlyCollected ) ->
+ ( CompleteTheSentence previous, CompleteTheSentence current ) ->
= let
= annotation =
= new.area.center
= |> Coordinates.fromPosition scale
- |> Annotation sentence.password fontSize
+ |> Annotation current.sentence.password fontSize
=
= fontSize =
- autoFontSize sentence.password
+ autoFontSize current.sentence.password
+
+ justCompleted : Bool
+ justCompleted =
+ CompleteTheSentence.isComplete current && not (CompleteTheSentence.isComplete previous)
= in
= applyIf
- ((previouslyCollected /= currentlyCollected) && (currentlyCollected == sentence.password))
+ justCompleted
= (insertAnnotation annotation)
= new
=
@@ -1377,11 +1388,11 @@ autoFontSize text =
=updateGoal : Snake -> Entities -> Goal -> Goal
=updateGoal snake entities goal =
= case goal of
- CompleteSentence sentence collected ->
+ CompleteTheSentence completion ->
= entities
= |> Entities.get snake.head
- |> List.foldr (Goal.collectLetter sentence) collected
- |> CompleteSentence sentence
+ |> List.foldr (\entity -> CompleteTheSentence.collect entity) completion
+ |> CompleteTheSentence
=
= ReadInstructions _ ->
= goal
@@ -1536,15 +1547,8 @@ processCollected goal snake entity entities =
=
= _ ->
= case goal of
- CompleteSentence sentence collected ->
- let
- wanted : Maybe Char
- wanted =
- collected
- |> Goal.missingLetters sentence
- |> List.head
- in
- if wanted == Just entity then
+ CompleteTheSentence completion ->
+ if CompleteTheSentence.isCorrect entity completion then
= Entities.collect snake.head entity entities
=
= else
@@ -1588,7 +1592,7 @@ outcome model =
=
= else
= case model.goal of
- CompleteSentence collected sentence ->
+ CompleteTheSentence _ ->
= if snakeEscaped model then
= -- TODO: Check if sentence is collected? And if not, then what?
= Won
@@ -1699,15 +1703,15 @@ randomSpawn area goal snake entities =
= entities
= in
= case goal of
- CompleteSentence sentence collected ->
+ CompleteTheSentence model ->
= let
= probability =
= 0.3
=
= charset =
- '🔥' :: sentence.charset
+ '🔥' :: model.sentence.charset
= in
- case Goal.missingLetters sentence collected of
+ case CompleteTheSentence.missing model of
= [] ->
= Random.constant entities
=index 9e650b3..5293a92 100644
--- a/src/Goal.elm
+++ b/src/Goal.elm
@@ -6,7 +6,7 @@ module Goal exposing
= , collectLetter
= , collectSingle
= , coundown
- , isCompleted
+ , isComplete
= , missingLetters
= , nextLetter
= , sentenceCompleted
@@ -16,10 +16,11 @@ module Goal exposing
=import Direction exposing (Direction(..))
=import Entities exposing (Entity)
=import Html exposing (Html)
+import Goal.CompleteTheSentence as CompleteTheSentence
=
=
=type Goal
- = CompleteSentence Sentence String
+ = CompleteTheSentence CompleteTheSentence.Model
= | ReadInstructions (List (Html Never))
= | ChangeDirections (List Direction)
= | CollectSingle Charset (List Collectible)
@@ -33,8 +34,9 @@ type alias Collectible =
=
=coundown : Goal -> Int
=coundown goal =
+ -- TODO: Let the countdown be part of relevant goal models
= case goal of
- CompleteSentence _ _ ->
+ CompleteTheSentence _ ->
= 5
=
= ReadInstructions _ ->
@@ -47,11 +49,11 @@ coundown goal =
= 5
=
=
-isCompleted : Goal -> Bool
-isCompleted goal =
+isComplete : Goal -> Bool
+isComplete goal =
= case goal of
- CompleteSentence sentence collected ->
- String.toUpper collected == String.toUpper sentence.password
+ CompleteTheSentence model ->
+ CompleteTheSentence.isComplete model
=
= ReadInstructions _ ->
= True
@@ -66,7 +68,7 @@ isCompleted goal =
=serialize : Goal -> String
=serialize goal =
= case goal of
- CompleteSentence sentence _ ->
+ CompleteTheSentence { sentence } ->
= [ "< "
= , sentence.charset
= |> List.map String.fromCharnew file mode 100644
index 0000000..5d371c2
--- /dev/null
+++ b/src/Goal/CompleteTheSentence.elm
@@ -0,0 +1,85 @@
+module Goal.CompleteTheSentence exposing
+ ( Charset
+ , Model
+ , Sentence
+ , collect
+ , isComplete
+ , isCorrect
+ , missing
+ , needsNext, init
+ )
+
+
+type alias Model =
+ { sentence : Sentence
+ , collected : String
+ }
+
+
+type alias Sentence =
+ { charset : Charset
+ , intro : String
+ , password : String
+ , outro : String
+ }
+
+
+type alias Charset =
+ List Char
+
+
+init : Sentence -> Model
+init sentence =
+ { sentence = sentence
+ , collected = ""
+ }
+
+
+isComplete : Model -> Bool
+isComplete model =
+ String.toUpper model.collected == String.toUpper model.sentence.password
+
+
+missing : Model -> List Char
+missing model =
+ model.sentence.password
+ |> String.toUpper
+ |> String.toList
+ |> List.drop (String.length model.collected)
+
+
+collect : Char -> Model -> Model
+collect letter model =
+ if isCorrect letter model then
+ append letter model
+
+ else
+ model
+
+
+{-| Blindly appends the given letter to the collected list
+
+Does not check if it's correct, so probably only good for internal use.
+
+-}
+append : Char -> Model -> Model
+append letter model =
+ { model
+ | collected =
+ model.collected
+ |> String.reverse
+ |> String.cons letter
+ |> String.reverse
+ }
+
+
+isCorrect : Char -> Model -> Bool
+isCorrect letter model =
+ needsNext model == Just letter
+
+
+needsNext : Model -> Maybe Char
+needsNext model =
+ model
+ |> missing
+ |> List.headindex f9b56f2..c28eef4 100644
--- a/src/Parser/Custom.elm
+++ b/src/Parser/Custom.elm
@@ -12,7 +12,7 @@ module Parser.Custom exposing
=import Goal exposing (Charset, Goal)
=import Parser exposing ((|.), (|=), Parser)
=import Goal exposing (Goal)
-import Goal exposing (Goal(..))
+import Goal.CompleteTheSentence as CompleteTheSentence
=
=
=levels : Parser (List Goal)
@@ -78,7 +78,7 @@ letter =
=
=line : Charset -> Parser Goal
=line set =
- Parser.succeed (Goal.Sentence set)
+ Parser.succeed (CompleteTheSentence.Sentence set)
= |= intro
= |. Parser.symbol "{"
= |. Parser.spaces
@@ -86,8 +86,8 @@ line set =
= |. Parser.spaces
= |. Parser.symbol "}"
= |= outro
- |> Parser.map (CompleteSentence)
- |> Parser.map (\constructor -> constructor "")
+ |> Parser.map (CompleteTheSentence.init)
+ |> Parser.map (Goal.CompleteTheSentence)
=
=
=intro : Parser Stringindex d473c9b..444692c 100644
--- a/src/Tutorial.elm
+++ b/src/Tutorial.elm
@@ -2,6 +2,7 @@ module Tutorial exposing (goals, intro)
=
=import Direction exposing (Direction(..))
=import Goal exposing (Goal)
+import Goal.CompleteTheSentence as CompleteTheSentence
=import Html
=import Tad.Html as Html
=
@@ -52,13 +53,13 @@ goals =
= [ "Collecting letters" |> Html.text |> Html.wrap Html.h1 []
= , "Navigate the Snake to collect the letters and fill the missing word in a sentence. Once the password is collected you will be able to escape the fire trap." |> Html.text |> Html.wrap Html.p []
= ]
- , Goal.CompleteSentence
- { charset = [ 'A', 'B', 'C' ]
- , intro = "Collect letters A B and C in order: "
- , password = "ABC"
- , outro = ""
- }
+ , CompleteTheSentence.Sentence
+ [ 'A', 'B', 'C' ]
+ "Collect letters A B and C in order: "
+ "ABC"
= ""
+ |> CompleteTheSentence.init
+ |> Goal.CompleteTheSentence
= , Goal.ReadInstructions
= [ "Save the Snake!" |> Html.text |> Html.wrap Html.h1 []
= , "You have finished the tutorial. Well done! Now play." |> Html.text |> Html.wrap Html.p []Factor area out of game model into CompleteTheSentence
On by
It only makes sense in this goal variant, but there was a lot of logic that incorrectly depended on it being part of the Game.Model, like viewbox, panning and some predicates.
Now the size of the viewbox is stored in the model. It could be a constant, but I'm considering making it a user setting.
index f511233..716af3d 100644
--- a/src/Area.elm
+++ b/src/Area.elm
@@ -12,7 +12,6 @@ module Area exposing
= , minX
= , minY
= , randomPosition
- , viewbox
= )
=
=import Position exposing (Position)
@@ -134,15 +133,6 @@ randomPosition area =
= (Random.int (minY area) (maxY area))
=
=
-viewbox : Int -> Area -> String
-viewbox scale area =
- [ (-2 - area.width // 2) * scale
- , (-2 - area.height // 2) * scale
- , (area.width + 4) * scale
- , (area.height + 4) * scale
- ]
- |> List.map String.fromInt
- |> String.join " "
=
=
=centerAround : Position -> Area -> Areaindex deb2a60..79ffb72 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -6,7 +6,6 @@ port module Game exposing
= , hasWon
= , init
= , outcome
- , snakeEscaped
= , subscriptions
= , update
= , view
@@ -58,7 +57,7 @@ pace =
=
=
=type alias Model =
- { area : Area
+ { viewbox : Int
= , tiles : Set Position
= , pan : Pan
= , entities : Entities
@@ -91,16 +90,14 @@ type alias Annotation =
=
=
=type alias Flags =
- { area : Area
- , goal : Goal
+ { goal : Goal
= , randomness : Random.Seed
= }
=
=
=init : Flags -> Model
=init flags =
- { area = flags.area
- , tiles =
+ { tiles =
= ( 0, 0 )
= |> Position.neighborhood
= |> Set.fromList
@@ -114,14 +111,14 @@ init flags =
= , countdown = Goal.coundown flags.goal
= , progress = 0
= , annotations = []
+ , viewbox = 20
= }
=
=
=continue : Goal -> Model -> Model
-continue goal ({ area, snake, entities } as game) =
+continue goal ({ snake, entities } as game) =
= { game
- | goal = goal
- , area = { area | center = game.snake.head }
+ | goal = Goal.restart snake.head goal
= , snake =
= if Snake.isAlive snake then
= snake
@@ -453,9 +450,20 @@ viewArea model =
= |> Html.Attributes.style "transform"
= , Html.Attributes.style "transition" "transform 200ms linear"
= ]
+
+ viewbox : String
+ viewbox =
+ [ model.viewbox // -2
+ , model.viewbox // -2
+ , model.viewbox
+ , model.viewbox
+ ]
+ |> List.map ((*) scale)
+ |> List.map String.fromInt
+ |> String.join " "
= in
= Svg.svg
- [ model.area |> Area.viewbox scale |> Svg.Attributes.viewBox
+ [ Svg.Attributes.viewBox viewbox
= , Html.Attributes.style "width" "100%"
= , Html.Attributes.style "height" "0"
= , Html.Attributes.style "flex-grow" "1"
@@ -486,7 +494,7 @@ viewCollected model =
= autoFontSize text
=
= coordinates =
- model.area.center
+ completion.area.center
= |> Coordinates.fromPosition scale
= |> Coordinates.shift (1.5 * scale) South
= in
@@ -501,7 +509,7 @@ viewCollected model =
= autoFontSize completion.sentence.password
=
= coordinates =
- model.area.center
+ completion.area.center
= |> Coordinates.fromPosition scale
=
= visible =
@@ -1211,13 +1219,21 @@ step model =
= |> Maybe.andThen swipeDirection
= |> Maybe.withDefault model.snake.direction
=
+ area =
+ case model.goal of
+ CompleteTheSentence completion ->
+ completion.area
+
+ _ ->
+ Area.centerAround snake.head Area.default
+
= ( entities, randomness ) =
= model.entities
= |> Entities.step
= |> randomDecay decayRatio
= |> Random.andThen
= (randomSpawn
- model.area
+ area
= model.goal
= model.snake
= )
@@ -1263,25 +1279,29 @@ step model =
= _ ->
= False
=
- panToMidpoint =
- -- This is a bit of a hack. Basically the concept of area
- -- only plays a role when the goal is to complete a sentence
- -- (with the firetrap and stuff).
- -- TODO: Move area from Game.Model to the Goal.CompleteSentence variant?
+ panTo : Coordinates
+ panTo =
+ -- If there is a fixed area (like in complete the sentence goal) and
+ -- the snake is inside it, then pan the camera to the midpoint
+ -- between the snake's head and the center of the area. Otherwise
+ -- just keep tracking the snake.
= case goal of
- CompleteTheSentence _ ->
- Area.isInside model.area snake.head
+ CompleteTheSentence completion ->
+ if Area.isInside area snake.head then
+ snake.head
+ |> Coordinates.fromPosition scale
+ |> Coordinates.midpoint (Coordinates.fromPosition scale completion.area.center)
+
+ else
+ snake.head
+ |> Coordinates.fromPosition scale
=
= _ ->
- False
+ snake.head
+ |> Coordinates.fromPosition scale
=
= pan =
- snake.head
- |> Coordinates.fromPosition scale
- |> applyIf
- panToMidpoint
- (Coordinates.midpoint (Coordinates.fromPosition scale model.area.center))
- |> (\coordinates -> Pan.setTarget coordinates model.pan)
+ Pan.setTarget panTo model.pan
= in
= { model
= | goal = goal
@@ -1290,7 +1310,7 @@ step model =
= , entities =
= entities
= |> updateEntities model.goal model.snake
- |> burnTheEdge model.area burn snake
+ |> applyIf burn (burnTheEdge area snake)
= , randomness = randomness
= , snake = snake
= , swipe = model.swipe |> Maybe.map (\swipe -> { swipe | from = swipe.to })
@@ -1306,7 +1326,7 @@ insertAnnotations old new =
= ( CompleteTheSentence previous, CompleteTheSentence current ) ->
= let
= annotation =
- new.area.center
+ current.area.center
= |> Coordinates.fromPosition scale
= |> Annotation current.sentence.password fontSize
=
@@ -1462,8 +1482,8 @@ This function is somewhat (too?) complicated. Basically this is what it does:
=As a result, the edge starts burning in front of the snake and then the fire spreads all around it. If the snake turns, a new fire is started in front of it, so it is impossible to escape. It creates a nice effect.
=
=-}
-burnTheEdge : Area -> Bool -> Snake -> Entities -> Entities
-burnTheEdge area burn snake entities =
+burnTheEdge : Area -> Snake -> Entities -> Entities
+burnTheEdge area snake entities =
= let
= setOnFire : Position -> Entities -> Entities
= setOnFire position =
@@ -1512,14 +1532,10 @@ burnTheEdge area burn snake entities =
= Just position ->
= setOnFire position
= in
- if burn then
- area
- |> Area.edge
- |> List.foldl spread entities
- |> ignite
-
- else
- entities
+ area
+ |> Area.edge
+ |> List.foldl spread entities
+ |> ignite
=
=
=updateEntities : Goal -> Snake -> Entities -> Entities
@@ -1592,8 +1608,8 @@ outcome model =
=
= else
= case model.goal of
- CompleteTheSentence _ ->
- if snakeEscaped model then
+ CompleteTheSentence { area } ->
+ if Area.isOutside area model.snake.head then
= -- TODO: Check if sentence is collected? And if not, then what?
= Won
=
@@ -1618,16 +1634,6 @@ outcome model =
= InProgress
=
=
-passwordCollected : String -> Goal.Sentence -> Bool
-passwordCollected collected sentence =
- String.toUpper collected == String.toUpper sentence.password
-
-
-snakeEscaped : Model -> Bool
-snakeEscaped model =
- Snake.isAlive model.snake && Area.isOutside model.area model.snake.head
-
-
=
=-- direction helpers
=index 5293a92..6ca7ea8 100644
--- a/src/Goal.elm
+++ b/src/Goal.elm
@@ -9,14 +9,16 @@ module Goal exposing
= , isComplete
= , missingLetters
= , nextLetter
+ , restart
= , sentenceCompleted
= , serialize
= )
=
=import Direction exposing (Direction(..))
=import Entities exposing (Entity)
-import Html exposing (Html)
=import Goal.CompleteTheSentence as CompleteTheSentence
+import Html exposing (Html)
+import Position exposing (Position)
=
=
=type Goal
@@ -163,3 +165,21 @@ collectSingle entity collectibles =
=
= [] ->
= collectibles
+
+
+restart : Position -> Goal -> Goal
+restart position goal =
+ case goal of
+ CompleteTheSentence completion ->
+ completion.sentence
+ |> CompleteTheSentence.init position
+ |> CompleteTheSentence
+
+ ReadInstructions _ ->
+ goal
+
+ ChangeDirections _ ->
+ goal
+
+ CollectSingle _ _ ->
+ goalindex 5d371c2..a5adb71 100644
--- a/src/Goal/CompleteTheSentence.elm
+++ b/src/Goal/CompleteTheSentence.elm
@@ -3,16 +3,21 @@ module Goal.CompleteTheSentence exposing
= , Model
= , Sentence
= , collect
+ , init
= , isComplete
= , isCorrect
= , missing
- , needsNext, init
+ , needsNext
= )
=
+import Area exposing (Area)
+import Position exposing (Position)
+
=
=type alias Model =
= { sentence : Sentence
= , collected : String
+ , area : Area
= }
=
=
@@ -28,10 +33,11 @@ type alias Charset =
= List Char
=
=
-init : Sentence -> Model
-init sentence =
- { sentence = sentence
+init : Position -> Sentence -> Model
+init position sentence =
+ { area = Area.centerAround position Area.default
= , collected = ""
+ , sentence = sentence
= }
=
=index 1e6d81f..2432baf 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -8,7 +8,6 @@ module Main exposing
= , view
= )
=
-import Area
=import Browser
=import Dict exposing (Dict)
=import Game exposing (update)
@@ -310,8 +309,7 @@ update msg model =
= ( { model
= | stage =
= Game.init
- { area = Area.default
- , goal = Tutorial.intro
+ { goal = Tutorial.intro
=
= -- TODO: Do we need randomness in tutorial?
= , randomness = Random.initialSeed 0
@@ -474,7 +472,6 @@ update msg model =
= , stage =
= { goal = level
= , randomness = randomness
- , area = Area.default
= }
= |> Game.init
= |> Playingindex c28eef4..9084bd3 100644
--- a/src/Parser/Custom.elm
+++ b/src/Parser/Custom.elm
@@ -10,9 +10,8 @@ module Parser.Custom exposing
= )
=
=import Goal exposing (Charset, Goal)
-import Parser exposing ((|.), (|=), Parser)
-import Goal exposing (Goal)
=import Goal.CompleteTheSentence as CompleteTheSentence
+import Parser exposing ((|.), (|=), Parser)
=
=
=levels : Parser (List Goal)
@@ -86,8 +85,9 @@ line set =
= |. Parser.spaces
= |. Parser.symbol "}"
= |= outro
- |> Parser.map (CompleteTheSentence.init)
- |> Parser.map (Goal.CompleteTheSentence)
+ -- For now area is centered at ( 0, 0 ), but will be reset when the game starts
+ |> Parser.map (CompleteTheSentence.init ( 0, 0 ))
+ |> Parser.map Goal.CompleteTheSentence
=
=
=intro : Parser Stringindex 343fbf8..0d3c9d5 100644
--- a/src/Position.elm
+++ b/src/Position.elm
@@ -13,6 +13,7 @@ import List.Extra as List
=
=
=type alias Position =
+ -- TODO: Convert to record with x and y fields for more expressive code?
= ( Int
= , Int
= )index 444692c..c31df08 100644
--- a/src/Tutorial.elm
+++ b/src/Tutorial.elm
@@ -58,7 +58,8 @@ goals =
= "Collect letters A B and C in order: "
= "ABC"
= ""
- |> CompleteTheSentence.init
+ -- For now area is centered at ( 0, 0 ), but will be reset when the game starts
+ |> CompleteTheSentence.init (0, 0)
= |> Goal.CompleteTheSentence
= , Goal.ReadInstructions
= [ "Save the Snake!" |> Html.text |> Html.wrap Html.h1 []Extend the "safe zone" in front of the snake to 5
On by
Too often a flame spawn right in front and give players a nasty surprise.
index 79ffb72..aeb4ee0 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -1697,7 +1697,7 @@ randomSpawn area goal snake entities =
= isInFrontOfTheSnake : Position -> Bool
= isInFrontOfTheSnake position =
= snake.head
- |> Position.line snake.direction 3
+ |> Position.line snake.direction 5
= |> List.member position
=
= insert : Bool -> Position -> Char -> EntitiesMake sure the next required letter is always there
On by
Sometimes the required letter is not spawning for a long time. It's annoying and makes the game too difficult. On the other hand, to prevent the correct letter from spawning too many times, lower it's frequency to same level as the rest of the character set.
index aeb4ee0..4c34cd8 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -1700,13 +1700,13 @@ randomSpawn area goal snake entities =
= |> Position.line snake.direction 5
= |> List.member position
=
- insert : Bool -> Position -> Char -> Entities
- insert spawn position entity =
+ insert : Entities -> Bool -> Position -> Char -> Entities
+ insert existing spawn position entity =
= if spawn && not (isUnderTheSnake position) && not (isInFrontOfTheSnake position) then
- Entities.spawn position entity entities
+ Entities.spawn position entity existing
=
= else
- entities
+ existing
= in
= case goal of
= CompleteTheSentence model ->
@@ -1716,16 +1716,34 @@ randomSpawn area goal snake entities =
=
= charset =
= '🔥' :: model.sentence.charset
+
+ spawnIfNotPresent : Entity -> Entities -> Random.Generator Entities
+ spawnIfNotPresent entity existing =
+ if Entities.isPresent entity existing then
+ Random.constant existing
+
+ else
+ area
+ |> Area.randomPosition
+ |> Random.map
+ (\position ->
+ insert
+ existing
+ True
+ position
+ entity
+ )
= in
= case CompleteTheSentence.missing model of
= [] ->
= Random.constant entities
=
= next :: following ->
- Random.map3 insert
+ Random.map3 (insert entities)
= (randomBoolean probability)
= (Area.randomPosition area)
= (randomEntity next charset)
+ |> Random.andThen (spawnIfNotPresent next)
=
= ReadInstructions _ ->
= Random.constant Dict.empty
@@ -1740,7 +1758,7 @@ randomSpawn area goal snake entities =
=
= spawnCollectible : Position -> Entities
= spawnCollectible position =
- insert True position collectible.entity
+ insert entities True position collectible.entity
= in
= if Entities.isPresent collectible.entity entities then
= case rubbish of
@@ -1748,7 +1766,7 @@ randomSpawn area goal snake entities =
= Random.constant entities
=
= one :: more ->
- Random.map3 insert
+ Random.map3 (insert entities)
= (randomBoolean probability)
= (Area.randomPosition { area | center = snake.head })
= (Random.uniform one more)
@@ -1801,7 +1819,7 @@ randomEntity : Entity -> Goal.Charset -> Random.Generator Entity
=randomEntity promoted charset =
= charset
= |> List.map (Tuple.pair 1)
- |> Random.weighted ( 3, promoted )
+ |> Random.weighted ( 0, promoted )
=
=
=randomBoolean : Float -> Random.Generator BoolAdd the Taal Cafe levels
On by
These are Dutch sentences I learn at language cafe in my local library. For extra hints I've added emojis to some of the sentences.
new file mode 100644
index 0000000..b444bd0
--- /dev/null
+++ b/static/levels/taal-cafe.snake
@@ -0,0 +1,23 @@
+A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Ë É Ï Ó Ö Ü
+
+In Nederland is de meerderheid van de bevolking { ATHEÏST } 🙅✝️🕉️☪️☸️✡️☯️.
+Vorige week { REGENDE } het. ☔
+Ik ben { ONDERE } de indruk. 🤩
+Het klimaat is { ANDERS } in Spanje 🇪🇸 en Nederland 🇳🇱.
+Het { WORDT } koud en het sneeuwt nu. 🌨
+Het regent nu. { NEEM } uw jas. ☔🧥
+Het { HEEFT } geregend. ☔
+Het is jouw { VERJAARDAG }. Jij bent jarig.
+Gisteren heeft het { GESNEEUWD } en de wei ligt nog steeds onder de sneeuw.
+Als je { JARIG } bent trakteer je de klas. 🎒🍬
+Pasen heeft geen { VASTE } datum.
+Dat { WIST } ik niet voordat je het me vertelde.
+Nu is het een bar, maar het { WORDT } een restaurant.
+Ze heeft rechten { GESTUDEERD } in Iran. 🇮🇷👩🎓👩⚖️
+Sri Lanka is { BEROEMD } door de rijst en thee. 🇱🇰🌾🫖
+Iran is beroemd om de Perzische { TAPIJTEN }.
+De meeste mensen in dit land zijn { REDELIJKER } dan in het mijne.
+Haast je niet. Wacht op je { BEURT }.
+We zijn nog niet { GETROUWD }. 🤵♀️👰♂️
+In Polen 🇵🇱 en Nederland 🇳🇱 is het { RECHTSSYSTEEM } 👩⚖️verschillend.
+De helft van de { BEVOLKING } is vrouw. 👭Rename the Game.viewArea function to viewWorld
On by
It makes more sense to me, as it presents the game world.
index 4c34cd8..43ab353 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -9,7 +9,7 @@ port module Game exposing
= , subscriptions
= , update
= , view
- , viewArea
+ , viewWorld
= , viewSentence
= )
=
@@ -180,19 +180,19 @@ view model =
=
= CompleteTheSentence completion ->
= [ viewSentence completion
- , viewArea model
+ , viewWorld model
= , viewCurtain model.countdown model
= ]
=
= ChangeDirections directions ->
= [ viewDirections directions
- , viewArea model
+ , viewWorld model
= , viewCurtain model.countdown model
= ]
=
= CollectSingle _ collectibles ->
= [ viewCollectibles collectibles
- , viewArea model
+ , viewWorld model
= , viewCurtain model.countdown model
= ]
= in
@@ -431,10 +431,11 @@ viewHeader contents =
= contents
=
=
-viewArea : Model -> Html Msg
-viewArea model =
+{-| Displays the snake and it's environment -}
+viewWorld : Model -> Html Msg
+viewWorld model =
= let
- entities =
+ world =
= [ viewBackground model.tiles
= , viewAnnotations model.annotations
= , viewCollected model
@@ -474,7 +475,7 @@ viewArea model =
= , Touch.onCancel TouchCanceled
= ]
= [ viewDefs
- , entities
+ , world
= ]
=
=Fix Android Chrome glitch + un-group SVG elements
On by
Today I've been troubleshooting the Android Chrome glitch described here
https://bugs.chromium.org/p/chromium/issues/detail?id=1412901
Following the advice from the Chromium developer I tried to "un-group" the elements that go into the SVG element. It did not solve the problem. Neither did changing the scale of appearing and disappearing entities from 0 to 0.01.
In the end I discovered that not setting the transform transition on the entities elements prevents the glitch. I do like the transition animation. It gives more organic feel to the game. So maybe I'll introduce a setting to enable or disable it. For now it's commented out.
Perhaps because the un-grouping took so much effort, I feel like it might be a good change to keep. I'm going to test on a different branch if just removing the transition resolves the glitch.
index 05ab84d..99fd5ef 100644
--- a/elm.json
+++ b/elm.json
@@ -20,6 +20,7 @@
= "elm-community/list-extra": "8.7.0",
= "elm-community/maybe-extra": "5.2.0",
= "elm-community/result-extra": "2.4.0",
+ "elm-community/string-extra": "4.0.1",
= "krisajenkins/remotedata": "6.0.1",
= "mpizenberg/elm-pointer-events": "4.0.2",
= "ohanhi/keyboard": "2.0.1",index 43ab353..9ee9928 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -9,8 +9,8 @@ port module Game exposing
= , subscriptions
= , update
= , view
- , viewWorld
= , viewSentence
+ , viewWorld
= )
=
=import Area exposing (Area)
@@ -34,11 +34,13 @@ import Position exposing (Position)
=import Random
=import Set exposing (Set)
=import Snake exposing (Snake)
+import String.Extra as String
=import Svg exposing (Svg)
=import Svg.Attributes
=import Svg.Keyed
=import Tad.Basics exposing (..)
=import Tad.Html as Html
+import Tad.String as String
=import Time
=import Transformations
=
@@ -431,18 +433,20 @@ viewHeader contents =
= contents
=
=
-{-| Displays the snake and it's environment -}
+{-| Displays the snake and it's environment
+-}
=viewWorld : Model -> Html Msg
=viewWorld model =
= let
= world =
- [ viewBackground model.tiles
- , viewAnnotations model.annotations
- , viewCollected model
+ [ viewBackgroundTiles model.tiles
+ , viewAnnotations 0 model.annotations
+ , viewTemporaryAnnotations model
= , viewEntities model.entities
= , viewSnake model.progress model.snake
= ]
- |> Svg.g
+ |> List.concat
+ |> Svg.Keyed.node "g"
= [ model.pan
= |> Pan.value
= |> Coordinates.multiply -1
@@ -450,6 +454,7 @@ viewWorld model =
= |> Transformations.toString
= |> Html.Attributes.style "transform"
= , Html.Attributes.style "transition" "transform 200ms linear"
+ , Html.Attributes.style "pointer-events" "none"
= ]
=
= viewbox : String
@@ -479,8 +484,16 @@ viewWorld model =
= ]
=
=
-viewCollected : Model -> Svg Msg
-viewCollected model =
+{-| These are annotations in the making
+
+For example, while the letters are collected, the annotation shows what has been
+collected so far. It will be updated with new letters. Once the whole word is
+collected, it will be displayed as a permanent annotation. Permanent annotations
+are not to be changed.
+
+-}
+viewTemporaryAnnotations : Model -> List ( String, Svg Msg )
+viewTemporaryAnnotations model =
= case model.goal of
= CompleteTheSentence completion ->
= if CompleteTheSentence.isComplete completion then
@@ -491,6 +504,13 @@ viewCollected model =
= text =
= "Right. Now escape the trap!"
=
+ key =
+ text
+ |> String.latinize
+ |> String.toLower
+ |> String.dasherize
+ |> String.append "temporary-annotation-"
+
= fontSize =
= autoFontSize text
=
@@ -498,11 +518,14 @@ viewCollected model =
= completion.area.center
= |> Coordinates.fromPosition scale
= |> Coordinates.shift (1.5 * scale) South
+
+ svg =
+ text
+ |> Svg.text
+ |> List.singleton
+ |> viewBigText fontSize coordinates
= in
- text
- |> Svg.text
- |> List.singleton
- |> viewBigText fontSize coordinates
+ [ ( key, svg ) ]
=
= else
= let
@@ -520,33 +543,49 @@ viewCollected model =
= [ Svg.Attributes.fill "none"
= , Svg.Attributes.stroke "none"
= ]
+
+ key =
+ completion.sentence.password
+ |> String.latinize
+ |> String.toLower
+ |> String.dasherize
+ |> String.append "temporary-annotation-"
+
+ svg =
+ completion.sentence.password
+ |> String.toList
+ |> List.indexedMap
+ (\index character ->
+ character
+ |> String.fromChar
+ |> Svg.text
+ |> Html.wrap Svg.tspan
+ (if index < String.length completion.collected then
+ visible
+
+ else
+ invisible
+ )
+ )
+ |> viewBigText fontSize coordinates
= in
- completion.sentence.password
- |> String.toList
- |> List.indexedMap
- (\index character ->
- character
- |> String.fromChar
- |> Svg.text
- |> Html.wrap Svg.tspan
- (if index < String.length completion.collected then
- visible
-
- else
- invisible
- )
- )
- |> viewBigText fontSize coordinates
+ [ ( key, svg ) ]
=
= _ ->
- "" |> Svg.text
+ []
+
=
+viewAnnotations : Int -> List Annotation -> List ( String, Svg Msg )
+viewAnnotations index annotations =
+ case annotations of
+ [] ->
+ []
=
-viewAnnotations : List Annotation -> Svg Msg
-viewAnnotations annotations =
- annotations
- |> List.reverseMap viewAnnotation
- |> Svg.g [ Svg.Attributes.id "annotations" ]
+ annotation :: rest ->
+ ( index |> String.fromInt
+ , annotation |> viewAnnotation
+ )
+ :: viewAnnotations (index + 1) rest
=
=
=viewAnnotation : Annotation -> Svg Msg
@@ -581,49 +620,54 @@ viewBigText fontSize coordinates text =
= ]
=
=
-viewBackground : Set Position -> Svg Msg
-viewBackground tiles =
+viewBackgroundTiles : Set Position -> List ( String, Svg Msg )
+viewBackgroundTiles tiles =
= tiles
= |> Set.toList
= |> List.map viewBackgroundTile
- |> Svg.g []
=
=
-viewBackgroundTile : Position -> Svg Msg
+viewBackgroundTile : Position -> ( String, Svg Msg )
=viewBackgroundTile ( x, y ) =
= let
= length =
= backgroundTileSize
= |> Position.sectorLength
= |> toFloat
+
+ key =
+ [ x, y ]
+ |> List.map String.fromInt
+ |> String.join "-"
+ |> String.append "background-tile-"
+
+ image =
+ Svg.image
+ [ length * scale |> String.fromFloat |> Svg.Attributes.width
+ , length * scale |> String.fromFloat |> Svg.Attributes.height
+ , Svg.Attributes.xlinkHref "/cartographer.webp"
+ , length * scale / -2 |> String.fromFloat |> Svg.Attributes.x
+ , length * scale / -2 |> String.fromFloat |> Svg.Attributes.y
+ , Transformations.Translate "px" (toFloat x * scale * length) (toFloat y * scale * length)
+ |> Transformations.toString
+ |> Html.Attributes.style "transform"
+ ]
+ []
= in
- Svg.image
- [ length * scale |> String.fromFloat |> Svg.Attributes.width
- , length * scale |> String.fromFloat |> Svg.Attributes.height
- , Svg.Attributes.xlinkHref "/cartographer.webp"
- , length * scale / -2 |> String.fromFloat |> Svg.Attributes.x
- , length * scale / -2 |> String.fromFloat |> Svg.Attributes.y
- , Transformations.Translate "px" (toFloat x * scale * length) (toFloat y * scale * length)
- |> Transformations.toString
- |> Html.Attributes.style "transform"
- ]
- []
+ ( key, image )
=
=
-viewEntities : Entities -> Svg Msg
+viewEntities : Entities -> List ( String, Svg Msg )
=viewEntities entities =
= entities
= |> Dict.toList
- |> List.map viewPosition
- |> Svg.Keyed.node "g"
- [ Html.Attributes.style "pointer-events" "none"
- ]
+ |> List.concatMap viewEntitiesAt
=
=
-viewSnake : Float -> Snake -> Svg Msg
+viewSnake : Float -> Snake -> List ( String, Svg Msg )
=viewSnake progress snake =
= if Snake.isDead snake then
- Svg.text ""
+ []
=
= else
= let
@@ -658,6 +702,8 @@ viewSnake progress snake =
= |> Transformations.toString
= |> Svg.Attributes.transform
= ]
+ |> Tuple.pair "snake-dead"
+ |> List.singleton
=
= [ neck ] ->
= -- Snake only has one segment. Head goes straight from it.
@@ -692,6 +738,8 @@ viewSnake progress snake =
= , Svg.Attributes.markerEnd "url(#tail-marker)"
= ]
= []
+ |> Tuple.pair "the-snake"
+ |> List.singleton
=
= neck :: torso :: tail ->
= -- A proper snake.
@@ -741,6 +789,8 @@ viewSnake progress snake =
= , Svg.Attributes.markerEnd "url(#tail-marker)"
= ]
= []
+ |> Tuple.pair "the-snake"
+ |> List.singleton
=
=
=dTail : Float -> Position -> List Direction -> List String
@@ -862,29 +912,23 @@ viewDefs =
= ]
=
=
-viewPosition : ( Position, Entities.State ) -> ( String, Svg Msg )
-viewPosition ( ( x, y ), state ) =
+viewEntitiesAt : ( Position, Entities.State ) -> List ( String, Svg Msg )
+viewEntitiesAt ( ( x, y ) as position, state ) =
= let
- key : String
- key =
- [ x, y ]
- |> List.map String.fromInt
- |> String.join "-"
- in
- state
- |> viewState
- |> Svg.g
- [ Transformations.Translate ""
- (Basics.toFloat x * scale)
- (Basics.toFloat y * scale)
- |> Transformations.toString
- |> Svg.Attributes.transform
+ key : Entity -> String
+ key entity =
+ [ entity |> String.fromChar
+ , x |> String.fromInt
+ , y |> String.fromInt
= ]
- |> Tuple.pair key
-
+ |> String.join "-"
=
-viewState : Entities.State -> List (Svg Msg)
-viewState state =
+ withKey : Entity -> Svg Msg -> ( String, Svg Msg )
+ withKey entity svg =
+ ( key entity
+ , svg
+ )
+ in
= case state of
= Entities.Empty ->
= []
@@ -892,75 +936,86 @@ viewState state =
= Entities.Appearing entity ->
= [ entity
= |> viewEntity
- |> Html.wrap Svg.g
- [ Transformations.Scale 0 0
- |> Transformations.toString
+ [ [ position |> Coordinates.fromPosition scale |> Coordinates.toTransformation "px"
+ , Transformations.Scale 0.01 0.01
+ ]
+ |> List.map Transformations.toString
+ |> String.join " "
= |> Html.Attributes.style "transform"
- , Html.Attributes.style "transition" "transform 200ms"
= ]
+ |> withKey entity
= ]
=
= Entities.Present entity ->
= [ entity
= |> viewEntity
- |> Html.wrap Svg.g
- [ Transformations.Scale 1 1
- |> Transformations.toString
+ [ [ position |> Coordinates.fromPosition scale |> Coordinates.toTransformation "px"
+ , Transformations.Scale 1 1
+ ]
+ |> List.map Transformations.toString
+ |> String.join " "
= |> Html.Attributes.style "transform"
- , Html.Attributes.style "transition" "transform 200ms"
= ]
+ |> withKey entity
= ]
=
= Entities.Disappearing entity ->
= [ entity
= |> viewEntity
- |> Html.wrap Svg.g
- [ Transformations.Scale 0 0
- |> Transformations.toString
+ [ [ position |> Coordinates.fromPosition scale |> Coordinates.toTransformation "px"
+ , Transformations.Scale 0.01 0.01
+ ]
+ |> List.map Transformations.toString
+ |> String.join " "
= |> Html.Attributes.style "transform"
- , Html.Attributes.style "transition" "transform 200ms"
+ , Html.Attributes.style "opacity" "0.3"
= ]
+ |> withKey entity
= ]
=
= Entities.Replacing old new ->
= [ old
= |> viewEntity
- |> Html.wrap Svg.g
- [ Transformations.Scale 0 0
- |> Transformations.toString
+ [ [ position |> Coordinates.fromPosition scale |> Coordinates.toTransformation "px"
+ , Transformations.Scale 0.01 0.01
+ ]
+ |> List.map Transformations.toString
+ |> String.join " "
= |> Html.Attributes.style "transform"
- , Html.Attributes.style "transition" "transform 200ms"
+ , Html.Attributes.style "opacity" "0.3"
= ]
+ |> withKey old
= , new
= |> viewEntity
- |> Html.wrap Svg.g
- [ Transformations.Scale 0 0
- |> Transformations.toString
+ [ [ position |> Coordinates.fromPosition scale |> Coordinates.toTransformation "px"
+ , Transformations.Scale 0.01 0.01
+ ]
+ |> List.map Transformations.toString
+ |> String.join " "
= |> Html.Attributes.style "transform"
- , Html.Attributes.style "transition" "transform 200ms"
= ]
+ |> withKey new
= ]
=
= Entities.Collected entity ->
= [ entity
= |> viewEntity
- |> Html.wrap Svg.g
- [ Transformations.Scale 20 20
- |> Transformations.toString
+ [ [ position |> Coordinates.fromPosition scale |> Coordinates.toTransformation "px"
+ , Transformations.Scale 20 20
+ ]
+ |> List.map Transformations.toString
+ |> String.join " "
= |> Html.Attributes.style "transform"
- , Html.Attributes.style "opacity" "0"
- , Html.Attributes.style "transition" "transform 200ms, opacity 200ms"
+ , Html.Attributes.style "opacity" "0.3"
= ]
+ |> withKey entity
= ]
=
=
-viewEntity : Entity -> Svg Msg
-viewEntity entity =
- entity
- |> String.fromChar
- |> Svg.text
- |> List.singleton
- |> Svg.text_
+viewEntity : List (Svg.Attribute Msg) -> Entity -> Svg Msg
+viewEntity customAttributes entity =
+ let
+ defaultAttributes =
= [ Html.Attributes.style "text-anchor" "middle"
= , Html.Attributes.style "dominant-baseline" "middle"
= , Svg.Attributes.width (scale |> String.fromFloat)
@@ -968,8 +1023,20 @@ viewEntity entity =
= , Svg.Attributes.fontSize (0.6 * scale |> String.fromFloat)
= , Svg.Attributes.fontWeight "bold"
= , Svg.Attributes.fill "hsl(0, 0%, 86%)"
+
+ -- With the transition enabled, we trigger the Android Chrome bug
+ -- https://bugs.chromium.org/p/chromium/issues/detail?id=1412901
+ -- , Html.Attributes.style "transition" "transform 200ms, opacity 200ms"
= ]
=
+ attributes =
+ defaultAttributes ++ customAttributes
+ in
+ entity
+ |> String.fromChar
+ |> Svg.text
+ |> Html.wrap Svg.text_ attributes
+
=
=
=-- UPDATERemove unnecessary transition animation
On by
The transform of the whole world view was being transitioned, despite it's updated every animation frame. This was wasteful.
When I discovered it I was hoping that this was the real trigger for the Android Chrome glitch, but unfortunately enabling transitions on individual entities after removing the world transition triggered the glitch again.
index 9ee9928..62edc59 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -453,7 +453,6 @@ viewWorld model =
= |> Coordinates.toTransformation "px"
= |> Transformations.toString
= |> Html.Attributes.style "transform"
- , Html.Attributes.style "transition" "transform 200ms linear"
= , Html.Attributes.style "pointer-events" "none"
= ]
=