Files
dragoncode-decoder/js/decoder.js

729 lines
20 KiB
JavaScript

// Dragon Code V2.6 Decoder - Converts parsed data to human-readable text
// Import dependencies for Node.js environment only
// Browser: scripts loaded via <script> tags define globals automatically
// Node.js: need to explicitly require dependencies
try {
if (typeof module !== 'undefined' && module.exports && typeof require === 'function') {
// We're in Node.js (CommonJS environment)
if (typeof TAG_DESCRIPTIONS === 'undefined') {
TAG_DESCRIPTIONS = require('./tags-data.js').TAG_DESCRIPTIONS;
}
if (typeof resolveSpeciesCode === 'undefined') {
resolveSpeciesCode = require('./species-data.js').resolveSpeciesCode;
}
}
} catch (e) {
// Silently ignore - we're likely in a browser environment
// where TAG_DESCRIPTIONS and resolveSpeciesCode are already loaded
}
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(decodeMagic(tags.magic));
}
if (tags.psyPower) {
result.abilities.push(decodePsyPower(tags.psyPower));
}
// Life & Relationships
if (tags.nativeLand) {
result.life.push(decodeNativeLand(tags.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) {
// Get base description
const baseDescription = TAG_DESCRIPTIONS.appendages.base[appendages.baseType];
if (!baseDescription) {
return {
label: 'Appendages',
value: appendages.baseType,
tag: appendages.raw
};
}
// If no modifiers, return base description
if (!appendages.modifiers || appendages.modifiers.length === 0) {
return {
label: 'Appendages',
value: baseDescription,
tag: appendages.raw
};
}
// Check for modifier
const modifier = appendages.modifiers[0];
const modifierDescription = TAG_DESCRIPTIONS.appendages.modifiers[modifier];
if (modifierDescription) {
// Replace placeholders in modifier description
let value = modifierDescription;
// Determine singular and plural forms
let singular = baseDescription.toLowerCase().replace(/^a pair of /, '').replace(/^a /, '');
let plural = singular;
// Handle specific cases
if (singular === 'legs' || singular === 'wings' || singular === 'arms') {
// already plural
} else if (singular === 'head') {
plural = 'heads';
singular = 'head';
} else if (singular === 'tail') {
plural = 'tails';
singular = 'tail';
} else if (singular.endsWith('limbs')) {
plural = singular;
singular = singular.replace(/s$/, '');
}
value = value.replace(/{type}/g, singular);
value = value.replace(/{plural}/g, plural.charAt(0).toUpperCase() + plural.slice(1));
return {
label: 'Appendages',
value: value,
tag: appendages.raw
};
}
return {
label: 'Appendages',
value: baseDescription,
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.distant) {
value += ' (distant from mate)';
}
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 modifierKey = getModifierString(money.modifiers);
const value = TAG_DESCRIPTIONS.money[modifierKey] || TAG_DESCRIPTIONS.money[''];
return {
label: 'Money',
value: value,
tag: money.raw
};
}
// Decode diet
function decodeDiet(diet) {
const modifierKey = getModifierString(diet.modifiers);
const modifierDesc = TAG_DESCRIPTIONS.diet.modifiers[modifierKey];
let value = '';
// If we have a modifier description, use it
if (modifierDesc) {
value = modifierDesc;
}
// If we have diet types, add them
if (diet.types.length > 0) {
const typeNames = diet.types.map(t =>
TAG_DESCRIPTIONS.diet.types[t] || t
);
if (value) {
value += ' - ' + typeNames.join(', ');
} else {
value = typeNames.join(', ');
}
} else if (!value) {
// No modifiers and no types - default to normal
value = TAG_DESCRIPTIONS.diet.modifiers[''];
}
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) {
// Remove trailing period if present before adding specialist
value = value.replace(/\.$/, '');
value += `. Specialist in ${tech.specialist}`;
}
return {
label: 'Technology',
value: value,
tag: tech.raw
};
}
// Decode native land
function decodeNativeLand(nativeLand) {
const value = TAG_DESCRIPTIONS.nativeLand[nativeLand.value] || nativeLand.value;
return {
label: 'Native-Land',
value: value,
tag: nativeLand.raw
};
}
// Decode magic
function decodeMagic(magic) {
const modifierKey = getModifierString(magic.modifiers);
let value = TAG_DESCRIPTIONS.magic[modifierKey] || TAG_DESCRIPTIONS.magic[''];
if (magic.specialist) {
// Remove trailing period if present before adding specialist
value = value.replace(/\.$/, '');
value += `. Specialist in ${magic.specialist}`;
}
return {
label: 'Magic',
value: value,
tag: magic.raw
};
}
// Decode psy-power
function decodePsyPower(psyPower) {
const modifierKey = getModifierString(psyPower.modifiers);
let value = TAG_DESCRIPTIONS.psyPower[modifierKey] || TAG_DESCRIPTIONS.psyPower[''];
if (psyPower.specialist) {
// Remove trailing period if present before adding specialist
value = value.replace(/\.$/, '');
value += `. Specialist in ${psyPower.specialist}`;
}
return {
label: 'Psy-Power',
value: value,
tag: psyPower.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., N!, U!)
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
if (modifiers.asterisk && key === '') {
key = '*';
}
if (modifiers.question && key === '') {
key = '?';
}
if (modifiers.tilde && key === '') {
key = '~';
}
// Special case for slash
if (modifiers.slash) {
key = '/';
}
// Special case for caret alone (N^)
if (modifiers.caret && key === '') {
key = '^';
}
return key;
}
// Export for Node.js (CommonJS)
if (typeof module !== 'undefined' && module.exports) {
module.exports = { decodeDragonCode };
}