Coder Social home page Coder Social logo

core's Introduction

ranvier

Node.js-based MUD engine

Ranvier is a MUD game engine whose goal is to be a simple but powerful way to build whatever MUD you want with special care given to extensibility. The core code strives to be completely unopinionated toward any specific style of game while using the bundle system to build the game you want without having to dig through the engine's code.

Special Features

  • Robust bundle system: Nearly every aspect of the game can be modified without changing the core and allows for easy packaging and sharing of commands/areas/items/npcs/channels/behaviors
  • Unopinionated network layer: easily swap out telnet for any network layer you like. No need to gut the whole codebase just to support a different transport type, just drop in a file.
  • Customizable data layer: You are not tied to saving in any particular database or file storage sytem
  • Optional coordinate based room system allowing for the flexibilty of a standard MUD world with the easy mappability of a strict 3D world.
  • Scripting for all entities in the game for any event along with behaviors to create shared, composable scripts
  • Skill system with passive/active skills
  • Effects e.g., buffs/debuffs
  • Quest system allowing for starting/progress/completion from any event in the game
  • Communication channels with custom audiences

Documentation

Ranvier prides itself on having thorough documentation which is available on our website: ranviermud.com

Slack

We have a Slack channel you can use to ask questions, suggest features, or just keep up to date with the project: https://ranviermud.slack.com

Get an invite

Requirements

Demo

Point your favorite client or telnet to ranviermud.com port 4000. This demo server is wiped and updated from the master branch every hour.

core's People

Contributors

atarsha avatar bostrt avatar clagiordano avatar heyitsmdr avatar holl0wstar avatar its-danny avatar jjwilliams42 avatar marado avatar marcelomoreli avatar markscho avatar nahilep avatar nelsonsbrian avatar ratacat avatar sakeran avatar shawncplus avatar thedandroid avatar tonymcnulty avatar vindexus avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

core's Issues

getDataFilePath(type, id) uses a static switch statement

Hi,

I just picked up RanvierMud two days ago and in testament to its extensiblity am already comfortable using it.

I have prototyped a bundle for RanvierMud which allows me to persist an extension of the Room class. While doing so I noticed that the Data class method getDataFilePath uses a static switch statement to decide the file paths. This would make it impossible to use Data in a bundle without also making a change to Ranvier core.

Do I have an incorrect usage and Data is not meant to be used by bundles? Is this an intentional limitation on persistent data? Are there any plans to make this dynamic, say to work off the EntityLoaders in ranvier.json?

Thanks.

Room.removePlayer() and Room.removeNpc() should stop combat

Currently, if you want to move a player (or an Npc) to a different room, you call .moveTo(). That method makes a call to room.removePlayer(). However, if I cast 'word of recall', I am magically transported to back to town, where I thought I would be safe and sound, but I will still be in combat with whatever I was fighting. Sure, I can call player.removeFromCombat() prior to .moveTo(), but that just seems like overhead. I'd like to believe that the game by default assumes that if you're NOT in the same room, then you're not fighting.
Solution:

  • add a call to .removeFromCombat() in room.removePlayer()/Npc()
  • then in Player, add the following:
    removeFromCombat() { super.removeFromCombat(); this.removePrompt('combat'); }

Unit tests for effects

Need some unittests for effects. Extra attention paid to less-used parts like stacking/refreshing

Item doesn't serialize all of its mutable properties; missing type, name, keywords, description, and roomdesc

I starting building a basic system for random equipment generation. And realized after getting into it that none of the items would survive a logout/quit. Diving into it a bit, it seems the current serialize / hydrate methods for Inventory and/or Items are relying on looking up the EntityReference for the item's ID, TYPE, NAME, KEYWORDS, DESCRIPTION, and ROOMDESC. All the metadata serializes and hydrates fine. It seems that an implication of this is that you wouldn't be able to make randomly generated equipment(that persists) without modifying core.

The only place I could see a possibility of that, is writing an ancillary save function that is called on the save event at @bundles/ranvier-player-events/player-events.js, to save items without EntityReference data, and I'm not sure where you could load it.

Alternatively if we look at what we could change in core, I think it would either mean changing all items over to serializing their entire object, or providing a hook of some sort for a bundle to serialize/hydrate items not defined with an EntityReference on disk.

edits
After talking with Nephila, it seems like there might be a sort of solution around writing the randomly generated items to datastore in their own zone. So that they hydrate properly from the EntityReference. Although we both think that maybe the bigger issue is the question around whether Items once created, persist and can be modified on their own, or whether they are 're-created' from their original form on login.

Effectable behavior refactor for rooms/items/characters

