This application is a Dungeons and Dragons Encounter Builder, with the intended user being a Dungeon Master. It allows the user to fill out a form to add encounter details including encounter name, party size, party level, summary, description, treasure & rewards, and monsters. Monsters from the D&D 5e compendium can be filtered and added based on size, name, hit points, and armor class attributes.
This GraphQL API handles the backend functionality including:
- Consumption of third party D&D 5e GraphQL API
- Query endpoint(s) for display of created encounters as well as monster details
- Mutation endpoint(s) for creation of new encounters
- Current deploy GraphQL API endpoint: `https://tavern-keeper-be.onrender.com/graphql`
Explore Backend docs »
Frontend Repository · Report Bug · Request Feature
To get a local copy up and running follow these simple example steps.
- Clone the repo
git clone https://github.com/Tavern-Keeper-2308/tavern_keeper_be.git
- Gem Bundle
bundle
- Rake
rails db:{drop,create,migrate,seed}
- To run on local server, http://localhost:5000/
rails s
- Live deploy via desired method
- Run test suite
bundle exec rspec
- Query endpoints are documented showing full scope of available attributes.
- Attributes should all be written in queries and mutations using
camelCase
. - Some attributes are nested under another, using curly brackets
{}
.
attribute {
nestedAttribute
anotherNestedAttribute
}
- Query and Mutation requests can be modified to include only desired attribute data in the response.
- Query and Mutation request attributes can be ordered designate the order of attributes in the response.
- Variables for mutations can be input in any order.
- Gets list of all available monsters, with simple base attributes (Example only shows a few monsters, not full respsonse).
- This endpoint is utilized by the frontend for encounter creation, displaying all possible monster choices from the D&D 5e compendium.
query getMonsters {
monsters {
monsterIndex
monsterName
size
hitPoints
armorClass
type
alignment
challengeRating
}
}
{
"data": {
"monsters": [
{
"monsterIndex": "acolyte",
"monsterName": "Acolyte",
"size": "MEDIUM",
"hitPoints": 9,
"armorClass": 10,
"type": "HUMANOID",
"alignment": "any alignment",
"challengeRating": "0.25"
},
{
"monsterIndex": "aboleth",
"monsterName": "Aboleth",
"size": "LARGE",
"hitPoints": 135,
"armorClass": 17,
"type": "ABERRATION",
"alignment": "lawful evil",
"challengeRating": "10"
},
{
"monsterIndex": "adult-black-dragon",
"monsterName": "Adult Black Dragon",
"size": "HUGE",
"hitPoints": 195,
"armorClass": 19,
"type": "DRAGON",
"alignment": "chaotic evil",
"challengeRating": "14"
}
]
}
}
- Gets list of all monster details for a single monster by
index
. - Requires variable(s):
index
-String
type. - This endpoint is utilized by the frontend for encounter details page, for monster details dropdowns.
query getMonster($index: String!) {
monster(index: $index) {
monsterIndex
monsterName
size
type
armorClass
speed {
walk
fly
swim
}
hitPoints
strength
dexterity
constitution
intelligence
wisdom
charisma
damageVulnerabilities
damageResistances
damageImmunities
conditionImmunities
proficiencyBonus
proficiencies {
name
value
}
senses {
blindsight
darkvision
passivePerception
}
specialAbilities {
name
desc
}
actions {
name
desc
}
legendaryActions {
name
desc
}
}
}
{
"index": "aboleth"
}
{
"data": {
"monster": {
"monsterIndex": "aboleth",
"monsterName": "Aboleth",
"size": "LARGE",
"type": null,
"armorClass": 17,
"speed": {
"walk": "10 ft.",
"fly": null,
"swim": "40 ft."
},
"hitPoints": 135,
"strength": 21,
"dexterity": 9,
"constitution": 15,
"intelligence": 18,
"wisdom": 15,
"charisma": 18,
"damageVulnerabilities": [],
"damageResistances": [],
"damageImmunities": [],
"conditionImmunities": [],
"proficiencyBonus": 4,
"proficiencies": [
{
"name": "Saving Throw: CON",
"value": "6"
},
{
"name": "Saving Throw: INT",
"value": "8"
},
{
"name": "Saving Throw: WIS",
"value": "6"
},
{
"name": "Skill: History",
"value": "12"
},
{
"name": "Skill: Perception",
"value": "10"
}
],
"senses": {
"blindsight": null,
"darkvision": "120 ft.",
"passivePerception": "20"
},
"specialAbilities": [
{
"name": "Amphibious",
"desc": "The aboleth can breathe air and water."
},
{
"name": "Mucous Cloud",
"desc": "While underwater, the aboleth is surrounded by transformative mucus. A creature that touches the aboleth or that hits it with a melee attack while within 5 ft. of it must make a DC 14 Constitution saving throw. On a failure, the creature is diseased for 1d4 hours. The diseased creature can breathe only underwater."
},
{
"name": "Probing Telepathy",
"desc": "If a creature communicates telepathically with the aboleth, the aboleth learns the creature's greatest desires if the aboleth can see the creature."
}
],
"actions": [
{
"name": "Multiattack",
"desc": "The aboleth makes three tentacle attacks."
},
{
"name": "Tentacle",
"desc": "Melee Weapon Attack: +9 to hit, reach 10 ft., one target. Hit: 12 (2d6 + 5) bludgeoning damage. If the target is a creature, it must succeed on a DC 14 Constitution saving throw or become diseased. The disease has no effect for 1 minute and can be removed by any magic that cures disease. After 1 minute, the diseased creature's skin becomes translucent and slimy, the creature can't regain hit points unless it is underwater, and the disease can be removed only by heal or another disease-curing spell of 6th level or higher. When the creature is outside a body of water, it takes 6 (1d12) acid damage every 10 minutes unless moisture is applied to the skin before 10 minutes have passed."
},
{
"name": "Tail",
"desc": "Melee Weapon Attack: +9 to hit, reach 10 ft., one target. Hit: 15 (3d6 + 5) bludgeoning damage."
},
{
"name": "Enslave",
"desc": "The aboleth targets one creature it can see within 30 ft. of it. The target must succeed on a DC 14 Wisdom saving throw or be magically charmed by the aboleth until the aboleth dies or until it is on a different plane of existence from the target. The charmed target is under the aboleth's control and can't take reactions, and the aboleth and the target can communicate telepathically with each other over any distance.\nWhenever the charmed target takes damage, the target can repeat the saving throw. On a success, the effect ends. No more than once every 24 hours, the target can also repeat the saving throw when it is at least 1 mile away from the aboleth."
}
],
"legendaryActions": [
{
"name": "Detect",
"desc": "The aboleth makes a Wisdom (Perception) check."
},
{
"name": "Tail Swipe",
"desc": "The aboleth makes one tail attack."
},
{
"name": "Psychic Drain (Costs 2 Actions)",
"desc": "One creature charmed by the aboleth takes 10 (3d6) psychic damage, and the aboleth regains hit points equal to the damage the creature takes."
}
]
}
}
}
- Gets list of all encounters for a single user, by
userName
. - Requires variable(s):
userName
-String
type - This endpoint is used by frontend to create an index page displaying all encounters created by a single user.
query getEncounters($userName: String!) {
encounters(userName: $userName) {
id
userName
encounterName
partySize
partyLevel
summary
description
treasure
encounterMonsters {
monsterName
}
}
}
{
"userName": "demo-many-encounters"
}
{
"data": {
"encounters": [
{
"id": "2",
"userName": "demo-many-encounters",
"encounterName": "Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn",
"partySize": 1,
"partyLevel": 3,
"summary": "Ere iron was found or tree was hewn",
"description": "Gambrel fungus antiquarian gibbous gibbering unnamable. Furtive blasphemous cyclopean comprehension manuscript non-euclidean tentacles decadent. Antediluvian shunned mortal. Squamous non-euclidean cyclopean eldritch tenebrous gibbering charnel. Cyclopean stench furtive gibbering.",
"treasure": "Amulet of Kynareth",
"encounterMonsters": [
{
"monsterName": "Giant Shark"
},
{
"monsterName": "Aboleth"
},
{
"monsterName": "Aboleth"
}
]
},
{
"id": "3",
"userName": "demo-many-encounters",
"encounterName": "Rawr R'lyeh wgah'nagl Ph'n ui mglw'nafh gl fhtagn",
"partySize": 1,
"partyLevel": 13,
"summary": "Under the mountain dark and tall",
"description": "Mortal madness furtive gibbering stygian. Mortal singular amorphous stygian stench antiquarian. Non-euclidean furtive decadent accursed comprehension cyclopean foetid fungus. Madness spectral stench charnel indescribable comprehension unutterable.",
"treasure": "Gold and Ruby Circlet",
"encounterMonsters": [
{
"monsterName": "Goblin"
},
{
"monsterName": "Goblin"
}
]
},
{
"id": "4",
"userName": "demo-many-encounters",
"encounterName": "Tnafh Dargrlw'l fhtagne R'Ph'nglui mg",
"partySize": 3,
"partyLevel": 9,
"summary": "The Fall of Gil-galad",
"description": "Antiquarian stygian lurk charnel unnamable furtive. Non-euclidean blasphemous unmentionable dank stygian immemorial. Effulgence gibbous foetid antediluvian ululate non-euclidean gibbering squamous. Antediluvian daemoniac dank.",
"treasure": "Nightweaver's Band",
"encounterMonsters": [
{
"monsterName": "Aboleth"
},
{
"monsterName": "Aboleth"
}
]
}
]
}
}
- Gets details for a single encounter, by encounter
id
- Requires variable(s):
id
-Integer
type - This endpoint is utilized by the frontend for create a display page for a single encounter.
query getEncounter($id: ID!) {
encounter(id: $id) {
id
userName
encounterName
partySize
partyLevel
summary
description
treasure
encounterMonsters {
monsterName
monsterIndex
}
}
}
{
"id": 2
}
{
"data": {
"encounter": {
"id": "2",
"userName": "demo-many-encounters",
"encounterName": "Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn",
"partySize": 1,
"partyLevel": 3,
"summary": "Ere iron was found or tree was hewn",
"description": "Gambrel fungus antiquarian gibbous gibbering unnamable. Furtive blasphemous cyclopean comprehension manuscript non-euclidean tentacles decadent. Antediluvian shunned mortal. Squamous non-euclidean cyclopean eldritch tenebrous gibbering charnel. Cyclopean stench furtive gibbering.",
"treasure": "Amulet of Kynareth",
"encounterMonsters": [
{
"monsterName": "Giant Shark",
"monsterIndex": "giant-shark"
},
{
"monsterName": "Aboleth",
"monsterIndex": "aboleth"
},
{
"monsterName": "Aboleth",
"monsterIndex": "aboleth"
}
]
}
}
}
- Creates a new encounter based on input variables,
userName
,encounterName
,partySize
,partyLevel
,summary
,description
,treasure
, andencounterMonsterIndexes
(this is an array of monster index strings). - Requires variable(s):
userName
-String
type
encounterName
-String
type
partySize
-Integer
type
partyLevel
-Integer
type
summary
-String
type
description
-String
type
treasure
-String
type
encounterMonsterIndexes
- [String
] type - This endpoint is utilized by the frontend to create a new encounter from user input on encounter builder page.
- Response include
encounter
anderrors
,errors
is an array and will be empty upon a successful mutation request and encounter creation. - If there are errors, only
errors
is returned in the response withmessage
detailing the issue andlocation
with where the error ocurred, andextensions
containing details of the error.
mutation CreateEncounter($userName: String!, $encounterName: String!, $partySize: Int!, $partyLevel: Int!, $summary: String!, $description: String!, $treasure: String!, $encounterMonsterIndexes: [String!]!) {
createEncounter(input: {
userName: $userName,
encounterName: $encounterName,
partySize: $partySize,
partyLevel: $partyLevel,
summary: $summary,
description: $description,
treasure: $treasure,
encounterMonsterIndexes: $encounterMonsterIndexes
}) {
encounter {
userName
id
encounterName
partySize
partyLevel
summary
description
treasure
encounterMonsters {
monsterIndex
monsterName
}
}
errors
}
}
{
"userName": "WhoAmI",
"encounterName": "Party Wipe",
"partySize": 4,
"partyLevel": 3,
"summary": "I hope this works",
"description": "Why does it have to be a string",
"treasure": "We not deserve anything",
"encounterMonsterIndexes": ["beholder", "goblin","goblin", "adult-black-dragon"]
}
{
"data": {
"createEncounter": {
"encounter": {
"userName": "WhoAmI",
"id": "13",
"encounterName": "Party Wipe",
"partySize": 4,
"partyLevel": 3,
"summary": "I hope this works",
"description": "Why does it have to be a string",
"treasure": "We not deserve anything",
"encounterMonsters": [
{
"monsterIndex": "beholder",
"monsterName": "Beholder"
},
{
"monsterIndex": "goblin",
"monsterName": "Goblin"
},
{
"monsterIndex": "goblin",
"monsterName": "Goblin"
},
{
"monsterIndex": "adult-black-dragon",
"monsterName": "Adult Black Dragon"
}
]
},
"errors": []
}
}
}
mutation CreateEncounter($userName: String!, $encounterName: String!, $partySize: Int!, $partyLevel: Int!, $summary: String!, $description: String!, $treasure: String!, $encounterMonsterIndexes: [String!]!) {
createEncounter(input: {
userName: $userName,
encounterName: $encounterName,
partySize: $partySize,
partyLevel: $partyLevel,
summary: $summary,
description: $description,
treasure: $treasure,
encounterMonsterIndexes: $encounterMonsterIndexes
}) {
encounter {
userName
id
encounterName
partySize
partyLevel
summary
description
treasure
encounterMonsters {
monsterIndex
monsterName
}
}
errors
}
}
{
"encounterName": "Party Wipe",
"partySize": 4,
"partyLevel": 3,
"summary": "I hope this works",
"description": "Why does it have to be a string",
"treasure": "We not deserve anything",
"encounterMonsterIndexes": ["beholder", "goblin","goblin", "adult-black-dragon"]
}
{
"errors": [
{
"message": "Variable $userName of type String! was provided invalid value",
"locations": [
{
"line": 1,
"column": 26
}
],
"extensions": {
"value": null,
"problems": [
{
"path": [],
"explanation": "Expected value to not be null"
}
]
}
}
]
}
- Database Schema
- Set Up Mock Server on Postman (browser version)
- JSON Contracts
- Configure gems and GraphQL for creating an API
- CircleCI and Render CI/CD
- Set up consumption architecture using Service/Facade/Poro design pattern
- Query:
getMonsters
, gets list of all monsters with some attributes used for filter and create page- Set up
MonsterType
class underTypes
module - Set up
field
for:monster
inQueryType
class and createmonsters
method usingMonsterFacade
- Set up
- Query:
getMonster
, gets a single monsters with full attributes, by index- Add additional fields to
MonsterType
class underTypes
module - Set up
field
for:monsters
with requiredargument
inQueryType
class and createmonster(index:)
method usingMonsterFacade
- Add additional fields to
- Query:
getEncounters
, gets list of encounters with attributes, by userName- Set up
EncounterType
class underTypes
module - Set up
EncounterMonsterType
class underTypes
module - Set up
field
for:encounters
inQueryType
class and createencounters(userName:)
method using ActiveRecord query
- Set up
- Query:
getEncounter
, gets a single encounter by id- Set up
field
for:encounter
with requiredargument
inQueryType
class and createencounter(id:)
method using ActiveRecord query
- Set up
- Mutation:
createEncounter
, creates new encounter and saves to database using input variables- Add
field
for:create_encounter
toMutationType
class inTypes
module - Set up
CreateEncounter
class for mutation, underapp/graphql/mutations
- Add arguments required for encounter creation (encounterMonsterIndexes is an array of index strings)
- Add fields for return in response upon creation,
:encounter
and:errors
- Error handling logic
- REFACTOR IDEA: Set up a Type for input,
EncounterInputType
, that can be used fo intake input as a single object variable for further mutations
- Add
See the open issues for a full list of proposed features (and known issues).
Organization: Tavern Keeper
Organization Link: https://github.com/Tavern-Keeper-2308
Project Link: https://github.com/Tavern-Keeper-2308/tavern_keeper_be
Xander Hendry
Sam Tran
Kevin Zolman
Erica Hagle
Arden Ranta