diff options
author | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-17 22:32:25 +0200 |
---|---|---|
committer | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-17 22:32:25 +0200 |
commit | a98f089035dbcc94c14c9cd6246c3150bee84241 (patch) | |
tree | 228ffa0d5e4a3e86c454cd297644c97abc994ef3 | |
parent | c954ca4d1ec677a34a6d787a23f9d01396f7e585 (diff) | |
download | scalevalapokalypsi-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.scala | 20 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Action.scala | 7 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Area.scala | 7 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Entities/Entity.scala | 41 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Entities/Player.scala | 53 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Event.scala | 28 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/SingEffects.scala | 14 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Server/Client.scala | 4 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Server/Server.scala | 16 |
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 */ |