Skip to content

Commit

Permalink
support adding warbands
Browse files Browse the repository at this point in the history
  • Loading branch information
haaase committed Oct 5, 2023
1 parent 8c38433 commit 980795c
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 49 deletions.
34 changes: 22 additions & 12 deletions src/main/scala/controller.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]()
Expand All @@ -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 ====
Expand Down Expand Up @@ -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")
Expand Down
89 changes: 55 additions & 34 deletions src/main/scala/uicomponents.scala
Original file line number Diff line number Diff line change
@@ -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)
)
10 changes: 7 additions & 3 deletions src/main/scala/view.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down

0 comments on commit 980795c

Please sign in to comment.