add a bestiary search form and catalog
This commit is contained in:
parent
327651ee6e
commit
6c0254f664
@ -4,15 +4,16 @@ from app.tables.creature import Creatures
|
|||||||
|
|
||||||
namespace = Namespace('rules', description='Gamma World Rules')
|
namespace = Namespace('rules', description='Gamma World Rules')
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument('creature', type=str, help='the name of a Gamma World creature to search for.')
|
parser.add_argument('creature', type=str, required=False, help='the name of a Gamma World creature to search for.')
|
||||||
|
|
||||||
|
|
||||||
@namespace.route('/creature') # resolves to: /rules/creature
|
@namespace.route('/creature') # resolves to: /rules/creature
|
||||||
class RollEncounter(Resource):
|
class RollEncounter(Resource):
|
||||||
@namespace.expect(parser)
|
@namespace.expect(parser)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
creatures = Creatures()
|
||||||
creature = request.args.get('creature', default=None, type=str)
|
creature = request.args.get('creature', default=None, type=str)
|
||||||
if creature is None:
|
if creature is None:
|
||||||
return {'error': 'Provide the name of a Gamma World creature to search for'}, 400
|
return creatures.get_creature_list(), 200
|
||||||
creatures = Creatures()
|
|
||||||
return creatures.get_creature(creature), 200
|
return creatures.get_creature(creature), 200
|
||||||
|
|
||||||
|
@ -17,3 +17,7 @@ class Creatures:
|
|||||||
def get_creature(self, creature_name):
|
def get_creature(self, creature_name):
|
||||||
"""Returns the dictionary of the specified creature."""
|
"""Returns the dictionary of the specified creature."""
|
||||||
return self.creatures.get(creature_name)
|
return self.creatures.get(creature_name)
|
||||||
|
|
||||||
|
def get_creature_list(self):
|
||||||
|
"""Returns a list of all creatures."""
|
||||||
|
return sorted(list(self.creatures.keys()))
|
||||||
|
72
web/bestiary.html
Normal file
72
web/bestiary.html
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" type="text/css" href="styles.css">
|
||||||
|
<style>
|
||||||
|
#resultSection {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.1fr 1fr 0.9fr;
|
||||||
|
grid-template-rows: 1.2fr 0.5fr 1.3fr;
|
||||||
|
gap: 0px 0px;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.android {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container for each type (thinker, worker, warrior) */
|
||||||
|
.typeContainer {
|
||||||
|
display: flex; /* Use flexbox for layout */
|
||||||
|
justify-content: space-between; /* Space out the child elements evenly */
|
||||||
|
margin-bottom: 20px; /* Add some bottom margin for separation between types */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Profile and abilities within each type */
|
||||||
|
.creature_profile,
|
||||||
|
.creature_abilities {
|
||||||
|
width: 45%; /* Give them each about half the container's width */
|
||||||
|
}
|
||||||
|
|
||||||
|
.creature_id { grid-area: 1 / 1 / 2 / 2; }
|
||||||
|
.creature_profile { grid-area: 1 / 2 / 2 / 3; }
|
||||||
|
.creature_abilities { grid-area: 1 / 3 / 2 / 4; }
|
||||||
|
.creature_attacks { grid-area: 2 / 1 / 3 / 4; }
|
||||||
|
.creature_mutations { grid-area: 2 / 2 / 3 / 2; }
|
||||||
|
.creature_description { grid-area: 3 / 1 / 4 / 4; }
|
||||||
|
</style>
|
||||||
|
<title>Gamma World Bestiary</title>
|
||||||
|
<script src="config.js"></script>
|
||||||
|
<script src="bestiary.js" defer></script>
|
||||||
|
<script src="catalog.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Look Up A Creature</h2>
|
||||||
|
|
||||||
|
<form id="searchForm">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="creature">Beast</label>
|
||||||
|
<input type="string" name="creature" id="creature">
|
||||||
|
<button id="search-button" type="submit">Search</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="catalog-button">Retrieve Catalog</label>
|
||||||
|
<button id="catalog-button" type="button">Catalog</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
<div id="resultSection"></div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
226
web/bestiary.js
Normal file
226
web/bestiary.js
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
document.getElementById('searchForm').addEventListener('submit', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const creatureName = document.getElementById('creature').value;
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
creature: creatureName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert the loading.gif before making the fetch request
|
||||||
|
let resultSection = document.getElementById('resultSection');
|
||||||
|
resultSection.innerHTML = '';
|
||||||
|
let imgElement = document.createElement('img');
|
||||||
|
imgElement.src = 'img/checking.gif';
|
||||||
|
imgElement.style.width = '500px';
|
||||||
|
imgElement.style.height = '500px';
|
||||||
|
imgElement.style.display = 'block';
|
||||||
|
imgElement.style.marginLeft = 'auto';
|
||||||
|
imgElement.style.marginRight = 'auto';
|
||||||
|
resultSection.appendChild(imgElement);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fetch(`${window.BASE_URL}/rules/creature?${queryParams}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw response;
|
||||||
|
}
|
||||||
|
return response.json(); // we only get here if there is no error
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
if (json === null) { // if json is null update the image and stop execution
|
||||||
|
imgElement.src = 'img/404.jpg';
|
||||||
|
throw new Error('No data found'); // this will stop the execution and go to the catch block
|
||||||
|
}
|
||||||
|
resultSection.innerHTML = ''; // Clear previous content
|
||||||
|
if (creatureName === 'android') {
|
||||||
|
androidResultTable(json);
|
||||||
|
} else {
|
||||||
|
setCreatureTable(json);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
if(!resultSection.hasChildNodes()) { // Only update the image if it wasn't already updated
|
||||||
|
imgElement.src = 'img/404.jpg';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function setResultImage(name) {
|
||||||
|
let imgElement = document.createElement('img');
|
||||||
|
imgElement.src = 'img/' + name + ".jpg";
|
||||||
|
console.log(imgElement.src);
|
||||||
|
imgElement.onerror = function () {
|
||||||
|
this.src = 'img/404.jpg';
|
||||||
|
};
|
||||||
|
resultSection.appendChild(imgElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDiceMnemonic(data) {
|
||||||
|
return Array.isArray(data) ?
|
||||||
|
data[0] + "d" + data[1] + (data[2] === 0 ? "" : (data[2] > 0 ? "+" : "") + data[2]) :
|
||||||
|
data.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSpeedMnemonic(data){
|
||||||
|
return data[0] + '/' + data[1] + '/' + data[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
function androidResultTable(data) {
|
||||||
|
let container = document.createElement('div');
|
||||||
|
container.className = 'container android';
|
||||||
|
|
||||||
|
let creatureTitle = document.createElement('h2');
|
||||||
|
creatureTitle.textContent = creatureName.toUpperCase();
|
||||||
|
container.appendChild(creatureTitle)
|
||||||
|
|
||||||
|
let image = document.createElement('img');
|
||||||
|
image.src = 'img/android.jpg';
|
||||||
|
image.onerror = () => { image.src = 'img/404.jpg' };
|
||||||
|
image.style.width = '275px';
|
||||||
|
image.style.height = '275px';
|
||||||
|
container.appendChild(image);
|
||||||
|
|
||||||
|
let description = createDescriptionSection(data);
|
||||||
|
container.appendChild(description);
|
||||||
|
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
|
||||||
|
if (key === 'thinker' || key === 'worker' || key === 'warrior') {
|
||||||
|
let typeTitle = document.createElement('h2');
|
||||||
|
typeTitle.textContent = key.toUpperCase();
|
||||||
|
container.appendChild(typeTitle);
|
||||||
|
|
||||||
|
// Create a div for each type (thinker, worker, or warrior)
|
||||||
|
let typeContainer = document.createElement('div');
|
||||||
|
typeContainer.className = 'typeContainer'; // we'll use this in css
|
||||||
|
|
||||||
|
let profile = createProfileSection(value);
|
||||||
|
let abilities = createAbilitiesSection(value);
|
||||||
|
|
||||||
|
// Append profile and abilities to the typeContainer, instead of the main container
|
||||||
|
typeContainer.appendChild(profile);
|
||||||
|
typeContainer.appendChild(abilities);
|
||||||
|
|
||||||
|
// Finally, append the typeContainer to the main container.
|
||||||
|
container.appendChild(typeContainer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resultSection.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProfileSection(data) {
|
||||||
|
let profile = document.createElement('div');
|
||||||
|
profile.className = 'creature_profile';
|
||||||
|
let profileHTML = `
|
||||||
|
<h3>Profile</h3>
|
||||||
|
<table>
|
||||||
|
<tr><td><b>NUMBER</b></td> <td>${formatDiceMnemonic(data.number)}</td></tr>
|
||||||
|
<tr><td><b>MORALE</b></td> <td>${formatDiceMnemonic(data.morale)}</td></tr>
|
||||||
|
<tr><td><b>HIT DICE</b></td> <td>${formatDiceMnemonic(data['hit dice'])}</td></tr>
|
||||||
|
<tr><td><b>ARMOUR</b></td> <td>${data.armour}</td></tr>
|
||||||
|
<tr><td><b>ENVIRON</b></td> <td>${data.environ.join(', ')}</td></tr>
|
||||||
|
`;
|
||||||
|
if(data['land speed']){
|
||||||
|
profileHTML += `<tr><td><b>LAND SPEED</b></td> <td>${formatSpeedMnemonic(data['land speed'])}</td></tr>`;
|
||||||
|
}
|
||||||
|
if(data['water speed']){
|
||||||
|
profileHTML += `<tr><td><b>WATER SPEED</b></td> <td>${formatSpeedMnemonic(data['water speed'])}</td></tr>`;
|
||||||
|
}
|
||||||
|
if(data['air speed']){
|
||||||
|
profileHTML += `<tr><td><b>AIR SPEED</b></td> <td>${formatSpeedMnemonic(data['air speed'])}</td></tr>`;
|
||||||
|
}
|
||||||
|
profileHTML += '</table>';
|
||||||
|
|
||||||
|
profile.innerHTML = profileHTML;
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAbilitiesSection(data) {
|
||||||
|
// Abilities section
|
||||||
|
let abilities = document.createElement('div');
|
||||||
|
abilities.className = 'creature_abilities';
|
||||||
|
abilities.innerHTML = `
|
||||||
|
<h3>Abilities</h3>
|
||||||
|
<table>
|
||||||
|
<tr><td><b>MS</b></td> <td>${formatDiceMnemonic(data.ms)}</td></tr>
|
||||||
|
<tr><td><b>IN</b></td> <td>${formatDiceMnemonic(data.in)}</td></tr>
|
||||||
|
<tr><td><b>DX</b></td> <td>${formatDiceMnemonic(data.dx)}</td></tr>
|
||||||
|
<tr><td><b>CH</b></td> <td>${formatDiceMnemonic(data.ch)}</td></tr>
|
||||||
|
<tr><td><b>CN</b></td> <td>${formatDiceMnemonic(data.cn)}</td></tr>
|
||||||
|
<tr><td><b>PS</b></td> <td>${formatDiceMnemonic(data.ps)}</td></tr>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
return abilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAttacksSection(data) {
|
||||||
|
// Attacks section
|
||||||
|
let attacks = document.createElement('div');
|
||||||
|
attacks.className = 'creature_attacks';
|
||||||
|
let attacksHTML = '<h3>Attacks</h3>';
|
||||||
|
let attacksTable = '<table>';
|
||||||
|
for (let attack in data.attacks) {
|
||||||
|
let mnemonic_display;
|
||||||
|
mnemonic_display = formatDiceMnemonic(data.attacks[attack]);
|
||||||
|
if (mnemonic_display === "0d0") {
|
||||||
|
mnemonic_display = 'See Description';
|
||||||
|
}
|
||||||
|
attacksTable += `<tr><td><b>${attack}</b></td><td>${mnemonic_display}</td></tr>`
|
||||||
|
}
|
||||||
|
attacksTable += '</table>';
|
||||||
|
attacks.innerHTML = attacksHTML + attacksTable;
|
||||||
|
return attacks
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMutationsSection(mutarray) {
|
||||||
|
// Mutations section
|
||||||
|
let mutations = document.createElement('div');
|
||||||
|
mutations.className = 'creature_mutations';
|
||||||
|
let mutationsHTML = '<h3>Mutations</h3>';
|
||||||
|
let mutationsList = mutarray.reduce((result, mutation) => result + `<li>${mutation}</li>`, '');
|
||||||
|
mutationsHTML += `<ul>${mutationsList}</ul>`;
|
||||||
|
mutations.innerHTML = mutationsHTML
|
||||||
|
return mutations
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDescriptionSection(data) {
|
||||||
|
// Description section
|
||||||
|
let description = document.createElement('div');
|
||||||
|
description.className = 'creature_description';
|
||||||
|
let descriptionHTML = `
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>${data.description}</p>
|
||||||
|
`;
|
||||||
|
description.innerHTML = descriptionHTML;
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCreatureTable(data) {
|
||||||
|
let container = document.createElement('div');
|
||||||
|
container.className = 'container';
|
||||||
|
|
||||||
|
// Creature ID section
|
||||||
|
let creatureId = document.createElement('div');
|
||||||
|
creatureId.className = 'creature_id';
|
||||||
|
creatureId.innerHTML = `
|
||||||
|
<h2>${creatureName.toUpperCase()}</h2>
|
||||||
|
<img src='img/${creatureName}.jpg' onerror='this.src="img/404.jpg";' style='width: 275px; height: 275px;'>
|
||||||
|
`;
|
||||||
|
|
||||||
|
let profile = createProfileSection(data);
|
||||||
|
let abilities = createAbilitiesSection(data);
|
||||||
|
let attacks = createAttacksSection(data);
|
||||||
|
let mutations = createMutationsSection(data.mutations); // Notice the data difference
|
||||||
|
let description = createDescriptionSection(data);
|
||||||
|
|
||||||
|
container.appendChild(creatureId);
|
||||||
|
container.appendChild(profile);
|
||||||
|
container.appendChild(abilities);
|
||||||
|
container.appendChild(attacks);
|
||||||
|
container.appendChild(mutations);
|
||||||
|
container.appendChild(description);
|
||||||
|
|
||||||
|
resultSection.appendChild(container);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
46
web/catalog.js
Normal file
46
web/catalog.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
document.getElementById('catalog-button').addEventListener('click', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Insert the loading.gif
|
||||||
|
let resultSection = document.getElementById('resultSection');
|
||||||
|
resultSection.innerHTML = '';
|
||||||
|
let imgElement = document.createElement('img');
|
||||||
|
imgElement.src = 'img/checking.gif';
|
||||||
|
imgElement.style.width = '500px';
|
||||||
|
imgElement.style.height = '500px';
|
||||||
|
imgElement.style.display = 'block';
|
||||||
|
imgElement.style.marginLeft = 'auto';
|
||||||
|
imgElement.style.marginRight = 'auto';
|
||||||
|
resultSection.appendChild(imgElement);
|
||||||
|
|
||||||
|
fetch(`${window.BASE_URL}/rules/creature`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
resultSection.innerHTML = ''; // Clear results
|
||||||
|
|
||||||
|
// Generate clickable elements for each creature
|
||||||
|
let table = document.createElement('table');
|
||||||
|
data.forEach(item => {
|
||||||
|
let row = document.createElement('tr');
|
||||||
|
let cell = document.createElement('td');
|
||||||
|
let btn = document.createElement('button');
|
||||||
|
btn.textContent = item;
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
document.getElementById('creature').value = item;
|
||||||
|
document.getElementById('search-button').click();
|
||||||
|
});
|
||||||
|
cell.appendChild(btn);
|
||||||
|
row.appendChild(cell);
|
||||||
|
table.appendChild(row);
|
||||||
|
});
|
||||||
|
resultSection.appendChild(table);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('There has been a problem with your fetch operation:', error);
|
||||||
|
});
|
||||||
|
});
|
@ -25,7 +25,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="gameref-panel" style="text-align: center; margin: 3px">
|
<div id="gameref-panel" style="text-align: center; margin: 3px">
|
||||||
<button id="chargen">Characters</button>
|
<button id="chargen">Characters</button>
|
||||||
<button id="tables">Show Tables</button>
|
<button id="tables">Tables</button>
|
||||||
|
<button id="bestiary">Bestiary</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
@ -35,6 +36,10 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
loadContent("intro.html");
|
loadContent("intro.html");
|
||||||
|
|
||||||
|
document.getElementById("bestiary").addEventListener("click", function () {
|
||||||
|
loadContent("bestiary.html?t=" + new Date().getTime());
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById("abcheck").addEventListener("click", function () {
|
document.getElementById("abcheck").addEventListener("click", function () {
|
||||||
loadContent("ability_check.html?t=" + new Date().getTime());
|
loadContent("ability_check.html?t=" + new Date().getTime());
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user