Character effects methods/properties should be abstracted into an Effectable behavior that can be used by both Character and GameEntity (room/item/area). Effectable should also include the functionality of attributes thus moving all attribute and effect code into this behavior

This will allow all game entities to have effects. In addition effects should be updated with some new functionality: property modifiers.

Property Modifiers

Attribute modifiers exist to allow effects to temporarily alter an attribute's max value but attributes are purely numeric stats on a character. Property modifiers should allow effects to temporarily modify any property on an entity:

  • A disguise effect that temporarily changes a how player's name appears to other players
  • a darken effect that changes a room's description to be "It is pitch dark."
  • a poison effect that changes an item's room desc to append " (poisoned)" in addition to its other effects

To facilitate this Effect.modifiers should support a new properties key. This should act nearly identically to attribute modifiers. Effect will also need a new modifyProperty a la modifyAttribute mutatis mutandis. EffectList therefor will need an evaluateProperty a la evaluateAttribute. Finally to actually access these modified properties the Effectable behavior should have a getProperty(string propertyName) method a la getAttribute(string attributeName).

Effect docs will need to be updated to demonstrate these features as well as reinforce the the fact that when using modifiers one should never directly modify a property via an effect, e.g., this.name = "newName"; as that could result in Very Bad Things(tm).

Move "critical" code from core Damage class

The "critical" code inside Damage is very opinionated in that it requires a "critical" attribute and assumes that the attribute is a percentage chance.

All of this could be handled at the time of instantiating the Damage instance and just configuring the initial damage amount instead of waiting for evaluate. As such all this logic can be moved into the ranvier-combat bundle

Allow for NPCs to respond to certain channel broadcasts.

Say seems to be the most obvious choice. Although perhaps a case could be made for tell (maybe just searching room for npc target, instead of whole area/mud).

I'm less sure about yell. Maybe supporting it wouldn't hurt, perhaps yelling for the guards, or a thief, would be useful, and could be executed by other NPCs, not necessarily players.

As for chat, maybe somebody would want to integrate some mudwide events, statistics, etc into a NPC announcer or something. Or perhaps an NPC that scans chat channels for common newbie questions and sends a tell to the player with helpful suggestions. I dunno. Just thoughts.

ItemFactory entityRef needs area name

the function create in ItemFactory wants the name of the item with the name of the area like: AreaName:ItemId.

I would change the entityRef with only the ItemId because the AreaName is already into the area variable that the function needs to work.

In this way using the function create will be more natural

Item Properties(name, keywords, roomdesc) not hydrating properly.

To replicate:
Load an item from item definition in datasource. Modify the name, keywords, or roomdesc. Save the player. Quit the game. Login again. It will reset to the original name, keywords, and roomdesc.

This seems to be an issue with hydrating, not serializing. Look at hydrate method in Inventory.js, aprox lines 86 - 110.

hydrate(state, belongsTo) {
    // Item is imported here to prevent circular dependency with Item having an Inventory
    const Item = require('./Item');

    for (const [uuid, def] of this) {
      if (def instanceof Item) {
        def.belongsTo = belongsTo;
        continue;
      }

      if (!def.entityReference) {
        continue;
      }

      const area = state.AreaManager.getAreaByReference(def.entityReference);
      let newItem = state.ItemFactory.create(area, def.entityReference);
\\=======================================================
\\ The line above this is where the item is created exactly from entityReference.
\\ Currently the 'serialized' state of the item is saved in def. 
\\=======================================================
      newItem.uuid = uuid;
      newItem.belongsTo = belongsTo;
      newItem.initializeInventory(def.inventory);
      newItem.hydrate(state, def);
\\=======================================================
\\ Here the serialized def is passed into hydrate on the newly created item.
\\=======================================================
      this.set(uuid, newItem);
      state.ItemManager.add(newItem);
    }
  }
}
  hydrate(state, serialized = {}) {
\\=======================================================
\\ serialized data is saved in serialized
\\=======================================================
    if (this.__hydrated) {
      Logger.warn('Attempted to hydrate already hydrated item.');
      return false;
    }

    // perform deep copy if behaviors is set to prevent sharing of the object between
    // item instances
    if (serialized.behaviors) {
      const behaviors = JSON.parse(JSON.stringify(serialized.behaviors));
      this.behaviors = new Map(Object.entries(behaviors));
    }
\\=======================================================
\\ serialized gets used here to set the behaviors
\\=======================================================
    this.setupBehaviors(state.ItemBehaviorManager);

    this.metadata = JSON.parse(JSON.stringify(serialized.metadata || this.metadata));
\\=======================================================
\\ and here for the metadata
\\=======================================================
    this.closed = 'closed' in serialized ? serialized.closed : this.closed;
    this.locked = 'locked' in serialized ? serialized.locked : this.locked;

    if (typeof this.area === 'string') {
      this.area = state.AreaManager.getArea(this.area);
    }

    // if the item was saved with a custom inventory hydrate it
    if (this.inventory) {
      this.inventory.hydrate(state, this);
    } else {
    // otherwise load its default inv
      this.defaultItems.forEach(defaultItemId => {
        Logger.verbose(`\tDIST: Adding item [${defaultItemId}] to item [${this.name}]`);
        const newItem = state.ItemFactory.create(this.area, defaultItemId);
        newItem.hydrate(state);
        state.ItemManager.add(newItem);
        this.addItem(newItem);
      });
    }

    this.__hydrated = true;
  }

