├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── elm.json ├── examples ├── elm.json └── src │ └── Main.elm └── src └── Logic ├── Component.elm ├── Entity.elm ├── Internal.elm └── System.elm /.gitattributes: -------------------------------------------------------------------------------- 1 | bundle.js linguist-generated=true 2 | gh-pages/* linguist-detectable=false 3 | *.html linguist-vendored 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Logs 2 | logs 3 | *.log 4 | 5 | #Dependencies 6 | node_modules/ 7 | elm-stuff/ 8 | assets/ 9 | gh-pages/assets 10 | gh-pages/assets/ 11 | # Yarn Integrity file 12 | .yarn-integrity 13 | 14 | 15 | #OS stuff 16 | .DS_Store 17 | .vscode 18 | *.iml 19 | .idea 20 | 21 | #Other stuff 22 | documentation.json 23 | 24 | /examples/index.html 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-present, Romāns Potašovs. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Game Logic 2 | 3 | An ECS library for Elm. `Logic` provides an easy way to build a full game, using common Entity-Component-System architecture. 4 | 5 | 6 | ## Entity–component–system (ECS) 7 | 8 | is an architectural pattern that is mostly used in game development. ECS follows the composition over inheritance principle that allows greater flexibility in defining entities where every object in a game's scene is an entity (e.g. enemies, bullets, vehicles, etc.). Every entity consists of one or more components that add behavior or functionality. Therefore, the behavior of an entity can be changed at runtime by adding or removing components. This eliminates the ambiguity problems of deep and wide inheritance hierarchies that are difficult to understand, maintain and extend. Common ECS approaches are highly compatible and often combined with data-oriented design techniques. 9 | 10 | ## Entity 11 | 12 | An entity is just a combination of components 13 | 14 | ```elm 15 | Entity.create id world 16 | |> Entity.with ( positionSpec, ( 100, 100 ) ) 17 | |> Entity.with ( velocitySpec, ( -1, -1 ) ) 18 | ``` 19 | 20 | ## Component 21 | 22 | A component can be any type of elm 23 | ```elm 24 | type alias Position = (Int, Int) 25 | type alias Velocity = (Int, Int) 26 | type alias Life = Int 27 | ``` 28 | 29 | ## System 30 | 31 | 32 | ```elm 33 | system : Logic.Component.Spec Velocity world -> Logic.Component.Spec Position world -> System world 34 | system = 35 | Logic.System.step2 36 | (\( velocity, _ ) ( pos, setPos ) -> setPos (Vec2.add velocity pos)) 37 | ``` 38 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "justgook/elm-game-logic", 4 | "summary": "An ECS library for Elm. Provides an easy way to build a full game", 5 | "license": "BSD-3-Clause", 6 | "version": "3.0.0", 7 | "exposed-modules": [ 8 | "Logic.System", 9 | "Logic.Entity", 10 | "Logic.Component" 11 | ], 12 | "elm-version": "0.19.0 <= v < 0.20.0", 13 | "dependencies": { 14 | "elm/core": "1.0.2 <= v < 2.0.0" 15 | }, 16 | "test-dependencies": { 17 | "elm-explorations/test": "1.0.0 <= v < 2.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src", 5 | "../src" 6 | ], 7 | "elm-version": "0.19.1", 8 | "dependencies": { 9 | "direct": { 10 | "elm/browser": "1.0.1", 11 | "elm/core": "1.0.2", 12 | "elm/html": "1.0.0" 13 | }, 14 | "indirect": { 15 | "elm/json": "1.1.3", 16 | "elm/time": "1.0.0", 17 | "elm/url": "1.0.0", 18 | "elm/virtual-dom": "1.0.2" 19 | } 20 | }, 21 | "test-dependencies": { 22 | "direct": {}, 23 | "indirect": {} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser 4 | import Browser.Events 5 | import Html exposing (..) 6 | import Html.Attributes exposing (style) 7 | import Logic.Component as Component 8 | import Logic.Entity as Entity 9 | import Logic.System as System exposing (System, applyIf) 10 | 11 | 12 | main : Program () World Float 13 | main = 14 | Browser.element 15 | { init = \_ -> ( spawn world, Cmd.none ) 16 | , update = \_ w -> ( system velocitySpec positionSpec w, Cmd.none ) 17 | , subscriptions = \_ -> Browser.Events.onAnimationFrameDelta identity 18 | , view = 19 | \w -> 20 | System.foldl 21 | (\( px, py ) acc -> 22 | div 23 | [ style "width" "30px" 24 | , style "height" "30px" 25 | , style "position" "absolute" 26 | , style "top" "0" 27 | , style "left" "0" 28 | , style "background" "red" 29 | , style "transform" ("translate(" ++ String.fromInt px ++ "px, " ++ String.fromInt py ++ "px)") 30 | ] 31 | [] 32 | :: acc 33 | ) 34 | (positionSpec.get w) 35 | [] 36 | |> div [] 37 | } 38 | 39 | 40 | system : Component.Spec ( Int, Int ) World -> Component.Spec ( Int, Int ) World -> System World 41 | system spec1 spec2 w = 42 | System.step2 43 | (\( ( vx, vy ), setVel ) ( ( px, py ), setPos ) acc -> 44 | let 45 | x = 46 | vx + px 47 | 48 | y = 49 | vy + py 50 | in 51 | acc 52 | |> setPos ( x, y ) 53 | |> applyIf (x < 0) (setVel ( abs vx, vy )) 54 | |> applyIf (x > w.windowWidth) (setVel ( abs vx * -1, vy )) 55 | |> applyIf (y < 0) (setVel ( vx, abs vy )) 56 | |> applyIf (y > w.windowHeight) (setVel ( vx, abs vy * -1 )) 57 | ) 58 | spec1 59 | spec2 60 | w 61 | 62 | 63 | type alias World = 64 | { position : Component.Set ( Int, Int ) 65 | , velocity : Component.Set ( Int, Int ) 66 | , windowWidth : Int 67 | , windowHeight : Int 68 | } 69 | 70 | 71 | world : World 72 | world = 73 | { position = Component.empty 74 | , velocity = Component.empty 75 | , windowWidth = 800 76 | , windowHeight = 600 77 | } 78 | 79 | 80 | spawn : World -> World 81 | spawn w_ = 82 | List.range 0 10 83 | |> List.foldl 84 | (\i -> 85 | Entity.create i 86 | >> Entity.with ( positionSpec, ( i * 3, i * 5 ) ) 87 | >> Entity.with ( velocitySpec, ( modBy 3 i + 1, modBy 5 i + 1 ) ) 88 | >> Tuple.second 89 | ) 90 | w_ 91 | 92 | 93 | positionSpec : Component.Spec ( Int, Int ) { world | position : Component.Set ( Int, Int ) } 94 | positionSpec = 95 | Component.Spec .position (\comps w -> { w | position = comps }) 96 | 97 | 98 | velocitySpec : Component.Spec ( Int, Int ) { world | velocity : Component.Set ( Int, Int ) } 99 | velocitySpec = 100 | Component.Spec .velocity (\comps w -> { w | velocity = comps }) 101 | -------------------------------------------------------------------------------- /src/Logic/Component.elm: -------------------------------------------------------------------------------- 1 | module Logic.Component exposing 2 | ( Set, Spec, empty 3 | , set, spawn, remove 4 | , get, get2, update 5 | , map, filterMap 6 | , fromList, toList 7 | , fromDict, toDict 8 | ) 9 | 10 | {-| **Component**: the raw data for one aspect of the object, and how it interacts with the world. "Labels the Entity as possessing this particular aspect". 11 | 12 | Example: 13 | 14 | type alias Velocity = 15 | { x : Float, y : Float } 16 | 17 | spec : Component.Spec Velocity { world | v : Component.Set Velocity } 18 | spec = 19 | Component.Spec .v (\comps world -> { world | v = comps }) 20 | 21 | empty : Component.Set Velocity 22 | empty = 23 | Component.empty 24 | 25 | @docs Set, Spec, empty 26 | 27 | 28 | # Manipulations 29 | 30 | @docs set, spawn, remove 31 | @docs get, get2, update 32 | @docs map, filterMap 33 | 34 | 35 | # List 36 | 37 | @docs fromList, toList 38 | 39 | 40 | # Dict 41 | 42 | @docs fromDict, toDict 43 | 44 | -} 45 | 46 | import Array exposing (Array) 47 | import Dict exposing (Dict) 48 | import Logic.Internal exposing (indexedFoldlArray) 49 | 50 | 51 | {-| Component storage, the main building block of the world 52 | -} 53 | type alias Set comp = 54 | Array (Maybe comp) 55 | 56 | 57 | type alias EntityID = 58 | Int 59 | 60 | 61 | {-| Component specification, how to get `Component.Set` from the world and set back into the world (mainly used by Systems) 62 | -} 63 | type alias Spec comp world = 64 | { get : world -> Set comp 65 | , set : Set comp -> world -> world 66 | } 67 | 68 | 69 | {-| Create an empty `Component.Set` - mostly used to init component sets in the world. 70 | -} 71 | empty : Set comp 72 | empty = 73 | Array.empty 74 | 75 | 76 | {-| Remove component from `Component.Set` by `EntityID`, or return unchanged if component not in `Set`. 77 | -} 78 | remove : EntityID -> Set a -> Set a 79 | remove entityID components = 80 | Array.set entityID Nothing components 81 | 82 | 83 | {-| Safe way to create a component, same as `set`, only if an index is out of range `Component.Set` will be stretched. 84 | 85 | test = 86 | -- Just 5 87 | empty |> spawn 5 10 |> get 5 88 | 89 | -} 90 | spawn : EntityID -> a -> Set a -> Set a 91 | spawn id value components = 92 | if id - Array.length components < 0 then 93 | Array.set id (Just value) components 94 | 95 | else 96 | Array.append components (Array.repeat (id - Array.length components) Nothing) 97 | |> Array.push (Just value) 98 | 99 | 100 | {-| Set the component at a particular index. Returns an updated `Component.Set`. If the index is out of range, the `Component.Set` is unaltered. 101 | 102 | test = 103 | -- Nothing 104 | empty |> set 5 10 |> get 5 105 | 106 | -} 107 | set : EntityID -> a -> Set a -> Set a 108 | set id value components = 109 | Array.set id (Just value) components 110 | 111 | 112 | {-| Get component for `EntityId`. 113 | -} 114 | get : EntityID -> Set comp -> Maybe comp 115 | get id = 116 | Array.get id >> Maybe.withDefault Nothing 117 | 118 | 119 | {-| Get components Tuple for `EntityId`. 120 | -} 121 | get2 : EntityID -> Set comp -> Set comp2 -> Maybe ( comp, comp2 ) 122 | get2 id set1 set2 = 123 | Maybe.map2 Tuple.pair 124 | (get id set1) 125 | (get id set2) 126 | 127 | 128 | {-| Filter out certain components. 129 | -} 130 | filterMap : (comp -> Maybe comp) -> Set comp -> Set comp 131 | filterMap f comps = 132 | Array.map (Maybe.andThen f) comps 133 | 134 | 135 | {-| Apply a function on every component in a `Component.Set`. 136 | 137 | map sqrt (fromList [ ( 0, 1 ), ( 1, 4 ), ( 2, 9 ) ]) 138 | |> (==) fromList [ ( 0, 1 ), ( 1, 2 ), ( 2, 3 ) ] 139 | 140 | -} 141 | map : (comp -> comp) -> Set comp -> Set comp 142 | map f comps = 143 | Array.map (Maybe.map f) comps 144 | 145 | 146 | {-| Update Component by `EntityID`. 147 | -} 148 | update : EntityID -> (comp -> comp) -> Set comp -> Set comp 149 | update i f = 150 | Logic.Internal.update i (Maybe.map f) 151 | 152 | 153 | {-| Create a `Component.Set` from a `List`. 154 | 155 | **Note**: Useful for data serialization. 156 | 157 | -} 158 | fromList : List ( EntityID, a ) -> Set a 159 | fromList = 160 | List.foldl (\( index, value ) components -> spawn index value components) empty 161 | 162 | 163 | {-| Convert a `Component.Set` into an association list of id-component pairs, sorted by id. 164 | 165 | **Note**: Useful for data deserialization. 166 | 167 | -} 168 | toList : Set a -> List ( EntityID, a ) 169 | toList = 170 | indexedFoldlArray (\i a acc -> Maybe.map (\a_ -> ( i, a_ ) :: acc) a |> Maybe.withDefault acc) [] 171 | 172 | 173 | {-| Create a `Component.Set` from a dictionary. 174 | 175 | **Note**: Useful for data serialization. 176 | 177 | -} 178 | fromDict : Dict EntityID a -> Set a 179 | fromDict = 180 | Dict.foldl (\index value components -> spawn index value components) empty 181 | 182 | 183 | {-| Create a dictionary from a `Component.Set`. 184 | 185 | **Note**: Useful for data deserialization. 186 | 187 | -} 188 | toDict : Set a -> Dict EntityID a 189 | toDict = 190 | indexedFoldlArray 191 | (\i a acc -> 192 | Maybe.map (\a_ -> Dict.insert i a_ acc) a 193 | |> Maybe.withDefault acc 194 | ) 195 | Dict.empty 196 | -------------------------------------------------------------------------------- /src/Logic/Entity.elm: -------------------------------------------------------------------------------- 1 | module Logic.Entity exposing (EntityID, create, with, remove) 2 | 3 | {-| **Entity**: The entity is a general-purpose object. It only consists of a unique ID. They "tag every coarse game object as a separate item". 4 | Example: 5 | 6 | Entity.create id world 7 | |> Entity.with ( positionSpec, positionComponent ) 8 | |> Entity.with ( velocitySpec, velocityComponent ) 9 | 10 | @docs EntityID, create, with, remove 11 | 12 | -} 13 | 14 | import Array 15 | import Logic.Component as Component 16 | 17 | 18 | {-| -} 19 | type alias EntityID = 20 | Int 21 | 22 | 23 | {-| Start point for spawning `Entity` 24 | 25 | Entity.create id world 26 | |> Entity.with ( positionSpec, positionComponent ) 27 | |> Entity.with ( velocitySpec, velocityComponent ) 28 | 29 | -} 30 | create : EntityID -> world -> ( EntityID, world ) 31 | create id world = 32 | ( id, world ) 33 | 34 | 35 | {-| Way to create `Entity` destruction functions, should pipe in all possible component specs. 36 | It also can be used to just disable (remove) some components from an entity. 37 | 38 | remove = 39 | Entity.remove positionSpec 40 | >> Entity.remove velocitySpec 41 | 42 | newWorld = 43 | remove ( id, world ) 44 | 45 | -} 46 | remove : Component.Spec comp world -> ( EntityID, world ) -> ( EntityID, world ) 47 | remove spec ( entityID, world ) = 48 | ( entityID, spec.set (Array.set entityID Nothing (spec.get world)) world ) 49 | 50 | 51 | {-| Set component to spawn with a new entity 52 | 53 | Entity.create ( id, world ) 54 | |> Entity.with ( positionSpec, positionComponent ) 55 | |> Entity.with ( velocitySpec, velocityComponent ) 56 | 57 | -} 58 | with : ( Component.Spec comp world, comp ) -> ( EntityID, world ) -> ( EntityID, world ) 59 | with ( spec, component ) ( entityID, world ) = 60 | let 61 | updatedComponents = 62 | spec.get world 63 | |> Component.spawn entityID component 64 | 65 | updatedWorld = 66 | spec.set updatedComponents world 67 | in 68 | ( entityID, updatedWorld ) 69 | -------------------------------------------------------------------------------- /src/Logic/Internal.elm: -------------------------------------------------------------------------------- 1 | module Logic.Internal exposing (indexedFoldlArray, indexedMap2, map2, map3, map4, map5, update) 2 | 3 | import Array exposing (Array) 4 | 5 | 6 | map2 : (a -> b -> result) -> Array a -> Array b -> Array result 7 | map2 f ws = 8 | apply (Array.map f ws) 9 | 10 | 11 | {-| -} 12 | map3 : (a -> b -> c -> result) -> Array a -> Array b -> Array c -> Array result 13 | map3 f ws xs = 14 | apply (map2 f ws xs) 15 | 16 | 17 | {-| -} 18 | map4 : (a -> b -> c -> d -> result) -> Array a -> Array b -> Array c -> Array d -> Array result 19 | map4 f ws xs ys = 20 | apply (map3 f ws xs ys) 21 | 22 | 23 | {-| -} 24 | map5 : (a -> b -> c -> d -> e -> result) -> Array a -> Array b -> Array c -> Array d -> Array e -> Array result 25 | map5 f ws xs ys zs = 26 | apply (map4 f ws xs ys zs) 27 | 28 | 29 | indexedMap2 : (Int -> a -> b -> result) -> Array a -> Array b -> Array result 30 | indexedMap2 f ws = 31 | apply (Array.indexedMap f ws) 32 | 33 | 34 | indexedFoldlArray : (Int -> a -> b -> b) -> b -> Array a -> b 35 | indexedFoldlArray func acc list = 36 | let 37 | step : a -> ( Int, b ) -> ( Int, b ) 38 | step x ( i, thisAcc ) = 39 | ( i + 1, func i x thisAcc ) 40 | in 41 | Tuple.second (Array.foldl step ( 0, acc ) list) 42 | 43 | 44 | {-| Apply an array of functions to an array of values. 45 | -} 46 | apply : Array (a -> b) -> Array a -> Array b 47 | apply fs xs = 48 | let 49 | l = 50 | min (Array.length fs) (Array.length xs) 51 | 52 | fs_ = 53 | Array.slice 0 l fs 54 | in 55 | fs_ 56 | |> Array.indexedMap 57 | (\n f -> 58 | Maybe.map f (Array.get n xs) 59 | ) 60 | |> Array.toList 61 | |> List.filterMap identity 62 | |> Array.fromList 63 | 64 | 65 | update : Int -> (a -> a) -> Array a -> Array a 66 | update n f a = 67 | case Array.get n a of 68 | Nothing -> 69 | a 70 | 71 | Just element_ -> 72 | Array.set n (f element_) a 73 | -------------------------------------------------------------------------------- /src/Logic/System.elm: -------------------------------------------------------------------------------- 1 | module Logic.System exposing 2 | ( System 3 | , update, step, step2, step3, step4, step5 4 | , foldl, foldl2, foldl3, foldl4, foldl5 5 | , indexedFoldl, indexedFoldl2, indexedFoldl3, indexedFoldl4, indexedFoldl5 6 | , applyIf, applyMaybe 7 | ) 8 | 9 | {-| **System**: main logic driver, that is used to stepping on each game-loop and update `World` 10 | 11 | @docs System 12 | @docs update, step, step2, step3, step4, step5 13 | 14 | @docs foldl, foldl2, foldl3, foldl4, foldl5 15 | @docs indexedFoldl, indexedFoldl2, indexedFoldl3, indexedFoldl4, indexedFoldl5 16 | 17 | 18 | # Util 19 | 20 | @docs applyIf, applyMaybe 21 | 22 | -} 23 | 24 | import Array 25 | import Logic.Component as Component 26 | import Logic.Entity exposing (EntityID) 27 | import Logic.Internal exposing (indexedFoldlArray) 28 | 29 | 30 | {-| Update whole `Component.Set` 31 | -} 32 | update : Component.Spec comp world -> (Component.Set comp -> Component.Set comp) -> System world 33 | update spec f world = 34 | spec.set (f (spec.get world)) world 35 | 36 | 37 | {-| -} 38 | type alias System world = 39 | world -> world 40 | 41 | 42 | {-| Reduce a `Component.Set` from the left. 43 | 44 | Example count how much enemies left in the world: 45 | 46 | enemySet = 47 | enemySpec.get world 48 | 49 | count = 50 | foldl (\_ -> (+) 1) enemySet 0 51 | 52 | -} 53 | foldl : (comp1 -> acc -> acc) -> Component.Set comp1 -> acc -> acc 54 | foldl f comp1 acc_ = 55 | Array.foldl 56 | (\value acc -> 57 | value |> Maybe.map (\a -> f a acc) |> Maybe.withDefault acc 58 | ) 59 | acc_ 60 | comp1 61 | 62 | 63 | {-| Variant of `foldl` that passes the index of the current element to the step function. 64 | 65 | `indexedFoldl` is to `foldl` as `List.indexedMap` is to `List.map`. 66 | 67 | -} 68 | indexedFoldl : (EntityID -> comp1 -> acc -> acc) -> Component.Set comp1 -> acc -> acc 69 | indexedFoldl f comp1 acc_ = 70 | indexedFoldlArray 71 | (\i value acc -> 72 | value |> Maybe.map (\a -> f i a acc) |> Maybe.withDefault acc 73 | ) 74 | acc_ 75 | comp1 76 | 77 | 78 | {-| Step over all entities that have both components and reduce an `Component.Set`s from the left. 79 | -} 80 | foldl2 : (comp1 -> comp2 -> acc -> acc) -> Component.Set comp1 -> Component.Set comp2 -> acc -> acc 81 | foldl2 f comp1 comp2 acc_ = 82 | indexedFoldlArray 83 | (\n value acc -> 84 | Maybe.map2 (\a b -> f a b acc) 85 | value 86 | (Component.get n comp2) 87 | |> Maybe.withDefault acc 88 | ) 89 | acc_ 90 | comp1 91 | 92 | 93 | {-| -} 94 | indexedFoldl2 : (EntityID -> comp1 -> comp2 -> acc -> acc) -> Component.Set comp1 -> Component.Set comp2 -> acc -> acc 95 | indexedFoldl2 f comp1 comp2 acc_ = 96 | indexedFoldlArray 97 | (\n value acc -> 98 | Maybe.map2 (\a b -> f n a b acc) 99 | value 100 | (Component.get n comp2) 101 | |> Maybe.withDefault acc 102 | ) 103 | acc_ 104 | comp1 105 | 106 | 107 | {-| Same as [`foldl2`](#foldl2) only with 3 components 108 | -} 109 | foldl3 : (comp1 -> comp2 -> comp3 -> acc -> acc) -> Component.Set comp1 -> Component.Set comp2 -> Component.Set comp3 -> acc -> acc 110 | foldl3 f comp1 comp2 comp3 acc_ = 111 | indexedFoldlArray 112 | (\n value acc -> 113 | Maybe.map3 (\a b c -> f a b c acc) 114 | value 115 | (Component.get n comp2) 116 | (Component.get n comp3) 117 | |> Maybe.withDefault acc 118 | ) 119 | acc_ 120 | comp1 121 | 122 | 123 | {-| Same as [`indexedFoldl2`](#indexedFoldl2) only with 3 components 124 | -} 125 | indexedFoldl3 : (EntityID -> comp1 -> comp2 -> comp3 -> acc -> acc) -> Component.Set comp1 -> Component.Set comp2 -> Component.Set comp3 -> acc -> acc 126 | indexedFoldl3 f comp1 comp2 comp3 acc_ = 127 | indexedFoldlArray 128 | (\n value acc -> 129 | Maybe.map3 (\a b c -> f n a b c acc) 130 | value 131 | (Component.get n comp2) 132 | (Component.get n comp3) 133 | |> Maybe.withDefault acc 134 | ) 135 | acc_ 136 | comp1 137 | 138 | 139 | {-| Same as [`foldl2`](#foldl2) only with 4 components 140 | -} 141 | foldl4 : 142 | (comp1 -> comp2 -> comp3 -> comp4 -> acc -> acc) 143 | -> Component.Set comp1 144 | -> Component.Set comp2 145 | -> Component.Set comp3 146 | -> Component.Set comp4 147 | -> acc 148 | -> acc 149 | foldl4 f comp1 comp2 comp3 comp4 acc_ = 150 | indexedFoldlArray 151 | (\n value acc -> 152 | Maybe.map4 (\a b c d -> f a b c d acc) 153 | value 154 | (Component.get n comp2) 155 | (Component.get n comp3) 156 | (Component.get n comp4) 157 | |> Maybe.withDefault acc 158 | ) 159 | acc_ 160 | comp1 161 | 162 | 163 | {-| Same as [`indexedFoldl2`](#indexedFoldl2) only with 4 components 164 | -} 165 | indexedFoldl4 : 166 | (EntityID -> comp1 -> comp2 -> comp3 -> comp4 -> acc -> acc) 167 | -> Component.Set comp1 168 | -> Component.Set comp2 169 | -> Component.Set comp3 170 | -> Component.Set comp4 171 | -> acc 172 | -> acc 173 | indexedFoldl4 f comp1 comp2 comp3 comp4 acc_ = 174 | indexedFoldlArray 175 | (\n value acc -> 176 | Maybe.map4 (\a b c d -> f n a b c d acc) 177 | value 178 | (Component.get n comp2) 179 | (Component.get n comp3) 180 | (Component.get n comp4) 181 | |> Maybe.withDefault acc 182 | ) 183 | acc_ 184 | comp1 185 | 186 | 187 | {-| Same as [`foldl2`](#foldl2) only with 5 components 188 | -} 189 | foldl5 : 190 | (comp1 -> comp2 -> comp3 -> comp4 -> acc -> acc) 191 | -> Component.Set comp1 192 | -> Component.Set comp2 193 | -> Component.Set comp3 194 | -> Component.Set comp4 195 | -> acc 196 | -> acc 197 | foldl5 f comp1 comp2 comp3 comp4 acc_ = 198 | indexedFoldlArray 199 | (\n value acc -> 200 | Maybe.map4 (\a b c d -> f a b c d acc) 201 | value 202 | (Component.get n comp2) 203 | (Component.get n comp3) 204 | (Component.get n comp4) 205 | |> Maybe.withDefault acc 206 | ) 207 | acc_ 208 | comp1 209 | 210 | 211 | {-| Same as [`indexedFoldl2`](#indexedFoldl2) only with 5 components 212 | -} 213 | indexedFoldl5 : 214 | (EntityID -> comp1 -> comp2 -> comp3 -> comp4 -> comp5 -> acc -> acc) 215 | -> Component.Set comp1 216 | -> Component.Set comp2 217 | -> Component.Set comp3 218 | -> Component.Set comp4 219 | -> Component.Set comp5 220 | -> acc 221 | -> acc 222 | indexedFoldl5 f comp1 comp2 comp3 comp4 comp5 acc_ = 223 | indexedFoldlArray 224 | (\n value acc -> 225 | Maybe.map5 (\a b c d e -> f n a b c d e acc) 226 | value 227 | (Component.get n comp2) 228 | (Component.get n comp3) 229 | (Component.get n comp4) 230 | (Component.get n comp5) 231 | |> Maybe.withDefault acc 232 | ) 233 | acc_ 234 | comp1 235 | 236 | 237 | {-| Single component mapping, Same as`List.map` - only for `Component.Set` inside `World` 238 | 239 | gravitySystem = 240 | Logic.System.step (Vec2.add gravity) accelerationSpec 241 | 242 | -} 243 | step : (comp -> comp) -> Component.Spec comp world -> System world 244 | step f { get, set } world = 245 | set (get world |> Array.map (Maybe.map f)) world 246 | 247 | 248 | type alias Acc2 a b = 249 | { a : Component.Set a 250 | , b : Component.Set b 251 | } 252 | 253 | 254 | {-| Step over all entities that have both components. 255 | 256 | Example: 257 | 258 | system = 259 | Logic.System.step2 260 | (\( v, _ ) ( p, setP ) -> setP (Vec2.add v p)) 261 | velocitySpec 262 | positionSpec 263 | 264 | -} 265 | step2 : 266 | (( a, a -> System (Acc2 a b) ) 267 | -> ( b, b -> System (Acc2 a b) ) 268 | -> System (Acc2 a b) 269 | ) 270 | -> Component.Spec a world 271 | -> Component.Spec b world 272 | -> System world 273 | step2 f spec1 spec2 world = 274 | let 275 | set1 i a acc = 276 | { acc | a = Array.set i (Just a) acc.a } 277 | 278 | set2 i b acc = 279 | { acc | b = Array.set i (Just b) acc.b } 280 | 281 | combined = 282 | { a = spec1.get world, b = spec2.get world } 283 | 284 | result = 285 | indexedFoldlArray 286 | (\n value acc -> 287 | Maybe.map2 (\a b -> f ( a, set1 n ) ( b, set2 n ) acc) 288 | value 289 | (Component.get n acc.b) 290 | |> Maybe.withDefault acc 291 | ) 292 | combined 293 | combined.a 294 | in 295 | world 296 | |> applyIf (result.a /= combined.a) (spec1.set result.a) 297 | |> applyIf (result.b /= combined.b) (spec2.set result.b) 298 | 299 | 300 | type alias Acc3 a b c = 301 | { a : Component.Set a 302 | , b : Component.Set b 303 | , c : Component.Set c 304 | } 305 | 306 | 307 | {-| Same as [`step2`](#step2) only with 3 components 308 | -} 309 | step3 : 310 | (( a, a -> System (Acc3 a b c) ) 311 | -> ( b, b -> System (Acc3 a b c) ) 312 | -> ( c, c -> System (Acc3 a b c) ) 313 | -> System (Acc3 a b c) 314 | ) 315 | -> Component.Spec a world 316 | -> Component.Spec b world 317 | -> Component.Spec c world 318 | -> System world 319 | step3 f spec1 spec2 spec3 world = 320 | let 321 | set1 i a acc = 322 | { acc | a = Array.set i (Just a) acc.a } 323 | 324 | set2 i b acc = 325 | { acc | b = Array.set i (Just b) acc.b } 326 | 327 | set3 i c acc = 328 | { acc | c = Array.set i (Just c) acc.c } 329 | 330 | combined = 331 | { a = spec1.get world, b = spec2.get world, c = spec3.get world } 332 | 333 | result = 334 | indexedFoldlArray 335 | (\n value acc -> 336 | Maybe.map3 337 | (\a b c -> f ( a, set1 n ) ( b, set2 n ) ( c, set3 n ) acc) 338 | value 339 | (Component.get n acc.b) 340 | (Component.get n acc.c) 341 | |> Maybe.withDefault acc 342 | ) 343 | combined 344 | combined.a 345 | in 346 | world 347 | |> applyIf (result.a /= combined.a) (spec1.set result.a) 348 | |> applyIf (result.b /= combined.b) (spec2.set result.b) 349 | |> applyIf (result.c /= combined.c) (spec3.set result.c) 350 | 351 | 352 | type alias Acc4 a b c d = 353 | { a : Component.Set a 354 | , b : Component.Set b 355 | , c : Component.Set c 356 | , d : Component.Set d 357 | } 358 | 359 | 360 | {-| Same as [`step2`](#step2) only with 4 components 361 | -} 362 | step4 : 363 | (( a, a -> System (Acc4 a b c d) ) 364 | -> ( b, b -> System (Acc4 a b c d) ) 365 | -> ( c, c -> System (Acc4 a b c d) ) 366 | -> ( d, d -> System (Acc4 a b c d) ) 367 | -> System (Acc4 a b c d) 368 | ) 369 | -> Component.Spec a world 370 | -> Component.Spec b world 371 | -> Component.Spec c world 372 | -> Component.Spec d world 373 | -> System world 374 | step4 f spec1 spec2 spec3 spec4 world = 375 | let 376 | set1 i a acc = 377 | { acc | a = Array.set i (Just a) acc.a } 378 | 379 | set2 i b acc = 380 | { acc | b = Array.set i (Just b) acc.b } 381 | 382 | set3 i c acc = 383 | { acc | c = Array.set i (Just c) acc.c } 384 | 385 | set4 i d acc = 386 | { acc | d = Array.set i (Just d) acc.d } 387 | 388 | combined = 389 | { a = spec1.get world 390 | , b = spec2.get world 391 | , c = spec3.get world 392 | , d = spec4.get world 393 | } 394 | 395 | result = 396 | indexedFoldlArray 397 | (\n value acc -> 398 | Maybe.map4 399 | (\a b c d -> f ( a, set1 n ) ( b, set2 n ) ( c, set3 n ) ( d, set4 n ) acc) 400 | value 401 | (Component.get n acc.b) 402 | (Component.get n acc.c) 403 | (Component.get n acc.d) 404 | |> Maybe.withDefault acc 405 | ) 406 | combined 407 | combined.a 408 | in 409 | world 410 | |> applyIf (result.a /= combined.a) (spec1.set result.a) 411 | |> applyIf (result.b /= combined.b) (spec2.set result.b) 412 | |> applyIf (result.c /= combined.c) (spec3.set result.c) 413 | |> applyIf (result.d /= combined.d) (spec4.set result.d) 414 | 415 | 416 | type alias Acc5 a b c d e = 417 | { a : Component.Set a 418 | , b : Component.Set b 419 | , c : Component.Set c 420 | , d : Component.Set d 421 | , e : Component.Set e 422 | } 423 | 424 | 425 | {-| Same as [`step2`](#step2) only with 5 components 426 | -} 427 | step5 : 428 | (( a, a -> System (Acc5 a b c d e) ) 429 | -> ( b, b -> System (Acc5 a b c d e) ) 430 | -> ( c, c -> System (Acc5 a b c d e) ) 431 | -> ( d, d -> System (Acc5 a b c d e) ) 432 | -> ( e, e -> System (Acc5 a b c d e) ) 433 | -> System (Acc5 a b c d e) 434 | ) 435 | -> Component.Spec a world 436 | -> Component.Spec b world 437 | -> Component.Spec c world 438 | -> Component.Spec d world 439 | -> Component.Spec e world 440 | -> System world 441 | step5 f spec1 spec2 spec3 spec4 spec5 world = 442 | let 443 | set1 i a acc = 444 | { acc | a = Array.set i (Just a) acc.a } 445 | 446 | set2 i b acc = 447 | { acc | b = Array.set i (Just b) acc.b } 448 | 449 | set3 i c acc = 450 | { acc | c = Array.set i (Just c) acc.c } 451 | 452 | set4 i d acc = 453 | { acc | d = Array.set i (Just d) acc.d } 454 | 455 | set5 i e acc = 456 | { acc | e = Array.set i (Just e) acc.e } 457 | 458 | combined = 459 | { a = spec1.get world 460 | , b = spec2.get world 461 | , c = spec3.get world 462 | , d = spec4.get world 463 | , e = spec5.get world 464 | } 465 | 466 | result = 467 | indexedFoldlArray 468 | (\n value acc -> 469 | Maybe.map5 470 | (\a b c d e -> f ( a, set1 n ) ( b, set2 n ) ( c, set3 n ) ( d, set4 n ) ( e, set5 n ) acc) 471 | value 472 | (Component.get n acc.b) 473 | (Component.get n acc.c) 474 | (Component.get n acc.d) 475 | (Component.get n acc.e) 476 | |> Maybe.withDefault acc 477 | ) 478 | combined 479 | combined.a 480 | in 481 | world 482 | |> applyIf (result.a /= combined.a) (spec1.set result.a) 483 | |> applyIf (result.b /= combined.b) (spec2.set result.b) 484 | |> applyIf (result.c /= combined.c) (spec3.set result.c) 485 | |> applyIf (result.d /= combined.d) (spec4.set result.d) 486 | 487 | 488 | {-| Just nice helper function to pipe into systems 489 | 490 | update msg world = 491 | world 492 | |> system1 493 | |> applyIf (msg === KeyUp "a") systemMoveLeft 494 | |> system2 495 | 496 | -} 497 | applyIf : Bool -> (a -> a) -> a -> a 498 | applyIf bool f world = 499 | if bool then 500 | f world 501 | 502 | else 503 | world 504 | 505 | 506 | {-| Same as [`applyIf`](#applyIf), but works with `Maybe` 507 | 508 | update msg world = 509 | world 510 | |> system1 511 | |> applyMaybe (decode saveDecoder msg) loadGame 512 | |> system2 513 | 514 | -} 515 | applyMaybe : Maybe a -> (a -> c -> c) -> c -> c 516 | applyMaybe m f world = 517 | case m of 518 | Just a -> 519 | f a world 520 | 521 | Nothing -> 522 | world 523 | --------------------------------------------------------------------------------