diff --git a/src/Action.elm b/src/Action.elm index 3a9b858..416669a 100644 --- a/src/Action.elm +++ b/src/Action.elm @@ -3,39 +3,24 @@ module Action exposing (..) --- Elm -import Time exposing (Time) +-- +-- Actions are things that a champ can enqueue at a Waypoint +-- type alias Angle = Float --- TODO: Figure out how to leverage the type system here better. --- For example, is it possible for a Warrior's waypoint to only --- enqueue Warrior actions? --- TODO: Will there be General actions not specific to a class? --- type Action --- -- WARRIOR --- = Charge Angle --- | WarCry Angle --- -- RANGER --- | Snipe Angle - -type Action - = WarAct WarriorAction - | RanAct RangerAction - - -- Starts at (1, tickDuration), ends at (tickDuration, tickDuration) type alias Duration = (Int, Int) -type WarriorAction +-- TODO: Will there be General actions not specific to a class? +type Action + -- WARRIOR = Charge Angle - | WarCry Angle Duration - - -type RangerAction - = Snipe Angle Duration + --| WarCry Angle Duration + -- RANGER + | Snipe Angle Duration diff --git a/src/Champ.elm b/src/Champ.elm index 768d694..05b9196 100644 --- a/src/Champ.elm +++ b/src/Champ.elm @@ -16,6 +16,13 @@ import Action exposing (Action) import Class exposing (Class) +type ClassStatus + -- ALL + = Acting Action + -- WARRIOR + | AutoAttacking (Int, Int) Champ + + type Status -- Champ is just standing there (no waypoints) = Idling @@ -24,7 +31,8 @@ type Status -- Champ is in the middle of its autoattack animation -- Holds the current tick and the total tick count of the status -- and also holds the target champ - | AutoAttacking (Int, Int) Champ + --| AutoAttacking (Int, Int) Champ + | ClassSpecific ClassStatus | Dead @@ -52,7 +60,7 @@ init name position (currHp, maxHp) = , speed = 2 , angle = 0 , waypoints = [] - , class = Class.Warrior Nothing + , class = Class.Warrior } @@ -85,15 +93,15 @@ faceWaypoint champ = -- Makes champ face its attack victim if there is one -faceVictim : Champ -> Champ -faceVictim champ = - case champ.status of - AutoAttacking _ enemy -> - { champ - | angle = Vector.angleTo champ.position enemy.position - } - _ -> - champ +-- faceVictim : Champ -> Champ +-- faceVictim champ = +-- case champ.status of +-- AutoAttacking _ enemy -> +-- { champ +-- | angle = Vector.angleTo champ.position enemy.position +-- } +-- _ -> +-- champ -- Sorting the dead champs first lets us render them behind the alive champs. @@ -158,10 +166,19 @@ statusToEmoji status = "⌛" Moving -> "⏊ī¸" - AutoAttacking _ _ -> - "👊" --"⚔" Dead -> "⚰" + ClassSpecific classStatus -> + case classStatus of + AutoAttacking _ _ -> + "👊" --"⚔" + Acting action -> + case action of + Action.Charge _ -> + "🚀" + Action.Snipe _ _ -> + "đŸ”Ģ" + statusToSimpleName : Status -> String @@ -171,10 +188,18 @@ statusToSimpleName status = "Idling" Moving -> "Moving" - AutoAttacking _ _ -> - "Auto-Attacking" Dead -> "Dead" + ClassSpecific classStatus -> + case classStatus of + AutoAttacking _ _ -> + "AutoAttacking" + Acting action -> + case action of + Action.Charge _ -> + "Charging" + Action.Snipe _ _ -> + "Sniping" viewWaypoint : Maybe Waypoint -> (Waypoint -> msg) -> Waypoint -> Svg msg @@ -287,16 +312,6 @@ view ctx champ = -- TODO: DRY or extract -- animSpeed 1.0 means play one loop of the animation per round case champ.status of - AutoAttacking (curr, duration) _ -> - let - animSpeed = - 1.0 - frames = - 9 -- frame count of animation - bucket = - floor (toFloat (curr - 1) / (toFloat duration / frames / animSpeed)) % frames - in - "./img/sprites/champ/attack_" ++ toString bucket ++ ".png" Moving -> let animSpeed = @@ -319,6 +334,29 @@ view ctx champ = "./img/sprites/champ/idle_" ++ toString bucket ++ ".png" Dead -> "./img/tombstone.png" + ClassSpecific classStatus -> + case classStatus of + AutoAttacking (curr, duration) _ -> + let + animSpeed = + 1.0 + frames = + 9 -- frame count of animation + bucket = + floor (toFloat (curr - 1) / (toFloat duration / frames / animSpeed)) % frames + in + "./img/sprites/champ/attack_" ++ toString bucket ++ ".png" + _ -> + -- For now use idling for other stuff + let + animSpeed = + 2.0 + frames = + 17 + bucket = + floor (toFloat ctx.tickIdx / (toFloat Constants.ticksPerRound / frames / animSpeed)) % frames + in + "./img/sprites/champ/idle_" ++ toString bucket ++ ".png" -- Scale the champ image to 128x128 instead of 64x64 -- unless they are dead (x', y', side) = diff --git a/src/Class.elm b/src/Class.elm index 9d63e43..027b4ee 100644 --- a/src/Class.elm +++ b/src/Class.elm @@ -3,19 +3,15 @@ module Class exposing (..) --- 1st -import Action - - type Class - = Warrior (Maybe Action.WarriorAction) - | Ranger (Maybe Action.RangerAction) + = Warrior + | Ranger toEmoji : Class -> String toEmoji class = case class of - Warrior _ -> + Warrior -> "⚔" - Ranger _ -> + Ranger -> "🏹" diff --git a/src/Main.elm b/src/Main.elm index acb0799..0954132 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -538,7 +538,13 @@ view model = } in Grid.view ctx model.grid - , Html.App.map SidebarMsg (Sidebar.view model.sidebar) + -- Only show the Sidebar in Planning mode since it doesn't update + -- as the simulation plays. + , case model.mode of + Planning _ -> + Html.App.map SidebarMsg (Sidebar.view model.sidebar) + _ -> + Html.text "" , Html.div [ Html.Attributes.id "footerbar" ] [ Html.button diff --git a/src/Round.elm b/src/Round.elm index b128d82..a012a64 100644 --- a/src/Round.elm +++ b/src/Round.elm @@ -9,6 +9,9 @@ import Array exposing (Array) -- 1st import Champ exposing (Champ) import Vector +import Util +import Class exposing (Class) +import Warrior type alias Tick = @@ -24,141 +27,94 @@ type alias Round = } +-- A champ only moves if status == Moving +moveChamp : String -> Dict String Champ -> Dict String Champ +moveChamp name dict = + let + champ = Util.forceUnwrap (Dict.get name dict) + in + if champ.status /= Champ.Moving then + dict + else + case champ.waypoints of + [] -> + -- No more waypoints, so idle + Dict.insert name { champ | status = Champ.Idling } dict + waypoint :: rest -> + -- if champ is on a waypoint, transition into any action that's on the + -- way point. + -- else, consume the waypoint and continue moving + if (Vector.dist champ.position waypoint.position < champ.speed * 1/60) then + case waypoint.actions of + [] -> + -- No actions, so consume the waypoint and head onwards + Dict.insert + name + { champ + | position = waypoint.position + , waypoints = List.drop 1 champ.waypoints + } + dict + action :: _ -> + -- Waypoint had an action in queue, so transition into it + let + -- update waypoint actions + waypoint' = + { waypoint + | actions = List.drop 1 waypoint.actions + } + in + Dict.insert + name + { champ + | position = waypoint.position + , status = Champ.ClassSpecific (Champ.Acting action) + , waypoints = waypoint' :: rest + } + dict + else + -- didn't hit a waypoint, so keep moving towards the next one + let + (prevX, prevY) = champ.position + (dx, dy) = + Vector.fromPoints champ.position waypoint.position + |> Vector.normalize + |> Vector.scale (champ.speed * 1/60) + position' = + (prevX + dx, prevY + dy) + in + Dict.insert + name + (Champ.faceWaypoint { champ | position = position' }) + dict + + +stepClass : String -> Class -> Dict String Champ -> Dict String Champ +stepClass name class dict = + case class of + Class.Warrior -> + Warrior.stepChamp name dict + _ -> + -- unimplemented + dict + + -- TODO: This is getting really nasty. stepChamp : String -> Champ -> (List String, Dict String Champ) -> (List String, Dict String Champ) stepChamp name _ (log, dict) = let - -- 0. Load current champ from the dict since another champ's step - -- may have mutated them - champ0 = - case Dict.get name dict of - Nothing -> - Debug.crash "Impossible" - Just champ -> - champ + -- Load current champ from the dict since another champ's step + -- may have mutated them + champ = Util.forceUnwrap (Dict.get name dict) in - -- Skip champ is they are dead - if champ0.status == Champ.Dead then + if champ.status == Champ.Dead then + -- Skip champ if they are dead (log, dict) else - let - -- 1. Move the champ (champ0 -> champ1) - champ1 = - case champ0.status of - Champ.Moving -> - case champ0.waypoints of - -- idle when we run out of waypoints - [] -> - { champ0 | status = Champ.Idling } - waypoint :: rest -> - -- consume waypoint if champ is on it, else move champ towards it - if Vector.dist champ0.position waypoint.position < champ0.speed * 1/60 then - { champ0 - | position = waypoint.position - , waypoints = List.drop 1 champ0.waypoints - } - else - let - (prevX, prevY) = champ0.position - (dx, dy) = - Vector.fromPoints champ0.position waypoint.position - |> Vector.normalize - |> Vector.scale (champ0.speed * 1/60) - position' = - (prevX + dx, prevY + dy) - in - { champ0 | position = position' } - |> Champ.faceWaypoint - _ -> - champ0 - -- 2. Check and advance auto-attack (champ1 -> champ2) - (champ2, maybeVictim) = - case champ1.status of - -- Champ is in the middle of an auto-attack, so advance it. - -- If it's finished, then transition to another state. - Champ.AutoAttacking (currTick, tickDuration) victim -> - let - currTick' = - currTick + 1 - status' = - if currTick' <= tickDuration then - -- still autoattacking - let - victim' = - case Dict.get victim.name dict of - Just enemy -> - enemy - _ -> - Debug.crash "Impossible" - in - Champ.AutoAttacking (currTick', tickDuration) victim' - else - -- done autoattacking, so transition to idling or moving - if List.isEmpty champ1.waypoints then - Champ.Idling - else - Champ.Moving - in - ( { champ1 | status = status' } - -- Point champ at victim every frame - |> Champ.faceVictim - , Nothing - ) - -- Champ is available to auto-attack something, so check enemies in range - _ -> - -- if another champ is within autoattack range, attack that champ - let - autoattackRange = - 1 -- radius in meters - champsToAttack = - List.filter - ( \other -> - -- ignore self - champ1.name /= other.name - -- ignore dead champs - && other.status /= Champ.Dead - -- ignore champs out of range - && (Vector.dist champ1.position other.position) <= autoattackRange - ) - (Dict.values dict) - in - case List.head champsToAttack of - Nothing -> - (champ1, Nothing) - Just enemy -> - ( { champ1 - | status = - Champ.AutoAttacking (1, 60) enemy - } - |> Champ.faceVictim - , Just enemy - ) - -- FIXME: the code/exprs assigned to maybeVictim' and log' are examples - -- of Elm code that feels wrong to me but I can't seem to avoid. - maybeVictim' = - Maybe.map (Champ.sufferDamage 25) maybeVictim - log' = - case maybeVictim' of - Just {name, status} -> - case status of - Champ.Dead -> - List.append log [champ0.name ++ " killed " ++ name] - _ -> - log - _ -> - log - in - ( log' - , dict - -- Update dict with this champ's move - |> Dict.insert champ0.name champ2 - -- Mutate attack victim if there was one - |> case maybeVictim' of - Nothing -> - identity - Just victim -> - Dict.insert victim.name victim - ) + dict + |> (stepClass name champ.class) + |> (moveChamp name) + |> (\d -> (log, d)) stepTick : Int -> List Tick -> List Tick diff --git a/src/Sidebar.elm b/src/Sidebar.elm index cec3711..5edcec2 100644 --- a/src/Sidebar.elm +++ b/src/Sidebar.elm @@ -13,7 +13,6 @@ import Waypoint exposing (Waypoint) import Champ exposing (Champ) import WaypointDetail import ChampDetail -import Action -- MODEL diff --git a/src/Vector.elm b/src/Vector.elm index 7c75347..2ae5469 100644 --- a/src/Vector.elm +++ b/src/Vector.elm @@ -45,6 +45,11 @@ flipY (x, y) = (x, -y) +add : Vector -> Vector -> Vector +add (x1, y1) (x2, y2) = + (x1 + x2, y1 + y2) + + -- radians angleTo : Vector -> Vector -> Float angleTo (x1, y1) (x2, y2) = diff --git a/src/Warrior.elm b/src/Warrior.elm new file mode 100644 index 0000000..ab88093 --- /dev/null +++ b/src/Warrior.elm @@ -0,0 +1,113 @@ + + +module Warrior exposing (..) + + +-- Elm +import Dict exposing (Dict) +-- 1st +import Champ exposing (Champ) +import Vector exposing (Vector) +import Action exposing (Action) +import Util + + +-- Progresses the cooldown and transitions to another status once finished. +tickAutoAttack : + Champ -> (Int, Int) -> Champ -> Dict String Champ -> Dict String Champ +tickAutoAttack champ (prevTick, tickDuration) victim dict = + let + currTick = + prevTick + 1 + status' = + if currTick <= tickDuration then + -- We are still auto-attacking + Champ.ClassSpecific (Champ.AutoAttacking (currTick, tickDuration) victim) + else + -- Done auto-attacking, so transition to Idling or Moving + if List.isEmpty champ.waypoints then + Champ.Idling + else + Champ.Moving + champ' = + { champ | status = status' } + in + Dict.insert champ'.name champ' dict + + +checkAutoAttack : Champ -> Dict String Champ -> Dict String Champ +checkAutoAttack champ dict = + let + attackRange = + 1 -- meters aka tiles + champsInRange = + List.filter + ( \other -> + -- ignore self + champ.name /= other.name + -- ignore dead champs + && other.status /= Champ.Dead + -- ignore champs out of range + && (Vector.dist champ.position other.position) <= attackRange + ) + (Dict.values dict) + in + case champsInRange of + [] -> + dict + victim :: _ -> + let + -- damage the victim + victim' = + Champ.sufferDamage 25 victim + champ' = + { champ + | status = + Champ.ClassSpecific (Champ.AutoAttacking (1, 60) victim') + -- Face the victim we're attacking + , angle = + Vector.angleTo champ.position victim'.position + } + in + dict + |> Dict.insert champ'.name champ' + |> Dict.insert victim'.name victim' + + + +stepChamp : String -> Dict String Champ -> Dict String Champ +stepChamp name dict = + let + champ = + Util.forceUnwrap (Dict.get name dict) + in + case champ.status of + Champ.Dead -> + dict + Champ.ClassSpecific classStatus -> + case classStatus of + Champ.AutoAttacking duration victim -> + tickAutoAttack champ duration victim dict + Champ.Acting action -> + case action of + Action.Charge angle -> + let + deltaTime = + 1/60 + chargeSpeed = + 10 + velocity = + ( chargeSpeed * cos angle * deltaTime + , chargeSpeed * sin angle * deltaTime -- maybe * -1? + ) + position' = + Vector.add champ.position velocity + champ' = + { champ | position = position' } + in + Dict.insert name champ' dict + _ -> + Debug.crash "Unexpected classStatus" + _ -> + -- Idling | Moving, so check if we can autoattack an enemy + checkAutoAttack champ dict