Nowhere does it set the other properties though. I believe it should be
name, keywords, roomdesc, desc

Move PlayerClass to bundle

There's no good reason for PlayerClass to exist in core, all the work it's doing could be done inside a bundle and it being in core confuses people. In this case the bundle-example-classes bundle.

  • Remove ClassManager
  • Move PlayerClass into bundle-example-classes
  • Remove playerClass top level property
  • Remove PlayerClass handling from Player#hydrate
  • Remove class loading from BundleManager
  • Use an EntityLoader to load the player classes from disk

Effect listeners should have access to GameState

The scenario is that a player has an effect which allows for a chance for their heal to do an extra 50% healing as a HoT. Currently this is not possible as you can't access the EffectFactory inside the heal listeners to create the HoT effect.

My initial idea for a solution is to allow the listeners property of an Effect configuration to either be the current { /* stuff */ } style or a new state => ({ /* stuff */ }) style.

  • Update EffectFactory#add to take a third parameter which is the GameState
    • When adding the Effect's listeners to the event manager check which form the listeners property is in and handle accordingly
  • Update BundleManager#loadEffects to pass GameState to EffectFactory.add

Channel should trigger a channelSend event on sender

Channel right now on send triggers a channelReceive event on targets of the message but does not trigger any analogous event on the sender of the message. As such it's not possible at the moment to script for a player saying something if there are no targets

Room doesn't have a spawn event

Area's get the roomAdded event to know when it gets a new room, and items/npcs get a spawn event but room's don't know when they're added.

  • Add a Room#spawn event fired when a room is created
  • Add a Room#ready event, after spawn, when a room is hydrated

lostFollower emit breaks teleport because follower is undefined

Error on line 537 of Character.js

when emitting lostFollower, follower is undefined. Passing target instead.

This is evident in the code and causes a reproducible issue in game:

Log in, teleport limbo:black where the puppy will start following you. Go west, then teleport limbo:black again. Check your Ranvier logs for the undefined error.

EffectFactory API is silly

Currently to create and attach an effect to a player it looks something like this

const myEffect = state.EffectFactory.create('some-effect', player, { duration: 5000 });
player.addEffect(myEffect);

In the EffectFactory#create(id, target, config, state) method you pass in the target for the effect. But the target doesn't have an effect yet; it hasn't been added to any entity. It's not until player.addEffect(myEffect) does that target become relevant. It's also too easy to misunderstand the purpose of that target parameter to mean the target of some damage the effect may cause which is not the case.

As such the API should be changed to the following:

const myEffect = state.EffectFactory.create('some-effect', { duration: 5000 });
player.addEffect(myEffect);

which is much more straightforward.

  • Update Character#addEffect to set effect.target = this
  • Remove the target parameter from Effect#constructor
  • Remove the target parameter from EffectFactory#create

AreaAudience tries to concat undefined

Line 21: return players.concat(area.npcs); area is undefined. This should be: return players.concat(this.sender.room.area.npcs);

This is causing nobody to see the text of the AreaAudience channels, not even the sender.

Inventory becomes null on purpose

Removing the last item from a Character's inventory intentionally sets the inventory to null. The comment in the code seems to indicate this is to prevent the case where an NPC has a default inventory, then during gameplay has its items removed, then is serialized, then is hydrated, and we needed a way to tell the difference between "This NPC had all of its items removed and should still have an empty inventory" and "This NPC has never had its default inventory setup and needs the items spawned"

I think this is irrelevant given that NPCs are never saved/rehydrated. And even if they were I would let someone else deal with that issue. We should no longer unset Character inventory when removing the last item, it creates way too many issues

Setting the Room in Player.hydrate() should be the last thing

Currently, in Player.hydrate(), the room is set at the very beginning of the method. This makes a call to .moveTo(), which then makes a call to emit('enterRoom'). I want to use this event to trigger the player to perform a LOOK, to see the room upon entering it.

