Initial version

This commit is contained in:
2026-01-30 12:08:22 +02:00
commit db3e46de18
13 changed files with 4248 additions and 0 deletions

180
js/app.js Normal file
View File

@@ -0,0 +1,180 @@
// Main application logic
let debounceTimer = null;
const EXAMPLE_CODE = 'DC2.Dw Gm L W T+ Phvwalt Sk? Cbk--^\\bl+lum B- A+ N? M+++!1 O/ H+++ $ F+o R++ Ac+++! J++ S U- I-- V- Q--- Tc+++[SE] E++';
// DOM elements
const input = document.getElementById('dragon-code-input');
const welcomeMessage = document.getElementById('welcome-message');
const decodedOutput = document.getElementById('decoded-output');
const loadExampleBtn = document.getElementById('load-example');
const clearInputBtn = document.getElementById('clear-input');
// Category sections
const sections = {
species: document.getElementById('species-content'),
physical: document.getElementById('physical-content'),
appearance: document.getElementById('appearance-content'),
abilities: document.getElementById('abilities-content'),
life: document.getElementById('life-content'),
personality: document.getElementById('personality-content'),
other: document.getElementById('other-content'),
errors: document.getElementById('errors-content')
};
// Event listeners
input.addEventListener('input', handleInput);
loadExampleBtn.addEventListener('click', loadExample);
clearInputBtn.addEventListener('click', clearInput);
function handleInput() {
// Debounce input to avoid excessive processing
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const code = input.value.trim();
if (code) {
processCode(code);
} else {
showWelcome();
}
}, 500);
}
function loadExample() {
input.value = EXAMPLE_CODE;
processCode(EXAMPLE_CODE);
}
function clearInput() {
input.value = '';
showWelcome();
}
function showWelcome() {
welcomeMessage.classList.remove('hidden');
decodedOutput.classList.add('hidden');
}
function processCode(code) {
try {
// Parse the Dragon Code
const parsed = parseDragonCode(code);
// Decode to human-readable format
const decoded = decodeDragonCode(parsed);
// Render the results
renderResults(decoded, parsed.errors);
} catch (error) {
console.error('Error processing code:', error);
showError('An unexpected error occurred while processing the code.');
}
}
function renderResults(decoded, errors) {
// Clear all sections
Object.values(sections).forEach(section => {
section.innerHTML = '';
});
// Hide welcome, show output
welcomeMessage.classList.add('hidden');
decodedOutput.classList.remove('hidden');
// Render each category
renderCategory('species', decoded.species);
renderCategory('physical', decoded.physical);
renderCategory('appearance', decoded.appearance);
renderCategory('abilities', decoded.abilities);
renderCategory('life', decoded.life);
renderCategory('personality', decoded.personality);
renderCategory('other', decoded.other);
// Render errors if any
if (errors && errors.length > 0) {
document.getElementById('errors-section').classList.remove('hidden');
errors.forEach(error => {
const errorElement = createTraitItem('Warning', error, '', 'warning');
sections.errors.appendChild(errorElement);
});
} else {
document.getElementById('errors-section').classList.add('hidden');
}
// Hide empty sections
hideEmptySections();
}
function renderCategory(categoryName, items) {
if (!items || items.length === 0) return;
const section = sections[categoryName];
items.forEach(item => {
const element = createTraitItem(item.label, item.value, item.tag, item.type || 'normal');
section.appendChild(element);
});
}
function createTraitItem(label, value, tag, type = 'normal') {
const div = document.createElement('div');
div.className = `trait-item ${type}`;
const labelDiv = document.createElement('div');
labelDiv.className = 'trait-label';
labelDiv.textContent = label;
const valueDiv = document.createElement('div');
valueDiv.className = 'trait-value';
valueDiv.textContent = value;
div.appendChild(labelDiv);
div.appendChild(valueDiv);
if (tag) {
const tagSpan = document.createElement('span');
tagSpan.className = 'trait-tag';
tagSpan.textContent = tag;
div.appendChild(tagSpan);
}
return div;
}
function hideEmptySections() {
const sectionElements = {
species: document.getElementById('species-section'),
physical: document.getElementById('physical-section'),
appearance: document.getElementById('appearance-section'),
abilities: document.getElementById('abilities-section'),
life: document.getElementById('life-section'),
personality: document.getElementById('personality-section'),
other: document.getElementById('other-section')
};
Object.keys(sectionElements).forEach(key => {
const content = sections[key];
const section = sectionElements[key];
if (content.children.length === 0) {
section.classList.add('hidden');
} else {
section.classList.remove('hidden');
}
});
}
function showError(message) {
welcomeMessage.classList.add('hidden');
decodedOutput.classList.remove('hidden');
Object.values(sections).forEach(section => {
section.innerHTML = '';
});
document.getElementById('errors-section').classList.remove('hidden');
const errorElement = createTraitItem('Error', message, '', 'error');
sections.errors.appendChild(errorElement);
hideEmptySections();
}

622
js/decoder.js Normal file
View File

