Coder Social home page Coder Social logo

sirwojtek / wyrm-engine Goto Github PK

View Code? Open in Web Editor NEW
2.0 2.0 2.0 468 KB

Fully typed game engine library designed to simulate battles between virtual characters.

JavaScript 12.28% TypeScript 85.52% HTML 2.20%
wyrm-engine game-engine battle-simulation rpg-engine character-generator simulation-engine

wyrm-engine's Introduction

wyrm-engine

Build Status codecov npm

wyrm-engine is fully typed game engine library designed to simulate battles between in-game characters. The main goal of wyrm-engine is to abstract the battle logic from the rest of the game, which makes it applicable for a wide variety of virtual worlds from fantasy to science fiction. To achieve that, wyrm-engine uses generic parameters to define battle sites, whilst keeping the API usage as simple as possible.

wyrm-engine main features:

  • fully-typed, fully-written in Typescript,
  • lightweight, depends only on lodash and uuid,
  • simple in use,
  • can be used in real-time and round based mode,
  • customizable engine parameters.

The quickest way to use wyrm-engine could be:

// create an engine instance with the default config
const wyrmEngine = createEngine();

// create two characters using the creator
const characterCreator = wyrmEngine.getCharacterCreator();
const kyle = characterCreator.createCharacter({
  name: 'Kyle',
  level: 10,
  type: CharacterTypeEnum.Strong,
  subtype: CharacterSubtypeEnum.Attacker,
});
const jenny = characterCreator.createCharacter({
  name: 'Jenny',
  level: 10,
  type: CharacterTypeEnum.Swift,
  subtype: CharacterSubtypeEnum.Balanced,
});

// form teams and build the encounter object
const teamA = [kyle];
const teamB = [jenny];
const encounter = wyrmEngine.createEncounter(teamA, teamB);

while (encounter.tick()) {
  // battle! (see Ticks for more info)
}

// print battle logs in human readable way
const logs = encounter.getEncounterLogs();
logs.forEach(l => console.log(l.message));

Which produces the following output:

=== Encounter summary ===
Round: 1
Round order:
	Jenny	teamB	HP: 38/38
	Kyle	teamA	HP: 38/38
Jenny's Attack hits Kyle for 14
Kyle current hp: 24
Kyle's Attack misses Jenny
=== Encounter summary ===
Round: 2
Round order:
	Jenny	teamB	HP: 38/38
	Kyle	teamA	HP: 24/38
Jenny's Attack hits Kyle for 14
Kyle current hp: 10
Kyle's Attack hits Jenny for 28
Jenny current hp: 10
=== Encounter summary ===
Round: 3
Round order:
	Kyle	teamA	HP: 10/38
	Jenny	teamB	HP: 10/38
Kyle's Attack hits Jenny for 28
Jenny current hp: -18
Jenny is dead!
teamA won the encounter!

Instalation

Via npm

npm install wyrm-engine

Via yarn

yarn wyrm-engine

API docs

API documentation is available here.

Usage

To create an ecounter, you start with praparing battleground, which includes defining characters and factions. Once you've created the encounter object, you can then control when to trigger the next round.

wyrm-engine produces envets (messages) during tthe battle simulation. They can be used to inform game users about the current state of encounter so they can decide about their next steps. Output from the engine can also be used to update game internal state, for example HP of game characters.

Creating engine

The first step is to create wyrm-engine instance, which can be done by:

import { createEngine } from 'wyrm-engine';

const wyrmEngine = createEngine();

The engine instance created using createEngine is configured to use the default engine parameters which are balanced to provide an equal chance of winning for characters with the same level. For more details about creating engine with custom parameters, see the Customising Engine section.

Modeling characters

The simplest way to create a character is to use CharacterCreator which can be obtained from the engine instance:

const wyrmEngine = createEngine();
const characterCreator = wyrmEngine.getCharacterCreator();

CharacterCreator.createCharacter

This method is the simplest way of creating a character. It automates stats generation, provides actions and AI logic.

// creates AI character using predefined template
const kyle: ICharacter = characterCreator.createCharacter({
  name: 'Kyle',
  level: 10,
  type: CharacterTypeEnum.Strong,
  subtype: CharacterSubtypeEnum.Balanced,
});

// creates human controled character using predefined template
const jenny: ICharacter = characterCreator.createCharacter({
  name: 'Jenny',
  level: 10,
  type: CharacterTypeEnum.Swift,
  subtype: CharacterSubtypeEnum.Attacker,
  autoControl: false
});

createCharacter uses following parameters:

Name Description
name (optional) A human-readable name of a character, will be used in battle event messages
level Defines general battle proficiency, higher level means more stats points to distribute
type Allows to choose a stats profile. Available values are:
Strong - prioritise adding stats points to power
Swift - prioritise adding stats points to dexterity
Tought - prioritise adding stats points to stamina
For more info about stats (attributes) see Stats description
subtype (optional) Customize damage / armor profile. Available values are:
Attacker - prioritise character damage over armor
Balanced - balance damage and armor
Defender - prioritise character armor over damage
The default value is Balanced. For more info check Damage and armor
autoControl (optional) Should the AI controller be generated for the charater?
overrideCharacter (optional) Specify character properties defined in Character model description to override them

Manual character creation

If you find the standard method involving createCharacter unsufficient, you can use other helper methods from CharacterCreator to achieve the goal.

const level = 10;

// computes stats for the given character type and level
const stats = characterCreator.getStats({
  type: CharacterTypeEnum.Strong,
  level
});

// gets maxHp for the stats block
const maxHp = characterCreator.getMaxHp(stats);

// retrieve standard attack action
const attackAction = characterCreator.getAttackAction();

// manual character creation
const kyle: ICharacter = {
  id: 'kyle-id',
  level,
  stats,
  maxHp,
  // character with only half of its HP
  currentHp: maxHp / 2,
  actions: [
    attackAction,
    // additional character action
    {
      id: 'power-smash-id',
      name: 'Power Smash',
      damageModifiers: {
        addFactor: 10,
        multiplyFactor: 1
      }
    }
  ],
};

You can also decide to manually distribute stat points:

const statsSum = characterCreator.getStatsPointsSum(level);
const maxDamage = characterCreator.getMaxDamage(level);
// manually defined minimum damage
const minDamage = 0.8 * maxDamage;
// armor for tanks
const armor = characterCreator.getArmor(level, CharacterSubtypeEnum.Defender);

// now our character is ready to take some damage
const stats: IStats = {
  damage: { min: minDamage, max: maxDamage },
  armor,
  power: statsSum * 0.2,
  dexterity: statsSum * 0.2,
  stamina: statsSum * 0.6
};

Character model description

The character model (ICharacter) used in wyrm-engine includes the folowing parameters:

Name Description
id this field is used to keep track of a chracter, the id will be returned along with encounter events later
name (optional) human-readable name, define it to get more user-friendly log messages
level level of a character, determines the number of available stats points
stats stats used to determine battle potential of a player, see Stats for more details
currentHp number of actual hit points, determines vitality
maxHp maximum allowed hit points
actions an array of moves which can be performed during a battle, see Actions for more details
controllerCallback a function which will be invoked before each round to determine which action should be chosed, see Controling characters

Stats description

Stats describes attributes which define battle abilities of characters. Each attribute is responsible for the different set of skills:

Damage
  • defines base damage of all attacks, for example damage of a welded weapon
Armor
  • defines damage reduction, can be sum of armor values from character equipment
Power
  • bonus damage for each hit (attack power)
  • higher values means better armor penetration, used to calculate damage reduction
Dexterity
  • better chance to hit the target (hit rating)
  • higher probability to dodge an attack
  • more chance to attack before opponent in round
Stamina
  • more hit points

Formulas

Each of the above attributes are converted to internal stats used during battles. Here are equations responsible for calculating battle events:

Initiative (dexterity) - higher initiative gives more chance to be higher in round order hierarchy.

// pseudo-code of determining round order

while (charactersLeft.length > 0) {
  sum = sum(charactersLeft.initiative)
  randomNumber = random(1, sum)
  counter = 0

  charactersLeft.forEach(characterLeft => {
    counter += characterLeft.initiative

    if (randomNumber < counter) {
      roundOrder.push(characterLeft)
      charactersLeft.remove(characterLeft)
      break
    }
  });
}

Hit rating (dexterity) - defines chance to hit the opponent

sum = sum(hitRating + opponentDodge)
randomNumber = random(0, sum)
missed = randomNumber >= hitRating

Dodge (dexterity) - used against hit rating to determine if attack misses

sum = sum(hitRating + opponentDodge)
randomNumber = random(0, sum)
missed = randomNumber >= hitRating

Armor Penetration (power) - gives oportunity do bypass the opponent's armor

armorPenetrationRatio = armorPenetration / (armorPenetration + opponentDamageReduction)
damageDone = armorPenetrationRatio * baseDamage + attackPower

Damage Reduction (armor) - indicates how much damage is absorbed by equipment

armorPenetrationRatio = opponentArmorPenetration / (opponentArmorPenetration + damageReduction)
damageTaken = armorPenetrationRatio * opponentBaseDamage + opponentAttackPower

Damage (damage) - base damage of all attacks

damageDone = armorPenetrationRatio * baseDamage + attackPower

Attack Power (power) - determines the bonus damage on hit.

damageDone = armorPenetrationRatio * baseDamage + attackPower

Actions

Each character should have at least one action defined to be capable to fight during an encounter. The below snippet illustrates how to define an action:

const action: IAction = {
  id: 'some-action-id',
  name: 'Some action',  // optional
  damageModifiers: {
    addFactor: 10,
    multiplyFactor: 1
  }
};

The damage of an action is computed using the formula:

total_damage = action_multiply_factor * base_damage + action_add_factor

Encounter

You can obtain an encounter object using a wyrmEngine instance. To create it, you need to form teams containing characters.

// creatiion of 1 vs. 1 encpunter
const encounter = wyrmEngine.createEncounter([ kyle ], [ jenny ]);

// creation of 2 vs. 2 encpunter
const encounter = wyrmEngine.createEncounter([ kyle, jenny ], [ coral, jeff ]);

createEncounter takes the following arguments:

Name Description
team1 Array of characters defining the first of the conflict side
team2 Array of characters defining the second of the conflict side
logMessageCallback (optional) Function which will be invoked if event occurs during the battle

Ticks

Encounters in wyrm-engine are round based and the progress of the battle (simulating round). The tasks performed during te one round of encounter:

  1. Winner check
  2. Determine order of the round, basing on the character stats
  3. Trigger characters' controllerCallback for both parties to plan AI actions
  4. For each of ordered characters perform an action and check for winner Events, which occur during a round, are reported via return value and logMessageCallback if defined.
Round-based battles

To use wyrm-engine's encounter in the round-based systems, plug tick function as a callback for an event which should advance a round:

// TODO: improve

roundButton.onClick(() => {
  const roundMessages = encounter.tick();
  console.log(roundMessages);
);

actionButton.onClick((actionIndex) => {
  encounter.addAction(player.id, player.action[actionIndex]);
});

For more information about planning actions, see Controling characters.

Real-time battles

It is possible to use wyrm-engine in real-time battles. In order to achieve that, invoke tick in the time-based loop:

actionButton.onClick((actionIndex) => {
  encounter.addAction(player.id, player.action[actionIndex]);
});

// NOTE: this will be invoked in TICK_FOR_ROUND interval
function tickMain() {
  setTimeout(
    () => {
      const roundLogs = encounter.tick();
      console.log(roundLogs);
      tickMain();
    },
    TIME_FOR_ROUND
  );
}

tickMain();

Logs (events)

The best way to get know what is happening during the battle is to put your hands on the log messages. The messages returned by wyrm-engine are always annotated with the round on which the event occurs and a human readable text.

There are three ways to get battle logs:

  1. Through logMessageCallback defined during the encounter creation. The callback will be invoked for each event independently.
  2. By saving output from tick(). The returned array contains logs for the round currently performed.
  3. By calling getEncounterLogs on the encounter instance. With this method you can get whole battle history.
Message types
Name Description
Summary message Emitted at the beginning of each round, contains information about the round order
Miss message Informs about character's attack that missed the opponent
Hit message Informs about character's attack that hit the opponent, includes information about damage
HP message This message is emitted wheen one of characters HP changes, usually because of attack
Death message Emitted when one of the player HP drops below 0
Win message Informs that one of the team won the battle. This is always the last message emitted

Controling characters

In wyrm-engine controlling a charater means scheduling an action for each encounter round. There are two ways to apply an action to the player:

Manual control

In this method, you schedule an action before playing a round. After the round is ended, you need to schedule action again.

const [ action1, actions 2 ] = player.actions;

// schedules an action for 1st round
encounter.addAction(player.id, action1);

// play 1st round
encounter.tick();

// schedules an action for 2nd round
encounter.addAction(player.id, action2);

// play 2nd round
encounter.tick();

Automated control

If you want to automate action choosing so you don't need to schedule actions for each round, you can add controllerCallback in character definition. That callback will be invoked before each round to determine what character should do.

const powerSmash: IAction = {
  id: 'power-smash-id',
  damageModifiers: {
    addFactor: 10,
    multiplyFactor: 1,
  },
};

const aiCharacter = characterCreator.createCharacter({
  type: CharacterTypeEnum.Strong,
  level: 10,
  overrideCharacter: {
    actions: [characterCreator.getAttackAction(), powerSmash],
    // NOTE: randomly pick between available actions
    controllerCallback: actions => actions[Math.round(2* Math.random())]
  },
});

wyrmEngine.createEncounter([ kyle ], [ aiCharacter ]);

Encounter state

It is possible to obtain/load the encounter's state via getState and loadState functions. Please note that state does not include callbacks defined during creation of encounter. Those can be re-added during load:

const state = encounter.getState();
encounter.loadState({
  ...state,
  logMessageCallback: console.log
})

Customising engine

Work in progress :)

Contact

Email: [email protected]

Discord: SirWojtek#3331

wyrm-engine's People

Contributors

dependabot[bot] avatar sirwojtek avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

Forkers

cmlarsen dkzdev

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.