However, my version of LOOK needs to perform some sort of check on whether the player can see the room and its contents. This check must be done using an equipped player. For example, if the room is DARK, then it will require some form of equipped light (e.g. a lantern) to see in it. But in hydrate(), the equip part isn't done until later. So when a player logs in, hydrate is called, room is set, look is performed, and the player is not yet equipped, resulting in an error.

Solution then would be to set the player's room at the very end of hydrate().

Positioning

Implement positioning (necessary for some commands such as "sleep", could also be interesting to use a "knockdown" skill to change the positioning of other characters).

Basically, some commands will only be available in certain positions: laying, sitting, or standing, for example.

sleep, for instance, would give a huge boost to health/energy/mana regen but would change the player character's position, which would affect which commands are available and even affect the way they receive certain inputs (such as not being able to "hear" someone in the same room saying something while they are asleep).

This could be implemented via having an attribute on the PC to describe their position (on all characters, in fact), commands to change their position (stand to recover from being knocked down before they can fight back, for example), and then a property on command objects to list any blacklisted positions (or whitelisted ones, either way is fine). For instance:

// look command config
return {
    usage: "look [thing]",
    blacklistedPositions: ["sleeping"],
    command: state => (args, player) => { } // etc. etc.
    }

This is just a suggestion as far as implementation goes, there is probably a better way ๐ŸŒต .

Cannot bring a attribute with formula below it's formulated result via modifiers.

In Character.js#getMaxAttribute

getMaxAttribute(attr) {
    if (!this.hasAttribute(attr)) {
      throw new RangeError(`Character does not have attribute [${attr}]`);
    }

    const attribute = this.attributes.get(attr);
    const currentVal = this.effects.evaluateAttribute(attribute);

    if (!attribute.formula) {
      return currentVal;
    }

    const { formula } = attribute;

    const requiredValues = formula.requires.map(
      reqAttr => this.getMaxAttribute(reqAttr)
    );

    return formula.evaluate.apply(formula, [attribute, this, currentVal, ...requiredValues]);
  }

evaluateAttribute (where modifiers are applied) is happening before the formula calculation. Coincidently, an attribute's value can never be lowered below the level calculated by the formula when using modifiers. For instance, having a calculated attribute called dodge, that requires reflexes, dexterity. With a certain set of stats, it has a base value of 7. If you were to apply this modifier on a stun effect to it.

      attributes: {
          dodge: function(current){
            return current * 0;
          }

      }

getMaxAttribute('dodge') will still return a value of 7. Seems like it would be better to calculate the formula first, and then evaluateAttribute? Maybe there's repercussions of that I'm not seeing?

Other issues with randomly generated items

Okay, I'm deleting original comment here to provide a more clear idea of things.

The item hydration path overview looks like this
Inventory.hydrate ->
state.ItemFactory.create ->
EntityFactory.createByType ->
Item.constructor

In Inventory.hydrate, the call to the ItemFactory.create is only passing in the entityReference to be used in the initial call of creating the item, even though Inventory.hydrate has the serialized item Definition. Thus none of the later methods are even accessing the serialized data. I took a shot at addressing this by adding in the serialized definition to this chain...

  hydrate(state, belongsTo) {
    // Item is imported here to prevent circular dependency with Item having an Inventory
    const Item = require('./Item');

    for (const [uuid, def] of this) {
      if (def instanceof Item) {
        def.belongsTo = belongsTo;
        continue;
      }

      if (!def.entityReference) {
        continue;
      }

      const area = state.AreaManager.getAreaByReference(def.entityReference);
      //==============added def as third argument on line below===============
      let newItem = state.ItemFactory.create(area, def.entityReference, def); 
      newItem.uuid = uuid;
      newItem.belongsTo = belongsTo;
      newItem.initializeInventory(def.inventory);
      newItem.hydrate(state, def);
      this.set(uuid, newItem);
      state.ItemManager.add(newItem);
    }
  }
}

then in ItemFactory

create(area, entityRef, def = {}) { // <-- added def here===========
    const item = this.createByType(area, entityRef, Item, def); <-- and here============
    item.area = area;
    return item;
  }

and EntityFactory

createByType(area, entityRef, Type, def = {}) { // <--- here=============
    const definition = this.getDefinition(entityRef) || def; <--- here===========
    /*
    if (!definition) {
      throw new Error('No Entity definition found for ' + entityRef)
    }
    */
    const entity = new Type(area, definition);

    if (this.scripts.has(entityRef)) {
      this.scripts.get(entityRef).attach(entity);
    }

    return entity;
  }

Which seems to successfully rebuild the items from the serialized data, instead of from the entityReference.

