anchor.diff:                           Entity.UpImage == Assets.Gui.ButtonBigUpImage
                            Entity.DownImage == Assets.Gui.ButtonBigDownImage
                            Entity.ClickEvent ==> msg (fieldMsg i)])
  +
  +    type FieldDispatcher () =
  +        inherit ScreenDispatcher<Field, FieldMessage, FieldCommand> (Field.debug)
  +
  +        static let detokenize (text : string) (field : Field) =
  +            text
  +                .Replace("$FEE", scstring (Field.getRecruitmentFee field))
  +                .Replace("$GOLD", scstring field.Inventory.Gold)
  +
  +        static let isFacingBodyShape bodyShape (avatar : Avatar) world =
  +            if bodyShape.Entity.Is<PropDispatcher> world then
  +                let v = bodyShape.Entity.GetBottom world - avatar.Bottom
  +                let direction = Direction.ofVector2 v
  +                direction = avatar.Direction
  +            else false
  +
  +        static let getFacingBodyShapes (avatar : Avatar) world =
  +            List.filter
  +                (fun bodyShape -> isFacingBodyShape bodyShape avatar world)
  +                avatar.IntersectedBodyShapes
  +
  +        static let tryGetFacingProp (avatar : Avatar) world =
  +            match getFacingBodyShapes avatar world with
  +            | head :: _ -> Some head.Entity
  +            | [] -> None
  +
  +        static let isTouchingSavePoint (avatar : Avatar) world =
  +            List.exists (fun shape ->
  +                match (shape.Entity.GetProp world).PropData with
  +                | SavePoint -> true
  +                | _ -> false)
  +                avatar.IntersectedBodyShapes
  +
  +        static let tryGetFacingInteraction dialogOpt advents prop field =
  +            match dialogOpt with
  +            | None ->
  +                match prop with
  +                | Portal (_, _, _, _, _, _, _) -> None
  +                | Door _ -> Some "Open"
  +                | Chest (_, _, chestId, _, _, _) -> if Set.contains (Opened chestId) advents then None else Some "Open"
  +                | Switch (_, _, _, _) -> Some "Use"
  +                | Sensor (_, _, _, _, _) -> None
  +                | Npc _ | NpcBranching _ -> Some "Talk"
  +                | Shopkeep _ -> Some "Shop"
  +                | Seal _ -> Some "Touch"
  +                | Flame _ -> None
  +                | SavePoint -> None
  +                | ChestSpawn -> None
  +                | EmptyProp -> None
  +            | Some dialog ->
  +                if  Dialog.canAdvance (flip detokenize field) dialog &&
  +                    not
  +                        (Dialog.isExhausted (flip detokenize field) dialog &&
  +                         Option.isSome dialog.DialogPromptOpt)
  +                then Some "Next"
  +                else None
  +
  +        static let tryGetInteraction dialogOpt advents (avatar : Avatar) field world =
  +            if isTouchingSavePoint avatar world then
  +                Some "Save"
  +            else
  +                match tryGetFacingProp avatar world with
  +                | Some prop -> tryGetFacingInteraction dialogOpt advents (prop.GetProp world).PropData field
  +                | None -> None
  +
  +        static let tryGetTouchingPortal omniSeedState (advents : Advent Set) (avatar : Avatar) world =
  +            avatar.IntersectedBodyShapes |>
  +            List.choose (fun shape ->
  +                match (shape.Entity.GetProp world).PropData with
  +                | Portal (_, _, _, fieldType, portalIndex, _, requirements) ->
  +                    if advents.IsSupersetOf requirements then
  +                        match Map.tryFind fieldType Data.Value.Fields with
  +                        | Some fieldData ->
  +                            match FieldData.tryGetPortal omniSeedState portalIndex fieldData world with
  +                            | Some portal ->
  +                                match portal.PropData with
  +                                | Portal (_, _, direction, _, _, extended, _) ->
  +                                    let destination =
  +                                        match direction with
  +                                        | Upward -> portal.PropBounds.Top + v2 0.0f 8.0f + if extended then v2 0.0f 48.0f else v2Zero
  +                                        | Rightward -> portal.PropBounds.Right + v2 32.0f 0.0f + if extended then v2 48.0f 0.0f else v2Zero
  +                                        | Downward -> portal.PropBounds.Bottom + v2 0.0f -54.0f - if extended then v2 0.0f 48.0f else v2Zero
  +                                        | Leftward -> portal.PropBounds.Left + v2 -32.0f 0.0f - if extended then v2 48.0f 0.0f else v2Zero
  +                                    Some (fieldType, destination, direction)
  +                                | _ -> None
  +                            | None -> None
  +                        | None -> None
  +                    else None
  +                | _ -> None) |>
  +            List.tryHead
  +
  +        static let getTouchedSensors (avatar : Avatar) world =
  +            List.choose (fun shape ->
  +                match (shape.Entity.GetProp world).PropData with
  +                | Sensor (sensorType, _, cue, _, requirements) -> Some (sensorType, cue, requirements)
  +                | _ -> None)
  +                avatar.CollidedBodyShapes
  +
  +        static let getUntouchedSensors (avatar : Avatar) world =
  +            List.choose (fun shape ->
  +                match (shape.Entity.GetProp world).PropData with
  +                | Sensor (sensorType, _, _, cue, requirements) -> Some (sensorType, cue, requirements)
  +                | _ -> None)
  +                avatar.SeparatedBodyShapes
  +        do ignore getUntouchedSensors // NOTE: suppressing warning.
  +
  +        static let interactDialog dialog field =
  +            match Dialog.tryAdvance (flip detokenize field) dialog with
  +            | (true, dialog) ->
  +                let field = Field.updateDialogOpt (constant (Some dialog)) field
  +                just field
  +            | (false, dialog) ->
  +                let field = Field.updateDialogOpt (constant None) field
  +                match dialog.DialogBattleOpt with
  +                | Some (battleType, consequence) -> withMsg (TryBattle (battleType, consequence)) field
  +                | None -> just field
  +
  +        static let interactChest itemType chestId battleTypeOpt cue requirements (field : Field) =
  +            if field.Advents.IsSupersetOf requirements then
  +                let field = Field.updateAdvents (Set.add (Opened chestId)) field
  +                // TODO: P1: rewrite this to use two new cues, Find and Guarded.
  +                let field = Field.updateInventory (Inventory.tryAddItem itemType >> snd) field
  +                let field =
  +                    match battleTypeOpt with
  +                    | Some battleType -> Field.updateDialogOpt (constant (Some { DialogForm = DialogThin; DialogTokenized = "Found " + ItemType.getName itemType + "!^But something approaches!"; DialogProgress = 0; DialogPage = 0; DialogPromptOpt = None; DialogBattleOpt = Some (battleType, Set.empty) })) field
  +                    | None -> Field.updateDialogOpt (constant (Some { DialogForm = DialogThin; DialogTokenized = "Found " + ItemType.getName itemType + "!"; DialogProgress = 0; DialogPage = 0; DialogPromptOpt = None; DialogBattleOpt = None })) field
  +                let field = Field.updateCue (constant cue) field
  +                withCmd (PlaySound (0L, Constants.Audio.SoundVolumeDefault, Assets.Field.ChestOpenSound)) field
  +            else just (Field.updateDialogOpt (constant (Some { DialogForm = DialogThin; DialogTokenized = "Locked!"; DialogProgress = 0; DialogPage = 0; DialogPromptOpt = None; DialogBattleOpt = None })) field)
  +
  +        static let interactDoor cue requirements (prop : Prop) (field : Field) =
  +            match prop.PropState with
  +            | DoorState false ->
  +                if field.Advents.IsSupersetOf requirements then
  +                    let field = Field.updateCue (constant cue) field
  +                    let field = Field.updatePropStates (Map.add prop.PropId (DoorState true)) field
  +                    withCmd (PlaySound (0L, Constants.Audio.SoundVolumeDefault, Assets.Field.DoorOpenSound)) field
  +                else just (Field.updateDialogOpt (constant (Some { DialogForm = DialogThin; DialogTokenized = "Locked!"; DialogProgress = 0; DialogPage = 0; DialogPromptOpt = None; DialogBattleOpt = None })) field)
  +            | _ -> failwithumf ()
  +
  +        static let interactSwitch cue cue2 requirements (prop : Prop) (field : Field) =
  +            match prop.PropState with
  +            | SwitchState on ->
  +                if field.Advents.IsSupersetOf requirements then
  +                    let on = not on
  +                    let field = Field.updatePropStates (Map.add prop.PropId (SwitchState on)) field
  +                    let field = Field.updateCue (constant (if on then cue else cue2)) field
  +                    withCmd (PlaySound (0L, Constants.Audio.SoundVolumeDefault, Assets.Field.UseSwitchSound)) field
  +                else just (Field.updateDialogOpt (constant (Some { DialogForm = DialogThin; DialogTokenized = "Won't budge!"; DialogProgress = 0; DialogPage = 0; DialogPromptOpt = None; DialogBattleOpt = None })) field)
  +            | _ -> failwithumf ()
  +        
  +        static let interactNpc branches requirements (field : Field) =
  +            if field.Advents.IsSupersetOf requirements then
  +                let branchesFiltered = branches |> List.choose (fun branch -> if field.Advents.IsSupersetOf branch.Requirements then Some branch.Cue else None) |> List.rev
  +                let branchCue = match List.tryHead branchesFiltered with Some cue -> cue | None -> Dialog "..."
  +                let field = Field.updateCue (constant branchCue) field
  +                just field
  +            else just field
  +
  +        static let interactShopkeep shopType (field : Field) =
  +            let shop = { ShopType = shopType; ShopState = ShopBuying; ShopPage = 0; ShopConfirmOpt = None }
  +            let field = Field.updateShopOpt (constant (Some shop)) field
  +            withCmd (PlaySound (0L, Constants.Audio.SoundVolumeDefault, Assets.Gui.AffirmSound)) field
  +
  +        static let interactSavePoint (field : Field) =
  +            let field = Field.restoreTeam field
  +            Field.save field
  +            withCmd (PlaySound (0L, Constants.Audio.SoundVolumeDefault, Assets.Gui.SlotSound)) field
  +
  +        static let rec runCue cue (field : Field) world : Cue * (Signal<FieldMessage, FieldCommand> list * Field) =
  +
  +            match cue with
  +            | Cue.Nil ->
  +                (cue, just field)
  +
  +            | Cue.PlaySound (volume, sound) ->
  +                (Cue.Nil, withCmd (PlaySound (0L, volume, sound)) field)
  +
  +            | Cue.PlaySong (fadeIn, fadeOut, volume, start, song) ->
  +                (Cue.Nil, withCmd (PlaySong (fadeIn, fadeOut, volume, start, song)) field)
  +
  +            | Cue.FadeOutSong fade ->
  +                (Cue.Nil, withCmd (FadeOutSong fade) field)
  +
  +            | Cue.Face (direction, target) ->
  +                match target with
  +                | AvatarTarget ->
  +                    let field = Field.updateAvatar (Avatar.updateDirection (constant direction)) field
  +                    (Cue.Nil, just field)
  +                | NpcTarget npcType ->
  +                    match Map.tryFindKey (constant (function NpcState (npcType2, _, _, _, _) -> npcType2 = npcType | _ -> false)) field.PropStates with
  +                    | Some propKey ->
  +                        match field.PropStates.[propKey] with
  +                        | NpcState (_, _, color, glow, exists) ->
  +                            let field = Field.updatePropStates (Map.add propKey (NpcState (npcType, direction, color, glow, exists))) field
  +                            (Cue.Nil, just field)
  +                        | _ -> (Cue.Nil, just field)
  +                    | None -> (Cue.Nil, just field)
  +                | ShopkeepTarget _ | AllyTarget _ | EnemyTarget _ ->
  +                    (Cue.Nil, just field)
  +
  +            | Glow (glow, target) ->
  +                match target with
  +                | NpcTarget npcType ->
  +                    match Map.tryFindKey (constant (function NpcState (npcType2, _, _, _, _) -> npcType2 = npcType | _ -> false)) field.PropStates with
  +                    | Some propKey ->
  +                        match field.PropStates.[propKey] with
  +                        | NpcState (_, direction, color, _, exists) ->
  +                            let field = Field.updatePropStates (Map.add propKey (NpcState (npcType, direction, color, glow, exists))) field
  +                            (Cue.Nil, just field)
  +                        | _ -> (Cue.Nil, just field)
  +                    | None -> (Cue.Nil, just field)
  +                | AvatarTarget _ | ShopkeepTarget _ | AllyTarget _ | EnemyTarget _ ->
  +                    (Cue.Nil, just field)
  +
  +            | Animate (characterAnimationType, target) ->
  +                match target with
  +                | AvatarTarget ->
  +                    let field = Field.updateAvatar (Avatar.animate (World.getUpdateTime world) characterAnimationType) field
  +                    (Cue.Nil, just field)
  +                | NpcTarget _ | ShopkeepTarget _ | AllyTarget _ | EnemyTarget _ ->
  +                    (Cue.Nil, just field)
  +
  +            | Recruit allyType ->
  +                let fee = Field.getRecruitmentFee field
  +                if field.Inventory.Gold >= fee then
  +                    let advent =
  +                        match allyType with
  +                        | Jinn -> failwithumf ()
  +                        | Shade -> ShadeRecruited
  +                        | Mael -> MaelRecruited
  +                        | Riain -> RiainRecruited
  +                        | Peric -> PericRecruited
  +                    let field = Field.recruit allyType field
  +                    let field = Field.updateAdvents (Set.add advent) field
  +                    let field = Field.updateInventory (Inventory.updateGold (fun gold -> gold - fee)) field
  +                    (Cue.Nil, withCmd (PlaySound (0L, Constants.Audio.SoundVolumeDefault, Assets.Field.PurchaseSound)) field)
  +                else runCue (Parallel [Dialog "You don't have enough..."; Cue.PlaySound (Constants.Audio.SoundVolumeDefault, Assets.Gui.MistakeSound)]) field world
  +
  +            | Unseal (fee, consequent) ->
  +                if field.Inventory.Gold >= fee then
  +                    let field = Field.updateInventory (Inventory.updateGold (fun gold -> gold - fee)) field
  +                    let field = Field.updateAdvents (Set.add consequent) field
  +                    (Cue.Nil, withCmd (PlaySound (0L, Constants.Audio.SoundVolumeDefault, Assets.Field.SealedSound)) field) // TODO: P1: rename sound to Unsealed.
  +                else runCue (Parallel [Dialog "You don't have enough..."; Cue.PlaySound (Constants.Audio.SoundVolumeDefault, Assets.Gui.MistakeSound)]) field world
  +
  +            | AddItem itemType ->
  +                (Cue.Nil, just (Field.updateInventory (Inventory.tryAddItem itemType >> snd) field))
  +
  +            | RemoveItem itemType ->
  +                (Cue.Nil, just (Field.updateInventory (Inventory.tryRemoveItem itemType >> snd) field))
  +
  +            | AddAdvent advent ->
  +                (Cue.Nil, just (Field.updateAdvents (Set.add advent) field))
  +
  +            | RemoveAdvent advent ->
  +                (Cue.Nil, just (Field.updateAdvents (Set.remove advent) field))
  +
  +            | Wait time ->
  +                (WaitState (World.getUpdateTime world + time), just field)
  +
  +            | WaitState time ->
  +                if World.getUpdateTime world < time
  +                then (cue, just field)
  +                else (Cue.Nil, just field)
  +
  +            | Fade (time, fadeIn, target) ->
  +                (FadeState (World.getUpdateTime world, time, fadeIn, target), just field)
  +
  +            | FadeState (startTime, totalTime, fadeIn, target) ->
  +                match target with
  +                | NpcTarget npcType ->
  +                    match Map.tryFindKey (constant (function NpcState (npcType2, _, _, _, _) -> npcType2 = npcType | _ -> false)) field.PropStates with
  +                    | Some propKey ->
  +                        match field.PropStates.[propKey] with
  +                        | NpcState (_, direction, _, glow, exists) ->
  +                            let localTime = World.getUpdateTime world - startTime
  +                            let progress = single localTime / single totalTime
  +                            let color = colWhite * if fadeIn then progress else 1.0f - progress
  +                            let field = Field.updatePropStates (Map.add propKey (NpcState (npcType, direction, color, glow, exists))) field
  +                            (Cue.Nil, just field)
  +                        | _ -> (Cue.Nil, just field)
  +                    | None -> (Cue.Nil, just field)
  +                | AvatarTarget _ | ShopkeepTarget _ | AllyTarget _ | EnemyTarget _ ->
  +                    (Cue.Nil, just field)
  +
  +            | Warp (fieldType, fieldDestination, fieldDirection) ->
  +                match field.FieldTransitionOpt with
  +                | Some _ ->
  +                    (cue, just field)
  +                | None ->
  +                    let fieldTransition =
  +                        { FieldType = fieldType
  +                          FieldDestination = fieldDestination
  +                          FieldDirection = fieldDirection
  +                          FieldTransitionTime = World.getUpdateTime world + Constants.Field.TransitionTime }
  +                    let field = Field.updateFieldTransitionOpt (constant (Some fieldTransition)) field
  +                    (WarpState, just field)
  +
  +            | WarpState ->
  +                match field.FieldTransitionOpt with
  +                | Some _ -> (cue, just field)
  +                | None -> (Cue.Nil, just field)
  +
  +            | Battle (battleType, consequents) ->
  +                match field.BattleOpt with
  +                | Some _ -> (cue, just field)
  +                | None -> (BattleState, withMsg (TryBattle (battleType, consequents)) field)
  +
  +            | BattleState ->
  +                match field.BattleOpt with
  +                | Some _ -> (cue, just field)
  +                | None -> (Cue.Nil, just field)
  +
  +            | Dialog text ->
  +                match field.DialogOpt with
  +                | Some _ ->
  +                    (cue, just field)
  +                | None ->
  +                    let dialog = { DialogForm = DialogThick; DialogTokenized = text; DialogProgress = 0; DialogPage = 0; DialogPromptOpt = None; DialogBattleOpt = None }
  +                    let field = Field.updateDialogOpt (constant (Some dialog)) field
  +                    (DialogState, just field)
  +
  +            | DialogState ->
  +                match field.DialogOpt with
  +                | None -> (Cue.Nil, just field)
  +                | Some _ -> (cue, just field)
  +
  +            | Prompt (text, leftPrompt, rightPrompt) ->
  +                match field.DialogOpt with
  +                | Some _ ->
  +                    (cue, just field)
  +                | None ->
  +                    let dialog = { DialogForm = DialogThick; DialogTokenized = text; DialogProgress = 0; DialogPage = 0; DialogPromptOpt = Some (leftPrompt, rightPrompt); DialogBattleOpt = None }
  +                    let field = Field.updateDialogOpt (constant (Some dialog)) field
  +                    (PromptState, just field)
  +
  +            | PromptState ->
  +                match field.DialogOpt with
  +                | None -> (Cue.Nil, just field)
  +                | Some _ -> (cue, just field)
  +
  +            | If (requirements, consequent, alternate) ->
  +                if field.Advents.IsSupersetOf requirements
  +                then (consequent, just field)
  +                else (alternate, just field)
  +
  +            | Not (requirements, consequent, alternate) ->
  +                if not (field.Advents.IsSupersetOf requirements)
  +                then (consequent, just field)
  +                else (alternate, just field)
  +
  +            | Parallel cues ->
  +                let (cues, (signals, field)) =
  +                    List.fold (fun (cues, (signals, field)) cue ->
  +                        let (cue, (signals2, field)) = runCue cue field world
  +                        if Cue.isNil cue
  +                        then (cues, (signals @ signals2, field))
  +                        else (cues @ [cue], (signals @ signals2, field)))
  +                        ([], ([], field))
  +                        cues
  +                match cues with
  +                | _ :: _ -> (Parallel cues, (signals, field))
  +                | [] -> (Cue.Nil, (signals, field))
  +
  +            | Sequence cues ->
  +                let (_, haltedCues, (signals, field)) =
  +                    List.fold (fun (halted, haltedCues, (signals, field)) cue ->
  +                        if halted
  +                        then (halted, haltedCues @ [cue], (signals, field))
  +                        else
  +                            let (cue, (signals2, field)) = runCue cue field world
  +                            if Cue.isNil cue
  +                            then (false, [], (signals @ signals2, field))
  +                            else (true, [cue], (signals @ signals2, field)))
  +                        (false, [], ([], field))
  +                        cues
  +                match haltedCues with
  +                | _ :: _ -> (Sequence haltedCues, (signals, field))
  +                | [] -> (Cue.Nil, (signals, field))
   
           override this.Channel (_, field) =
               [Simulants.Field.Scene.Avatar.Avatar.ChangeEvent =|> fun evt -> msg (UpdateAvatar (evt.Data.Value :?> Avatar))
  
positive.code:                     Content.panel Gen.name
                         [Entity.Position == v2 -448.0f -256.0f; Entity.Elevation == Constants.Field.GuiElevation; Entity.Size == v2 896.0f 512.0f
                          Entity.LabelImage == Assets.Gui.DialogXXLImage]
                         [sidebar (v2 24.0f 420.0f) 1.0f field
                          team (v2 138.0f 420.0f) 1.0f Int32.MaxValue field tautology2 MenuTeamAlly
                          Content.label Gen.name
                             [Entity.PositionLocal == v2 438.0f 288.0f; Entity.ElevationLocal == 1.0f; Entity.Size == v2 192.0f 192.0f
                              Entity.LabelImage <== field --> fun field ->
  
negative.code:                        [Entity.Position == v2 -448.0f -256.0f; Entity.Elevation == Constants.Field.GuiElevation; Entity.Size == v2 896.0f 512.0f
                          Entity.LabelImage == Assets.Gui.DialogXXLImage
                          Entity.Enabled <== field --> fun field -> Option.isNone field.Menu.MenuUseOpt]
                         [Content.sidebar (v2 24.0f 420.0f) 1.0f field
                          Content.items (v2 136.0f 420.0f) 1.0f 5 field MenuItemSelect
                          Content.text Gen.name
                             [Entity.PositionLocal == v2 368.0f 3.0f; Entity.ElevationLocal == 1.0f
                              Entity.Justification == Justified (JustifyCenter, JustifyMiddle)
  