aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-17 17:06:56 +0200
committerJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-17 17:06:56 +0200
commitc954ca4d1ec677a34a6d787a23f9d01396f7e585 (patch)
treec6b00b5046bde3a98c18f9557198f852b4ce9d46
parenta6b0330c845d4edad87c7059bac56e194a276c6f (diff)
downloadscalevalapokalypsi-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.xml124
-rw-r--r--protocol.txt18
-rw-r--r--src/scalevalapokalypsi/Client/Client.scala52
-rw-r--r--src/scalevalapokalypsi/Client/ReceivedLineParser.scala2
-rw-r--r--src/scalevalapokalypsi/Model/Action.scala40
-rw-r--r--src/scalevalapokalypsi/Model/Adventure.scala9
-rw-r--r--src/scalevalapokalypsi/Model/Area.scala2
-rw-r--r--src/scalevalapokalypsi/Model/Entities/Entity.scala30
-rw-r--r--src/scalevalapokalypsi/Model/Entities/Player.scala23
-rw-r--r--src/scalevalapokalypsi/Model/SingEffects.scala7
-rw-r--r--src/scalevalapokalypsi/Server/Client.scala23
-rw-r--r--src/scalevalapokalypsi/Server/Clients.scala3
-rw-r--r--src/scalevalapokalypsi/Server/Server.scala21
-rw-r--r--src/scalevalapokalypsi/constants/constants.scala1
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='+'