The only issue remaining after this that I've found with making randomly generated equipment survive serialization, is that it currently seems to lose it's TYPE, which is a SYMBOL. I attempted to add
type: this.type,
to the Item.serialize method...however it would appear that JSON.stringify will strip out any properties with symbols for a value. So Item.constructor method will set the TYPE of the item to Object as it's default since the serialized item no longer has a type. I haven't figured out what to do about this yet.

Data Loader RFC

Problem

The current way data is loaded into the engine is terrible. The Data class is nothing but static methods with hardcoded paths. This makes it impossible for someone to change where/how entities are stored without directly modifying core.

Goal

  • There should be an extensible datasource system in which one can specify which datasource an entity is loaded from.
  • A DataSource may be used for more than one entity, e.g., the current default setup of everything being in YAML files would be represented by all entities using the YamlDataSource.
  • To specify a datasource for an entity it must be configured and registered in the DataSourceRegistry. I'm envisioning something like this in ranvier.json
{
  // ...
  "dataSources": {
    "Yaml": {
      /* 'require' specifies which file or package to require. It will follow
      the same API as the node require() method, which is to say you could
      either have the class locally or it could be from a node module */
      "require": "./lib/path/to/classFile.js",
      /* An arbitrary config passed to the DataSource constructor.
      Each DataSorce might have a different config: file paths,
      database details, etc. */
      "config": {
        "bundlePath": "bundles",
      }
    },

    "Mysql": {
      "require": "ranvier-mysql-datasource",
      "config": {
        /* A config should also be able to pull from env variables
         to keep things like DB connection details out of source control */
        "hostname": "[ENV(RANVIER_MYSQL_HOSTNAME)]",
        /* FEEDBACK: seanohue: could just make ranvier.json a .js file and use process.env */
      }
    },

    "Json": {
      /* If a node module exports more than one data source you may specify
      which export to use with <module>.<object> */
      "require": "cool-datasources.JsonDataSource",
      "config": {
        "root": "data"
      }
    }
  },
  // ...
}
  • Configuring the entities to use a data source would also be done in ranvier.json
{
  // ...
  "entitySources": {
    /* The keys here will be a specific list of game entities which the
    BundleManager will use to load data. However, you should also be able
    to add additional entities if you have a bundle with some custom data
    like a vendor's product list or loot tables */
    "items": {
      /* specifies which registered DataSource to use */ 
      "source": "Yaml",
      /* Additional configuration for the datasource specific to this entity */
      "config": {
        "path": "items.yml",
      },
    },
    "npcs": {
      "source": "Mysql",
      "config": {
        "table": "npc",
      },
    },

    "players": {
      "source": "Json",
      "config": {
        "path": "data/players"
      }
    },
    "accounts": {
      "source": "Json",
      "config": {
        "path": "data/accounts"
      }
    }
  }
}
  • The DataSourceRegistry will be populated by instances of the classes defined/configured from the dataSources in ranvier.json. It will be the low-level API for data access, generally used only internally by each EntityLoader class.
  • Each configured EntityLoader instance will be registered to the EntityLoaderRegistry
  • The EntityLoader should have an async fetchAll() method to retrieve all data. As well as singular async fetch(key) method to fetch a single entity
  • For higher level data access there will be an EntityLoader class which will be constructed from the entitySources configuration in ranvier.json. It will be the class used to actually fetch entity data, example:
const itemLoader = state.EntityLoaderRegistry.getLoader('items');
/* EntityLoader will have setBundle/setArea methods to optionally specify
which bundle/area the data is coming from */
itemLoader.setBundle('example-areas');
itemLoader.setArea('limbo');
const items = await itemLoader.fetchAll();

/* player data won't be stored in a bundle, for example */
const playerLoader = state.EntityLoaderRegistry.getLoader('players');
const playerData = await playerLoader.fetch(playerName);
const player = new Player(playerData);
  • The default ranviermud setup will be the same as it is today: all game entities loaded from YAML. It will come with the ranvier-yaml-datasource already installed/configured. Additionally player/account data will be loaded from JSON using the preinstalled/configured ranvier-json-datasource

Movement direction names

The coordinates are embedded in this code. THis means that if I want to change it because I am not English, I need to change the code at different points

Character follow methods should emit events.

Currently, when Player.moveTo() is called, the following emits are made, triggering the appropriate messages sent to all relevant players:
nextRoom.emit('playerEnter', this);
this.emit('enterRoom', nextRoom);

Similarly, all four follow methods should do the same, in so far as using an emit to trigger an event, which then performs the logic of informing all relevant players of the action.

Spawned mobs are not fully cloned from reference.

