From 49985d1d11c426968fc298469671326aace96d00 Mon Sep 17 00:00:00 2001 From: Joel Kronqvist Date: Thu, 21 Nov 2024 23:19:00 +0200 Subject: Fixed singing from last commit --- src/scalevalapokalypsi/Model/Entities/Entity.scala | 5 +---- src/scalevalapokalypsi/Model/Entities/Player.scala | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) (limited to 'src/scalevalapokalypsi/Model') diff --git a/src/scalevalapokalypsi/Model/Entities/Entity.scala b/src/scalevalapokalypsi/Model/Entities/Entity.scala index e7cd45c..0427297 100644 --- a/src/scalevalapokalypsi/Model/Entities/Entity.scala +++ b/src/scalevalapokalypsi/Model/Entities/Entity.scala @@ -58,10 +58,7 @@ class Entity( ("Olet täysin kunnossa.", s"$name näyttää kuin vastasyntyneeltä.") /** Does nothing, except possibly in inherited classes. */ - 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") + def observe(event: Event): Unit = () /** 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 cac5bf1..a66f521 100644 --- a/src/scalevalapokalypsi/Model/Entities/Player.scala +++ b/src/scalevalapokalypsi/Model/Entities/Player.scala @@ -18,8 +18,6 @@ class Player(name: String, initialLocation: Area) extends Entity(name, initialLo private val observedEvents: Buffer[Event] = Buffer.empty private var pendingSingEffect: Option[SingEffect] = None - override def observeString(observation: String): Unit = - this.observations.append(observation) override def observe(event: Event): Unit = this.observedEvents.append(event) -- cgit v1.2.3 From db5612ed9734d51e6fcd0d7b5a7635e49b773581 Mon Sep 17 00:00:00 2001 From: Joel Kronqvist Date: Fri, 22 Nov 2024 22:42:22 +0200 Subject: Character safety checking, supported terminals updated --- src/scalevalapokalypsi/Model/Action.scala | 5 ++++- src/scalevalapokalypsi/Model/Area.scala | 27 --------------------------- 2 files changed, 4 insertions(+), 28 deletions(-) (limited to 'src/scalevalapokalypsi/Model') diff --git a/src/scalevalapokalypsi/Model/Action.scala b/src/scalevalapokalypsi/Model/Action.scala index a781ee8..fdfbf75 100644 --- a/src/scalevalapokalypsi/Model/Action.scala +++ b/src/scalevalapokalypsi/Model/Action.scala @@ -29,7 +29,10 @@ class Action(input: String): val resOption: Option[(Boolean, Event)] = this.verb match case "go" => val result = actor.go(this.modifiers) - result.foreach(r => oldLocation.observeEvent(r)) + result.foreach(r => + if actor.location != oldLocation then + oldLocation.observeEvent(r) + ) result.map((true, _)) case "rest" => Some((true, actor.rest())) case "get" => Some((false, actor.pickUp(this.modifiers))) diff --git a/src/scalevalapokalypsi/Model/Area.scala b/src/scalevalapokalypsi/Model/Area.scala index 96392ba..c07f2f9 100644 --- a/src/scalevalapokalypsi/Model/Area.scala +++ b/src/scalevalapokalypsi/Model/Area.scala @@ -103,33 +103,6 @@ class Area(val name: String, var description: String): def removeEntity(entityName: String): Option[Entity] = this.entities.remove(entityName.toLowerCase()) - /** Returns a multi-line description of the area as a player sees it. This - * includes a basic description of the area as well as information about - * exits and items. If there are no items present, the return value has the - * form "DESCRIPTION\n\nExits available: - * DIRECTIONS SEPARATED BY SPACES". If there are one or more items present, - * the return value has the form "DESCRIPTION\nYou see here: ITEMS - * SEPARATED BY SPACES\n\nExits available: DIRECTIONS SEPARATED BY SPACES". - * The items and directions are listed in an arbitrary order. - */ - def fullDescription: String = - val exitList = this.neighbors.keys.mkString(" ") - val itemList = this.items.keys.mkString(" ") - val entityList = this.getEntityNames.mkString(" ") - val itemDescription = - if this.items.nonEmpty then - s"\nYou see here: ${itemList}" - else "" - val entityDescription = - if this.entities.nonEmpty then - s"\nThere are entities: ${entityList}" - else "" - (this.description + - itemDescription + - entityDescription + - s"\n\nExits available: $exitList") - - /** Returns a single-line description of the area for debugging purposes. */ override def toString = this.name + ": " + this.description.replaceAll("\n", " ").take(150) -- cgit v1.2.3 From 28b83db50f33cb704311ffe608dcd8c4412635cf Mon Sep 17 00:00:00 2001 From: Joel Kronqvist Date: Sat, 23 Nov 2024 05:24:10 +0200 Subject: NPCs, zombies and talking --- src/scalevalapokalypsi/Model/Action.scala | 65 +++++++++++++--- src/scalevalapokalypsi/Model/Adventure.scala | 23 +++++- .../Model/Entities/NPCs/NPC.scala | 89 ++++++++++++++++++++++ src/scalevalapokalypsi/Model/Entities/Player.scala | 2 +- 4 files changed, 164 insertions(+), 15 deletions(-) create mode 100644 src/scalevalapokalypsi/Model/Entities/NPCs/NPC.scala (limited to 'src/scalevalapokalypsi/Model') 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 -- cgit v1.2.3