${lockIconHtml}
@@ -393,7 +393,7 @@ export function renderAssetsView(assets, viewMode = 'list') {
if (viewMode === 'grid') {
// Grid view: card-style items
itemsHtml = items.map((item, index) => {
- const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`);
+ const lockIconHtml = getLockIconHtml('userStats', `inventory.assets.${item}`);
return `
${lockIconHtml}
@@ -406,7 +406,7 @@ export function renderAssetsView(assets, viewMode = 'list') {
} else {
// List view: full-width rows
itemsHtml = items.map((item, index) => {
- const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`);
+ const lockIconHtml = getLockIconHtml('userStats', `inventory.assets.${item}`);
return `
${lockIconHtml}
diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js
index b5db03a..f6d5552 100644
--- a/src/systems/rendering/thoughts.js
+++ b/src/systems/rendering/thoughts.js
@@ -513,17 +513,20 @@ export function renderThoughts() {
const fieldNameLower = field.name.toLowerCase();
// Skip lock icons for thoughts field
const showLock = !fieldNameLower.includes('thought');
+ // Add placeholder for empty fields
+ const placeholder = fieldValue ? '' : `data-placeholder="${field.name}"`;
+ const emptyClass = fieldValue ? '' : ' rpg-empty-field';
if (showLock) {
const lockIconHtml = getLockIconHtml('characters', `${char.name}.${field.name}`);
html += `
${lockIconHtml}
- ${fieldValue}
+ ${fieldValue}
`;
} else {
html += `
-
${fieldValue}
+
${fieldValue}
`;
}
}
@@ -571,6 +574,16 @@ export function renderThoughts() {
}
debugLog('[RPG Thoughts] Finished building all character cards');
+
+ // Add "Add Character" button if data exists (inside rpg-thoughts-content)
+ if (presentCharacters.length > 0) {
+ html += `
+
+ `;
+ }
+
html += '
';
}
@@ -669,6 +682,31 @@ export function renderThoughts() {
fileInput.trigger('click');
});
+ // Add event listener for "Add Character" button (support both click and touch for mobile)
+ $thoughtsContainer.find('.rpg-add-character-btn').on('click touchend', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ addNewCharacter();
+ });
+
+ // Handle empty field focus - remove placeholder styling on focus
+ $thoughtsContainer.find('.rpg-editable.rpg-empty-field').on('focus', function() {
+ $(this).removeClass('rpg-empty-field');
+ $(this).removeAttr('data-placeholder');
+ });
+
+ // Restore placeholder if field becomes empty on blur (after the main blur handler)
+ $thoughtsContainer.find('.rpg-editable').on('blur', function() {
+ const $this = $(this);
+ if (!$this.text().trim()) {
+ const field = $this.data('field');
+ if (field) {
+ $this.addClass('rpg-empty-field');
+ $this.attr('data-placeholder', field);
+ }
+ }
+ });
+
// Remove updating class after animation
if (extensionSettings.enableAnimations) {
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600);
@@ -795,6 +833,136 @@ export function removeCharacter(characterName) {
renderThoughts();
}
+/**
+ * Adds a new blank character to Present Characters data.
+ * Creates a character with empty fields based on the tracker template.
+ */
+export function addNewCharacter() {
+ const presentCharsConfig = extensionSettings.trackerConfig?.presentCharacters;
+ const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
+ const characterStats = presentCharsConfig?.characterStats;
+ const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
+ const hasRelationship = presentCharsConfig?.relationshipFields?.length > 0;
+
+ // Check if data is in JSON format
+ let isJSON = false;
+ let parsedData = null;
+
+ try {
+ parsedData = typeof lastGeneratedData.characterThoughts === 'string'
+ ? JSON.parse(lastGeneratedData.characterThoughts)
+ : lastGeneratedData.characterThoughts;
+
+ if (Array.isArray(parsedData) || (parsedData && parsedData.characters)) {
+ isJSON = true;
+ }
+ } catch (e) {
+ // Not JSON, treat as text format
+ }
+
+ if (isJSON) {
+ // JSON format - add new character object
+ const charactersArray = Array.isArray(parsedData) ? parsedData : (parsedData.characters || []);
+
+ const newCharacter = {
+ name: 'New Character',
+ emoji: '👤',
+ details: {}
+ };
+
+ // Add all enabled custom fields as empty
+ for (const field of enabledFields) {
+ newCharacter.details[field.name] = '';
+ }
+
+ // Add relationship if enabled
+ if (hasRelationship) {
+ newCharacter.relationship = 'Neutral';
+ }
+
+ // Add stats if enabled
+ if (enabledCharStats.length > 0) {
+ newCharacter.stats = {};
+ for (const stat of enabledCharStats) {
+ newCharacter.stats[stat.name] = 100;
+ }
+ }
+
+ charactersArray.push(newCharacter);
+
+ // Save back as JSON string
+ lastGeneratedData.characterThoughts = JSON.stringify(
+ Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray },
+ null,
+ 2
+ );
+ committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
+ } else {
+ // Text format - add new character block
+ const lines = lastGeneratedData.characterThoughts.split('\n');
+ const dividerIndex = lines.findIndex(line => line.includes('---'));
+
+ if (dividerIndex >= 0) {
+ const newCharacterLines = ['- New Character'];
+
+ // Add custom detail fields as standalone lines
+ for (const customField of enabledFields) {
+ newCharacterLines.push(` ${customField.name}: `);
+ }
+
+ // Add Relationship field if enabled
+ if (hasRelationship) {
+ newCharacterLines.push(` Relationship: Neutral`);
+ }
+
+ // Add Stats if enabled
+ if (enabledCharStats.length > 0) {
+ const statsParts = enabledCharStats.map(s => `${s.name}: 100%`);
+ newCharacterLines.push(` Stats: ${statsParts.join(' | ')}`);
+ }
+
+ // Find the last character and add after it, or after divider if no characters
+ let insertIndex = dividerIndex + 1;
+ for (let i = lines.length - 1; i > dividerIndex; i--) {
+ if (lines[i].trim().startsWith('- ')) {
+ // Find the end of this character block
+ insertIndex = i + 1;
+ while (insertIndex < lines.length && lines[insertIndex].trim() && !lines[insertIndex].trim().startsWith('- ')) {
+ insertIndex++;
+ }
+ break;
+ }
+ }
+
+ lines.splice(insertIndex, 0, ...newCharacterLines);
+ lastGeneratedData.characterThoughts = lines.join('\n');
+ committedTrackerData.characterThoughts = lines.join('\n');
+ }
+ }
+
+ // Update message swipe data
+ const chat = getContext().chat;
+ if (chat && chat.length > 0) {
+ for (let i = chat.length - 1; i >= 0; i--) {
+ const message = chat[i];
+ if (!message.is_user) {
+ if (message.extra && message.extra.rpg_companion_swipes) {
+ const swipeId = message.swipe_id || 0;
+ if (message.extra.rpg_companion_swipes[swipeId]) {
+ message.extra.rpg_companion_swipes[swipeId].characterThoughts = lastGeneratedData.characterThoughts;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ saveChatData();
+
+ // Re-render to show new character
+ renderThoughts();
+}
+
/**
* Updates a specific character field in Present Characters data and re-renders.
* Works with the new multi-line format.
@@ -862,18 +1030,27 @@ export function updateCharacterField(characterName, field, value) {
} else if (field === 'emoji') {
char.emoji = value;
} else if (field === 'Relationship') {
- // Store relationship as text, converting emoji if needed
+ // Store relationship in the correct nested format
+ // Remove old flat format if it exists
+ if (char.Relationship) {
+ delete char.Relationship;
+ }
+
// First check if it's an emoji → convert to text
+ let relationshipValue;
if (emojiToRelationship[value]) {
- char.Relationship = emojiToRelationship[value];
+ relationshipValue = emojiToRelationship[value];
} else {
// It's text - find matching relationship name (case-insensitive)
const matchingRelationship = Object.keys(relationshipEmojis).find(
name => name.toLowerCase() === value.toLowerCase()
);
- char.Relationship = matchingRelationship || value;
+ relationshipValue = matchingRelationship || value;
}
- // console.log('[RPG Companion] After update - char.Relationship:', char.Relationship);
+
+ // Store in the correct nested format
+ char.relationship = { status: relationshipValue };
+ // console.log('[RPG Companion] After update - char.relationship:', char.relationship);
// console.log('[RPG Companion] relationshipEmojis:', relationshipEmojis);
// console.log('[RPG Companion] emojiToRelationship:', emojiToRelationship);
} else if (field.toLowerCase() === 'thoughts' || field === (presentCharsConfig?.thoughts?.name || 'Thoughts')) {
@@ -889,15 +1066,44 @@ export function updateCharacterField(characterName, field, value) {
numValue = Math.max(0, Math.min(100, numValue));
char.stats[field] = numValue;
} else {
- // It's a custom detail field
+ // It's a custom detail field - store in details object
if (!char.details) char.details = {};
char.details[field] = value;
+
+ // Clean up snake_case version if it exists (from AI generation)
+ const fieldKey = toSnakeCase(field);
+ if (fieldKey !== field && char.details[fieldKey] !== undefined) {
+ delete char.details[fieldKey];
+ }
+
+ // Clean up old root-level field if it exists (from v2 format)
+ if (char[field] !== undefined && field !== 'name' && field !== 'emoji') {
+ delete char[field];
+ }
+ if (char[fieldKey] !== undefined && fieldKey !== 'name' && fieldKey !== 'emoji') {
+ delete char[fieldKey];
+ }
+ }
+ }
+
+ // Clean up ALL duplicate snake_case fields in details (not just the edited field)
+ // This prevents duplicates from AI-generated data
+ if (char.details) {
+ for (const customField of enabledFields) {
+ const fieldName = customField.name;
+ const snakeCaseKey = toSnakeCase(fieldName);
+ // If both versions exist, keep the properly-cased one and remove snake_case
+ if (snakeCaseKey !== fieldName &&
+ char.details[fieldName] !== undefined &&
+ char.details[snakeCaseKey] !== undefined) {
+ delete char.details[snakeCaseKey];
+ }
}
}
}
- // Save back to lastGeneratedData
- lastGeneratedData.characterThoughts = Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray };
+ // Save back to lastGeneratedData as JSON string (consistent with infoBox and userStats)
+ lastGeneratedData.characterThoughts = JSON.stringify(Array.isArray(parsedData) ? charactersArray : { ...parsedData, characters: charactersArray }, null, 2);
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
// console.log('[RPG Companion] Saved to lastGeneratedData.characterThoughts:', JSON.stringify(lastGeneratedData.characterThoughts));
@@ -978,6 +1184,9 @@ export function updateCharacterField(characterName, field, value) {
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
const isThoughtsField = field.toLowerCase() === 'thoughts' || field === thoughtsFieldName;
+ // Track if field was found and updated
+ let fieldUpdated = false;
+
// First pass: check if Stats line exists and update other fields
for (let i = characterStartIndex; i < characterEndIndex; i++) {
const line = lines[i].trim();
@@ -985,35 +1194,37 @@ export function updateCharacterField(characterName, field, value) {
if (line.startsWith('Stats:')) {
statsLineExists = true;
statsLineIndex = i;
+ continue; // Skip to next line
}
+ // Check for name update
if (field === 'name' && line.startsWith('- ')) {
lines[i] = `- ${value}`;
+ fieldUpdated = true;
+ continue;
}
- else if (field === 'emoji' && line.startsWith('Details:')) {
- const parts = line.substring(line.indexOf(':') + 1).split('|').map(p => p.trim());
- parts[0] = value;
- lines[i] = `Details: ${parts.join(' | ')}`;
- }
- else if (line.startsWith('Details:')) {
- const fieldIndex = enabledFields.findIndex(f => f.name === field);
- if (fieldIndex !== -1) {
- const parts = line.substring(line.indexOf(':') + 1).split('|').map(p => p.trim());
- if (parts.length > fieldIndex + 1) {
- parts[fieldIndex + 1] = value;
- lines[i] = `Details: ${parts.join(' | ')}`;
- }
- }
- }
- else if (field === 'Relationship' && line.startsWith('Relationship:')) {
+
+ // Check for Relationship field
+ if (field === 'Relationship' && line.startsWith('Relationship:')) {
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
const relationshipValue = emojiToRelationship[value] || value;
lines[i] = `Relationship: ${relationshipValue}`;
+ fieldUpdated = true;
+ continue;
}
- else if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) {
- // Update thoughts field
- lines[i] = `${thoughtsFieldName}: ${value}`;
- // console.log('[RPG Companion] Updated thoughts:', lines[i]);
+
+ // Check for Thoughts field
+ if (isThoughtsField && line.startsWith(thoughtsFieldName + ':')) {
+ lines[i] = ` ${thoughtsFieldName}: ${value}`;
+ fieldUpdated = true;
+ continue;
+ }
+
+ // Check for v3 text format standalone field lines (e.g., "Appearance: ...", "Demeanor: ...")
+ if (line.startsWith(field + ':')) {
+ lines[i] = ` ${field}: ${value}`;
+ fieldUpdated = true;
+ // Don't break - update ALL instances of this field (in case of duplicates from previous bugs)
}
}
@@ -1080,23 +1291,28 @@ export function updateCharacterField(characterName, field, value) {
}
}
} else {
- // Create new character block
+ // Create new character block (v3 text format only)
const dividerIndex = lines.findIndex(line => line.includes('---'));
if (dividerIndex >= 0) {
const newCharacterLines = [`- ${characterName}`];
- let detailsParts = [field === 'emoji' ? value : '😊'];
- for (let i = 0; i < enabledFields.length; i++) {
- detailsParts.push(field === enabledFields[i].name ? value : '');
+ // Add custom detail fields as standalone lines
+ for (const customField of enabledFields) {
+ if (field === customField.name) {
+ newCharacterLines.push(` ${customField.name}: ${value}`);
+ } else {
+ newCharacterLines.push(` ${customField.name}: `);
+ }
}
- newCharacterLines.push(`Details: ${detailsParts.join(' | ')}`);
+ // Add Relationship field if enabled
if (presentCharsConfig?.relationshipFields?.length > 0) {
const emojiToRelationship = { '⚔️': 'Enemy', '⚖️': 'Neutral', '⭐': 'Friend', '❤️': 'Lover' };
const relationshipValue = field === 'Relationship' ? (emojiToRelationship[value] || value) : 'Neutral';
- newCharacterLines.push(`Relationship: ${relationshipValue}`);
+ newCharacterLines.push(` Relationship: ${relationshipValue}`);
}
+ // Add Stats if enabled
if (enabledCharStats.length > 0) {
const statsParts = enabledCharStats.map(s => {
if (field === s.name) {
@@ -1111,7 +1327,7 @@ export function updateCharacterField(characterName, field, value) {
}
return `${s.name}: 0%`;
});
- newCharacterLines.push(`Stats: ${statsParts.join(' | ')}`);
+ newCharacterLines.push(` Stats: ${statsParts.join(' | ')}`);
}
lines.splice(dividerIndex + 1, 0, ...newCharacterLines);
diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js
index 6620321..0bef359 100644
--- a/src/systems/ui/weatherEffects.js
+++ b/src/systems/ui/weatherEffects.js
@@ -105,41 +105,48 @@ function getCurrentTime() {
return null;
}
+// Patterns for specific weather conditions (order matters - combined effects first)
+// Grouped by languages for easy editing
+const WEATHER_PATTERNS_BY_LANGUAGE = {
+ en: [
+ { id: "blizzard", patterns: [ "blizzard" ] }, // Snow + Wind
+ { id: "storm", patterns: [ "storm", "thunder", "lightning" ] }, // Rain + Lightning
+ { id: "wind", patterns: [ "wind", "breeze", "gust", "gale" ] },
+ { id: "snow", patterns: [ "snow", "flurries" ] },
+ { id: "rain", patterns: [ "rain", "drizzle", "shower" ] },
+ { id: "mist", patterns: [ "mist", "fog", "haze" ] },
+ { id: "sunny", patterns: [ "sunny", "clear", "bright" ] },
+ { id: "none", patterns: [ "cloud", "overcast", "indoor", "inside" ] },
+ ],
+ ru: [
+ { id: "blizzard", patterns: [ "метель" ] },
+ { id: "storm", patterns: [ "гроза", "буря", "шторм" ] },
+ { id: "wind", patterns: [ "ветер", "ветрено", "ветерок", "бриз", "легкий бриз", "слегка ветрено", "легкий ветер", "шквал,буря" ] },
+ { id: "snow", patterns: [ "снег", "снегопад" ] },
+ { id: "rain", patterns: [ "дождь", "морось", "ливень" ] },
+ { id: "mist", patterns: [ "мгла", "туман", "туманно" ] },
+ { id: "sunny", patterns: [ "солнечно", "ясно", "ярко", "ясное утро", "ясный день" ] },
+ { id: "none", patterns: [ "облачно", "пасмурно", "в помещении", "внутри" ] },
+ ],
+}
+
/**
* Parse weather text to determine effect type
*/
function parseWeatherType(weatherText) {
- if (!weatherText) return 'none';
+ if (!weatherText) return "none";
const text = weatherText.toLowerCase();
- // Check for specific weather conditions (order matters - check combined effects first)
- if (text.includes('blizzard')) {
- return 'blizzard'; // Snow + Wind
- }
- if (text.includes('storm') || text.includes('thunder') || text.includes('lightning')) {
- return 'storm'; // Rain + Lightning
- }
- if (text.includes('wind') || text.includes('breeze') || text.includes('gust') || text.includes('gale')) {
- return 'wind';
- }
- if (text.includes('snow') || text.includes('flurries')) {
- return 'snow';
- }
- if (text.includes('rain') || text.includes('drizzle') || text.includes('shower')) {
- return 'rain';
- }
- if (text.includes('mist') || text.includes('fog') || text.includes('haze')) {
- return 'mist';
- }
- if (text.includes('sunny') || text.includes('clear') || text.includes('bright')) {
- return 'sunny';
- }
- if (text.includes('cloud') || text.includes('overcast') || text.includes('indoor') || text.includes('inside')) {
- return 'none';
+ for (const language of Object.values(WEATHER_PATTERNS_BY_LANGUAGE)) {
+ for (const { id, patterns } of language) {
+ if (patterns.some(p => text.includes(p))) {
+ return id;
+ }
+ }
}
- return 'none';
+ return "none";
}
/**
@@ -240,24 +247,24 @@ function calculateSunPosition(hour) {
// Daytime is roughly 5 AM to 8 PM (5-20)
// Map hour to position along an arc
// 5 AM = far left, low | 12 PM = center, high | 8 PM = far right, low
-
+
if (hour === null) hour = 12; // Default to noon if unknown
-
+
// Clamp to daytime hours
const clampedHour = Math.max(5, Math.min(20, hour));
-
+
// Normalize to 0-1 range (5 AM = 0, 20 PM = 1)
const progress = (clampedHour - 5) / 15;
-
+
// Horizontal position: 3% to 92% (left to right, wider range)
const left = 3 + progress * 89;
-
+
// Vertical position: parabolic arc (high at noon, low at dawn/dusk)
// At progress 0.5 (noon), top should be ~8% (high)
// At progress 0 or 1, top should be ~40% (low, near horizon)
const normalizedProgress = (progress - 0.5) * 2; // -1 to 1
const top = 8 + 32 * (normalizedProgress * normalizedProgress);
-
+
return { left, top };
}
@@ -270,7 +277,7 @@ function createSunshine(hour) {
// Create the sun based on current hour
const sunPos = calculateSunPosition(hour);
-
+
const sun = document.createElement('div');
sun.className = 'rpg-weather-particle rpg-clear-sun';
sun.style.left = `${sunPos.left}vw`;
@@ -572,9 +579,9 @@ function calculateMoonPosition(hour) {
// Nighttime is roughly 8 PM to 5 AM (20-5)
// Map hour to position along an arc
// 8 PM = far left, low | midnight = center-left, high | 5 AM = far right, low
-
+
if (hour === null) hour = 0; // Default to midnight if unknown
-
+
// Normalize night hours to 0-1 range
// 20 (8 PM) = 0, 0 (midnight) = ~0.44, 5 (5 AM) = 1
let progress;
@@ -585,16 +592,16 @@ function calculateMoonPosition(hour) {
// Midnight to 5 AM: 0-5 maps to 0.44-1
progress = (hour + 4) / 9;
}
-
+
// Horizontal position: 10% to 80% (left to right)
const left = 10 + progress * 70;
-
+
// Vertical position: parabolic arc (high at ~2 AM, low at dusk/dawn)
// Peak should be around progress 0.67 (~2 AM)
const peakProgress = 0.5;
const normalizedProgress = (progress - peakProgress) * 2; // -1 to 1
const top = 8 + 25 * (normalizedProgress * normalizedProgress);
-
+
return { left, top };
}
@@ -607,7 +614,7 @@ function updateCelestialPosition(hour) {
// Update sun position if it exists
const sun = weatherContainer.querySelector('.rpg-clear-sun');
const sunGlow = weatherContainer.querySelector('.rpg-clear-sun-glow');
-
+
if (sun && sunGlow) {
const sunPos = calculateSunPosition(hour);
sun.style.left = `${sunPos.left}vw`;
@@ -620,7 +627,7 @@ function updateCelestialPosition(hour) {
// Update moon position if it exists
const moon = weatherContainer.querySelector('.rpg-night-moon');
const moonGlow = weatherContainer.querySelector('.rpg-night-moon-glow');
-
+
if (moon && moonGlow) {
const moonPos = calculateMoonPosition(hour);
moon.style.left = `${moonPos.left}vw`;
diff --git a/style.css b/style.css
index 69eb7dc..4029010 100644
--- a/style.css
+++ b/style.css
@@ -2329,6 +2329,40 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
transform: scale(0.95);
}
+/* Add Character Button (inside rpg-thoughts-content, after last character) */
+.rpg-add-character-btn {
+ background: var(--rpg-accent);
+ border: 1px solid var(--rpg-border);
+ color: var(--rpg-text);
+ padding: 0;
+ margin: 0.5rem auto 0;
+ font-size: clamp(0.625rem, 0.6vw, 0.75rem);
+ font-weight: 500;
+ cursor: pointer;
+ border-radius: 3px;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ opacity: 0.6;
+ width: auto;
+}
+
+.rpg-add-character-btn:hover {
+ background: var(--rpg-highlight);
+ border-color: var(--rpg-highlight);
+ color: var(--rpg-bg);
+ opacity: 1;
+}
+
+.rpg-add-character-btn:active {
+ transform: scale(0.95);
+}
+
+.rpg-add-character-btn i {
+ font-size: 0.875em;
+}
+
/* Character traits/status line and custom fields */
.rpg-character-traits,
.rpg-character-field {
@@ -2340,12 +2374,20 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
word-wrap: break-word;
}
-/* Placeholder for empty editable character fields */
-.rpg-character-field.rpg-editable:empty::before {
- content: 'Click to edit...';
+/* Empty field placeholder using data-placeholder attribute */
+.rpg-editable.rpg-empty-field:empty::before {
+ content: attr(data-placeholder);
color: var(--rpg-highlight);
- opacity: 0.5;
+ opacity: 0.4;
font-style: italic;
+ pointer-events: none;
+}
+
+/* Ensure empty fields have minimum height for clickability */
+.rpg-editable.rpg-empty-field {
+ min-height: 1.2em;
+ display: inline-block;
+ min-width: 3em;
}
/* Character stats display */