Currently, when you spawn a mob, it will have a keywords property equal to the prototype (entity definition). Since keywords is an array, the modifying it means you'll end up modifying the keywords for every other instance spawned, including the definition as stored in the MobFactory. This seems like a bug.

Use case: We let players buy pets. Such pets can be nicknamed (at time of purchase or anytime thereafter) for the purpose of making it easier to control your pet. (e.g., order fido sit). To facilitate appropriate contextual mob-referencing, we would normally unshift the nickname onto the mob's keywords. So you might have something like: pet.keywords : [fido, dog, puppy, pooch]. However, the .shift() method does not return a new array, so you end up realizing that every dog spawned can now be referenced by "fido".

We can obviously do something like pet.keywords = ['fido', ...pet.keywords]; as a workaround. But it's probably better to make sure that mobs returned from the factory are fully cloned so that you're not modifying the entity definition.

Duplicate `Quest.config.requires` into the QuestFactory

Currently in order to check if a player can start a quest you need to actually instantiate a quest instance which is not optimal. Duplicate that data into the QuestFactory entry for a quest config. This should make the quest lookup inside the look command must less expensive

Item.belongsTo doesn't work for NPCs

If an item is on an NPC, Item.belongsTo() returns FALSE, yet the value of the property does still point to the NPC that possesses it. I'd like to see this opened up so that it points to either an NPC or a Player. And while we're at it, I'd also like to rename this property to any one of the following:

  • possessedBy
  • with
  • carriedBy
    Just because an item is in your possession, doesn't make you the "owner". This allows us to establish our own ideas of ownership.

Damage.finalAmount decays

When Damage#commit is called this.finalAmount is set to this.evaluate(target). The intention was that one could access finalAmount inside a script to see the damage they wanted to cause versus the damage that was actually caused after taking into account the target and attacker's incoming/outgoingDamage modifiers.

This works if you only ever commit the damage to one target. But Damage is made to be able to commit to multiple targets. In the case of an AoE spell you might do something like:

const d = new Damage({ amount: 20, attacker: this});
for (const target of this.combatants) {
  d.commit(target);
}

In this case finalAmount will no longer be accurate because each of the targets may have a different final amount based on their active effects.

There are a couple possible solutions I can think of:

My idea is to just get rid of the finalAmount property, just pass the value as an argument to the events.

This will be a breaking change for any bundles that listen to the hit, damaged, heal, or healed events. Though they're all currently displaying incorrect values anyway (or correct values by pure accident)

Create base AoE damage/heal classes

Related to #60

Use-case

  • Player A uses a skill which coats the room in oil (adding an Oil-Covered effect to the room).
  • Player B uses an area of effect fire spell. The room's oil effect should be able to listen for AoE fire damage and explode dealing extra damage

Solution

Create new AreaOfEffectDamage and AreaOfEffectHeal classes in core which extend Damage and Heal respectively. They will have two differences from base damage/heal:

  • A Character[] getValidTargets(Room|Character commitTarget) method which will find valid targets based on where the damage is committed, e.g., if the commit target is a room it will return all players/npcs in the room. This method can be overridden by a subclass to customize valid targets.
  • An override of the commit(Character target) method to accept Room|Character target. If the target is a Room then it should call getValidTargets(room), call super.commit(target) for each valid target, then finally emit a areaDamage or areaHeal event on the room passing in AreaOfEffectDamage/Heal instance and the commit target.
class AreaOfEffectDamage extends Damage
{
  /**
   * @param {Room|Character} target
   */
  commit(room) {
    if (!(room instanceof Room)) {
      super.commit(target);
      return;
    }

    const targets = this.getValidTargets(room);
    for (const target of targets) {
      super.commit(target);
    }

    room.emit('areaDamage', this, targets);
  }

  /**
   * Override this method to customize valid targets such as
   * only targeting hostile npcs, or only targeting players, etc.
   * @param {Room} room
   * @return {Array<Character>}
   */
  getValidTargets(room) {
    return [...room.npcs];
  },
}

Allow for composite behaviors

There are currently 2 tiers of scripting for entities: a script, one-off and specific for a single entity; and behaviors shareable/configurable chunk of code.

The use case for composite behaviors is the following:

I have 3 types of town guard: gate guard, tower archers, and cityguards. I want all guards to wander, I want them all to attack any npc from a different area that is currently attacking an npc in this area or a player. The wander configurations differ between the archers and the cityguards but regardless they both wander, but the "aggro" behavior config is the same between them and quite complex.

Currently to accomplish this I would have to copy and paste the behavior configuration for each guard. Hence the need for "composite behaviors".

