From 980795cbe90bf2268cc1053e20880403bc43193a Mon Sep 17 00:00:00 2001 From: Julian Date: Thu, 5 Oct 2023 23:10:50 +0200 Subject: [PATCH] support adding warbands --- src/main/scala/controller.scala | 34 +++++++----- src/main/scala/uicomponents.scala | 89 +++++++++++++++++++------------ src/main/scala/view.scala | 10 ++-- 3 files changed, 84 insertions(+), 49 deletions(-) diff --git a/src/main/scala/controller.scala b/src/main/scala/controller.scala index 940dad6..7bb7fe1 100644 --- a/src/main/scala/controller.scala +++ b/src/main/scala/controller.scala @@ -3,7 +3,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import rescala.default._ import scala.util.Failure import scala.util.Success -import miniscribe.data.{AppState, Force} +import miniscribe.data.{AppState, Force, Hero, Model, Warband} import java.util.Base64 import org.scalajs.dom import org.scalajs.dom.URLSearchParams @@ -13,9 +13,10 @@ import scala.xml.{Document => XMLDocument} import rescala.default // ==== World events ==== -object ForceEvents: - val add = Evt[String]() - val delete = Evt[String]() +object ArmyEvents: + val addForce = Evt[String]() // forceName + val deleteForce = Evt[String]() // forceName + val addWarband = Evt[(Force, String)]() // force, heroName object NavigationEvents: val forwardBackward = Evt[Unit]() @@ -34,18 +35,28 @@ class Controller: else AppState() // app state can be changed through these events - private val addAct = ForceEvents.add.act[AppState] { f => + private val addForceAct = ArmyEvents.addForce.act[AppState] { f => current.copy(forces = current.forces :+ Force(f, List())) } - private val delAct = ForceEvents.delete.act[AppState] { f => + private val delForceAct = ArmyEvents.deleteForce.act[AppState] { f => current.copy(forces = current.forces.filter(_._1 != f)) } + private val addWarbandAct = ArmyEvents.addWarband.act[AppState] { (f, h) => + current.copy(forces = current.forces.map { + case g @ Force(name, warbands, _) if f == g => + val newHero = Hero(model = Some(Model(name = h))) + Force(name, warbands = (warbands.:+(Warband(hero = Some(newHero))))) + case g => g + }) + } private val forwBackwAct = NavigationEvents.forwardBackward.act[AppState] { _ => parseState() // whenever we detect a forward/backward event, simply parse state from proto } val state: Signal[AppState] = - Fold(parseState())(addAct, delAct, forwBackwAct) + Fold(parseState())(addForceAct, delForceAct, addWarbandAct, forwBackwAct) + + state.observe(s => println(s"state changed: $s")) // ========================= // ==== Derived values ==== @@ -91,15 +102,14 @@ class Controller: // ===== Browser history API a.k.a. handle forward/backward events ======= // update history when AppState changes but not on forward/backward events private val lastEvent = - ((ForceEvents.add || ForceEvents.delete).map(_ => - "force" - ) || NavigationEvents.forwardBackward.map(_ => "fb")) + (state.changed.map(_ => "armyChange") || NavigationEvents.forwardBackward + .map(_ => "fb")) .latest() Signal { (lastEvent(), state()) }.observe { case ("fb", _) => () - case (_, s) => + case (_, state) => val p = Base64.getEncoder.encodeToString( - data.AppState(forces = s.forces).toByteArray + state.toByteArray ) if !(p.isEmpty()) then dom.window.history.pushState(p, "title", s"?state=$p") diff --git a/src/main/scala/uicomponents.scala b/src/main/scala/uicomponents.scala index 6d6c9bf..54bdd4e 100644 --- a/src/main/scala/uicomponents.scala +++ b/src/main/scala/uicomponents.scala @@ -1,65 +1,86 @@ package miniscribe import rescala.default._ -import miniscribe.data.{Force} +import miniscribe.data.{Force, Warband} import scalatags.JsDom._ import scalatags.JsDom.all._ -import rescala.extra.Tags._ trait UIComponent: def render: HtmlTag object UIComponent: type ID = String - val triggerMenuEvt: Evt[ID] = Evt() - val triggered: Signal[Map[ID, Boolean]] = - triggerMenuEvt.fold(Map.WithDefault(Map.empty[ID, Boolean], _ => false))( - (acc, id) => acc.updated(id, !acc(id)) - ) + val toggleMenuEvt: Evt[ID] = Evt() + // val toggled: Signal[Map[ID, Boolean]] = + // toggleMenuEvt.fold(Map.WithDefault(Map.empty[ID, Boolean], _ => false))( + // (acc, id) => acc.updated(id, !acc(id)) + // ) + + // things that change the UI state + val toggled: Signal[Map[ID, Boolean]] = + Events.foldAll(Map.WithDefault(Map.empty[ID, Boolean], _ => false)) { acc => + Seq( + // toggle button presses + toggleMenuEvt act2 (id => acc.updated(id, !acc(id))), + // force removals + ArmyEvents.deleteForce act2 (forceName => acc.updated(forceName, false)) + ) + } -class ForceComponent( +case class ForceComponent( force: Force, - heroOptions: Either[String, List[String]] + heroOptions: Either[String, List[String]], + toggled: Boolean = false ) extends UIComponent: def render: HtmlTag = div( `class` := "force", h2(force.name), + force.warbands.map(w => WarbandComponent(w).render), div( a( - Signal { - span( - s"${if UIComponent.triggered()(force.name) then "▼" else "▶"} " + - s"${ - if force.warbands.isEmpty then "add warband" - else "manage warbands" - }" - ) - }.asModifier, - onclick := { () => UIComponent.triggerMenuEvt.fire(force.name) } + span( + s"${if toggled then "▼" else "▶"} " + + s"${ + if force.warbands.isEmpty then "add warband" + else "manage warbands" + }" + ), + onclick := { () => UIComponent.toggleMenuEvt.fire(force.name) } ), " | ", a( "delete", onclick := { () => - miniscribe.ForceEvents.delete.fire(force.name) + miniscribe.ArmyEvents.deleteForce.fire(force.name) } ) ), - Signal { - div( - `class` := "heroOptions", - display := s"${ - if UIComponent.triggered()(force.name) then "inherit" else "none" - }", - heroOptions match - case Left(error) => div(error) - case Right(options) => ul(options.map(li(_))) - ) - }.asModifier + div( + `class` := "heroOptions", + display := s"${if toggled then "inherit" else "none"}", + heroOptions match + case Left(error) => div(error) + case Right(options) => + ul( + options.map(heroName => + li( + a( + heroName, + onclick := { () => + ArmyEvents.addWarband.fire((force, heroName)) + } + ) + ) + ) + ) + ) ) -class HeroOptionsComponent( - options: Seq[String] +case class WarbandComponent( + warband: Warband ) extends UIComponent: - def render: HtmlTag = div(options) + def render: HtmlTag = div( + h3(warband.hero.flatMap(_.model.map(_.name))), + warband.troops.map(_.name) + ) diff --git a/src/main/scala/view.scala b/src/main/scala/view.scala index 0b0529d..068dd13 100644 --- a/src/main/scala/view.scala +++ b/src/main/scala/view.scala @@ -81,14 +81,14 @@ class View(controller: Controller): def addForceButton(army: String): TypedTag[Element] = val cb = Events.fromCallback[UIEvent](cb => a(army, onclick := cb)) - cb.event.observe(_ => miniscribe.ForceEvents.add.fire(army)) + cb.event.observe(_ => miniscribe.ArmyEvents.addForce.fire(army)) cb.event.observe(_ => forcesMenu.toggle.fire("addForce")) return cb.data def removeForceButton(army: String): TypedTag[Element] = val cb = Events.fromCallback[UIEvent](cb => a("delete", onclick := cb)) - cb.event.observe(_ => miniscribe.ForceEvents.delete.fire(army)) + cb.event.observe(_ => miniscribe.ArmyEvents.deleteForce.fire(army)) return cb.data val forcesMenu = toggleMenu( @@ -107,7 +107,11 @@ class View(controller: Controller): h1(title.asModifier), div(Signal { appState().forces.map(f => - ForceComponent(f, controller.heroOptions()(f)).render + ForceComponent( + f, + controller.heroOptions()(f), + UIComponent.toggled()(f.name) + ).render ) }.asModifierL), forcesMenu.show.asModifier