diff options
author | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-17 17:06:56 +0200 |
---|---|---|
committer | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-17 17:06:56 +0200 |
commit | c954ca4d1ec677a34a6d787a23f9d01396f7e585 (patch) | |
tree | c6b00b5046bde3a98c18f9557198f852b4ce9d46 | |
parent | a6b0330c845d4edad87c7059bac56e194a276c6f (diff) | |
download | scalevalapokalypsi-c954ca4d1ec677a34a6d787a23f9d01396f7e585.tar.gz scalevalapokalypsi-c954ca4d1ec677a34a6d787a23f9d01396f7e585.zip |
Template for singing, WIP.
* The line to sing is always the same.
* The client recovers weirdly from singing before the next turn and my brain is currently too fried to figure out why
-rw-r--r-- | .idea/uiDesigner.xml | 124 | ||||
-rw-r--r-- | protocol.txt | 18 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Client/Client.scala | 52 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Client/ReceivedLineParser.scala | 2 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Action.scala | 40 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Adventure.scala | 9 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Area.scala | 2 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Entities/Entity.scala | 30 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Entities/Player.scala | 23 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/SingEffects.scala | 7 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Server/Client.scala | 23 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Server/Clients.scala | 3 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Server/Server.scala | 21 | ||||
-rw-r--r-- | src/scalevalapokalypsi/constants/constants.scala | 1 |
14 files changed, 317 insertions, 38 deletions
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Palette2"> + <group name="Swing"> + <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" /> + </item> + <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" /> + </item> + <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true"> + <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" /> + <initial-values> + <property name="text" value="Button" /> + </initial-values> + </item> + <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="RadioButton" /> + </initial-values> + </item> + <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="CheckBox" /> + </initial-values> + </item> + <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="Label" /> + </initial-values> + </item> + <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> + <preferred-size width="200" height="200" /> + </default-constraints> + </item> + <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> + <preferred-size width="200" height="200" /> + </default-constraints> + </item> + <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" /> + </item> + <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1"> + <preferred-size width="-1" height="20" /> + </default-constraints> + </item> + <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" /> + </item> + <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" /> + </item> + </group> + </component> +</project>
\ No newline at end of file diff --git a/protocol.txt b/protocol.txt new file mode 100644 index 0000000..f38e778 --- /dev/null +++ b/protocol.txt @@ -0,0 +1,18 @@ +Client: [version number]CRLF[client name|] +Server: [good/version old] +... +Server: [time limit in int/secs]CRLF # signifies game start + [instantly gives turn info] + +Before turn: +[Action blocking|nonblocking|sing indicator] + if action indicator => [Description of action during turn]CRLF + if sing indicator => [Line to sing]CRLF +At start of turn: +Server: [turn indicator]CRLF + [Description of area]CRLF + [Directions separated with semicolon]CRLF + [Visible items separated with semicolon]CRLF + [Entities separated with semicolon]CRLF + +When running turn: [CRLF-separated list of things happening in the players room] diff --git a/src/scalevalapokalypsi/Client/Client.scala b/src/scalevalapokalypsi/Client/Client.scala index 41b1003..f37b1cc 100644 --- a/src/scalevalapokalypsi/Client/Client.scala +++ b/src/scalevalapokalypsi/Client/Client.scala @@ -18,7 +18,7 @@ import java.lang.System.currentTimeMillis */ enum ServerLineState: case WaitingForTimeLimit, - ActionDescription, + ActionsAndSong, TurnIndicator, AreaDescription, Directions, @@ -29,8 +29,8 @@ enum ServerLineState: /** Creates a new client. * * @param name the name the client and its player should have - * @ip the ip of the server to connect to - * @port the port of the server to connect to + * @param ip the ip of the server to connect to + * @param port the port of the server to connect to * @return the client created, if all was successful */ def newClient(name: String, ip: String, port: Int): Option[Client] = @@ -66,10 +66,12 @@ class Client(socket: Socket): private var serverLineState = ServerLineState.WaitingForTimeLimit /** Variables about the status of the current turn for the client */ - private var canAct = false + private var canAct = false // TODO: is really never true when it should private var timeLimit: Long = 0 private var lastTurnStart: Long = 0 private var lastExecutedTurn: Long = 0 + private var isSinging: Boolean = false + private val bufferedActions: Buffer[String] = Buffer.empty assert( lastTurnStart <= lastExecutedTurn, "don't initialize with unexecuted turn" @@ -83,14 +85,23 @@ class Client(socket: Socket): stdinReader.startReading() while true do + sleep(POLL_INTERVAL) this.readAndParseDataFromServer() - if this.lastExecutedTurn < this.lastTurnStart then + this.displayActions() + + if + this.lastExecutedTurn < this.lastTurnStart && + !this.isSinging + then print(this.giveTurn()) + // 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")) ) @@ -111,11 +122,21 @@ class Client(socket: Socket): this.lastExecutedTurn = currentTimeMillis / 1000 s"\n\n${this.turnInfo}\n${this.actionGetterIndicator}" - private def displayAction(action: String): Unit = - println(s"$action") - if this.canAct then + private def bufferAction(action: String): Unit = + this.bufferedActions += action + + 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) + private def startSong(verse: String): Unit = + this.isSinging = true + print(s"\nLaula: “$verse”\n> ") + private def actionGetterIndicator = val timeOfTurnEnd = this.lastTurnStart + this.timeLimit val timeToTurnEnd = -currentTimeMillis()/1000 + timeOfTurnEnd @@ -148,10 +169,17 @@ class Client(socket: Socket): this.serverLineState = ServerLineState.TurnIndicator this.lastTurnStart = currentTimeMillis / 1000 - case ServerLineState.ActionDescription => - if line.nonEmpty && line.head == ACTION_BLOCKING_INDICATOR then + 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.displayAction(line.tail) + 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 @@ -170,7 +198,7 @@ class Client(socket: Socket): case ServerLineState.Entities => this.turnInfo.visibleEntities = line.split(LIST_SEPARATOR) - this.serverLineState = ServerLineState.ActionDescription + this.serverLineState = ServerLineState.ActionsAndSong this.lastTurnStart = currentTimeMillis() / 1000 end parseLineFromServer diff --git a/src/scalevalapokalypsi/Client/ReceivedLineParser.scala b/src/scalevalapokalypsi/Client/ReceivedLineParser.scala index dfcc2d2..9337ce1 100644 --- a/src/scalevalapokalypsi/Client/ReceivedLineParser.scala +++ b/src/scalevalapokalypsi/Client/ReceivedLineParser.scala @@ -6,7 +6,7 @@ import scalevalapokalypsi.constants.* /** A class for checking asynchronously for received lines */ class ReceivedLineParser: - private var serverLineState = ServerLineState.ActionDescription + private var serverLineState = ServerLineState.ActionsAndSong private var bufferedData: Buffer[Byte] = Buffer.empty // TODO: suboptimal DS diff --git a/src/scalevalapokalypsi/Model/Action.scala b/src/scalevalapokalypsi/Model/Action.scala index e59d5f1..cee0ee5 100644 --- a/src/scalevalapokalypsi/Model/Action.scala +++ b/src/scalevalapokalypsi/Model/Action.scala @@ -2,10 +2,13 @@ package scalevalapokalypsi.Model import scalevalapokalypsi.Model.Entities.* -/** The class `Action` represents actions that a player may take in a text adventure game. - * `Action` objects are constructed on the basis of textual commands and are, in effect, - * parsers for such commands. An action object is immutable after creation. - * @param input a textual in-game command such as “go east” or “rest” */ +/** The class `Action` represents actions that a player may take in a text + * adventure game. `Action` objects are constructed on the basis of textual + * commands and are, in effect, parsers for such commands. An action object is + * immutable after creation. + * + * @param input a textual in-game command such as “go east” or “rest” + */ class Action(input: String): private val commandText = input.trim.toLowerCase @@ -19,12 +22,19 @@ class Action(input: String): case "get" => actor.location.hasItem(this.modifiers) case "drop" => actor.canDrop(this.modifiers) case "say" => false + case "laula" => false case other => false - /** Causes the given player to take the action represented by this object, assuming - * that the command was understood. Returns a description of what happened as a result - * of the action (such as “You go west.”). The description is returned in an `Option` - * wrapper; if the command was not recognized, `None` is returned. */ + /** Causes the given player to take the action represented by this object, + * assuming that the command was understood. Returns a description of what + * happened as a result of the action (such as “You go west.”). The + * description is returned in an `Option` wrapper; if the command was not + * recognized, `None` is returned. + * + * @param actor the acting player + * @return A textual description of the action, or `None` if the action + * was not recognized. + */ def execute(actor: Player): Option[String] = val oldLocation = actor.location val resOption: Option[(String, String)] = this.verb match @@ -46,6 +56,20 @@ class Action(input: String): else Some(actor.say(modifiers)) case "drop" => Some(actor.drop(this.modifiers)) + case "laula" => + val end = modifiers.takeRight("suohon".length) + val start = + modifiers.take(modifiers.length - "suohon".length).trim + if end == "suohon" then + val targetEntity = actor.location.getEntity(start) + targetEntity + .foreach(e => actor.setSingEffect(defaultSingAttack(e))) + targetEntity.map(e => ( + "Aloitat suohonlaulun.", + s"${actor.name} aloittaa suohonlaulun." + )) + else + None case "xyzzy" => Some(( "The grue tastes yummy.", s"${actor.name} tastes some grue.") diff --git a/src/scalevalapokalypsi/Model/Adventure.scala b/src/scalevalapokalypsi/Model/Adventure.scala index 9d07ba6..0fbf6cd 100644 --- a/src/scalevalapokalypsi/Model/Adventure.scala +++ b/src/scalevalapokalypsi/Model/Adventure.scala @@ -36,14 +36,14 @@ class Adventure(val playerNames: Vector[String]): "Et vielä voi tehdä sillä mitään, koska et edes osaa laula." )) - val players: Map[String, Player] = Map() - playerNames.foreach(this.addPlayer(_)) - val entities: Map[String, Entity] = Map() private val gruu = Entity("Gruu", northForest) northForest.addEntity(gruu) this.entities += gruu.name -> gruu + val players: Map[String, Player] = Map() + playerNames.foreach(this.addPlayer(_)) + /** Adds a player entity with the specified name to the game. * * @param name the name of the player entity to add @@ -52,9 +52,10 @@ class Adventure(val playerNames: Vector[String]): def addPlayer(name: String): Player = val newPlayer = Player(name, middle) middle.addEntity(newPlayer) + this.entities += name -> newPlayer players += name -> newPlayer newPlayer - + /** Gets the player entity with the specified name. * * @param name name of the player to find diff --git a/src/scalevalapokalypsi/Model/Area.scala b/src/scalevalapokalypsi/Model/Area.scala index a11b0e5..f534309 100644 --- a/src/scalevalapokalypsi/Model/Area.scala +++ b/src/scalevalapokalypsi/Model/Area.scala @@ -30,8 +30,8 @@ class Area(val name: String, var description: String): def getNeighborNames: Iterable[String] = this.neighbors.keys def getItemNames: Iterable[String] = this.items.keys def getEntityNames: Iterable[String] = this.entities.values.map(_.name) - def getEntity(name: String): Option[Entity] = this.entities.get(name) def getEntities: Iterable[Entity] = this.entities.values + def getEntity(name: String): Option[Entity] = this.entities.get(name) /** Tells whether this area has a neighbor in the given direction. * diff --git a/src/scalevalapokalypsi/Model/Entities/Entity.scala b/src/scalevalapokalypsi/Model/Entities/Entity.scala index b90a61a..1592f2e 100644 --- a/src/scalevalapokalypsi/Model/Entities/Entity.scala +++ b/src/scalevalapokalypsi/Model/Entities/Entity.scala @@ -1,22 +1,44 @@ package scalevalapokalypsi.Model.Entities -import scala.collection.mutable.Map +import scala.collection.mutable.{Buffer,Map} import scalevalapokalypsi.Model.* + /** An in-game entity. * * @param name the name of the entity * @param initialLocation the Area where the entity is instantiated */ -class Entity(val name: String, initialLocation: Area): +class Entity( + val name: String, + initialLocation: Area, + initialHP: Int = 100, + val maxHP: Int = 100 +): + private var currentLocation: Area = initialLocation private var quitCommandGiven = false // one-way flag private val inventory: Map[String, Item] = Map() + private var hp = initialHP + + def takeDamage(amount: Int): Unit = + hp -= amount + if hp < 0 then + println("Oh no, I died!") + + def condition: String = + if hp < maxHP * .25 then + s"$name näyttää maansa myyneeltä." + else if hp < maxHP * .50 then + s"$name näyttää sinnittelevän yhä." + else if hp < maxHP * .75 then + s"$name näyttää aavistuksen lannistuneelta." + else + s"$name on yhä täysissä voimissaan." /** Does nothing, except possibly in inherited classes. */ def observe(observation: String): Unit = - println("no observation made.") - () + println("[debug] entity got observation & discarded it") /** Returns the player’s current location. */ def location = this.currentLocation diff --git a/src/scalevalapokalypsi/Model/Entities/Player.scala b/src/scalevalapokalypsi/Model/Entities/Player.scala index 6e82837..7e441c1 100644 --- a/src/scalevalapokalypsi/Model/Entities/Player.scala +++ b/src/scalevalapokalypsi/Model/Entities/Player.scala @@ -15,6 +15,7 @@ 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 override def observe(observation: String): Unit = this.observations.append(observation) @@ -23,5 +24,27 @@ class Player(name: String, initialLocation: Area) extends Entity(name, initialLo val res = this.observations.toVector observations.clear() res + + /** Returns whether this player has a pending sing effect. */ + def isSinging: Boolean = this.pendingSingEffect.isDefined + /** Makes this player start singing, i.e. gives it this sing effect to + * complete. + * + * @param effect the effect to apply based on the song. + */ + def setSingEffect(effect: Float => String): Unit = + this.pendingSingEffect = Some(effect) + + /** 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)) + this.pendingSingEffect = None + res + end Player diff --git a/src/scalevalapokalypsi/Model/SingEffects.scala b/src/scalevalapokalypsi/Model/SingEffects.scala index 981b772..247d672 100644 --- a/src/scalevalapokalypsi/Model/SingEffects.scala +++ b/src/scalevalapokalypsi/Model/SingEffects.scala @@ -1,6 +1,7 @@ -/*package scalevalapokalypsi.Model +package scalevalapokalypsi.Model + +import scalevalapokalypsi.Model.Entities.Entity def defaultSingAttack(targetEntity: Entity)(singQuality: Float): String = targetEntity.takeDamage((singQuality * 30).toInt) - targetEntity.condition -*/
\ No newline at end of file + targetEntity.condition
\ No newline at end of file diff --git a/src/scalevalapokalypsi/Server/Client.scala b/src/scalevalapokalypsi/Server/Client.scala index 8716ca9..ceeff1f 100644 --- a/src/scalevalapokalypsi/Server/Client.scala +++ b/src/scalevalapokalypsi/Server/Client.scala @@ -1,11 +1,12 @@ package scalevalapokalypsi.Server import java.net.Socket -import scala.math.min +import scala.math.{min,max} import scalevalapokalypsi.constants.* import ServerProtocolState.* import scalevalapokalypsi.Model.Action import scalevalapokalypsi.Model.Entities.Player +import java.lang.System.currentTimeMillis class Client(val socket: Socket): private var incompleteMessage: Array[Byte] = @@ -17,6 +18,11 @@ class Client(val socket: Socket): private var protocolIsIntact = true private var name: Option[String] = None private var nextAction: Option[Action] = None + private var singStartTime: Option[Long] = None + + def clientHasSong = this.singStartTime.isDefined + def startSong(): Unit = + this.singStartTime = Some(currentTimeMillis() / 1000) /** Calculates the amount of bytes available for future incoming messages */ def spaceAvailable: Int = MAX_MSG_SIZE - incompleteMessageIndex @@ -109,7 +115,6 @@ class Client(val socket: Socket): /** Makes the client play its turn */ def act(): Unit = - this.addDataToSend(ACTION_BLOCKING_INDICATOR.toString) this.nextAction.foreach(a => this.addDataToSend( s"$ACTION_BLOCKING_INDICATOR${this.executeAction(a)}" )) @@ -161,7 +166,19 @@ class Client(val socket: Socket): ) then this.nextAction = Some(action) else if this.nextAction.isEmpty then - this.addDataToSend(s"$ACTION_NONBLOCKING_INDICATOR${this.executeAction(action)}") + this.singStartTime match + case Some(t) => + val timePassed = currentTimeMillis()/1000 - t + this.player.flatMap(_.applySingEffect( + 5 / max(5, timePassed) + )).foreach(s => this.player.foreach((c: Player) => + c.observe(s"Lakkaat laulamasta.\n$s") + )) + this.singStartTime = None + case None => + this.addDataToSend( + s"$ACTION_NONBLOCKING_INDICATOR${this.executeAction(action)}" + ) /** Executes the specified action and returns its description */ private def executeAction(action: Action): String = diff --git a/src/scalevalapokalypsi/Server/Clients.scala b/src/scalevalapokalypsi/Server/Clients.scala index 377050d..9ad0e84 100644 --- a/src/scalevalapokalypsi/Server/Clients.scala +++ b/src/scalevalapokalypsi/Server/Clients.scala @@ -25,6 +25,9 @@ class Clients(maxClients: Int): * @return an iterable of all the clients */ def allClients: Iterable[Client] = clients.toVector.flatten + + def filter(p: Client => Boolean): Iterable[Client] = + this.allClients.filter(p) /** Applies the function `f` to all the clients for its side effects. */ def foreach(f: Client => Any): Unit = this.clients.flatten.foreach(f) diff --git a/src/scalevalapokalypsi/Server/Server.scala b/src/scalevalapokalypsi/Server/Server.scala index f18d5c0..db30283 100644 --- a/src/scalevalapokalypsi/Server/Server.scala +++ b/src/scalevalapokalypsi/Server/Server.scala @@ -52,6 +52,7 @@ class Server( this.readFromAll() this.clients.foreach(_.interpretData()) this.writeClientDataToClients() + this.makeClientsSing() this.writeObservations() if this.canExecuteTurns then this.clients.inRandomOrder(_.act()) @@ -68,7 +69,11 @@ class Server( ) startGameForClient(c) ) - else if this.adventure.isEmpty && !this.clients.isEmpty && this.clients.forall(_.isReadyForGameStart) then + else if + this.adventure.isEmpty && + !this.clients.isEmpty && + this.clients.forall(_.isReadyForGameStart) + then this.adventure = Some(Adventure(this.clients.names)) this.clients.foreach(startGameForClient(_)) this.previousTurn = currentTimeMillis() / 1000 @@ -97,7 +102,9 @@ class Server( this.clients.foreach(c => if c.player != playerEntity then - c.player.foreach(_.observe(s"${name.getOrElse("Unknown player")} joins the game.")) + c.player.foreach(_.observe( + s"${name.getOrElse("Unknown player")} joins the game.") + ) ) @@ -109,6 +116,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 + ) + c.startSong() + ) + /** Helper function to determine if the next turn can be taken */ private def canExecuteTurns: Boolean = val requirement1 = this.adventure.isDefined diff --git a/src/scalevalapokalypsi/constants/constants.scala b/src/scalevalapokalypsi/constants/constants.scala index d5abb43..cb08962 100644 --- a/src/scalevalapokalypsi/constants/constants.scala +++ b/src/scalevalapokalypsi/constants/constants.scala @@ -6,6 +6,7 @@ val CRLF: Vector[Byte] = Vector(13.toByte, 10.toByte) val POLL_INTERVAL = 100 // millisec. val GAME_VERSION = "0.1.0" val TURN_INDICATOR = ">" +val SING_INDICATOR = "~" val ACTION_BLOCKING_INDICATOR='.' val ACTION_NONBLOCKING_INDICATOR='+' |