@@ -0,0 +1,622 @@
// Dragon Code V2.6 Decoder - Converts parsed data to human-readable text
function decodeDragonCode(parsed) {
const result = {
species: [],
physical: [],
appearance: [],
abilities: [],
life: [],
personality: [],
other: []
};
// Decode species
if (parsed.species) {
const speciesItems = decodeSpecies(parsed.species);
result.species.push(...speciesItems);
}
// Decode tags
const tags = parsed.tags;
// Physical characteristics
if (tags.gender) {
result.physical.push(decodeGender(tags.gender));
}
if (tags.length) {
result.physical.push(decodeLength(tags.length));
}
if (tags.width) {
result.physical.push(decodeSimpleTag('Width', tags.width, TAG_DESCRIPTIONS.width));
}
if (tags.weight) {
result.physical.push(decodeSimpleTag('Weight', tags.weight, TAG_DESCRIPTIONS.weight));
}
if (tags.age) {
result.physical.push(decodeSimpleTag('Age', tags.age, TAG_DESCRIPTIONS.age));
}
// Appearance
if (tags.appendages) {
result.appearance.push(decodeAppendages(tags.appendages));
}
if (tags.skinType) {
result.appearance.push(decodeSkinType(tags.skinType));
}
if (tags.color) {
result.appearance.push(...decodeColor(tags.color));
}
// Abilities
if (tags.breath) {
result.abilities.push(decodeBreath(tags.breath));
}
if (tags.magic) {
result.abilities.push(decodeSimpleTag('Magic', tags.magic, TAG_DESCRIPTIONS.magic));
}
if (tags.psyPower) {
result.abilities.push(decodeSimpleTag('Psy-Power', tags.psyPower, TAG_DESCRIPTIONS.psyPower));
}
// Life & Relationships
if (tags.nativeLand) {
result.life.push(decodeSimpleTag('Native-Land', tags.nativeLand, TAG_DESCRIPTIONS.nativeLand));
}
if (tags.mating) {
result.life.push(decodeMating(tags.mating));
}
if (tags.offspring) {
result.life.push(decodeOffspring(tags.offspring));
}
if (tags.hoard) {
result.life.push(decodeSimpleTag('Hoard', tags.hoard, TAG_DESCRIPTIONS.hoard));
}
if (tags.money) {
result.life.push(decodeMoney(tags.money));
}
if (tags.diet) {
result.life.push(decodeDiet(tags.diet));
}
// Personality & Behavior
if (tags.reality) {
result.personality.push(decodeSimpleTag('Reality', tags.reality, TAG_DESCRIPTIONS.reality));
}
if (tags.activity) {
result.personality.push(decodeSimpleTag('Activity', tags.activity, TAG_DESCRIPTIONS.activity));
}
if (tags.humor) {
result.personality.push(decodeSimpleTag('Humor', tags.humor, TAG_DESCRIPTIONS.humor));
}
if (tags.social) {
result.personality.push(decodeSimpleTag('Social', tags.social, TAG_DESCRIPTIONS.social));
}
if (tags.irritability) {
result.personality.push(decodeSimpleTag('Irritability', tags.irritability, TAG_DESCRIPTIONS.irritability));
}
if (tags.emotion) {
result.personality.push(decodeSimpleTag('Emotion', tags.emotion, TAG_DESCRIPTIONS.emotion));
}
// Other traits
if (tags.ubiquity) {
result.other.push(decodeSimpleTag('Ubiquity', tags.ubiquity, TAG_DESCRIPTIONS.ubiquity));
}
if (tags.technology) {
result.other.push(decodeTechnology(tags.technology));
}
if (tags.dragonFriend) {
result.other.push(decodeSimpleTag('Dragon-Friend', tags.dragonFriend, TAG_DESCRIPTIONS.dragonFriend));
}
return result;
}
// Decode species with all special cases
function decodeSpecies(species) {
const items = [];
if (species.type === 'custom') {
items.push({
label: 'Species',
value: species.value,
tag: species.raw
});
return items;
}
if (species.type === 'shapechanger') {
const forms = species.forms.map(form => {
if (typeof form === 'object') {
const speciesName = resolveSpeciesCode(form.species);
if (form.modifier) {
return `${speciesName} (${form.modifier})`;
}
return speciesName;
}
return resolveSpeciesCode(form);
});
items.push({
label: 'Species',
value: `I change between ${forms.join(' and ')} form`,
tag: species.raw
});
return items;
}
if (species.type === 'cross') {
const speciesNames = species.species.map(s => resolveSpeciesCode(s) || s);
items.push({
label: 'Species',
value: `A cross between ${speciesNames.join(' and ')}`,
tag: species.raw
});
return items;
}
if (species.type === 'trapped') {
const trueName = resolveSpeciesCode(species.trueForm);
const trappedName = resolveSpeciesCode(species.trappedIn);
items.push({
label: 'Species',
value: `I am a ${trueName}, trapped in ${trappedName} form`,
tag: species.raw,
type: 'warning'
});
return items;
}
if (species.type === 'shaped') {
const trueName = resolveSpeciesCode(species.trueForm);
const shapedName = resolveSpeciesCode(species.shapedAs);
items.push({
label: 'Species',
value: `I am a ${trueName}, currently shaped as ${shapedName}`,
tag: species.raw
});
return items;
}
// Simple species
const speciesName = resolveSpeciesCode(species.value);
if (speciesName) {
items.push({
label: 'Species',
value: speciesName,
tag: species.raw
});
}
return items;
}
// Decode gender
function decodeGender(gender) {
if (gender.type === 'custom') {
return {
label: 'Gender',
value: gender.value,
tag: gender.raw
};
}
const genderValue = gender.value.toLowerCase();
const description = TAG_DESCRIPTIONS.gender[genderValue] || gender.value;
let modifierText = '';
if (gender.modifiers.question) {
modifierText = ' (not telling)';
} else if (gender.modifiers.tilde) {
modifierText = ' (variable)';
}
return {
label: 'Gender',
value: description + modifierText,
tag: gender.raw
};
}
// Decode length
function decodeLength(length) {
let value = '';
if (length.value && length.unit) {
// Quantitative length (L10m, L10m4t, etc.)
const unitName = TAG_DESCRIPTIONS.length.units[length.unit] || length.unit;
value = `${length.value} ${unitName}`;
if (length.dimensions && length.dimensions.length > 0) {
const modParts = length.dimensions.map(m => {
const dimName = TAG_DESCRIPTIONS.length.dimensions[m];
// Extract number before modifier if present
const numMatch = length.raw.match(new RegExp(`(\\d+)${m}`));
if (numMatch) {
return `${numMatch[1]}${unitName} ${dimName}`;
}
return dimName;
});
value += ` (with ${modParts.join(', ')})`;
}
} else if (length.modifiers) {
// Qualitative length (L+++, L-, L, etc.)
const modifierKey = getModifierString(length.modifiers);
value = TAG_DESCRIPTIONS.length[modifierKey] || 'Normal (Draco-sized)';
} else {
// Fallback
value = 'Normal (Draco-sized)';
}
return {
label: 'Length',
value: value,
tag: length.raw
};
}
// Decode simple tags with +/- modifiers
function decodeSimpleTag(label, tag, descriptions) {
const modifierKey = getModifierString(tag.modifiers);
let value = descriptions[modifierKey] || descriptions[''] || 'Normal';
const type = tag.modifiers.question ? 'warning' : 'normal';
return {
label: label,
value: value,
tag: tag.raw,
type: type
};
}
// Decode appendages
function decodeAppendages(appendages) {
const parts = [];
appendages.parts.forEach(part => {
const typeName = TAG_DESCRIPTIONS.appendages.types[part.type] || part.type;
let description = typeName;
// Handle count
if (part.count) {
description = `${part.count} ${typeName}`;
} else if (part.modifiers.plus > 0) {
description = `multiple ${typeName}`;
}
// Handle modifiers
if (part.modifiers.caret) {
description = `retractable ${description}`;
}
if (part.modifiers.minus > 0) {
description = `vestigial ${description}`;
}
if (part.modifiers.exclaim) {
description = `unusual ${description}`;
}
// Add pair for common paired appendages
if (['a', 'l', 'w', 'v'].includes(part.type) && !part.count && part.modifiers.plus === 0) {
description = `pair of ${typeName}`;
}
parts.push(description);
});
return {
label: 'Appendages',
value: parts.join(', '),
tag: appendages.raw
};
}
// Decode skin type
function decodeSkinType(skinType) {
let value = '';
// Main skin type
if (skinType.mainType) {
const mainTypeName = TAG_DESCRIPTIONS.skinType[skinType.mainType] || skinType.mainType;
value = mainTypeName;
} else {
value = 'Unknown';
}
// Body part modifiers (e.g., ",ak" = arms with skin, ",bm" = belly with metal)
if (skinType.bodyPartTypes.length > 0) {
const partDescriptions = skinType.bodyPartTypes.map(item => {
const partName = TAG_DESCRIPTIONS.skinType.bodyParts[item.part] || item.part;
const typeName = TAG_DESCRIPTIONS.skinType[item.type] || item.type;
return `${typeName} on ${partName}`;
});
value += `, with ${partDescriptions.join(', ')}`;
}
const hasQuestion = skinType.modifiers.question;
return {
label: 'Skin Type',
value: value,
tag: skinType.raw,
type: hasQuestion ? 'warning' : 'normal'
};
}
// Decode color (complex)
function decodeColor(color) {
const items = [];
// Build a comprehensive description
let fullDescription = '';
const colorDescriptions = [];
color.colors.forEach((colorPart, index) => {
let description = '';
// Base color
const baseName = TAG_DESCRIPTIONS.color.bases[colorPart.base] || colorPart.base;
// Intensity modifiers
const intensityParts = [];
let darknessLevel = 0;
let brightnessLevel = 0;
colorPart.intensity.forEach(mod => {
if (mod === '-') darknessLevel++;
else if (mod === '+') brightnessLevel++;
else {
const modName = TAG_DESCRIPTIONS.color.intensity[mod];
if (modName) {
intensityParts.push(modName);
}
}
});
// Add darkness/brightness descriptions
if (darknessLevel === 1) intensityParts.unshift('dark');
else if (darknessLevel === 2) intensityParts.unshift('very dark');
else if (darknessLevel >= 3) intensityParts.unshift('extremely dark');
if (brightnessLevel === 1) intensityParts.unshift('light');
else if (brightnessLevel === 2) intensityParts.unshift('very light');
else if (brightnessLevel >= 3) intensityParts.unshift('extremely light');
// Build color description
if (intensityParts.length > 0) {
description = `${intensityParts.join(', ')} ${baseName}`;
} else {
description = baseName;
}
// Pattern modifiers
const patternParts = [];
colorPart.patterns.forEach(pat => {
const patternName = TAG_DESCRIPTIONS.color.patterns[pat];
if (patternName) {
patternParts.push(patternName);
}
});
if (patternParts.length > 0) {
description += ` (${patternParts.join(', ')})`;
}
// Body parts
if (colorPart.bodyParts.length > 0) {
const parts = colorPart.bodyParts.map(p =>
TAG_DESCRIPTIONS.skinType.bodyParts[p] || p
);
description += ` on ${parts.join(', ')}`;
}
colorDescriptions.push(description);
});
// Combine all colors
if (colorDescriptions.length === 1) {
fullDescription = colorDescriptions[0];
} else if (colorDescriptions.length === 2) {
fullDescription = `${colorDescriptions[0]}, with ${colorDescriptions[1]}`;
} else {
const last = colorDescriptions.pop();
fullDescription = `${colorDescriptions.join(', ')}, with ${last}`;
}
items.push({
label: 'Color',
value: fullDescription,
tag: color.raw
});
return items;
}
// Decode breath weapon
function decodeBreath(breath) {
if (breath.type === 'custom') {
return {
label: 'Breath Weapon',
value: breath.value,
tag: breath.raw
};
}
if (breath.simple) {
const modifierKey = getModifierString(breath.simple);
const value = TAG_DESCRIPTIONS.breath.simple[modifierKey] || 'Normal breath weapon';
return {
label: 'Breath Weapon',
value: value,
tag: breath.raw
};
}
const types = breath.types.map(t => {
const cleaned = t.replace(/[+\-!?~]/g, '');
return TAG_DESCRIPTIONS.breath.types[cleaned] || cleaned;
});
let value = types.join(' / ');
if (breath.modifiers.length > 0) {
value += ` (${breath.modifiers.join(', ')})`;
}
return {
label: 'Breath Weapon',
value: value,
tag: breath.raw
};
}
// Decode mating
function decodeMating(mating) {
const modifierKey = getModifierString(mating.modifiers);
let value = TAG_DESCRIPTIONS.mating[modifierKey] || 'Not specified';
if (mating.count) {
value += `, ${mating.count} mate${mating.count > 1 ? 's' : ''}`;
}
if (mating.separations) {
value += `, ${mating.separations} separation${mating.separations > 1 ? 's' : ''}`;
}
return {
label: 'Mating',
value: value,
tag: mating.raw
};
}
// Decode offspring
function decodeOffspring(offspring) {
const modifierKey = getModifierString(offspring.modifiers);
let value = TAG_DESCRIPTIONS.offspring[modifierKey] || TAG_DESCRIPTIONS.offspring[''];
if (offspring.count) {
value = `${offspring.count} offspring`;
}
if (offspring.adopted) {
value += ' (adopted)';
}
return {
label: 'Offspring',
value: value,
tag: offspring.raw
};
}
// Decode money
function decodeMoney(money) {
const dollarSigns = money.raw.match(/\$/g);
const count = dollarSigns ? dollarSigns.length : 0;
let key = '';
if (count === 3) key = '$$$';
else if (count === 2) key = '$$';
else if (count === 1) key = '$';
const minusSigns = money.raw.match(/-/g);
const minusCount = minusSigns ? minusSigns.length : 0;
if (minusCount === 3) key = '---';
else if (minusCount === 2) key = '--';
else if (minusCount === 1) key = '-';
const value = TAG_DESCRIPTIONS.money[key] || TAG_DESCRIPTIONS.money[''];
return {
label: 'Money',
value: value,
tag: money.raw
};
}
// Decode diet
function decodeDiet(diet) {
const typeNames = diet.types.map(t =>
TAG_DESCRIPTIONS.diet.types[t] || t
);
let value = typeNames.join(', ') || 'omnivore';
const modifierKey = getModifierString(diet.modifiers);
const modifierDesc = TAG_DESCRIPTIONS.diet.modifiers[modifierKey];
if (modifierDesc) {
value = `${modifierDesc} ${value}`;
}
return {
label: 'Diet',
value: value,
tag: diet.raw
};
}
// Decode technology
function decodeTechnology(tech) {
const modifierKey = getModifierString(tech.modifiers);
let value = TAG_DESCRIPTIONS.technology[modifierKey] || TAG_DESCRIPTIONS.technology[''];
if (tech.specialist) {
value += `, specialist in ${tech.specialist}`;
}
return {
label: 'Technology',
value: value,
tag: tech.raw
};
}
// Helper: Convert modifiers object to string key
function getModifierString(modifiers) {
let key = '';
// Handle exclamation special cases
if (modifiers.exclaim) {
if (modifiers.plus === 3) return '+++!';
if (modifiers.minus === 3) return '---!';
// Exclamation alone (e.g., S!)
if (modifiers.plus === 0 && modifiers.minus === 0) return '!';
}
// Build key from +/- counts
if (modifiers.plus > 0) {
key = '+'.repeat(modifiers.plus);
} else if (modifiers.minus > 0) {
key = '-'.repeat(modifiers.minus);
}
// Add special modifiers (but not if they're already the key)
if (modifiers.question && key !== '!') {
key += '?';
}
if (modifiers.tilde && key === '') {
key = '~';
}
// Special case for slash
if (modifiers.slash) {
key = '/';
}
return key;
}

