package scalevalapokalypsi.Model import scala.collection.mutable.{Map,Buffer} import scala.collection.immutable import scalevalapokalypsi.Model.Entities.* import scalevalapokalypsi.Model.Entities.NPCs.* import scala.math.min // This file is different from other files in that this file needs to have lots // of long lines of text. Thus I've chosen to take the liberty to write // carelessly lines longer than 80 characters, which is not allowed in other // files (obviously excluding long strings that will be printed, because // splitting them apart in the file would make it a pain to grep for them.) /** The class `Adventure` holds data of the game world and provides methods * for implementing a user interface for it. * * Data about the game world includes remote players, other entities and areas. * The methods are documented below. * * @param playerNames a vector of the names of players to create and add * to the adventure straight away */ class Adventure(val playerNames: Vector[String]): // Structure: // * Area definitions // * Area neighbor definitions // * Item definitions // * Public values for containing entities // * NPC definitions & adding them // * Adding players // * Public methods // Area definitions private val chieftainsGates = Area( "Kyläpäällikön portti", "Olet kyläpäällikön talon porttien edessä. Kylänraitti jatkuu pohjoiseen." ) private val villageMain = Area( "Kylänraitti", "Olet kylänraitilla. Monet kylän asukkaista ovat liikkeellä." ) private val bar = Area( "Baari", "Olet baarissa. Tavallisesti täällä soisi musiikki ja ihmiset tanssivat ilosta, mutta nyt täällä nuokkuu vain muutama ihmistä baaritiskillä." ) private val villageNorth = Area( "Kylän pohjoinen portti", "Olet kylän pohjoisen sisäänkäynnin ulkopuolella. Luukallio siintää idässä. Kylästänne vie tie naapurikylää kohti pohjoiseen. Lännessä virtaa Kattojoki, jonka anneista kylänne kalastajat saavat elantonsa. Heillä onkin ollut töitä viime aikoina, kun metsästäjät ovat olleet vastahakoisia lähtemään Luumetsään." ) private val robberRoad = Area( "Tie naapurikylään", "Kuljet tietä naapurikylän suuntaan. Yhtäkkiä rosvojoukko yllättää sinut." ) private val villageEast = Area( "Itäinen pelto", "Olet pellolla. Näet idässä Luukalliota ympäröivän, synkän näköisen Luumetsän. Voit vannoa näkeväsi siellä epämääräistä liikettä." ) private val westForest = Area( "Luumetsä", "Olet metsässä Luukallion länsipuolella. Kuulet ympäriltäsi epämääräisiä ääniä, eivätkä ne kaikki ole eläinten aiheuttamia." ) private val northForest = Area( "Luumetsä", "Olet metsässä Luukallion pohjoispuolella. Kuulet ympäriltäsi epämääräisiä ääniä." ) private val northOfNorthForest = Area( "Luumetsä", "Luumetsä jatkuu jatkumistaan..." ) private val eastOfEastForest = Area( "Luumetsä", "Luumetsä jatkuu jatkumistaan..." ) private val southOfSouthForest = Area( "Luumetsä", "Luumetsä jatkuu jatkumistaan..." ) private val eastForest = Area( "Luumetsä", "Olet Luumetsässä. Luukallio kohoaa lännessä." ) private val southForest = Area( "Luumetsä", "Olet Luukallion eteläpuolella. Näet kallion seinällä sileän kohdan, johon on kirjoitettu riimuja, joita et tunnista. Seinää katsomassa seisoo yksi kyläläisistä, jotka koottiin etsimään teitä piinaavan ilmiön alkulähdettä." ) private val villageSouth = Area( "Eteläinen pelto", "Olet pellolla kylän eteläpuolella. Näet pellon laidalla pienen metsäaukion. Joki virtaa länsipuolellasi." ) private val forestInSouthField = Area( "Metsäaukio", "Olet pienellä metsäaukiolla. Näet puiden lomassa pienen majan, josta nousee savua." ) private val hutInFieldForest = Area( "Erakon tupa", "Olet pimeässä majassa, jonka ilmassa hiki ja yrtinkatku sekoittuvat." ) private val caveDescent = Area( "Käytävä kallion uumeniin", "Olet hämärässä, soihduin valaistussa käytävässä. Se näyttää vievän syvälle kallion uumeniin. Katosta tippuu hiljalleen jotakin punaista nestettä." ) private val secondCaveDescent = Area( "Käytävä kallion uumeniin", "Olet käytävässä, joka vie maan pinnalta syvälle kallion uumeniin. Käytävä on tässä kohtaa hieman laajempi." ) private val bossDoor = Area( "Vuoren aula", "Tunnet kallion painostavan kivimassan päälläsi. Olet puolikaaren muotoisessa huoneessa. Edessäsi on avainreiällinen ovi." ) private val storageRoom = Area( "Varastohuone", "Olet varastohuoneessa. Näet täällä tynnyreitä, joiden sisältö ei näytä siltä, että tahdot koskea siihen. Tavarat huoneessa näyttävät siltä, että niitä saatettaisiin käyttää joihinkin kulttimenoihin. Luulet, että yksi tynnyreistä on täynnä verta." ) private val otherCaveRoom = Area( "Sellihuone", "Olet huoneessa, joka on täynnä sellejä. Selleihin on lukittu pahoinvoivan näköisiä ihmisiä, joilla on paiseita." ) private val bossFightRoom = Area( "Suuri huone kallion sisässä", "Olet suuressa, ympyränmuotoisessa huoneessa. Lattialle on piirretty verinen kuvio, ja soihdut lepattavat punaista tulta." ) private val mountaintop = Area( "Kalliolla kukkulalla", "Olet kiivennyt Luukallion huipulle. Näet täältä kotikyläsi peukun kokoisena. Takanasi levittyy Luumetsä pitkänä ja pimeänä, mutta taivaalla valkoiset pilvet halkovat horisonttia. Rinteet ovat kaikkiin suuntiin jyrkkiä paitsi länteen." ) private val chieftainsGarden = Area( "Kyläpäällikön piha", "Olet kyläpäällikön pihalla. Sinun ei todellakaan pitäisi olla täällä ilman lupaa." ) private val chieftainsHouse = Area( "Kylänpäällikön talo", "Olet suuressa aulassa kyläpäällikön talossa." ) // Area neighbor definitions chieftainsGates .setNeighbors(Vector("pohjoiseen" -> villageMain)) villageMain .setNeighbors(Vector("etelään" -> chieftainsGates, "baariin" -> bar, "pohjoiseen" -> villageNorth)) bar .setNeighbors(Vector("ulos" -> villageMain)) villageNorth .setNeighbors(Vector("etelään" -> villageMain, "pohjoiseen" -> robberRoad, "itään" -> villageEast)) robberRoad .setNeighbors(Vector("etelään" -> villageNorth, "pohjoiseen" -> robberRoad)) villageEast .setNeighbors(Vector("pohjoiseen" -> villageNorth, "etelään" -> villageSouth, "itään" -> westForest)) villageSouth .setNeighbors(Vector("pohjoiseen" -> villageEast, "itään" -> villageEast, "metsikköön" -> forestInSouthField)) forestInSouthField.setNeighbors(Vector("mökkiin" -> hutInFieldForest, "pois metsiköstä" -> villageSouth)) hutInFieldForest .setNeighbors(Vector("ulos" -> forestInSouthField)) mountaintop .setNeighbors(Vector("länteen" -> westForest)) northForest .setNeighbors(Vector("pohjoiseen" -> northOfNorthForest, "itään" -> eastForest, "länteen" -> westForest)) eastForest .setNeighbors(Vector("pohjoiseen" -> northForest, "etelään" -> southForest, "itään" -> eastOfEastForest)) southForest .setNeighbors(Vector("itään" -> eastForest, "länteen" -> westForest, "etelään" -> southOfSouthForest)) westForest .setNeighbors(Vector("pohjoiseen" -> northForest, "etelään" -> southForest, "länteen" -> villageEast, "itään" -> mountaintop)) northOfNorthForest.setNeighbors(Vector("pohjoiseen" -> northOfNorthForest, "itään" -> northOfNorthForest, "länteen" -> northOfNorthForest, "etelään" -> northForest)) eastOfEastForest .setNeighbors(Vector("pohjoiseen" -> eastOfEastForest, "itään" -> eastOfEastForest, "länteen" -> eastForest, "etelään" -> eastOfEastForest)) southOfSouthForest.setNeighbors(Vector("pohjoiseen" -> southForest, "itään" -> southOfSouthForest, "länteen" -> southOfSouthForest, "etelään" -> southOfSouthForest)) caveDescent .setNeighbors(Vector("syvemmälle" -> secondCaveDescent)) secondCaveDescent .setNeighbors(Vector("ylemmäs" -> caveDescent, "syvemmälle" -> bossDoor)) bossDoor .setNeighbors(Vector("länteen" -> storageRoom, "itään" -> otherCaveRoom, "ulos" -> secondCaveDescent)) storageRoom .setNeighbors(Vector("ulos" -> bossDoor)) otherCaveRoom .setNeighbors(Vector("ulos" -> bossDoor)) chieftainsGarden .setNeighbors(Vector("pohjoiseen" -> chieftainsGates, "kyläpäällikön taloon" -> chieftainsHouse)) chieftainsHouse .setNeighbors(Vector("ulos" -> chieftainsGarden)) chieftainsHouse.addItem(Item("siivilä", "mystinen, musta esine, jonka pohjassa on reikiä ja jota koristaa yliviivattua silmää kuvastava logo", 1)) // Item definitions private val oluttuoppi = Item("oluttuoppi", "Tuopillinen kylmää ja kuohuvaa juomaa. Se tuoksuu aika tujulta.", 1) object SeparatorScroll extends Item( "separoitumisen laulukäärö", "Laulukäärö, joka antaa sanat separoitumisen laululle, joka saa aikaan raon jopa harmaaseen kiveen.", 1 ): class SeparatorEffect(actor: Entity) extends SingEffect(actor): def getVerses: Vector[String] = Vector( "Separoidu kova kivi", "jakaudu ja ratkeapi", "laske minut syvyyksiisi", "kalmankylmiin lähteisiisi" ) def apply(quality: Float): Event = if actor.location == southForest && quality > .1 && !southForest.hasNeighbor("kallion sisään") then southForest.setNeighbor("kallion sisään", caveDescent) caveDescent.setNeighbor("ulos", southForest) Event( Vector(actor -> "Kivi halkeaa rytinällä. Nyt näet sisään soihduilla valaistuun käytävään, joka johtaa kallion uumeniin.").toMap, s"Näet kun kivi halkeaa kahtia henkilön ${actor.name} laulun voimasta. Kallion sisästä paljastuu käytävä." ) else if actor.location == southForest && quality <= .1 then Event( Vector(actor -> "Kivi natisee liitoksissaan, mutta jää odottamaan voimallisempaa laulua.").toMap, s"Kivi natisee hieman kun ${actor.name} laulaa, mutta mitään merkittävää ei tapahdu." ) else Event(Vector(actor -> "Laulat laulun, mutta mitään ei tapahdu.").toMap, "") end SeparatorEffect override def use(user: Entity): Option[Event] = user match case p: Player => p.setSingEffect(SeparatorEffect(p)) Some(Event( Vector(user -> "Avaat käärön varovasti ja aloitat separoitumisen laulun.").toMap, s"${user.name} alkaa laulamaan kääröstä." )) case other => None end use end SeparatorScroll // Public values for containing entities val entities: Map[String, Entity] = Map() val players: Map[String, Player] = Map() val npcs: Map[String, NPC] = Map() // Adding players playerNames.foreach(this.addPlayer(_)) // NPC definitions & adding them private val zombieAttrs = Vector( ("mädäntyvä kyläläinen rääsyissä", westForest, 20), ("mädäntyvä kyläläinen mekossa", westForest, 20), ("räsyinen olento Joukon vaatteissa", northOfNorthForest, 10), ("vaappuva paiseinen henkilö", otherCaveRoom, 30), ("epämuodostunut henkilö", otherCaveRoom, 20), ("kädetön olento", otherCaveRoom, 20) ) zombieAttrs.foreach(z => val zombie = Zombie(this, z(0), z(1), z(2)) npcs += z(0) -> zombie z(1).addEntity(zombie) ) object JoukosDad extends NPC(this, "Joukon isä", bar, 100, 100): def act(): Unit = () private var hasBeenTalkedTo: Boolean = false def getDialog: String = if northForest.getEntity( "räsyinen olento Joukon vaatteissa" ).isDefined || !hasBeenTalkedTo then hasBeenTalkedTo = true "Voi minun poikaani! Mikä karmea kohtalo! Löytäisitkö hänet vallanneen olennon vuorilta? Päästäisitkö poikani kärsimyksestä?" else "Ai se on tehty? Voi sentään. No, nyt hän ainakin on saavuttanut levon. Kunpa näin ei olisi tarvinnut käydä." end JoukosDad object VillageChieftainGuard extends NPC(this, "Vartija", chieftainsGates, 100, 100): private var isAsleep = false def act(): Unit = if this.location.getItemNames.exists(_ == "oluttuoppi") then this.location.observeEvent(this.pickUp("oluttuoppi")) this.location.observeEvent(Event(immutable.Map.empty, s"${this.name} juo oluen ja sammuu.")) this.isAsleep = true this.location.setNeighbor("etelään", chieftainsGarden) def getDialog: String = if !this.isAsleep then "Et voi mennä kyläpäällikön taloon ilman kutsua." else "ZzzzZzZZZz..." end VillageChieftainGuard val bartender = Villager( this, "Baarin pitäjä", bar, Vector( ( "Onnea matkaan. Talo tarjoaa tuopin olutta rohkaisuksi.", Some((me: Entity) => me.location.addItem(oluttuoppi) Event(immutable.Map.empty, "Baarimikko kaataa tuoppiin olutta ja asettaa sen pöydälle.") ) ), ("Onnea matkaan", None) ) ) val hermit = Villager( this, "erakkotietäjä", hutInFieldForest, Vector( "Olen kuullut tuollaisista riimuista. Ota tämä käärö mukaasi ja laula sen mukaisesti näkemäsi sisäänkäynnin luona." -> Some((me: Villager) => me.location.addItem(SeparatorScroll) Event(immutable.Map.empty, "Tietäjä loihtii esiin laulukäärön, joka leijailee maahan jalkojesi juureen.") ), "Onnea matkaan. Muista valmistautua huolella, ennen kuin avaat portin." -> None ) ) val uolevi = Villager(this, "Uolevi", southForest, Vector( "Tunnen synkeän pulssin, joka virtaa tämän kiven alla. Uskon, että tämä on jonkinlainen sisäänkäynti, mutta en tunne sitä avaavaa loitsua. Mene sinä sen erakoituneen tietäjän luokse pyytämään neuvoa, niin minä jään tänne pitämään vahtia." -> Some((me: Entity) => this.npcs += hermit.name -> hermit this.hutInFieldForest.addEntity(hermit) Event(immutable.Map.empty, "") ) , "Ai, et tiedä sitä erakkoa? Löydät hänen majansa kyllä kylän eteläpuolelta." -> None ) ) val cultist1 = Cultist(this, "Kaapupäinen henkilö", secondCaveDescent) val cultist2 = Cultist(this, "Silmätön kaljupää", secondCaveDescent) val cultist3 = Cultist(this, "Silmitön kaljupää", otherCaveRoom) val cthulthu = Cthulthu(this, bossFightRoom) val miikkulainen = Miikkulainen(this, bossFightRoom) this.npcs += bartender.name -> bartender this.bar.addEntity(bartender) this.npcs += JoukosDad.name -> JoukosDad this.bar.addEntity(JoukosDad) this.npcs += VillageChieftainGuard.name -> VillageChieftainGuard this.chieftainsGates.addEntity(VillageChieftainGuard) this.npcs += cultist1.name -> cultist1 this.secondCaveDescent.addEntity(cultist1) this.npcs += cultist2.name -> cultist2 this.secondCaveDescent.addEntity(cultist2) this.npcs += cultist3.name -> cultist3 this.otherCaveRoom.addEntity(cultist3) this.npcs += uolevi.name -> uolevi this.southForest.addEntity(uolevi) this.npcs += cthulthu.name -> cthulthu this.bossFightRoom.addEntity(cthulthu) this.npcs += miikkulainen.name -> miikkulainen this.bossFightRoom.addEntity(miikkulainen) // Public methods def takeNpcTurns(): Unit = npcs.values.foreach(_.act()) /** Adds a player entity with the specified name to the game. * * @param name the name of the player entity to add * @return the created player entity */ def addPlayer(name: String): Player = val newPlayer = Player(this, name, chieftainsGates) chieftainsGates.addEntity(newPlayer) this.entities += name -> newPlayer players += name -> newPlayer newPlayer /** Removes the given entity without further observations. Makes sense in the * game mostly if the entity's HP is nonpositive. * * Removes the entity both from the adventure and the game world * (i.e. the entitys area). * * @param name the name of the entity to remove * @return whether there was an entity to remove with the given name */ def removeEntity(name: String): Boolean = this.players.remove(name) this.entities.remove(name).orElse(this.npcs.remove(name)) match case Some(e) => e.location.removeEntity(name) true case None => false /** Gets the player entity with the specified name. * * @param name name of the player to find * @return the player, if one with the name was found */ def getPlayer(name: String): Option[Player] = this.players.get(name) def getEntity[A >: Entity](name: String) = this.players.get(name) .orElse(this.npcs.get(name)) .getOrElse(this.entities.get(name)) end Adventure