Currently behaviors live in behaviors/<entity type>/<name.js> a new file would need to be created in `behaviors//composites.yml"

  • Composite behaviors should be able to be recursive: so a composite behavior can be a collection of composite behaviors as long as there is no circular dependency.
  • Composite behaviors should not share a name with an existing behavior of the same entity type. NOTE: There is likely currently a bug where if two bundles define a behavior with the same name weird shit will happen.
  • Composite behaviors should be able to be used exactly like standard behaviors such that when using a composite behavior one should be able to override the configurations.

Example composites.yml file:

# name of composite behavior
ranvier-guard:
  # list of behavior configurations (essentially exactly what you would otherwise put in the `behaviors` setting for a single entity
  ranvier-wander:
    areaRestricted: true
  ranvier-aggro:
    players: false
    # attack any NPC that is not from this area that wanders into it (unless it is a follower of a player)
    areaDefender: true
    # attack anything attacking an npc with the entityRef in this list, i.e., they are teammates with these npcs
    teammates: [ "somearea:cityguard", "somearea:archer-guard", "somearea:gateguard" ]

When using this composite behavior it would be like any other behavior, example in npcs.yml

- id: cityguard
  name: Cityguard
  behaviors:
    # cityguard doesn't need any extra configuration on top of ranvier-guard
    ranvier-guard: true
- id: archer-guard
  name: Tower Archer
  behaviors:
    # the tower archer wanders only along the tower wall
    ranvier-guard:
      ranvier-wander:
        restrictTo: [ "somearea:towerwall-1", "somearea:towerwall-2", "somearea:towerwall-3" ]
- id: gateguard
  name: City Gate Guard
  behaviors:
    # the gate guard doesn't wander from their room
    ranvier-guard:
      ranvier-wander:
        disabled: true

moveTo on Npc and Player should pass the previousRoom to the event

When you think about the fact that Npc passes nextRoom to the event: this.room.emit('npcLeave', this, nextRoom); (thisRoom can determine the direction the Npc went based on nextRoom)

Then, why shouldn't nextRoom also received thisRoom as an argument of where the npc arrived from instead of just nextRoom.emit('npcEnter', this);

I think this should be added to the moveTo method of both Npc and Player as they both work the same way.

Refactor Broadcast allowing for customizable transport stream decorators

The sty coloration library is currently hard coded inside the Broadcast class. Because Channel is a core class which uses Broadcast that makes core opinionated about how color should work. This makes it impossible to customize if you:

  • Don't want color
  • Don't like the sty syntax
  • Aren't using ANSI color

My high level concept is to create a system of customizable StreamDecorators that can be attached to TransportStreams in such a a way that a developer wouldn't have to modify core code AND wouldn't have to modify the transport bundle's (like telnet-networking code).

The difficulty is that TransportStream types aren't registered anywhere in the way that EntityLoaders are registered. This means there is currently no good central place to create the Stream+Decorators bindings.

Possible solutions

Bundle-centric

In this approach the transport bundle (e.g., telnet-networking) would entirely be in charge of this: they'd handle the initialization, customization, and management of decorators.

Pros

  • Maximum dev control

Cons

  • Little to no inter-bundle portability as every bundle might have a different configuration scheme, different decorator interfaces, etc.
  • All the work is foisted on the bundle dev
  • Likely to unintentionally imply that decorators should be hard coded into a transport bundle

Core-centric

In this approach there would be a base TransportDecorator class in core in the same way there is a base TransportStream class. Additionally the core would have to decide upon and own the configuration/binding strategy for Stream+Decorators

Pros

  • Easier on bundle developers
  • Easier for bundle users
  • Portable decorators
  • Unified configuration style

Cons

  • Less control for stream bundle developers since decorator configuration/binding will happen outside of their code
  • Having core do the configuration/binding means that the scheme will have to be more abstract and thereby potentially more complex than if it were up to the bundle itself

For me the core-centric approach seems like the obvious choice. The pieces of that puzzle look something like this:

TransportStream

TransportStream will need some way to identify themselves in such a way that configuration could link a transport stream to its decorators, my initial idea is that the class should get a new identifier getter, for example:

class TelnetStream extends TransportStream
{
  static get identifier() {
    return 'telnet';
  }

  // ...
}

TransportDecoratorRegistry

My idea for how to actually bind decorators to a stream involves creating a new TransportDecoratorRegistry. This class will be in charge of holding the configuration from ranvier.json which will look similar to DataLoader configuration:

// ranvier.json
{
  // register available decorators
  "transportDecorators": {
    "ansi": {
      // value follows same rules as DataLoader require config
      "require": "./lib/MyAnsiDecorator",
      // Serves as the default configuration for this decorator
      // which can be overridden by 'config' in the binding below
      "config": {
        "someDecoratorOption": 25
      }
    }
  },

  "transportDecoration": {
    "telnet": [
      {
        "decorator": "ansi",
        "config": {
          // additional configuration specific to this telnet:ansi binding
          "anotherOption": "foobar",
          // override of default config from registration
          "someDecoratorOption": 12,
        }
      }
    ]
  }
}

This is really verbose with all the configs but in practice it will look more like this:

{
  "streamDecorators": {
    "ansi": {
      "require": "cool-ansi-decorator"
    }
  },
  "streamDecoration": {
    "telnet": [
      { "decorator": "ansi" }
    ]
  }
}

To actually facilitate the usage of these decorators TransportDecoratorRegistry will have a decorate(TransportStream streamConstructor) method. This method will be used by transport bundle developers which means if a bundle developer so chooses they can not allow applying decorators:

class TransportDecoratorRegistry {
  decorate(streamConstructor) {
    const decorators = this.getDecorators(streamConstructor.identifier);
    return class extends streamConstructor {
      write(message, encoding) {
        message = decorators.reduce((acc, d) => d.decorate(acc), message);
        super.write(message, encoding);
      }
    };
  },
}

Writing a decorator

To write a new decorator, similar to DataLoader, there is no base class to extend. Instead you just need to follow the prescribed API:

const sty = require('sty');
class MyAnsiDecorator {
  // required configure(object config)
  configure(config = {}) {
    this.config = config;
  },

  // required decorate(string message)
  decorate(message) {
    return sty.parse(message);
  }
}

This class can either be an npm module or just a local file.

Changes for transport bundle devs

All that is in core. For the actual transport bundle developer they would write the following:

// inside their server-event script, e.g., server-events/telnet-server.js

// current:
const stream = new TelnetStream();
stream.attach(telnetSocket);

// new:
const stream = state.TransportDecoratorRegistry.decorate(TelnetStream);
stream.attach(telnetSocket);

CommandType

I feel that CommandType is no longer of any meaning in core. It's used twice in core. Once for defaulting a command without type in Command constructor and in BundleManager it gets imported, but I can't see it being used.

Proposal:

  • leave it in core, but deprecate it
  • remove unnecessary import in BundleManager
  • change default in Command constructor to a harcoded Symbol("COMMAND") null
  • add the exact same file to bundle-example-lib and update CommandParser and bundle-example-input-events/input-events/commands.js to use this

`Item#spawn` event is not consistently emitted