738
js/parser.js Normal file
View File

@@ -0,0 +1,738 @@
// Dragon Code V2.6 Parser
function parseDragonCode(input) {
const result = {
species: null,
tags: {},
errors: []
};
try {
// Tokenize the input
const tokens = tokenize(input);
// Process each token
tokens.forEach((token, index) => {
try {
const tagType = identifyTagType(token, index);
if (tagType === 'species') {
result.species = parseSpecies(token);
} else if (tagType) {
const parsed = parseTag(token, tagType);
if (parsed) {
result.tags[tagType] = parsed;
}
}
} catch (error) {
result.errors.push(`Error parsing token "${token}": ${error.message}`);
}
});
} catch (error) {
result.errors.push(`Error tokenizing input: ${error.message}`);
}
return result;
}
// Tokenizer: splits Dragon Code into individual tokens
function tokenize(input) {
// Remove DC2. prefix if present
let code = input.trim();
if (code.startsWith('DC2.')) {
code = code.substring(4);
} else if (code.startsWith('DC.')) {
code = code.substring(3);
}
const tokens = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < code.length; i++) {
const char = code[i];
if (char === '"') {
inQuotes = !inQuotes;
current += char;
} else if (char === ' ' && !inQuotes) {
if (current.trim()) {
tokens.push(current.trim());
}
current = '';
} else {
current += char;
}
}
if (current.trim()) {
tokens.push(current.trim());
}
return tokens;
}
// Identifies the type of tag
function identifyTagType(token, index) {
// IMPORTANT: Check two-letter tags before single-letter tags to avoid conflicts
// (e.g., Tc before T, Ac before A, Sk before S, Df before D)
// Two-letter tags first
if (token.startsWith('Tc')) return 'technology';
if (token.startsWith('Ac')) return 'activity';
if (token.startsWith('Sk')) return 'skinType';
if (token.startsWith('Df')) return 'dragonFriend';
if (token.startsWith('Ph') || token.startsWith('Pa') || token.startsWith('Pl') ||
token.startsWith('Pw') || token.startsWith('Pt') || token.startsWith('Pv') ||
token.startsWith('Pk') || token.startsWith('Pf') || token.startsWith('Pp')) return 'appendages';
// Species tag is typically first, or contains species indicators
if (index === 0 || token.match(/^[~^]?[A-Z][a-z]*[\[+{^~]?/) || token.includes('/') || token.includes('[') || token.includes('^')) {
// At index 0, check if it's a single-letter or multi-letter species code first
// Species codes can be: D, Dw, H, A, Ag, etc.
if (index === 0) {
// At index 0, check in this order:
// 1. Two-letter tag prefixes (Tc, Ac, Sk, Df, Ph, etc.)
// 2. Single-letter tags (G, L, W, T, etc.)
// 3. Species indicators (~, ^, [, +) - clear species markers
// 4. Multi-letter species codes (Dw, De, etc.)
// 5. Fallback to species for remaining patterns
// Two-letter tags (must check BEFORE single-letter checks)
if (token.startsWith('Tc')) return 'technology';
if (token.startsWith('Ac')) return 'activity';
if (token.startsWith('Sk')) return 'skinType';
if (token.startsWith('Df')) return 'dragonFriend';
if (token.startsWith('Ph') || token.startsWith('Pa') || token.startsWith('Pl') ||
token.startsWith('Pw') || token.startsWith('Pt') || token.startsWith('Pv') ||
token.startsWith('Pk') || token.startsWith('Pf') || token.startsWith('Pp')) return 'appendages';
// Special case: bare "H" at index 0 is ambiguous
// "H" alone = Human (species), "H+++" = Hoard (tag)
// Check if it's exactly "H" with no modifiers
if (token === 'H') {
return 'species'; // Treat bare "H" as Human species
}
// Single-letter tags (with or without modifiers)
if (token.startsWith('G')) return 'gender';
if (token.startsWith('L')) return 'length';
if (token.startsWith('W')) return 'width';
if (token.startsWith('T')) return 'weight';
if (token.startsWith('C') && token[1] && token[1].match(/[a-z]/)) return 'color';
if (token.startsWith('B')) return 'breath';
if (token.startsWith('A')) return 'age';
if (token.startsWith('N')) return 'nativeLand';
if (token.startsWith('M')) return 'mating';
if (token.startsWith('O')) return 'offspring';
if (token.startsWith('H')) return 'hoard';
if (token.startsWith('$')) return 'money';
if (token.startsWith('F')) return 'diet';
if (token.startsWith('R')) return 'reality';
if (token.startsWith('J')) return 'humor';
if (token.startsWith('S')) return 'social';
if (token.startsWith('U')) return 'ubiquity';
if (token.startsWith('I')) return 'irritability';
if (token.startsWith('V')) return 'magic';
if (token.startsWith('Q')) return 'psyPower';
if (token.startsWith('E')) return 'emotion';
// Check for species-specific indicators (after tag checks)
// ~ at start (shapeshifter), or [ ] (trapped), or + (cross)
if (token.startsWith('~') || token.includes('[') || token.includes('+')) {
return 'species';
}
// ^ for shaped (H^Dw), but not if it's a tag with ^ modifier
if (token.includes('^') && token.match(/[A-Z][a-z]*\^[A-Z]/)) {
return 'species';
}
// / for multiple species in shapeshifter (must have species codes on both sides)
// E.g., "Dw/H" or within "~Dw/H", but NOT "O/" or "M/"
if (token.includes('/') && token.match(/[A-Z][a-z]*\/[A-Z]/)) {
return 'species';
}
// Multi-letter species codes (Dw, De, Ag, etc.)
if (token.match(/^[A-Z][a-z]+/)) {
return 'species';
}
}
// Check if it's actually a non-species tag
if (token.startsWith('G')) return 'gender';
if (token.startsWith('L')) return 'length';
if (token.startsWith('W')) return 'width';
if (token.startsWith('T') && !token.startsWith('Tc')) return 'weight';
if (token.startsWith('C') && (token.length > 1 && token[1].match(/[a-z]/))) return 'color';
if (token.startsWith('B')) return 'breath';
if (token.startsWith('A') && !token.startsWith('Ac')) return 'age';
if (token.startsWith('N')) return 'nativeLand';
if (token.startsWith('M')) return 'mating';
if (token.startsWith('O')) return 'offspring';
if (token.startsWith('H')) return 'hoard';
if (token.startsWith('$')) return 'money';
if (token.startsWith('F')) return 'diet';
if (token.startsWith('R')) return 'reality';
if (token.startsWith('J')) return 'humor';
if (token.startsWith('S') && !token.startsWith('Sk')) return 'social';
if (token.startsWith('U')) return 'ubiquity';
if (token.startsWith('I')) return 'irritability';
if (token.startsWith('V')) return 'magic';
if (token.startsWith('Q')) return 'psyPower';
if (token.startsWith('E')) return 'emotion';
// If none of the above, likely species
if (index === 0) return 'species';
}
// Single-letter tags (checked after two-letter tags)
// Gender
if (token.startsWith('G')) return 'gender';
// Length (with or without numbers)
if (token.startsWith('L')) return 'length';
// Width (with any modifiers)
if (token.startsWith('W')) return 'width';
// Weight (Tonnage) - already checked it's not Technology
if (token.startsWith('T')) return 'weight';
// Color
if (token.startsWith('C')) return 'color';
// Breath Weapon
if (token.startsWith('B')) return 'breath';
// Age - already checked it's not Activity
if (token.startsWith('A')) return 'age';
// Native Land
if (token.startsWith('N')) return 'nativeLand';
// Mating
if (token.startsWith('M')) return 'mating';
// Offspring
if (token.startsWith('O')) return 'offspring';
// Hoard
if (token.startsWith('H')) return 'hoard';
// Money
if (token.startsWith('$')) return 'money';
// Diet
if (token.startsWith('F')) return 'diet';
// Reality
if (token.startsWith('R')) return 'reality';
// Humor
if (token.startsWith('J')) return 'humor';
// Social - already checked it's not Skin Type
if (token.startsWith('S')) return 'social';
// Ubiquity
if (token.startsWith('U')) return 'ubiquity';
// Irritability
if (token.startsWith('I')) return 'irritability';
// Magic
if (token.startsWith('V')) return 'magic';
// Psy Power
if (token.startsWith('Q')) return 'psyPower';
// Emotion
if (token.startsWith('E')) return 'emotion';
return null;
}
// Parse species (handles ~, ^, [], +, {})
function parseSpecies(token) {
const result = {
type: 'simple',
value: null,
modifiers: [],
raw: token
};
// Handle quoted custom species
if (token.startsWith('"') && token.endsWith('"')) {
result.type = 'custom';
result.value = token.substring(1, token.length - 1);
return result;
}
// Shapechanger: ~species1/species2
if (token.includes('~') && token.includes('/')) {
result.type = 'shapechanger';
const parts = token.split('/');
result.forms = parts.map(p => p.replace('~', '').trim());
// Check for {} modifiers
result.forms = result.forms.map(form => {
if (form.includes('{')) {
const match = form.match(/([^{]+)\{([^}]+)\}/);
if (match) {
return { species: match[1], modifier: match[2] };
}
}
return { species: form, modifier: null };
});
return result;
}
// Cross-breed: species1+species2
if (token.includes('+') && !token.match(/^[+\-]/)) {
result.type = 'cross';
result.species = token.split('+').map(s => s.trim());
return result;
}
// Trapped form: species1[species2]
if (token.includes('[') && token.includes(']')) {
result.type = 'trapped';
const match = token.match(/([^\[]+)\[([^\]]+)\]/);
if (match) {
result.trueForm = match[1];
result.trappedIn = match[2];
}
return result;
}
// Shaped: species1^species2
if (token.includes('^')) {
result.type = 'shaped';
const parts = token.split('^');
result.trueForm = parts[0];
result.shapedAs = parts[1];
return result;
}
// Simple species code
result.value = token;
return result;
}
// Parse individual tag based on type
function parseTag(token, type) {
const parsers = {
gender: parseGender,
length: parseLength,
width: parseSimpleModifier,
weight: parseSimpleModifier,
appendages: parseAppendages,
skinType: parseSkinType,
color: parseColor,
breath: parseBreath,
age: parseSimpleModifier,
nativeLand: parseSimpleModifier,
mating: parseMating,
offspring: parseOffspring,
hoard: parseSimpleModifier,
money: parseSimpleModifier,
diet: parseDiet,
reality: parseSimpleModifier,
activity: parseSimpleModifier,
humor: parseSimpleModifier,
social: parseSimpleModifier,
ubiquity: parseSimpleModifier,
irritability: parseSimpleModifier,
magic: parseSimpleModifier,
psyPower: parseSimpleModifier,
technology: parseTechnology,
emotion: parseSimpleModifier,
dragonFriend: parseSimpleModifier
};
const parser = parsers[type];
if (parser) {
return parser(token);
}
return { raw: token };
}
// Parse gender
function parseGender(token) {
// Handle quoted custom gender
if (token.includes('"')) {
const match = token.match(/"([^"]+)"/);
return {
type: 'custom',
value: match ? match[1] : token,
raw: token
};
}
return {
value: token.substring(1), // Remove 'G' prefix
modifiers: extractModifiers(token.substring(1)),
raw: token
};
}
// Parse length (quantitative with units)
function parseLength(token) {
const result = {
raw: token,
value: null,
unit: null,
dimensions: [], // For quantitative modifiers like 4t (4m tail)
modifiers: null // For qualitative modifiers like +++, -, etc.
};
// Check if it has a numeric value (quantitative)
const match = token.match(/L(\d+)([a-z]+)?/i);
if (match) {
result.value = parseInt(match[1]);
result.unit = match[2] || null;
// Extract dimension modifiers (a, l, n, t, w, h)
const dimMatch = token.match(/\d+[a-z]*(\d*[alntwh])/);
if (dimMatch) {
result.dimensions = dimMatch[1].split('').map(m => {
if (m.match(/\d/)) return null;
return m;
}).filter(Boolean);
}
} else {
// Qualitative length (L+++, L-, L, etc.)
// Remove 'L' prefix and extract modifiers
const modifierPart = token.substring(1);
result.modifiers = extractModifiers(modifierPart);
}
return result;
}
// Parse simple modifier tags (+, -, !, ?, ~, etc.)
function parseSimpleModifier(token) {
// Remove tag prefix to get modifiers
let modifierPart = token.replace(/^[A-Z][a-z]?/, '');
return {
modifiers: extractModifiers(modifierPart),
raw: token
};
}
// Extract modifiers from a string
function extractModifiers(str) {
const modifiers = {
plus: 0,
minus: 0,
exclaim: false,
question: false,
tilde: false,
caret: false,
slash: false,
real: false,
virtual: false
};
for (let char of str) {
if (char === '+') modifiers.plus++;
else if (char === '-') modifiers.minus++;
else if (char === '!') modifiers.exclaim = true;
else if (char === '?') modifiers.question = true;
else if (char === '~') modifiers.tilde = true;
else if (char === '^') modifiers.caret = true;
else if (char === '/') modifiers.slash = true;
}
// Check for r/v (real/virtual)
if (str.includes('r')) modifiers.real = true;
if (str.includes('v')) modifiers.virtual = true;
return modifiers;
}
// Parse appendages (complex sequence)
function parseAppendages(token) {
const result = {
parts: [],
raw: token
};
// Parse the appendage code
let current = '';
for (let i = 0; i < token.length; i++) {
const char = token[i];
if (char === 'P') continue; // Skip prefix
if (char.match(/[halvwtkfp']/)) {
if (current) {
result.parts.push(parseAppendagePart(current));
}
current = char;
} else {
current += char;
}
}
if (current) {
result.parts.push(parseAppendagePart(current));
}
return result;
}
function parseAppendagePart(part) {
const type = part[0];
const modifiers = part.substring(1);
return {
type: type,
modifiers: extractModifiers(modifiers),
count: modifiers.match(/\d+/) ? parseInt(modifiers.match(/\d+/)[0]) : null
};
}
// Parse skin type
function parseSkinType(token) {
const result = {
mainType: null,
bodyPartTypes: [], // Array of {part, type} objects
modifiers: {},
raw: token
};
// Remove Sk prefix
let content = token.substring(2);
// Check for ? modifier
if (content.includes('?')) {
result.modifiers.question = true;
content = content.replace('?', '');
}
// Split by comma to separate main type from body part modifiers
const parts = content.split(',');
// First part is the main skin type (single letter)
if (parts[0]) {
result.mainType = parts[0][0]; // First character is the main type
}
// Remaining parts are body part modifiers (format: <part><type>)
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
if (part.length >= 2) {
// First character is body part, rest is skin type
const bodyPart = part[0];
const skinType = part.substring(1);
result.bodyPartTypes.push({
part: bodyPart,
type: skinType
});
}
}
return result;
}
// Parse color (MOST COMPLEX)
function parseColor(token) {
const result = {
colors: [],
raw: token
};
// Remove C prefix
let content = token.substring(1);
// Split by / for multiple colors, and handle \ as additional color/effect
const colorParts = content.split(/[\/\\]/);
colorParts.forEach(colorPart => {
if (colorPart) {
result.colors.push(parseColorPart(colorPart));
}
});
return result;
}
function parseColorPart(part) {
const color = {
base: null,
intensity: [],
patterns: [],
bodyParts: [],
modifiers: []
};
// Extract base color (2-3 letter code)
const baseMatch = part.match(/^([a-z]{2,3})/);
if (baseMatch) {
color.base = baseMatch[1];
part = part.substring(baseMatch[1].length);
}
// Check for 'lum' (luminescent) special modifier
if (part.includes('lum')) {
color.intensity.push('lum');
part = part.replace('lum', '');
}
// Extract intensity modifiers (+, -, ^, _, ', %, !)
for (let char of part) {
if (['+', '-', '^', '_', "'", '%', '!'].includes(char)) {
color.intensity.push(char);
} else if (['|', '=', ':', '*', '@', '#', '&', '>'].includes(char)) {
color.patterns.push(char);
} else if (char === ',') {
// Body part modifier follows
break;
}
}
// Extract body part modifiers (,a, ,b, etc.)
const bodyPartMatches = part.match(/,([a-z])/g);
if (bodyPartMatches) {
color.bodyParts = bodyPartMatches.map(m => m.substring(1));
}
// Check for special patterns like &1, &2
const specialPattern = part.match(/&(\d+)/);
if (specialPattern) {
color.patterns.push('&' + specialPattern[1]);
}
return color;
}
// Parse breath weapon
function parseBreath(token) {
// Handle quoted custom breath
if (token.includes('"')) {
const match = token.match(/"([^"]+)"/);
return {
type: 'custom',
value: match ? match[1] : token,
raw: token
};
}
const result = {
types: [],
modifiers: [],
raw: token
};
// Remove B prefix
let content = token.substring(1);
// Check for modifiers
if (content.includes('|')) result.modifiers.push('beam');
if (content.includes('#')) result.modifiers.push('cloud');
// Handle simple +/- modifiers (no specific type)
if (content.match(/^[+\-!?~]+$/)) {
result.simple = extractModifiers(content);
return result;
}
// Extract breath types (fl, ac, ic, etc.)
if (content.length > 0) {
const types = content.split('/').map(t => t.replace(/[|#]/g, '').trim()).filter(Boolean);
result.types = types;
} else {
// "B" with no content - normal breath
result.simple = extractModifiers('');
}
return result;
}
// Parse mating
function parseMating(token) {
const result = {
modifiers: extractModifiers(token.substring(1)),
count: null,
separations: null,
raw: token
};
// Extract count (number at end)
const countMatch = token.match(/(\d+)$/);
if (countMatch) {
result.count = parseInt(countMatch[1]);
}
// Extract separations (^number)
const sepMatch = token.match(/\^(\d+)/);
if (sepMatch) {
result.separations = parseInt(sepMatch[1]);
}
return result;
}
// Parse offspring
function parseOffspring(token) {
const result = {
modifiers: extractModifiers(token.substring(1)),
count: null,
adopted: false,
raw: token
};
// Check for adopted (a modifier)
if (token.includes('a')) {
result.adopted = true;
}
// Extract count
const countMatch = token.match(/(\d+)/);
if (countMatch) {
result.count = parseInt(countMatch[1]);
}
return result;
}
// Parse diet
function parseDiet(token) {
const result = {
modifiers: extractModifiers(token.substring(1)),
types: [],
raw: token
};
// Extract diet type letters (c, h, o, v)
const content = token.substring(1);
for (let char of content) {
if (['c', 'h', 'o', 'v'].includes(char)) {
result.types.push(char);
}
}
return result;
}
// Parse technology
function parseTechnology(token) {
const result = {
modifiers: extractModifiers(token.substring(2)),
specialist: null,
raw: token
};
// Extract specialist field in []
const specialistMatch = token.match(/\[([^\]]+)\]/);
if (specialistMatch) {
result.specialist = specialistMatch[1];
}
return result;
}

249
js/species-data.js Normal file
View File

@@ -0,0 +1,249 @@
// Hierarchical species tree based on Dragon Code V2.6 specification
const SPECIES_DATA = {
// Dragons (D)
D: {
name: "Dragon",
subtypes: {
w: "Western Dragon",
e: "Eastern Dragon",
p: "Pernese Dragon",
f: "Faerie Dragon",
r: "Reformed Dragon",
c: "Chromatic Dragon",
m: "Metallic Dragon",
g: "Gem Dragon"
}
},
// Humanoid (H)
H: {
name: "Human",
subtypes: {}
},
// Avian (A)
A: {
name: "Avian",
subtypes: {
c: "Cockatrice",
g: "Griffin",
h: "Hippogriff",
p: "Phoenix",
r: "Roc"
}
},
// Bovine (B)
B: {
name: "Bovine",
subtypes: {
c: "Cattle",
m: "Minotaur"
}
},
// Canine (C)
C: {
name: "Canine",
subtypes: {
c: "Coyote",
d: "Dog",
f: "Fox",
j: "Jackal",
w: "Wolf"
}
},
// Serpent (S)
S: {
name: "Serpent",
subtypes: {
b: "Basilisk",
h: "Hydra",
n: "Naga",
s: "Sea Serpent"
}
},
// Equine (E)
E: {
name: "Equine",
subtypes: {
c: "Centaur",
h: "Horse",
p: "Pegasus",
u: "Unicorn",
z: "Zebra"
}
},
// Feline (F)
F: {
name: "Feline",
subtypes: {
c: "Cat",
h: "Cheetah",
j: "Jaguar",
l: "Lion",
o: "Ocelot",
p: "Panther",
t: "Tiger",
x: "Lynx"
}
},
// Insectoid (I)
I: {
name: "Insectoid",
subtypes: {}
},
// Leporine (L)
L: {
name: "Leporine",
subtypes: {
h: "Hare",
r: "Rabbit"
}
},
// Mammal (M)
M: {
name: "Mammal",
subtypes: {
a: {
name: "Aquatic",
subtypes: {
d: "Dolphin",
o: "Orca",
p: "Porpoise",
s: "Seal",
w: "Walrus",
h: "Whale"
}
},
b: "Badger",
e: "Elephant",
f: {
name: "Feral",
subtypes: {
p: {
name: "Pantherine",
subtypes: {
c: "Cougar",
j: "Jaguar",
l: "Leopard",
n: "Panther",
s: "Snow Leopard",
t: "Tiger"
}
}
}
},
k: "Skunk",
m: "Mink",
o: "Otter",
r: "Rodent",
t: "Taur",
w: "Weasel"
}
},
// Otter (O)
O: {
name: "Otter",
subtypes: {}
},
// Mythological (Y)
Y: {
name: "Mythological",
subtypes: {
c: "Chimera",
g: "Gargoyle",
m: "Manticore",
s: "Sphinx"
}
},
// Porcine (P)
P: {
name: "Porcine",
subtypes: {
b: "Boar",
p: "Pig"
}
},
// Reptile (R)
R: {
name: "Reptile",
subtypes: {
a: "Alligator",
c: "Crocodile",
d: "Dinosaur",
l: "Lizard",
t: "Turtle"
}
},
// Aquatic (Q)
Q: {
name: "Aquatic",
subtypes: {
d: "Dolphin",
f: "Fish",
m: "Mermaid/Merman",
o: "Orca",
s: "Shark"
}
},
// Ursine (U)
U: {
name: "Ursine",
subtypes: {
b: "Bear",
g: "Grizzly",
p: "Panda",
l: "Polar Bear"
}
}
};
// Function to resolve a species code to its full name
function resolveSpeciesCode(code) {
if (!code || code.length === 0) return null;
const parts = code.split('');
let current = SPECIES_DATA;
let path = [];
for (let i = 0; i < parts.length; i++) {
const char = parts[i];
if (!current[char]) {
// If we can't find the next level, return what we have
break;
}
current = current[char];
if (typeof current === 'string') {
// We've reached a leaf node (species name)
return current;
} else if (current.name) {
// We're at a node with a name, but there might be subtypes
path.push(current.name);
if (current.subtypes) {
current = current.subtypes;
} else {
// No more subtypes, return the name
return current.name;
}
}
}
// Return the accumulated path or the name if we have it
return path.length > 0 ? path[path.length - 1] : null;
}

408
js/tags-data.js Normal file
View File

@@ -0,0 +1,408 @@
// Tag descriptions and meanings for Dragon Code V2.6
const TAG_DESCRIPTIONS = {
gender: {
m: "Male",
f: "Female",
h: "Hermaphrodite",
n: "Neuter",
'?': "Not telling",
'~': "Variable"
},
length: {
'+++!': "Celestial",
'+++': "Mistaken for mountain ranges",
'++': "Lair fits a regiment",
'+': "Bigger than most dragons",
'': "Normal (Draco-sized)",
'-': "Smaller than most dragons",
'--': "Fits on your shoulder",
'---': "Fits in your pocket",
'---!': "Microscopic",
units: {
'i': 'inches',
'f': 'feet',
'y': 'yards',
'c': 'centimeters',
'm': 'meters',
'k': 'kilometers',
'mi': 'miles'
},
dimensions: {
'a': 'arm length',
'l': 'leg length',
'n': 'neck length',
't': 'tail length',
'w': 'wingspan',
'h': 'height'
}
},
width: {
'+++!': "I am Athelind! My belly is now several galaxies wide ... while I'm only a few hundred feet long!",
'+++': "Planets have been known to crack in half with my arrival!",
'++': "My digestion of food has been known to cause earthquakes",
'+': "I move by rolling. Flying has always been an effort for me",
'': "What can I say ... I'm normal, except for a few feasts here or there",
'~': "Variable - 'My girth depends on what I've just eaten!'",
'-': "I'm slightly on the slim side!",
'--': "Ever heard of serpentine?",
'---': "Whoah! Whaddaya mean I look like a long string with wings?",
'---!': "I'm one-dimensional - all length and no width or depth. Just one long super-string!"
},
weight: {
'+++': "Obese",
'++': "Fat",
'+': "Over-weight",
'': "Normal weight",
'-': "Under-weight",
'--': "Skeleton with scales",
'---': "Anorexic"
},
appendages: {
types: {
'h': 'head',
'a': 'arms',
'l': 'legs',
'v': 'horns',
'w': 'wings',
't': 'tail',
'k': 'spikes/ridge',
'f': 'fins',
'p': 'pouch',
"'": 'feathers'
},
modifiers: {
'^': 'retractable',
'+': 'extra/multiple',
'-': 'missing/vestigial',
'!': 'unusual',
'~': 'variable'
}
},
skinType: {
's': 'scales',
'h': 'hide',
'u': 'fur',
'k': 'skin',
'l': 'leather',
'm': 'metal',
'r': 'rock',
'f': 'feathers',
'e': 'exoskeleton',
'b': 'bark',
'c': 'cellulose',
'x': 'crystals',
'': 'none (just bones)',
bodyParts: {
'a': 'arms',
'b': 'belly',
'h': 'head',
'l': 'legs',
'n': 'neck',
't': 'tail',
'w': 'wings'
}
},
color: {
bases: {
'bk': 'black',
'bl': 'blue',
'br': 'brown',
'au': 'gold',
'ag': 'silver',
'gr': 'green',
're': 'red',
'wh': 'white',
'ye': 'yellow',
'or': 'orange',
'pu': 'purple',
'pi': 'pink',
'gy': 'grey',
'cy': 'cyan',
'mg': 'magenta',
'tn': 'tan',
'be': 'beige',
'iv': 'ivory',
'cr': 'crimson',
'sc': 'scarlet',
'bz': 'bronze',
'cp': 'copper',
'bs': 'brass',
'lm': 'lime',
'tq': 'turquoise',
'aq': 'aqua',
'nv': 'navy',
'in': 'indigo',
'vi': 'violet',
'la': 'lavender',
'ms': 'mauve',
'ch': 'chartreuse',
'ol': 'olive',
'ma': 'maroon',
'rs': 'rust',
'sa': 'salmon',
'pc': 'peach',
'ap': 'apricot',
'co': 'coral'
},
intensity: {
'+': 'lighter/brighter',
'-': 'darker/duller',
'^': 'metallic/shiny',
'_': 'matte/flat',
"'": 'iridescent',
'%': 'translucent',
'!': 'glowing',
'lum': 'luminescent'
},
patterns: {
'|': 'striped',
'=': 'banded',
':': 'spotted',
'*': 'starred',
'@': 'swirled',
'\\': 'scaled pattern',
'/': 'mixed with',
'#': 'checkered',
'&': 'gradiated',
'&1': 'gradient type 1',
'&2': 'gradient type 2',
'>': 'highlighted'
}
},
breath: {
types: {
'fl': 'flame',
'fi': 'fire',
'ac': 'acid',
'co': 'corrosive',
'ic': 'ice',
'fr': 'frost',
'li': 'lightning',
'el': 'electricity',
'ga': 'gas',
'po': 'poison',
'st': 'steam',
'wa': 'water',
'wi': 'wind',
'so': 'sonic',
'ch': 'chlorine',
'di': 'disintegration',
'ma': 'magic'
},
simple: {
'+++': 'Legendary breath weapon',
'++': 'Very powerful breath',
'+': 'Above average breath',
'': 'Normal breath weapon',
'-': 'Weak breath',
'--': 'Very weak breath',
'---': 'No breath weapon'
}
},
age: {
'+++!': 'Eternal',
'+++': 'Ancient beyond measure',
'++': 'Ancient',
'+': "You've been around",
'': 'Adult',
'-': 'Young adult',
'--': 'Adolescent',
'---': 'Hatchling',
'?': 'Unknown age'
},
nativeLand: {
'+++': 'Known across the realm',
'++': 'Well-known region',
'+': 'Specific location',
'': 'General area',
'-': 'Wanderer',
'--': 'Nomad',
'?': 'Unknown/uncertain'
},
mating: {
'+++!': 'Mated in RL to online mate\'s RL self',
'+++': 'Mated to RL person I met online',
'++': 'Mated online, not met in RL',
'+': 'Mated/married',
'': 'Dating',
'-': 'Looking',
'--': 'Not interested',
'---': 'Sworn celibate',
'!': 'notation for RL connection'
},
offspring: {
'+++': 'Large family',
'++': 'Several offspring',
'+': 'Have offspring',
'': 'Interested in having',
'-': 'Not interested',
'--': "Don't even think about it",
'---': 'Absolutely not',
'/': 'If one was my fault, I\'d faint',
'a': 'adopted'
},
hoard: {
'+++': "Really can't tell when someone steals",
'++': 'Large hoard',
'+': 'Growing collection',
'': 'Small hoard',
'-': 'Starting out',
'--': 'No hoard yet',
'---': 'No interest in hoarding'
},
money: {
'$$$': 'Richest dragon alive',
'$$': 'Wealthy',
'$': "Don't touch my hoard",
'': 'Comfortable',
'-': 'Getting by',
'--': 'Poor',
'---': 'Broke'
},
diet: {
types: {
'c': 'carnivore',
'h': 'herbivore',
'o': 'omnivore',
'v': 'vegetarian'
},
modifiers: {
'+++': 'Glutton',
'++': 'Overindulgent',
'+': 'Healthy appetite',
'': 'Normal',
'-': 'Light eater',
'--': 'Picky eater',
'---': 'Barely eats'
}
},
reality: {
'+++': 'I AM a dragon',
'++': 'Strongly identify with on-line form',
'+': 'Identify with character',
'': 'Just playing',
'-': "It's only a game",
'--': "Don't confuse RL with online",
'---': 'Completely separate'
},
activity: {
'+++!': 'T1 connection, never off the net',
'+++': 'Online most of the time',
'++': 'Online frequently',
'+': 'Online regularly',
'': 'Moderate activity',
'-': 'Occasional visits',
'--': 'Rare appearances',
'---': 'Almost never online'
},
humor: {
'+++': 'Constantly joking',
'++': 'Laughing is good for you',
'+': 'Good sense of humor',
'': 'Normal humor',
'-': 'Serious',
'--': 'Very serious',
'---': 'No sense of humor'
},
social: {
'+++': 'Everyone knows',
'++': 'Most people know',
'+': 'Several people know',
'': 'A few friends know',
'-': 'Very select few know',
'--': 'One or two people know',
'---': 'Complete secret',
'!': 'Out and proud'
},
ubiquity: {
'+++': 'Legendary status',
'++': 'Well known',
'+': 'Known in community',
'': 'Regular member',
'-': 'Pretty sure did something important',
'--': 'Mostly unnoticed',
'---': 'Unknown'
},
irritability: {
'+++': 'Constantly angry',
'++': 'Short temper',
'+': 'Get annoyed easily',
'': 'Normal temperament',
'-': 'Patient',
'--': 'Take everything in stride',
'---': 'Nothing bothers me'
},
magic: {
'+++': 'Archmage level',
'++': 'Powerful magic',
'+': 'Competent magic user',
'': 'Some magical ability',
'-': 'Minor magic',
'--': 'Magicians worry when near',
'---': 'Magic has no effect'
},
psyPower: {
'+++': 'Master psychic',
'++': 'Strong psychic',
'+': 'Psychic abilities',
'': 'Minor psychic talent',
'-': 'Resistant to psionics',
'--': 'Immune to psionics',
'---': 'Psionics have no effect'
},
technology: {
'+++': 'Program in assembly',
'++': 'Expert programmer',
'+': 'Competent with tech',
'': 'Normal tech skills',
'-': 'Basic computer use',
'--': 'Technology challenged',
'---': 'What\'s a computer?'
},
emotion: {
'+++': 'Extremely affectionate',
'++': 'Fairly free with hugs',
'+': 'Affectionate',
'': 'Normal emotional expression',
'-': 'Reserved',
'--': 'Emotionally distant',
'---': 'Cold/unemotional'
},
dragonFriend: {
'+++!': "Have you noticed? You've started growing scales!",
'+++': 'Popular with dragons - you exchange overnight visits',
'++': 'Reasonably popular - you exchange social visits',
'+': 'Polite acquaintance - you exchange social pleasantries',
'': "Tolerance - they don't eat you, you don't try to slice them!",
'-': 'Irritant - they think about having you over for lunch!',
'--': 'Maddening - they think about a quick snack ... now!',
'---': "Infuriating - you're not good enough for a snack",
'---!': "Cold fury - they're going to hunt you, and find you, and..."
}
};