This is a PHP Game Text Engine to create games based on texts, with choices, items, and more.
If you wish to install it in your project, require it via composer:
composer require velkuns/game-text-enginedeclare(strict_types=1);
namespace Application;
use Velkuns\GameTextEngine\Api\ItemsApi;
use Velkuns\GameTextEngine\Api\PlayerApi;
use Velunns\GameTextEngine\Api\GameApi;
use Velkuns\GameTextEngine\Core\Factory\AttributeFactory;
use Velkuns\GameTextEngine\Core\Loader\JsonLoader;
//~ Factories
$modifierFactory = new ModifierFactory();
$itemFactory = new ItemFactory($modifierFactory);
$conditionFactory = new ConditionsFactory(new ConditionParser(), new ConditionElementResolver(), new ConditionValidator());
$graphFactory = new GraphFactory($conditionFactory);
$attributeFactory = new AttributeFactory();
$entityFactory = new EntityFactory(
$attributeFactory,
new TraitFactory($modifierFactory, $conditionFactory),
$itemFactory
);
$items = new ItemsApi($itemFactory);
$gameApi = new GameApi(
new JsonLoader(),
new StoryApi($graphFactory),
$items,
new BestiaryApi($entityFactory, $items),
new AttributesApi($attributeFactory),
new PlayerApi($entityFactory, $items),
new CombatApi(new Randomizer(new Mt19937())),
);
//~ Load json data (can be from files or strings if came from database)
$storyData = $game->loader->fromFile($dataDir . '/stories/test.json');
$itemsData = $game->loader->fromFile($dataDir . '/items.json');
$bestiaryData = $game->loader->fromFile($dataDir . '/bestiary.json');
$attributesRulesData = $game->loader->fromFile($dataDir . '/rules/rules_attributes.json');
$traitsRulesData = $game->loader->fromFile($dataDir . '/rules/rules_traits.json');
$alterationsRulesData = $game->loader->fromFile($dataDir . '/rules/rules_alterations.json');
$combatsRulesData = $game->loader->fromFile($dataDir . '/rules/rules_combat.json');
$playerRulesData = $game->loader->fromFile($dataDir . '/rules/rules_player.json');
$playerData = $game->loader->fromFile($dataDir . '/templates/player.json');
//~ Load data into the game api
$gameApi->load(
$storyData,
$itemsData,
$bestiaryData,
$traitsRulesData,
$alterationsRulesData,
$combatsRulesData,
$playerRulesData,
$playerData
);
//~ Access to the other apis
$gameApi->storyApi->[...];
$gameApi->bestiaryApi->[...];
$gameApi->itemsApi->[...];
$gameApi->attributesApi->[...];
$gameApi->traitsApi->[...];
$gameApi->alterationssApi->[...];
$gameApi->playerApi->[...];
//~ Dumping apis into json data
/**
* @phpstan-return array{
* story: string,
* items: string,
* bestiary: string,
* attributesRules: string,
* traitsRules: string,
* alterationsRules: string,
* combatRules: string,
* playerRules: string,
* playerData: string,
* } $data Array of json data, to save in files or database
*/
$data = $gameApi->dump(/* true */); // true to pretty json output
//~ Exporting story graph into DOT data
$gameApi->exporter->toFile($gameApi->storyApi->graph, [...]); // export story graph to file
$string = $gameApi->exporter->toString($gameApi->storyApi->graph); // export story graph to string
//~ Game API read
$source = '1';
$target = '2';
$gameApi->read($source, $target);<?php
declare(strict_types=1);
namespace Application;
use Velkuns\GameTextEngine\Util\Loader\JsonLoader;
$loader = new JsonLoader();
//~ To load data from a file
$data = $loader->fromFile('/path/to/data.json');
//~ To load data from a string (JSON format, can be stored in database)
$data = $loader->fromString('{"key": "value"}');<?php
declare(strict_types=1);
namespace Application;
// [... game api init code here ... ]
//~ Get an item by its name
$item = $game->items->get('Rusty Sword');
$staff = $itemFactory->from(['name' => 'Staff', ...]);
$gameApi->items->set($staff); // Adds or replaces the item in the items dictionary
$gameApi->items->remove($staff->getName()); // Removes the item from the items dictionary<?php
declare(strict_types=1);
namespace Application;
// [... game api init code here ... ]
//~ Get a creature by its name
$entity = $gameApi->bestiary->get('Goblin');
$goblinWarrior = $entityFactory->from(['name' => 'Goblin Warrior', ...]);
$gameApi->bestiary->set($goblinWarrior); // Adds or replaces the creature in the bestiary
$gameApi->bestiary->remove('Goblin'); // Removes the creature from the bestiary<?php
declare(strict_types=1);
namespace Application;
// [... game api init code here ... ]
//~ Return all attributes: array{simples: array<string, AttributeInterface>, compounds: array<string, AttributeInterface>}
$attributes = $gameApi->attributes->getAll();
$attribute = $gameApi->attributes->get('strength'); // Get one attribute (cloned)
$gameApi->attributes->set($attribute); // Set an attribute
$gameApi->attributes->remove('strength');<?php
declare(strict_types=1);
namespace Application;
// [... game api init code here ... ]
//~ Return all traits: array<string, array<string, TraitInterface>>
$traits = $gameApi->traits->getAll();
$trait = $gameApi->traits->get('skill', 'Goblin Hunter'); // Get one trait (cloned)
$gameApi->traits->set($trait); // Set a trait
$gameApi->traits->remove('skill', 'Goblin Hunter');<?php
declare(strict_types=1);
namespace Application;
// [... game api init code here ... ]
//~ Return all alterations: array<string, array<string, TraitInterface>>
$alterations = $gameApi->alterations->getAll();
$alteration = $gameApi->alterations->get('state', 'poisoned'); // Get one alteration (cloned)
$gameApi->alterations->set($alteration); // Set an alteration
$gameApi->alterations->remove('state', 'poisoned');<?php
declare(strict_types=1);
namespace Application;
// [... game api init code here, including player ... ]
//~ Start the story - retrieve the first node of the story
$sourceNodeId = '0';
$targetNodeId = '1';
[$text, $choices, $logs] = $gameApi->story->read($sourceNodeId, $targetNodeId);<?php
declare(strict_types=1);
namespace Application;
// [... game api init code here ... ]
//~ Create a new player based on the given data
$data = [
'name' => 'New Hero',
'age' => 25, // optional, default 20
'race' => 'elf', // optional, default 'human'
'description' => 'A brave adventurer.', // optional, default ''
'background' => 'Born in a small village.', // optional, default ''
'attributes' => [,
'strength' => 10,
'endurance' => 12,
'agility' => 14,
'intuition' => 13,
],
'inventory' => ['Rusty Sword'], // optional, default []
];
$gameApi->player->new($data);
//~ Get player object
$player = $gameApi->player->player;
<?php
declare(strict_types=1);
namespace Application;
// [... game api init code here ... ]
$enemies = [
$gameApi->bestiary->get('Rat'), // get clone
$gameApi->bestiary->get('Rat'), // get clone
];
$logs = $gameApi->combat->auto($gameApi->player->player, $enemies);
//~ Display combat results
// ... your code to display combat turns ...<?php
declare(strict_types=1);
namespace Application;
// [... game api init code here ... ]
$graph = $gameApi->story->graph;
//~ Manipulate graph nodes
$graph->addNode(new Node(...));
$graph->removeNode('node_id');
//~ Manipulate graph edges (between nodes).
$graph->addEdge(new Edge(...));
$graph->removeEdgeBetweenNodes('node_id_source', 'node_id_target');Important
When add edges, source node and target node must have already been added to the graph.
parameters:
# Path to initial game data
game.data.dir: '../../vendor/velkuns/game-text-engine/data'
# Files to load when create new story. Should be stored in database after.
game.data.story: '%game.data.dir%/stories/test.json'
game.data.bestiary: '%game.data.dir%/bestiary.json'
game.data.items: '%game.data.dir%/items.json'
game.data.player: '%game.data.dir%/templates/player.json'
# Rules files to load when create new story. Should be stored in database after.
game.data.rules.attributes: '%game.data.dir%/rules/rules_attributes.json'
game.data.rules.traits: '%game.data.dir%/rules/rules_traits.json'
game.data.rules.alterations: '%game.data.dir%/rules/rules_alterations.json'
game.data.rules.combat: '%game.data.dir%/rules/rules_combat.json'
game.data.rules.player: '%game.data.dir%/rules/rules_player.json'
services:
_defaults:
autowire: true
bind:
$typeResolvers:
- '@Velkuns\GameTextEngine\Core\Resolver\AttributeResolver'
- '@Velkuns\GameTextEngine\Core\Resolver\EntityDamagesResolver'
- '@Velkuns\GameTextEngine\Core\Resolver\EntityInfoResolver'
- '@Velkuns\GameTextEngine\Core\Resolver\EntityInventoryItemsResolver'
- '@Velkuns\GameTextEngine\Core\Resolver\TraitResolver'
- '@Velkuns\GameTextEngine\Core\Resolver\AlterationResolver'
$valueResolvers:
- '@Velkuns\GameTextEngine\Core\Resolver\AttributeResolver'
- '@Velkuns\GameTextEngine\Core\Resolver\EntityDamagesResolver'
- '@Velkuns\GameTextEngine\Core\Resolver\EntityInfoResolver'
- '@Velkuns\GameTextEngine\Core\Resolver\EquippedWeaponItemResolver'
- '@Velkuns\GameTextEngine\Core\Resolver\RollResolver'
$validators:
- '@Velkuns\GameTextEngine\Core\Validator\AttributeConditionValidator'
- '@Velkuns\GameTextEngine\Core\Validator\EntityInfoConditionValidator'
- '@Velkuns\GameTextEngine\Core\Validator\EntityInventoryItemsConditionValidator'
- '@Velkuns\GameTextEngine\Core\Validator\TraitConditionValidator'
- '@Velkuns\GameTextEngine\Core\Validator\AlterationConditionValidator'
#~ Game text engine source
Velkuns\GameTextEngine\:
resource: '../../vendor/velkuns/game-text-engine/src'
#~ Randomizer for random game part like combat
Random\Engine\Mt19937: ~
Random\Randomizer: ~
<?php
/*
* Copyright (c) velkuns
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Application\Domain\Book\Service;
use Application\Domain\Book\Entity\BookInteractiveGame;
use Application\Domain\Book\Repository\BookInteractiveGameRepositoryInterface;
use Application\Domain\Book\Repository\BookInteractiveRepositoryInterface;
use Velkuns\GameTextEngine\Api\GameApi;
use Velkuns\GameTextEngine\Api\PlayerApi;
use Velkuns\GameTextEngine\Core\Log\LootLog;
use Velkuns\GameTextEngine\Core\Log\XpLog;
use Velkuns\GameTextEngine\Graph\Edge;
use Velkuns\GameTextEngine\Graph\Node;
use Velkuns\GameTextEngine\Core\Log\CombatLog;
/**
* @phpstan-import-type NewPlayerData from PlayerApi
*/
readonly class StoryPlay
{
public function __construct(
public GameApi $api,
private BookInteractiveRepositoryInterface $bookInteractiveRepository,
private BookInteractiveGameRepositoryInterface $bookInteractiveGameRepository,
) {}
/**
* Load a game from database and load content in GameApi to set up the Game Text Engine
*/
public function load(int $gameId): BookInteractiveGame
{
$bookGame = $this->bookInteractiveGameRepository->findByIdWithJoined($gameId, ['BookInteractive']);
$bookInteractive = $bookGame->getBookInteractive();
//~ Load json data into GameApi
$this->api->loadFromJsons(
$bookInteractive->getStory(), // json string
$bookInteractive->getItems(), // json string
$bookInteractive->getBestiary(), // json string
$bookInteractive->getRulesAttributes(), // json string
$bookInteractive->getRulesTraits(), // json string
$bookInteractive->getRulesCombat(), // json string
$bookInteractive->getRulesPlayer(), // json string
$bookGame->getCharacter(),
);
return $bookGame;
}
/**
* Pre-load the Game Text Engine with rules, bestiary & items data, but without player info
*/
public function preload(int $bookInteractiveId): void
{
$bookInteractive = $this->bookInteractiveRepository->findById($bookInteractiveId);
//~ Load json data into GameApi
$this->api->loadFromJsons(
$bookInteractive->getStory(), // json string
$bookInteractive->getItems(), // json string
$bookInteractive->getBestiary(), // json string
$bookInteractive->getRulesAttributes(), // json string
$bookInteractive->getRulesTraits(), // json string
$bookInteractive->getRulesCombat(), // json string
$bookInteractive->getRulesPlayer(), // json string
);
}
/**
* Start a new game. Initialize new player with given basic info, and save game state in database.
* @phpstan-param NewPlayerData $characterData
*/
public function start(int $bookInteractiveId, int $userId, array $characterData): BookInteractiveGame
{
$this->preload($bookInteractiveId);
//~ Init new player from given character data
$this->api->player->new($characterData);
//$this->api->player->player->getInventory()->get('Rusty Sword')
$game = $this->bookInteractiveGameRepository->newEntity();
$game->setBookInteractiveId($bookInteractiveId);
$game->setUserId($userId);
return $this->save($game, 1, 0);
}
/**
* Read target node, and if not a page refresh, save new state
* @return array{
* 0: Node,
* 1: Edge[],
* 2: array{combat: array<int, array{player: CombatLog, enemy?: CombatLog}>, loot: list<LootLog>, xp: list<XpLog>}
* }
*/
public function read(BookInteractiveGame $bookGame, int $targetId): array
{
$source = (string) $bookGame->getTextTargetId(); // Previous target become source
$target = (string) $targetId;
$result = $this->api->read($source, $target);
if ($source !== $target) {
$this->save($bookGame, $targetId);
}
return $result;
}
/**
* Delete game from database
*/
public function delete(int $gameId): void
{
//~ Set data into entity
$bookGame = $this->bookInteractiveGameRepository->findById($gameId);
$this->bookInteractiveGameRepository->delete($bookGame);
}
/**
* Save game into database. Overwrite the current saved state.
*/
public function save(BookInteractiveGame $game, int $textTargetId, ?int $textSourceId = null): BookInteractiveGame
{
//~ Set data into entity
$character = $this->api->player->dumpPlayer();
$game->setCharacter($character);
$game->setTextSourceId($textSourceId ?? $game->getTextTargetId());
$game->setTextTargetId($textTargetId);
$this->bookInteractiveGameRepository->persist($game);
return $game;
}
}---
Tables diagram: nodes (v1)
---
classDiagram
class book_interactive {
int book_interactive_id
int book_id
string book_interactive_story
string book_interactive_items
string book_interactive_bestiary
string book_interactive_rules_attributes
string book_interactive_rules_traits
string book_interactive_rules_combat
string book_interactive_rules_player
}
class book_interactive_game {
int game_id
int book_interactive_id
int user_id
string game_character
int game_text_source_id
int game_text_target_id
}
See the CONTRIBUTING file.
You can install project with the following command:
make installAnd update with the following command:
make updateNB: For the components, the composer.lock file is not committed.
You can run unit tests (with coverage) on your side with following command:
make php/testsYou can run integration tests (without coverage) on your side with following command:
make php/integrationFor prettier output (but without coverage), you can use the following command:
make php/testdox # run tests without coverage reports but with prettified outputYou also can run code style check with following commands:
make php/cs-checkYou also can run code style fixes with following commands:
make php/cs-fixYou can check if any explicit dependency is missing with the following command:
make php/depsTo perform a static analyze of your code (with phpstan, lvl 9 at default), you can use the following command:
make php/analyseAnd the last "helper" commands, you can run before commit and push, is:
make ci This project is currently under The MIT License (MIT). See LICENCE file for more information.