It seems like the Item#spawn event is only emitted when an item spawns directly into a room. It is not emitted when items are added in other places, such as to a container or to an NPC's inventory.

Area does not receive channelReceive event

AreaAudience should be refactored. The logic inside getBroadcastTargets() should be moved into Area#getBroadcastTargets() thereby making an Area broadcastable. In doing this the method should also be updated to return the area instance itself as well as the room instances.

When sending a message to an AreaAudience the area, all rooms in the area, all players in the area, and all NPCs in the area should receive the channelReceive event. As such the Area#getBroadcastTargets() should look something like:

getBroadcastTargets() {
 const roomTargets = [...this.rooms].reduce((acc, [, room]) => acc.concat(room.getBroadcastTargets()), []);
 return [this, ...roomTargets];
}

Option for defaulting an attribute to zero on attribute formula

Today if you try to create an attribute (i.e: health) and have different formulas with different attributes based on class / char type or anything else fails if the entity doesn't have all the attributes.

Example:
Let's suppose NPC health is 10 * strength * (100 * bare_hand_damage)
and Non-Npc health: level * vitality

where NPCs doesn't have vitality and Players don't have bare_hand_damage, it fails.

A suggestion discussed in slack would be a toggle option on ./ranvier with something like state.AttributeFactory.setAllowSomethingSomethingRequires(true);

Thanks!

Remove `pacifist` property from Npc

There is currently confusion created by Npc.pacifist. Is it a behavior, is it a property, how is it being checked?

Going forward we should remove Npc.pacifist and references to it, as well as removing any reference to a pacifist behavior. Instead only use the presence or lack thereof of the combat behavior for this purpose. The docs should be updated accordingly in extending/areas/npcs.md and extending/areas/scripting.md

Fix or document core requiring a 'cooldown' effect

Right now the Skill class requires that an effect with the id cooldown exists for cooldowns to work. Possible approaches:

  • Have the core throw a server error during bootup if a skill is loaded that has a cooldown configured but there is no cooldown effect registered.
  • In the documentation for skill creation make note of the fact that one must have a 'cooldown' effect for cooldowns to work.
  • Both

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.