diff options
author | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-23 05:24:10 +0200 |
---|---|---|
committer | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-23 05:24:10 +0200 |
commit | 28b83db50f33cb704311ffe608dcd8c4412635cf (patch) | |
tree | c1be7986ef783973ceeffb2b966bec0019feff1b | |
parent | db5612ed9734d51e6fcd0d7b5a7635e49b773581 (diff) | |
download | scalevalapokalypsi-28b83db50f33cb704311ffe608dcd8c4412635cf.tar.gz scalevalapokalypsi-28b83db50f33cb704311ffe608dcd8c4412635cf.zip |
NPCs, zombies and talking
-rw-r--r-- | src/scalevalapokalypsi/Model/Action.scala | 65 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Adventure.scala | 23 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala | 89 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Model/Entities/Player.scala | 2 | ||||
-rw-r--r-- | src/scalevalapokalypsi/Server/Server.scala | 3 |
5 files changed, 165 insertions, 17 deletions
diff --git a/src/scalevalapokalypsi/Model/Action.scala b/src/scalevalapokalypsi/Model/Action.scala index fdfbf75..287b008 100644 --- a/src/scalevalapokalypsi/Model/Action.scala +++ b/src/scalevalapokalypsi/Model/Action.scala @@ -1,6 +1,7 @@ package scalevalapokalypsi.Model import scalevalapokalypsi.Model.Entities.* +import scalevalapokalypsi.Model.Entities.NPCs.* /** The class `Action` represents actions that a player may take in a text * adventure game. `Action` objects are constructed on the basis of textual @@ -36,20 +37,60 @@ class Action(input: String): result.map((true, _)) case "rest" => Some((true, actor.rest())) case "get" => Some((false, actor.pickUp(this.modifiers))) - case "say" => - val to = "to" - val recipient = modifiers.reverse.takeWhile(_ != ' ').reverse - val recipientEntity = actor.location.getEntity(recipient) - val maybeTo = modifiers.slice( - modifiers.length - recipient.length - s"$to ".length, - modifiers.length - recipient.length - 1 + case "sano" => + val entityNames = actor.location.getEntityNames.map(_.toLowerCase) + val recipientNamePair = entityNames.map(name => + val possibleNamesWithSuffix = (0 to "ille".length).map(i => + modifiers.takeRight(name.length + i) + ) + possibleNamesWithSuffix.find(s => + s.take(name.length) == name + ) + .map(_.splitAt(name.length)) + ).flatten.headOption + + val recipient = recipientNamePair.flatMap(p => + actor.location.getEntity(p(0)) ) - val message = - modifiers.take(modifiers.length - recipient.length - 4) - if maybeTo == to then - recipientEntity.map(e => (false, actor.sayTo(e, message))) - else + + val message = recipientNamePair + .map(p => modifiers.dropRight(p(0).length + p(1).length)) + .filter(_.takeRight(1) == " ") + .map(_.dropRight(1)) + + message.map(m => + recipient.map(e => (false, actor.sayTo(e, m))) + ).getOrElse( Some((false, actor.say(modifiers))) + ) + case "puhu" => + val recipient = modifiers + .indices.take("ille".length + 1) + .map(i => modifiers.take(modifiers.length - i)) + .find(name => actor.location.getEntity(name).isDefined) + .flatMap(name => actor.location.getEntity(name)) + val dialog = recipient match + case Some(npc: NPC) => + s"${npc.name}: ”${npc.getDialog}”" + case Some(player: Player) => + "Et voi puhua pelaajille, vain sanoa asioita heille." + case Some(other) => + "Et voi puhua tälle olennolle." + case None => + "Kyseistä puhujaa ei löytynyt." + + val fromThirdPerson = recipient + .filter(a => a.isInstanceOf[NPC]) + .map(a => s"${actor.name} puhuu $modifiers") + + Some( + ( + false, + Event(Vector(( + actor, dialog + )).toMap, fromThirdPerson.getOrElse("")) + ) + ) case "drop" => Some((false, actor.drop(this.modifiers))) case "laula" => val end = modifiers.takeRight("suohon".length) diff --git a/src/scalevalapokalypsi/Model/Adventure.scala b/src/scalevalapokalypsi/Model/Adventure.scala index 9347bfa..09eed54 100644 --- a/src/scalevalapokalypsi/Model/Adventure.scala +++ b/src/scalevalapokalypsi/Model/Adventure.scala @@ -2,6 +2,7 @@ package scalevalapokalypsi.Model import scala.collection.mutable.Map import scalevalapokalypsi.Model.Entities.* +import scalevalapokalypsi.Model.Entities.NPCs.* /** The class `Adventure` holds data of the game world and provides methods * for implementing a user interface for it. @@ -20,7 +21,6 @@ class Adventure(val playerNames: Vector[String]): private val clearing = Area("Forest Clearing", "You are at a small clearing in the middle of forest.\nNearly invisible, twisted paths lead in many directions.") private val tangle = Area("Tangle of Bushes", "You are in a dense tangle of bushes. It's hard to see exactly where you're going.") private val home = Area("Home", "Home sweet home! Now the only thing you need is a working remote control.") - private val destination = home middle.setNeighbors(Vector("north" -> northForest, "east" -> tangle, "south" -> southForest, "west" -> clearing)) northForest.setNeighbors(Vector("east" -> tangle, "south" -> middle, "west" -> clearing)) @@ -37,6 +37,23 @@ class Adventure(val playerNames: Vector[String]): )) val entities: Map[String, Entity] = Map() + + val npcs: Map[String, NPC] = Map() + + private val zombieAttrs = Vector( + ("Weary zombie", clearing, 20), + ("Smelly zombie", home, 20), + ("Rotten zombie", tangle, 10) + ) + zombieAttrs.foreach(z => + val zombie = Zombie(z(0), z(1), z(2)) + npcs += z(0) -> zombie + z(1).addEntity(zombie) + ) + + def takeNpcTurns(): Unit = + npcs.values.foreach(_.act()) + private val gruu = Entity("Gruu", northForest) northForest.addEntity(gruu) this.entities += gruu.name -> gruu @@ -64,7 +81,9 @@ class Adventure(val playerNames: Vector[String]): def getPlayer(name: String): Option[Player] = this.players.get(name) def getEntity[A >: Entity](name: String) = - this.players.getOrElse(name, this.entities.get(name)) + this.players.get(name) + .orElse(this.npcs.get(name)) + .getOrElse(this.entities.get(name)) end Adventure diff --git a/src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala b/src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala new file mode 100644 index 0000000..21709ba --- /dev/null +++ b/src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala @@ -0,0 +1,89 @@ + +package scalevalapokalypsi.Model.Entities.NPCs + +import scala.collection.mutable.Buffer +import scalevalapokalypsi.Model.* +import scalevalapokalypsi.Model.Entities.* +import scala.util.Random + +/** A `NPC` object represents a non-playable in-game character controlled by + * the server using this objects `act` method. It can also be "talked to": it + * returns a dialog when asked for. + * + * A NPC object’s state is mutable: the NPC’s location and possessions can change, + * for instance. + * + * @param name the NPC's name + * @param initialLocation the NPC’s initial location + */ +abstract class NPC( + name: String, + initialLocation: Area, + initialHP: Int, + maxHp: Int +) extends Entity(name, initialLocation, initialHP, maxHp): + def getDialog: String + def act(): Unit + +class Zombie( + identifier: String, + initialLocation: Area, + initialHP: Int = 20 +) extends NPC(identifier, initialLocation, initialHP, 20): + + private val damage = 10 + private val dialogs = Vector( + "örvlg", + "grr", + "äyyrrrgrlgb ww", + "aaak brzzzwff ååö", + "äkb glan abglum", + "öub gpa" + ) + + override def getDialog: String = + val dialogIndex = Random.between(0, this.dialogs.length) + this.dialogs(dialogIndex) + + override def act(): Unit = + val possibleVictims = this.location + .getEntities + .filter(_ != this) + .toVector + val index: Int = + if possibleVictims.isEmpty then 0 + else Random.between(0, possibleVictims.length) + if possibleVictims.isEmpty then + val possibleDirections = this.location.getNeighborNames.toVector + val directionIndex = Random.between(0, possibleDirections.length*2) + possibleDirections + .toVector + .lift(directionIndex) + .flatMap(this.go(_)) + .map(this.location.observeEvent(_)) + else + this.location.observeEvent( + this.attack(possibleVictims(index)) + ) + + + private def attack(entity: Entity): Event = + if Random.nextBoolean() then + entity.takeDamage(this.damage) + Event( + Map.from(Vector(( + entity, + s"${this.name} puree sinua, hyi yäk!\n" + + s"${entity.condition(0)}" + ))), + s"${this.name} puree henkilöä ${entity.name}.\n" + + s"${entity.condition(1)}" + ) + else + Event( + Map.from(Vector(( + entity, + s"${this.name} yrittää purra sinua mutta kaatuu ohitsesi." + ))), + s"${this.name} yrittää purra henkilöä ${entity.name}, mutta epäonnistuu surkeasti." + ) diff --git a/src/scalevalapokalypsi/Model/Entities/Player.scala b/src/scalevalapokalypsi/Model/Entities/Player.scala index a66f521..7d166b4 100644 --- a/src/scalevalapokalypsi/Model/Entities/Player.scala +++ b/src/scalevalapokalypsi/Model/Entities/Player.scala @@ -27,7 +27,7 @@ class Player(name: String, initialLocation: Area) extends Entity(name, initialLo val res = (res1 ++ res2).toVector observations.clear() observedEvents.clear() - res + res.filter(s => !(s.isEmpty)) /** Returns whether this player has a pending sing effect. */ def isSinging: Boolean = this.pendingSingEffect.isDefined diff --git a/src/scalevalapokalypsi/Server/Server.scala b/src/scalevalapokalypsi/Server/Server.scala index 89db302..2ea8bd4 100644 --- a/src/scalevalapokalypsi/Server/Server.scala +++ b/src/scalevalapokalypsi/Server/Server.scala @@ -56,8 +56,7 @@ class Server( this.writeObservations() if this.canExecuteTurns then this.clients.foreach(_.giveTurn()) - //this.writeClientDataToClients() - //this.writeObservations() + this.adventure.foreach(_.takeNpcTurns()) this.clients.foreach(c => this.writeToClient(this.turnStartInfo(c), c) ) |