aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-17 22:32:25 +0200
committerJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-17 22:32:25 +0200
commita98f089035dbcc94c14c9cd6246c3150bee84241 (patch)
tree228ffa0d5e4a3e86c454cd297644c97abc994ef3
parentc954ca4d1ec677a34a6d787a23f9d01396f7e585 (diff)
downloadscalevalapokalypsi-a98f089035dbcc94c14c9cd6246c3150bee84241.tar.gz
scalevalapokalypsi-a98f089035dbcc94c14c9cd6246c3150bee84241.zip
Improved client recovery from singing & added better logic for observations
The logic should still be implemented for all observations
-rw-r--r--src/scalevalapokalypsi/Client/Client.scala20
-rw-r--r--src/scalevalapokalypsi/Model/Action.scala7
-rw-r--r--src/scalevalapokalypsi/Model/Area.scala7
-rw-r--r--src/scalevalapokalypsi/Model/Entities/Entity.scala41
-rw-r--r--src/scalevalapokalypsi/Model/Entities/Player.scala53
-rw-r--r--src/scalevalapokalypsi/Model/Event.scala28
-rw-r--r--src/scalevalapokalypsi/Model/SingEffects.scala14
-rw-r--r--src/scalevalapokalypsi/Server/Client.scala4
-rw-r--r--src/scalevalapokalypsi/Server/Server.scala16
9 files changed, 141 insertions, 49 deletions
diff --git a/src/scalevalapokalypsi/Client/Client.scala b/src/scalevalapokalypsi/Client/Client.scala
index f37b1cc..aab6bc3 100644
--- a/src/scalevalapokalypsi/Client/Client.scala
+++ b/src/scalevalapokalypsi/Client/Client.scala
@@ -55,7 +55,7 @@ def newClient(name: String, ip: String, port: Int): Option[Client] =
*/
class Client(socket: Socket):
- /** Essential IO variables */
+ // Essential IO variables
private val input = socket.getInputStream
private val output = socket.getOutputStream
private val buffer: Array[Byte] = Array.ofDim(MAX_MSG_SIZE)
@@ -65,7 +65,7 @@ class Client(socket: Socket):
private var serverLineState = ServerLineState.WaitingForTimeLimit
- /** Variables about the status of the current turn for the client */
+ // Variables about the status of the current turn for the client
private var canAct = false // TODO: is really never true when it should
private var timeLimit: Long = 0
private var lastTurnStart: Long = 0
@@ -100,7 +100,6 @@ class Client(socket: Socket):
// TODO: we probably want to quit at EOF
stdinReader.newLine().foreach((s: String) =>
- println("not singing anymore!")
this.isSinging = false
output.write(stringToByteArray(s+"\r\n"))
)
@@ -127,16 +126,18 @@ class Client(socket: Socket):
private def displayActions(): Unit =
val somethingToShow = this.bufferedActions.nonEmpty
- if !this.isSinging then
- this.bufferedActions.foreach(println(_))
- this.bufferedActions.clear()
- if !this.isSinging && this.canAct && somethingToShow then
- print(this.actionGetterIndicator)
+ if somethingToShow then
+ if !this.isSinging then
+ this.bufferedActions.foreach(println(_))
+ this.bufferedActions.clear()
+ if !this.isSinging && this.canAct && somethingToShow then
+ print(this.actionGetterIndicator)
private def startSong(verse: String): Unit =
this.isSinging = true
print(s"\nLaula: “$verse”\n> ")
+ // TODO: this is sometimes in front of actions, use an indicator to test if newline before actions?
private def actionGetterIndicator =
val timeOfTurnEnd = this.lastTurnStart + this.timeLimit
val timeToTurnEnd = -currentTimeMillis()/1000 + timeOfTurnEnd
@@ -172,14 +173,11 @@ class Client(socket: Socket):
case ServerLineState.ActionsAndSong =>
if line.headOption.exists(_.toString == SING_INDICATOR) then
this.startSong(line.tail)
- this.canAct = false
else if line.headOption.contains(ACTION_BLOCKING_INDICATOR) then
this.canAct = false
this.bufferAction(line.tail)
else if line.nonEmpty then
this.bufferAction((line.tail))
- else
- println("We should not get empty lines from the server!")
case ServerLineState.TurnIndicator =>
this.serverLineState = ServerLineState.AreaDescription
diff --git a/src/scalevalapokalypsi/Model/Action.scala b/src/scalevalapokalypsi/Model/Action.scala
index cee0ee5..30fbf46 100644
--- a/src/scalevalapokalypsi/Model/Action.scala
+++ b/src/scalevalapokalypsi/Model/Action.scala
@@ -63,7 +63,8 @@ class Action(input: String):
if end == "suohon" then
val targetEntity = actor.location.getEntity(start)
targetEntity
- .foreach(e => actor.setSingEffect(defaultSingAttack(e)))
+ .foreach(e => actor.setSingEffect(DefaultSingAttack(e)))
+ targetEntity.foreach(_.observeString(s"${actor.name} laulaa sinua suohon!"))
targetEntity.map(e => (
"Aloitat suohonlaulun.",
s"${actor.name} aloittaa suohonlaulun."
@@ -78,9 +79,9 @@ class Action(input: String):
resOption.map(_(1)).filter(_.length > 0)
.foreach(s =>
- actor.location.getEntities.filter(_ != actor).foreach(_.observe(s))
+ actor.location.getEntities.filter(_ != actor).foreach(_.observeString(s))
if oldLocation != actor.location then
- oldLocation.getEntities.foreach(_.observe(s))
+ oldLocation.getEntities.foreach(_.observeString(s))
)
resOption.map(_(0))
diff --git a/src/scalevalapokalypsi/Model/Area.scala b/src/scalevalapokalypsi/Model/Area.scala
index f534309..96392ba 100644
--- a/src/scalevalapokalypsi/Model/Area.scala
+++ b/src/scalevalapokalypsi/Model/Area.scala
@@ -33,6 +33,13 @@ class Area(val name: String, var description: String):
def getEntities: Iterable[Entity] = this.entities.values
def getEntity(name: String): Option[Entity] = this.entities.get(name)
+ /** Makes all entities in this area observe the given event.
+ *
+ * @param event the event to observe.
+ */
+ def observeEvent(event: Event): Unit =
+ this.getEntities.foreach(_.observe(event))
+
/** Tells whether this area has a neighbor in the given direction.
*
* @param direction the direction to check
diff --git a/src/scalevalapokalypsi/Model/Entities/Entity.scala b/src/scalevalapokalypsi/Model/Entities/Entity.scala
index 1592f2e..26dd7dc 100644
--- a/src/scalevalapokalypsi/Model/Entities/Entity.scala
+++ b/src/scalevalapokalypsi/Model/Entities/Entity.scala
@@ -20,25 +20,46 @@ class Entity(
private var quitCommandGiven = false // one-way flag
private val inventory: Map[String, Item] = Map()
private var hp = initialHP
-
+
+ // TODO: add logic for choosing from multiplu lines - can depend on HP etc.
+ /** Gets a verse to sing when attacking against this entity.
+ *
+ * @return the verse to sing against this entity
+ */
+ def getVerseAgainst: String = "Esimerkkirivi laulettavaksi"
+
def takeDamage(amount: Int): Unit =
hp -= amount
if hp < 0 then
println("Oh no, I died!")
-
- def condition: String =
+
+ /** Returns a description of the physical condition of this entity,
+ * i.e. the damage it has taken.
+ *
+ * @return a pair of strings, both of which describe the condition of this
+ * entity, the first of which is in first person and the second in
+ * third person.
+ */
+ def condition: (String, String) =
if hp < maxHP * .25 then
- s"$name näyttää maansa myyneeltä."
+ ("Sinua heikottaa ja tunnet olevasi lähellä häviötä.",
+ s"$name näyttää maansa myyneeltä.")
else if hp < maxHP * .50 then
- s"$name näyttää sinnittelevän yhä."
+ ("Sinnittelet yhä, mutta kuntosi on laskenut suuresti.",
+ s"$name näyttää sinnittelevän yhä.")
else if hp < maxHP * .75 then
- s"$name näyttää aavistuksen lannistuneelta."
+ ("Tunnet koettelemusten vaikutuksen, mutta et anna niiden lannistaa itseäsi",
+ s"$name näyttää aavistuksen lannistuneelta.")
+ else if hp < maxHP then
+ ("Olet voimissasi.", s"$name on yhä voimissaan.")
else
- s"$name on yhä täysissä voimissaan."
+ ("Olet täysin kunnossa.", s"$name näyttää kuin vastasyntyneeltä.")
/** Does nothing, except possibly in inherited classes. */
- def observe(observation: String): Unit =
- println("[debug] entity got observation & discarded it")
+ def observeString(observation: String): Unit =
+ println(" [debug] entity got observation string & discarded it")
+ def observe(event: Event): Unit =
+ println(" [debug] entity got observation event & discarded it")
/** Returns the player’s current location. */
def location = this.currentLocation
@@ -83,7 +104,7 @@ class Entity(
)
def sayTo(entity: Entity, message: String): (String, String) =
- entity.observe(s"${this.name}: \"$message\"")
+ entity.observeString(s"${this.name}: \"$message\"")
(s"You say so to ${entity.name}.", "")
def say(message: String): (String, String) =
diff --git a/src/scalevalapokalypsi/Model/Entities/Player.scala b/src/scalevalapokalypsi/Model/Entities/Player.scala
index 7e441c1..f231c28 100644
--- a/src/scalevalapokalypsi/Model/Entities/Player.scala
+++ b/src/scalevalapokalypsi/Model/Entities/Player.scala
@@ -15,14 +15,20 @@ import scalevalapokalypsi.Model.*
class Player(name: String, initialLocation: Area) extends Entity(name, initialLocation):
private val observations: Buffer[String] = Buffer.empty
- private var pendingSingEffect: Option[Float => String] = None
+ private val observedEvents: Buffer[Event] = Buffer.empty
+ private var pendingSingEffect: Option[SingEffect] = None
- override def observe(observation: String): Unit =
+ override def observeString(observation: String): Unit =
this.observations.append(observation)
+ override def observe(event: Event): Unit =
+ this.observedEvents.append(event)
def readAndClearObservations(): Vector[String] =
- val res = this.observations.toVector
+ val res1 = this.observations
+ val res2 = this.observedEvents.map(_.descriptionFor(this))
+ val res = (res1 ++ res2).toVector
observations.clear()
+ observedEvents.clear()
res
/** Returns whether this player has a pending sing effect. */
@@ -33,18 +39,37 @@ class Player(name: String, initialLocation: Area) extends Entity(name, initialLo
*
* @param effect the effect to apply based on the song.
*/
- def setSingEffect(effect: Float => String): Unit =
+ def setSingEffect(effect: SingEffect): Unit =
this.pendingSingEffect = Some(effect)
+
+ def getSingEffectTarget: Option[Entity] =
+ this.pendingSingEffect.map(_.target)
- /** Applies the pending sing effect.
- *
- * @param singQuality the quality of the song
- * @return a textual description of the effects of the song,
- * or None if there was no pending sing effect.
- */
- def applySingEffect(singQuality: Float): Option[String] =
- val res = this.pendingSingEffect.map(f => f(singQuality))
+ /** Applies the pending sing effect and informs the surronding Entities
+ * about the effects of the song.
+ *
+ * @param singQuality the quality of the song
+ */
+ def applySingEffect(singQuality: Float): Unit =
+ val res = this.pendingSingEffect.map(ef => ef(singQuality))
this.pendingSingEffect = None
- res
-
+ val qualityDescriptions =
+ if singQuality < .10 then
+ ("säälittävää", "epsilonin suuruinen")
+ else if singQuality < .30 then
+ ("heikkoa", "vähäinen")
+ else if singQuality < .60 then
+ ("keskinkertaista", "huomattavissa")
+ else if singQuality < .80 then
+ ("hyvää", "huomattava")
+ else ("erinomaista", "merkittävä")
+ val quality =
+ s"Laulu on ${qualityDescriptions(0)} ja sen vaikutus on ${qualityDescriptions(1)}."
+ val event = res.map(ev => Event(
+ ev.target,
+ s"$quality\n${ev.inFirstPerson}",
+ s"$quality\n${ev.inThirdPerson}"
+ ))
+ event.foreach(this.location.observeEvent(_))
+
end Player
diff --git a/src/scalevalapokalypsi/Model/Event.scala b/src/scalevalapokalypsi/Model/Event.scala
new file mode 100644
index 0000000..cba611d
--- /dev/null
+++ b/src/scalevalapokalypsi/Model/Event.scala
@@ -0,0 +1,28 @@
+package scalevalapokalypsi.Model
+
+import scalevalapokalypsi.Model.Entities.Entity
+
+/** A description of an action.
+ *
+ * @param target the entity that was as a target in this event
+ * @param inFirstPerson textual description of the event in first person
+ * @param inThirdPerson textual description of the event in third person
+ */
+class Event(
+ val target: Entity,
+ val inFirstPerson: String,
+ val inThirdPerson: String
+):
+
+ /** Gets the description of this event as seen by the given
+ * entity. Note that this method does no checks whether the given entity
+ * could even see the event, only what it would have looked like to them.
+ *
+ * @param entity the entity whose perspective to use
+ * @return a textual description of the event
+ */
+ def descriptionFor(entity: Entity): String =
+ if entity == target then inFirstPerson
+ else inThirdPerson
+
+end Event \ No newline at end of file
diff --git a/src/scalevalapokalypsi/Model/SingEffects.scala b/src/scalevalapokalypsi/Model/SingEffects.scala
index 247d672..6702df5 100644
--- a/src/scalevalapokalypsi/Model/SingEffects.scala
+++ b/src/scalevalapokalypsi/Model/SingEffects.scala
@@ -2,6 +2,16 @@ package scalevalapokalypsi.Model
import scalevalapokalypsi.Model.Entities.Entity
-def defaultSingAttack(targetEntity: Entity)(singQuality: Float): String =
+def defaultSingAttack(targetEntity: Entity)(singQuality: Float): Event =
targetEntity.takeDamage((singQuality * 30).toInt)
- targetEntity.condition \ No newline at end of file
+ val condition = targetEntity.condition
+ Event(targetEntity, condition(0), condition(1))
+
+trait SingEffect(val target: Entity):
+ def apply(singQuality: Float): Event
+
+class DefaultSingAttack(target: Entity) extends SingEffect(target):
+ def apply(singQuality: Float): Event =
+ this.target.takeDamage((singQuality * 50).toInt) // TODO: remove magic value
+ val condition = this.target.condition
+ Event(target, condition(0), condition(1))
diff --git a/src/scalevalapokalypsi/Server/Client.scala b/src/scalevalapokalypsi/Server/Client.scala
index ceeff1f..1af83bf 100644
--- a/src/scalevalapokalypsi/Server/Client.scala
+++ b/src/scalevalapokalypsi/Server/Client.scala
@@ -169,10 +169,8 @@ class Client(val socket: Socket):
this.singStartTime match
case Some(t) =>
val timePassed = currentTimeMillis()/1000 - t
- this.player.flatMap(_.applySingEffect(
+ this.player.foreach(_.applySingEffect(
5 / max(5, timePassed)
- )).foreach(s => this.player.foreach((c: Player) =>
- c.observe(s"Lakkaat laulamasta.\n$s")
))
this.singStartTime = None
case None =>
diff --git a/src/scalevalapokalypsi/Server/Server.scala b/src/scalevalapokalypsi/Server/Server.scala
index db30283..609b581 100644
--- a/src/scalevalapokalypsi/Server/Server.scala
+++ b/src/scalevalapokalypsi/Server/Server.scala
@@ -102,7 +102,7 @@ class Server(
this.clients.foreach(c =>
if c.player != playerEntity then
- c.player.foreach(_.observe(
+ c.player.foreach(_.observeString(
s"${name.getOrElse("Unknown player")} joins the game.")
)
)
@@ -118,12 +118,16 @@ class Server(
private def makeClientsSing(): Unit =
this.clients.foreach(c =>
- if c.player.exists(_.isSinging) && !c.clientHasSong then
- this.writeToClient(
- s"${SING_INDICATOR}Esimerkkirivi laulettavaksi, lirulirulei\r\n",
- c
- )
+ val target = c.player.flatMap(_.getSingEffectTarget)
+ target.foreach(t =>
+ if c.player.exists(_.isSinging) && !c.clientHasSong then
+ this.writeToClient(
+ s"${SING_INDICATOR}${t.getVerseAgainst}\r\n",
+ // TODO: store the verse and check how close client input is when determining sing quality
+ c
+ )
c.startSong()
+ )
)
/** Helper function to determine if the next turn can be taken */