4 ms·
This is basically the "pass a callback" approach that's popular in the JS world. Down this road lies madness. Let's see how this might actually work. First t
by stevelosh 14y ago
This is basically the "pass a callback" approach that's popular in the JS world. Down this road lies madness.
Let's see how this might actually work. First the easy part, the inventory selector. It takes the user's input and figures out what item they picked, then calls the callback:
(defn pop-ui [game]
(update-in game [:uis] butlast))
(defn push-ui [game ui]
(update-in game [:uis] conj ui))
(defmethod process-input :inventory-select [ui game input]
(let [selected-item (get-item game input)
callback (:on-select ui)]
(callback (pop-ui game)
selected-item)))
That's not too bad. It calls the callback with the game (after popping itself off the UI stack) and the item they picked.
Now to the main play UI that's going to set up the throwing when the user presses t:
(defmethod process-input :play [ui game input]
; ...
\t (update-in game [:uis] concat (make-throw-ui-stack))
; ...
)
Also pretty clean. We're going to be appending two UIs to the stack (throw and inventory select) so we use concat, but that's fine. You could have another multimethod if you really didn't want the play UI to know anything about targeting, but that's irrelvant for now.
So far so good, but now it's time to dive down the rabbit hole:
(defn make-throw-ui-stack []
[(new UI :throw)
(new UI :inventory-select
:on-select (fn [game selected-item]
,,,))])
Okay so we have the basic stack of UIs we'll need to start, all that's left is the callback. Well it needs to:
* Take the game and the item that was selected.
* Return a game with the throw UI storing the item for later, and a targeting UI after that so the user can pick a monster to kill.
That doesn't sound so bad:
(defn make-throw-ui-stack []
[(new UI :throw)
(new UI :inventory-select
:on-select (fn [game selected-item]
(push-ui (new UI :target))))])
Okay, a bit indentation-heavy but readable. At least we don't have to store the selected-item in the throw UI any more, because our callback closes over it. Let's make the targeting UI:
(defmethod process-input :target [ui game input]
(let [selected-monster (get-monster game input)
callback (:on-select ui)]
(callback (pop-ui game)
selected-monster)))
Cool, but wait, now we need another callback in the throw UI!
(defn make-throw-ui-stack []
[(new UI :throw)
(new UI :inventory-select
:on-select (fn [game selected-item]
(push-ui (new UI :target
:on-select (fn [game selected-item]
(let [game (pop-ui game)]
(throw-item-at game selected-item selected-monster)))))))])
Okay this looks pretty bad. It's ugly, and all the logic is in the "make throw UI stack" call.
Now notice that we haven't handled the "user pressed escape to cancel" case at all. Oh god.
Contrast this to the "reduce over input" method:
(defn pop-ui [game]
(update-in game [:uis] butlast))
(defn push-ui [game ui]
(update-in game [:uis] conj ui))
(defmethod process-input :inventory-select [ui game input]
(let [result (if (= input :escape)
:cancel
(get-item game input))]
(-> game
pop-ui
(assoc :input {:item result})))
(defmethod process-input :target [ui game input]
(let [result (if (= input :escape)
:cancel
(get-monster game input))]
(-> game
pop-ui
(assoc :input {:monster result})))
(defmethod process-input :throw [ui game item]
(cond
(= input :escape) (pop-ui game)
(contains? input :item) (-> game
pop-ui
(push-ui (assoc ui :item (:item input)))
(push-ui (new UI :target)))
(contains? input :monster) (throw-item-at game (:item ui) (:monster input))))
(defn make-throw-ui-stack []
[(new UI :throw)
(new UI :inventory-select)])
(defmethod process-input :play [ui game input]
; ...
\t (update-in game [:uis] concat (make-throw-ui-stack))
; ...
)
Not only do we now handle escaping from the menus, but this looks much cleaner to me because we're no longer nesting functions inside each other. We've also decoupled the "make the throwing UI" from the "game logic of the throwing UI".