initial commit

This commit is contained in:
Aodhan
2025-07-12 23:10:19 +01:00
commit 95ec96e0e7
27 changed files with 9073 additions and 0 deletions

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
# Server configuration
PORT=3000
NODE_ENV=development
# Database configuration
PSQL_HOST=10.0.0.199
PSQL_PORT=5432
PSQL_USER=postgres
PSQL_PASSWORD=DP3Wv*QM#t8bY*N
PSQL_DBNAME=ffxi_items

63
.gitignore vendored Normal file
View File

@@ -0,0 +1,63 @@
# Node.js
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
package-lock.json
yarn.lock
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
db.conf
# Docker
.dockerignore
.docker/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Database
*.sql
*.dump
*.sqlite
*.db
# Build directories
dist/
build/
coverage/
# Temporary files
.tmp/
.temp/
.cache/
tmp/
# OS files
.DS_Store
Thumbs.db
.directory
Desktop.ini
# Editor directories and files
.idea/
.vscode/
*.swp
*.swo
*~
.project
.settings/
.classpath
.factorypath
# Specific to this project
# Add any project-specific files to ignore here

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
# Use Node.js LTS as the base image
FROM node:20-slim
# Create app directory and set ownership
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install production dependencies only
RUN npm ci --only=production
# Copy application files
COPY . .
# Create a non-root user and switch to it
RUN groupadd -r nodejs && useradd -r -g nodejs nodejs \
&& chown -R nodejs:nodejs /app
USER nodejs
# Expose the port the app runs on
EXPOSE 3000
# Set NODE_ENV to production
ENV NODE_ENV=production
# Command to run the application
CMD ["node", "server.js"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 FFXI Trust Characters Web App
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

139
README.md Normal file
View File

@@ -0,0 +1,139 @@
# FFXI Trust Characters Web App
A web application for browsing and managing Final Fantasy XI Trust characters.
## Features
- Browse characters by role
- Search characters by name, role, job, abilities, or spells
- View detailed character information
- Track which characters you have acquired
- Explore synergy sets between characters
## Deployment with Docker
This application can be easily deployed using Docker and Docker Compose.
### Prerequisites
- [Docker](https://docs.docker.com/get-docker/)
- [Docker Compose](https://docs.docker.com/compose/install/)
### Quick Start
1. Clone the repository:
```bash
git clone <repository-url>
cd TRUSTS
```
2. Install dependencies:
```bash
npm install
```
3. Start the application:
```bash
docker-compose up -d
```
4. Test the Docker setup:
```bash
./test-docker.sh
```
5. Access the application:
- Web app: http://localhost:3000
- API: http://localhost:3000/api/trusts
### Building and Pushing the Docker Image
If you want to build and push the Docker image to a registry:
1. Build the image:
```bash
docker build -t your-username/ffxi-trusts:latest .
```
2. Push the image to a registry:
```bash
docker push your-username/ffxi-trusts:latest
```
3. Pull and run the image on your server:
```bash
docker pull your-username/ffxi-trusts:latest
docker run -d -p 3000:3000 \
-e PSQL_HOST=your-db-host \
-e PSQL_PORT=5432 \
-e PSQL_USER=postgres \
-e PSQL_PASSWORD=your-password \
-e PSQL_DBNAME=ffxi_items \
your-username/ffxi-trusts:latest
```
### Configuration
The application connects to an existing PostgreSQL database for storing character data. The default configuration in the docker-compose.yml file is:
- Database host: `10.0.0.199`
- Database port: `5432`
- Database name: `ffxi_items`
- Database user: `postgres`
- Database password: `DP3Wv*QM#t8bY*N`
You can modify these settings in the `docker-compose.yml` file if your database connection details are different.
### Database Requirements
The application expects a PostgreSQL database with a `trusts` table that has the following schema:
```sql
CREATE TABLE trusts (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
alt_name VARCHAR(255),
role VARCHAR(100),
job TEXT,
spells TEXT,
abilities TEXT,
weapon_skills TEXT,
invincible BOOLEAN DEFAULT FALSE,
acquisition TEXT,
special_features TEXT,
trust_synergy TEXT,
trust_synergy_names TEXT[],
acquired BOOLEAN DEFAULT FALSE
);
```
If your database doesn't have this schema, you can use the provided `init.sql` script to create it.
### Development
For development purposes, you can run the application without Docker:
1. Install dependencies:
```bash
npm install
```
2. Configure the database connection in `db.conf`
3. Start the server:
```bash
node server.js
```
## API Endpoints
- `GET /api/trusts` - Get all characters
- `GET /api/trusts/:id` - Get a specific character by ID
- `GET /api/trusts/role/:role` - Get characters by role
- `GET /api/trusts/search/:query` - Search characters
- `PUT /api/trusts/:id/toggle-acquired` - Toggle the acquired status of a character
- `GET /health` - Health check endpoint for monitoring the application status
## License
This project is licensed under the MIT License - see the LICENSE file for details.

86
add_acquired_column.js Normal file
View File

@@ -0,0 +1,86 @@
/**
* Script to add an "acquired" column to the trusts table
*/
const { Pool } = require('pg');
const fs = require('fs');
// Read database configuration
const dbConfFile = fs.readFileSync('db.conf', 'utf8');
const dbConfig = {};
// Parse the db.conf file
dbConfFile.split('\n').forEach(line => {
if (line.trim() === '') return;
const [key, value] = line.split('=');
if (key && value) {
// Remove quotes if present
const cleanValue = value.replace(/^['"]|['"]$/g, '');
dbConfig[key] = cleanValue;
}
});
// Configure PostgreSQL connection
const pool = new Pool({
user: dbConfig.PSQL_USER,
host: dbConfig.PSQL_HOST,
database: dbConfig.PSQL_DBNAME,
password: dbConfig.PSQL_PASSWORD,
port: dbConfig.PSQL_PORT,
});
// Function to add the acquired column
async function addAcquiredColumn() {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Check if the column already exists
const checkResult = await client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'trusts' AND column_name = 'acquired'
`);
if (checkResult.rows.length === 0) {
// Add the acquired column with a default value of false
await client.query(`
ALTER TABLE trusts
ADD COLUMN acquired BOOLEAN NOT NULL DEFAULT false
`);
console.log('Added "acquired" column to trusts table');
} else {
console.log('The "acquired" column already exists in the trusts table');
}
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
console.error('Error adding acquired column:', e);
throw e;
} finally {
client.release();
}
}
// Main function
async function main() {
try {
// Add the acquired column
await addAcquiredColumn();
// Close the pool
await pool.end();
console.log('Database update completed successfully');
} catch (e) {
console.error('Error updating database:', e);
process.exit(1);
}
}
// Run the main function
main();

76
add_alt_name.js Normal file
View File

@@ -0,0 +1,76 @@
/**
* Script to add alt_name column to trusts table
*/
const fs = require('fs');
const { Pool } = require('pg');
const path = require('path');
// Read database configuration
const dbConfFile = fs.readFileSync('db.conf', 'utf8');
const dbConfig = {};
// Parse the db.conf file
dbConfFile.split('\n').forEach(line => {
if (line.trim() === '') return;
const [key, value] = line.split('=');
if (key && value) {
// Remove quotes if present
const cleanValue = value.replace(/^['"]|['"]$/g, '');
dbConfig[key] = cleanValue;
}
});
// Configure PostgreSQL connection
const pool = new Pool({
user: dbConfig.PSQL_USER,
host: dbConfig.PSQL_HOST,
database: dbConfig.PSQL_DBNAME,
password: dbConfig.PSQL_PASSWORD,
port: dbConfig.PSQL_PORT,
});
// Function to execute SQL from a file
async function executeSqlFile(filePath) {
const client = await pool.connect();
try {
const sql = fs.readFileSync(filePath, 'utf8');
await client.query('BEGIN');
await client.query(sql);
await client.query('COMMIT');
console.log(`SQL file ${filePath} executed successfully`);
return true;
} catch (e) {
await client.query('ROLLBACK');
console.error(`Error executing SQL file ${filePath}:`, e);
return false;
} finally {
client.release();
}
}
// Main function
async function main() {
try {
// Add the alt_name column
console.log('Adding alt_name column...');
const success = await executeSqlFile('add_alt_name_column.sql');
if (success) {
console.log('alt_name column added successfully');
} else {
console.log('Failed to add alt_name column. It might already exist.');
}
// Close the pool
await pool.end();
} catch (e) {
console.error('Error in main process:', e);
process.exit(1);
}
}
// Run the main function
main();

89
add_uc_to_json.js Normal file
View File

@@ -0,0 +1,89 @@
/**
* Script to parse Unity Concord characters from uc.txt and add them to trusts.json
*/
const fs = require('fs');
// Read the uc.txt file
const ucText = fs.readFileSync('uc.txt', 'utf8');
// Read the existing trusts.json file
const trustsJson = JSON.parse(fs.readFileSync('trusts.json', 'utf8'));
// Parse the Unity Concord characters
const ucCharacters = [];
const lines = ucText.split('\n');
let currentChar = null;
let currentSection = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line === 'Unity Concord' || line === '') continue;
// Check if this is a character name (ends with (UC))
if (line.includes('(UC)')) {
// Start a new character
currentChar = {
name: line.replace(' (UC)', ''),
role: 'Unity Concord'
};
ucCharacters.push(currentChar);
currentSection = 'name';
continue;
}
// Check for section headers
if (line === 'Job') {
currentSection = 'job';
continue;
} else if (line === 'Spells') {
currentSection = 'spells';
continue;
} else if (line === 'Abilities') {
currentSection = 'abilities';
continue;
} else if (line === 'Weapon Skills') {
currentSection = 'weapon_skills';
continue;
} else if (line === 'Acquisition') {
currentSection = 'acquisition';
continue;
} else if (line === 'Special Features') {
currentSection = 'special_features';
continue;
} else if (line === 'Trust Synergy') {
currentSection = 'trust_synergy';
continue;
}
// Handle section content
if (currentChar && currentSection) {
// Skip the tab character lines
if (line === '\t') continue;
// For job section, we need to handle it specially
if (currentSection === 'job' && line.includes('/')) {
currentChar[currentSection] = line;
continue;
}
// For other sections, append the content
if (currentSection !== 'name') {
if (!currentChar[currentSection]) {
currentChar[currentSection] = line;
} else {
currentChar[currentSection] += '\n' + line;
}
}
}
}
// Add the Unity Concord characters to the trusts.json file
trustsJson.push(...ucCharacters);
// Write the updated JSON back to the file
fs.writeFileSync('trusts.json', JSON.stringify(trustsJson, null, 2), 'utf8');
console.log(`Added ${ucCharacters.length} Unity Concord characters to trusts.json`);

1098
app.js Normal file

File diff suppressed because it is too large Load Diff

16
char_list.txt Normal file
View File

@@ -0,0 +1,16 @@
Tank
Amchuchu · Ark Angel EV · Ark Angel HM · August · Curilla · Gessho · Mnejing · Rahal · Rughadjeen · Trion · Valaineral
Melee Fighter
Abenzio · Abquhbah · Aldo · Aldo (UC) · Areuhat · Ark Angel GK · Ark Angel MR · Ayame · Ayame (UC) · Babban Mheillea · Balamor · Chacharoon · Cid · Darrcuiln · Excenmille · Excenmille (S) · Fablinix · Flaviria (UC) · Gilgamesh · Halver · Ingrid II · Invincible Shield (UC) · Iroha · Iroha II · Iron Eater · Jakoh Wahcondalo (UC) · Klara · Lehko Habhoka · Lhe Lhangavo · Lhu Mhakaracca · Lilisette · Lilisette II · Lion · Lion II · Luzaf · Maat · Maat (UC) · Matsui-P · Maximilian · Mayakov · Mildaurion · Morimar · Mumor · Naja Salaheem · Naja Salaheem (UC) · Naji · Nanaa Mihgo · Nashmeira · Nashmeira II · Noillurie · Prishe · Prishe II · Rainemard · Romaa Mihgo · Rongelouts · Selh'teus · Shikaree Z · Tenzen · Teodor · Uka Totlihn · Volker · Zazarg · Zeid · Zeid II
Ranged Fighter
Elivira · Makki-Chebukki · Margret · Najelith · Semih Lafihna · Tenzen II
Offensive Caster
Adelheid · Ajido-Marujido · Ark Angel TT · Domina Shantotto · Gadalar · Ingrid · Kayeel-Payeel · Kukki-Chebukki · Leonoyne · Mumor II · Ovjang · Robel-Akbel · Rosulatia · Shantotto · Shantotto II · Ullegore
Healer
Apururu (UC) · Cherukiki · Ferreous Coffin · Karaha-Baruha · Kupipi · Mihli Aliapoh · Monberaux · Pieuje (UC) · Yoran-Oran (UC) · Ygnas
Support
Arciela · Arciela II · Joachim · King of Hearts · Koru-Moru · Qultada · Sylvie (UC) · Ulmia
Special
Brygid · Cornelia · Kupofried · Kuyin Hathdenna · Moogle · Sakura · Star Sibyl
Unity Concord
Aldo (UC) · Apururu (UC) · Ayame (UC) · Flaviria (UC) · Invincible Shield (UC) · Jakoh Wahcondalo (UC) · Maat (UC) · Naja Salaheem (UC) · Pieuje (UC) · Sylvie (UC) · Yoran-Oran (UC)

114
convert_to_json.js Normal file
View File

@@ -0,0 +1,114 @@
/**
* Script to convert trusts.txt to JSON format
*/
const fs = require('fs');
// Read the trusts.txt file
const trustsText = fs.readFileSync('trusts.txt', 'utf8');
// Define the sections we want to extract
const sections = [
'Job',
'Spells',
'Abilities',
'Weapon Skills',
'Acquisition',
'Special Features',
'Trust Synergy'
];
// Function to parse the trusts data
function parseTrustsData(text) {
// First, let's clean up the text and split it into character entries
const cleanedText = text.replace(/\r\n/g, '\n');
// Split by character name pattern (name followed by "Job")
const characterEntries = cleanedText.split(/\n\n(?=[A-Za-z][A-Za-z\s\-']+\n(?:Job|Spells|Abilities|Weapon Skills|Acquisition|Special Features|Trust Synergy))/);
const characters = [];
// Process each character entry
for (const entry of characterEntries) {
if (!entry.trim()) continue;
// Extract the character name (first line)
const lines = entry.split('\n');
const name = lines[0].trim();
// Skip if this doesn't look like a character name
if (!name || name.length > 50 || name.includes(':') || name.includes('\t')) continue;
// Create a new character entry
const character = {
name: name,
role: ''
};
// Extract role from char_list.txt if available
try {
const charList = fs.readFileSync('char_list.txt', 'utf8');
const roleRegex = new RegExp(`(Tank|Melee Fighter|Ranged Fighter|Offensive Caster|Healer|Support|Special|Unity Concord)\\s*\\n[^\\n]*${name}\\b`, 'i');
const roleMatch = charList.match(roleRegex);
if (roleMatch) {
character.role = roleMatch[1];
}
} catch (err) {
// If char_list.txt can't be read, continue without role information
}
// Process the character's data
let currentSection = '';
let sectionContent = '';
// Skip the name line
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
// Check for section headers
let sectionFound = false;
for (const section of sections) {
if (line.startsWith(section)) {
// If we were collecting content for a previous section, save it
if (currentSection) {
character[currentSection] = sectionContent.trim();
}
// Start a new section
currentSection = section.toLowerCase().replace(/\s+/g, '_');
sectionContent = line.substring(section.length).trim();
sectionFound = true;
break;
}
}
// If no section header was found, add content to the current section
if (!sectionFound && currentSection) {
sectionContent += '\n' + line;
} else if (!sectionFound && !currentSection) {
// This is part of the character's basic info (like Job)
if (line.startsWith('Job')) {
character.job = line;
}
}
}
// Save the last section if there is one
if (currentSection) {
character[currentSection] = sectionContent.trim();
}
characters.push(character);
}
return characters;
}
// Parse the data
const characters = parseTrustsData(trustsText);
// Write the JSON file
fs.writeFileSync('trusts.json', JSON.stringify(characters, null, 2));
console.log(`Converted ${characters.length} characters to JSON format.`);

259
db_parser.js Normal file
View File

@@ -0,0 +1,259 @@
/**
* Parser for FFXI Trust character data from PostgreSQL database
*/
class TrustParser {
constructor() {
this.roles = [];
this.characters = {};
this.charactersByRole = {};
this.allCharacters = [];
this.dbConfig = null;
}
/**
* Initialize the database configuration
*/
async initDbConfig() {
try {
// Try to read from environment variables first (for Docker deployment)
if (window.ENV && window.ENV.PSQL_HOST) {
console.log('Using database configuration from environment variables');
this.dbConfig = {
PSQL_USER: window.ENV.PSQL_USER,
PSQL_HOST: window.ENV.PSQL_HOST,
PSQL_DBNAME: window.ENV.PSQL_DBNAME,
PSQL_PASSWORD: window.ENV.PSQL_PASSWORD,
PSQL_PORT: window.ENV.PSQL_PORT || '5432',
};
return true;
}
// Fall back to db.conf file
console.log('Reading database configuration from db.conf file');
const response = await fetch('db.conf');
const text = await response.text();
// Parse the db.conf file
const dbConfig = {};
text.split('\n').forEach(line => {
if (line.trim() === '') return;
const [key, value] = line.split('=');
if (key && value) {
// Remove quotes if present
const cleanValue = value.replace(/^['"]|['"]$/g, '');
dbConfig[key] = cleanValue;
}
});
this.dbConfig = dbConfig;
return true;
} catch (error) {
console.error('Error loading database configuration:', error);
return false;
}
}
/**
* Process the data from the database
* @param {Array} data - Array of character objects from the database
*/
processData(data) {
// Extract unique roles
const roleSet = new Set();
data.forEach(char => {
if (char.role && char.role.trim()) {
roleSet.add(char.role.trim());
}
});
this.roles = Array.from(roleSet);
// Initialize charactersByRole
this.roles.forEach(role => {
this.charactersByRole[role] = [];
});
// Process each character
data.forEach(char => {
// Create a character object with normalized property names
const character = {
id: char.id,
name: char.name,
altName: char.alt_name || '',
role: char.role || 'Unknown',
job: char.job || '',
spells: char.spells || '',
abilities: char.abilities || '',
weaponSkills: char.weapon_skills || '',
invincible: char.invincible || false,
acquisition: char.acquisition || '',
specialFeatures: char.special_features || '',
trustSynergy: char.trust_synergy || '',
trustSynergyNames: char.trust_synergy_names || [],
acquired: char.acquired || false
};
// Add to characters object
this.characters[char.name] = character;
// Add to charactersByRole
if (character.role && this.charactersByRole[character.role]) {
this.charactersByRole[character.role].push(character.name);
}
// Add to allCharacters
this.allCharacters.push({
name: character.name,
role: character.role
});
});
}
/**
* Load data from the database
* @param {Function} callback - Function to call when loading is complete
*/
async loadData(callback) {
try {
// Initialize database configuration if not already done
if (!this.dbConfig) {
const success = await this.initDbConfig();
if (!success) {
throw new Error('Failed to initialize database configuration');
}
}
// Create the API endpoint URL
const apiUrl = `/api/trusts`;
// Fetch data from the API
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
const data = await response.json();
// Process the data
this.processData(data);
// Call the callback with the processed data
if (callback) {
callback({
roles: this.roles,
characters: this.characters,
charactersByRole: this.charactersByRole,
allCharacters: this.allCharacters
});
}
} catch (error) {
console.error('Error loading trust data:', error);
// Fallback to JSON file if database connection fails
console.log('Falling back to JSON file...');
this.loadFromJson(callback);
}
}
/**
* Fallback method to load data from JSON file
* @param {Function} callback - Function to call when loading is complete
*/
loadFromJson(callback) {
fetch('trusts.json')
.then(response => response.json())
.then(data => {
this.processData(data);
// Call the callback with the processed data
if (callback) {
callback({
roles: this.roles,
characters: this.characters,
charactersByRole: this.charactersByRole,
allCharacters: this.allCharacters
});
}
})
.catch(error => {
console.error('Error loading trust data from JSON:', error);
});
}
/**
* Search for characters by name or other attributes
* @param {string} query - Search query
* @returns {Array} - Array of matching characters
*/
searchCharacters(query) {
if (!query) return this.allCharacters;
query = query.toLowerCase();
return this.allCharacters.filter(char => {
const character = this.characters[char.name];
// Search by name
if (char.name.toLowerCase().includes(query)) return true;
// Search by alt_name
if (character.altName && character.altName.toLowerCase().includes(query)) return true;
// Search by role
if (char.role.toLowerCase().includes(query)) return true;
// Search by job
if (character.job && character.job.toLowerCase().includes(query)) return true;
// Search by abilities
if (character.abilities && character.abilities.toLowerCase().includes(query)) return true;
// Search by spells
if (character.spells && character.spells.toLowerCase().includes(query)) return true;
return false;
});
}
/**
* Toggle the acquired status of a character
* @param {number} id - The ID of the character to toggle
* @returns {Promise<Object>} - The updated character
*/
async toggleAcquired(id) {
try {
// Create the API endpoint URL
const apiUrl = `/api/trusts/${id}/toggle-acquired`;
// Send a PUT request to toggle the acquired status
const response = await fetch(apiUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
// Get the updated character
const updatedChar = await response.json();
// Update the character in our local data
if (this.characters[updatedChar.name]) {
this.characters[updatedChar.name].acquired = updatedChar.acquired;
}
return updatedChar;
} catch (error) {
console.error('Error toggling acquired status:', error);
throw error;
}
}
}
// Create a global instance of the parser
const trustParser = new TrustParser();

18
docker-compose.yml Normal file
View File

@@ -0,0 +1,18 @@
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- PORT=3000
- PSQL_HOST=10.0.0.199
- PSQL_PORT=5432
- PSQL_USER=postgres
- PSQL_PASSWORD=DP3Wv*QM#t8bY*N
- PSQL_DBNAME=ffxi_items
volumes:
- ./:/app
- /app/node_modules
restart: unless-stopped

74
index.ejs Normal file
View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FFXI Trust Characters</title>
<link rel="stylesheet" href="styles.css">
<script>
// Pass environment variables to client-side JavaScript
window.ENV = {
PSQL_HOST: '<%= process.env.PSQL_HOST %>',
PSQL_PORT: '<%= process.env.PSQL_PORT %>',
PSQL_USER: '<%= process.env.PSQL_USER %>',
PSQL_PASSWORD: '<%= process.env.PSQL_PASSWORD %>',
PSQL_DBNAME: '<%= process.env.PSQL_DBNAME %>'
};
</script>
</head>
<body>
<header>
<h1>Final Fantasy XI Trust Characters</h1>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search for characters...">
</div>
<div class="view-toggle-container">
<div class="view-toggle">
<button class="view-toggle-btn active" data-view="all">All</button>
<button class="view-toggle-btn" data-view="acquired">Acquired</button>
<button class="view-toggle-btn" data-view="unacquired">Unacquired</button>
</div>
</div>
</header>
<main>
<div class="container">
<div class="roles-container">
<h2>Roles</h2>
<ul id="roles-list">
<!-- Roles will be populated here -->
</ul>
</div>
<div class="characters-container">
<h2 id="current-role">All Characters</h2>
<div id="characters-grid">
<!-- Characters will be populated here -->
</div>
<div id="synergy-sets-container" class="synergy-sets-container">
<h2>Synergy Sets</h2>
<div id="synergy-sets">
<!-- Synergy sets will be populated here -->
</div>
</div>
</div>
<div class="character-details">
<div id="character-info">
<!-- Character details will be displayed here -->
<div class="placeholder-message">
<p>Select a character to view details</p>
</div>
</div>
</div>
</div>
</main>
<footer>
<p>Final Fantasy XI Trust Characters Database</p>
</footer>
<script src="db_parser.js"></script>
<script src="app.js"></script>
</body>
</html>

161
load_to_db.js Normal file
View File

@@ -0,0 +1,161 @@
/**
* Script to load trust character data from JSON into PostgreSQL database
*/
const fs = require('fs');
const { Pool } = require('pg');
const path = require('path');
// Read database configuration
const dbConfFile = fs.readFileSync('db.conf', 'utf8');
const dbConfig = {};
// Parse the db.conf file
dbConfFile.split('\n').forEach(line => {
if (line.trim() === '') return;
const [key, value] = line.split('=');
if (key && value) {
// Remove quotes if present
const cleanValue = value.replace(/^['"]|['"]$/g, '');
dbConfig[key] = cleanValue;
}
});
// Configure PostgreSQL connection
const pool = new Pool({
user: dbConfig.PSQL_USER,
host: dbConfig.PSQL_HOST,
database: dbConfig.PSQL_DBNAME,
password: dbConfig.PSQL_PASSWORD,
port: dbConfig.PSQL_PORT,
});
// Read the JSON data
const trustsData = JSON.parse(fs.readFileSync('trusts.json', 'utf8'));
// Function to truncate the table and reset the sequence
async function clearTable() {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('TRUNCATE TABLE trusts RESTART IDENTITY');
await client.query('COMMIT');
console.log('Table cleared successfully');
} catch (e) {
await client.query('ROLLBACK');
console.error('Error clearing table:', e);
throw e;
} finally {
client.release();
}
}
// Function to insert data into the database
async function insertData() {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Insert each character
for (let i = 0; i < trustsData.length; i++) {
const char = trustsData[i];
// Skip invalid entries
if (!char.name || char.name === 'Tank' ||
char.name.startsWith('Complete') ||
char.name.startsWith('Trade') ||
char.name.startsWith('Notice:') ||
char.name.startsWith('Be in') ||
char.name.startsWith('*')) {
continue;
}
// Function to truncate text to fit in VARCHAR(255)
const truncateText = (text, maxLength = 255) => {
if (!text) return '';
return text.length > maxLength ? text.substring(0, maxLength - 3) + '...' : text;
};
// Function to clean text by removing unwanted strings
const cleanText = (text) => {
if (!text) return '';
// Remove "SC Icon.png" strings
return text.replace(/SC Icon\.png/g, '');
};
// Prepare the data for insertion
const data = {
name: truncateText(cleanText(char.name)),
role: truncateText(cleanText(char.role || 'Unknown')),
job: truncateText(cleanText(char.job || '')),
spells: truncateText(cleanText(char.spells || '')),
abilities: truncateText(cleanText(char.abilities || '')),
weapon_skills: truncateText(cleanText(char.weapon_skills || '')),
invincible: char.invincible || false,
acquisition: cleanText(char.acquisition || ''), // No truncation for TEXT columns
special_features: cleanText(char.special_features || ''), // No truncation for TEXT columns
trust_synergy: cleanText(char.trust_synergy || '') // No truncation for TEXT columns
};
// Insert the data
const query = `
INSERT INTO trusts (
id, name, role, job, spells, abilities, weapon_skills,
invincible, acquisition, special_features, trust_synergy
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
)
`;
const values = [
i + 1, // Use the index + 1 as the ID
data.name,
data.role,
data.job,
data.spells,
data.abilities,
data.weapon_skills,
data.invincible,
data.acquisition,
data.special_features,
data.trust_synergy
];
await client.query(query, values);
console.log(`Inserted character: ${data.name}`);
}
await client.query('COMMIT');
console.log('All data inserted successfully');
} catch (e) {
await client.query('ROLLBACK');
console.error('Error inserting data:', e);
throw e;
} finally {
client.release();
}
}
// Main function
async function main() {
try {
// Clear the table first
await clearTable();
// Insert the data
await insertData();
// Close the pool
await pool.end();
console.log('Database update completed successfully');
} catch (e) {
console.error('Error updating database:', e);
process.exit(1);
}
}
// Run the main function
main();

18
package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "ffxi-trusts",
"version": "1.0.0",
"description": "FFXI Trust Characters Web App",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^5.1.0",
"pg": "^8.16.3",
"ejs": "^3.1.9"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

132
parser.js Normal file
View File

@@ -0,0 +1,132 @@
/**
* Parser for FFXI Trust character data
*/
class TrustParser {
constructor() {
this.roles = [];
this.characters = {};
this.charactersByRole = {};
this.allCharacters = [];
}
/**
* Process the JSON data to organize it for the application
* @param {Array} jsonData - Array of character objects from the JSON file
*/
processJsonData(jsonData) {
// Extract unique roles
const roleSet = new Set();
jsonData.forEach(char => {
if (char.role && char.role.trim()) {
roleSet.add(char.role.trim());
}
});
this.roles = Array.from(roleSet);
// Initialize charactersByRole
this.roles.forEach(role => {
this.charactersByRole[role] = [];
});
// Process each character
jsonData.forEach(char => {
if (!char.name || char.name === 'Tank' ||
char.name.startsWith('Complete') ||
char.name.startsWith('Trade') ||
char.name.startsWith('Notice:') ||
char.name.startsWith('Be in') ||
char.name.startsWith('*')) {
return; // Skip invalid entries
}
// Create a character object with normalized property names
const character = {
name: char.name,
role: char.role || 'Unknown',
job: char.job || '',
spells: char.spells || '',
abilities: char.abilities || '',
weaponSkills: char.weapon_skills || '',
acquisition: char.acquisition || '',
specialFeatures: char.special_features || '',
trustSynergy: char.trust_synergy || ''
};
// Add to characters object
this.characters[char.name] = character;
// Add to charactersByRole
if (character.role && this.charactersByRole[character.role]) {
this.charactersByRole[character.role].push(character.name);
}
// Add to allCharacters
this.allCharacters.push({
name: character.name,
role: character.role
});
});
}
/**
* Load the JSON data file
* @param {Function} callback - Function to call when loading is complete
*/
loadData(callback) {
fetch('trusts.json')
.then(response => response.json())
.then(data => {
this.processJsonData(data);
// Call the callback with the processed data
if (callback) {
callback({
roles: this.roles,
characters: this.characters,
charactersByRole: this.charactersByRole,
allCharacters: this.allCharacters
});
}
})
.catch(error => {
console.error('Error loading trust data:', error);
});
}
/**
* Search for characters by name or other attributes
* @param {string} query - Search query
* @returns {Array} - Array of matching characters
*/
searchCharacters(query) {
if (!query) return this.allCharacters;
query = query.toLowerCase();
return this.allCharacters.filter(char => {
const character = this.characters[char.name];
// Search by name
if (char.name.toLowerCase().includes(query)) return true;
// Search by role
if (char.role.toLowerCase().includes(query)) return true;
// Search by job
if (character.job && character.job.toLowerCase().includes(query)) return true;
// Search by abilities
if (character.abilities && character.abilities.toLowerCase().includes(query)) return true;
// Search by spells
if (character.spells && character.spells.toLowerCase().includes(query)) return true;
return false;
});
}
}
// Create a global instance of the parser
const trustParser = new TrustParser();

228
server.js Normal file
View File

@@ -0,0 +1,228 @@
/**
* Express server to serve FFXI Trust character data from PostgreSQL database
*/
const express = require('express');
const { Pool } = require('pg');
const fs = require('fs');
const path = require('path');
// Database configuration
let dbConfig = {};
// Try to read from environment variables first
if (process.env.PSQL_HOST && process.env.PSQL_USER && process.env.PSQL_DBNAME) {
console.log('Using database configuration from environment variables');
dbConfig = {
PSQL_USER: process.env.PSQL_USER,
PSQL_HOST: process.env.PSQL_HOST,
PSQL_DBNAME: process.env.PSQL_DBNAME,
PSQL_PASSWORD: process.env.PSQL_PASSWORD,
PSQL_PORT: process.env.PSQL_PORT || '5432',
};
} else {
// Fall back to db.conf file
try {
console.log('Reading database configuration from db.conf file');
const dbConfFile = fs.readFileSync('db.conf', 'utf8');
// Parse the db.conf file
dbConfFile.split('\n').forEach(line => {
if (line.trim() === '') return;
const [key, value] = line.split('=');
if (key && value) {
// Remove quotes if present
const cleanValue = value.replace(/^['"]|['"]$/g, '');
dbConfig[key] = cleanValue;
}
});
} catch (err) {
console.error('Error reading db.conf file:', err.message);
console.error('Please ensure db.conf exists or provide environment variables');
process.exit(1);
}
}
// Configure PostgreSQL connection
const pool = new Pool({
user: dbConfig.PSQL_USER,
host: dbConfig.PSQL_HOST,
database: dbConfig.PSQL_DBNAME,
password: dbConfig.PSQL_PASSWORD,
port: dbConfig.PSQL_PORT,
});
// Create Express app
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to parse JSON bodies
app.use(express.json());
// Set up EJS as the template engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '.'));
// Serve static files (except index.html which will be rendered with EJS)
app.use(express.static('.', {
index: false
}));
// Render index.ejs with environment variables
app.get('/', (req, res) => {
res.render('index.ejs', {
process: {
env: {
PSQL_HOST: dbConfig.PSQL_HOST,
PSQL_PORT: dbConfig.PSQL_PORT,
PSQL_USER: dbConfig.PSQL_USER,
PSQL_PASSWORD: dbConfig.PSQL_PASSWORD,
PSQL_DBNAME: dbConfig.PSQL_DBNAME
}
}
});
});
// API endpoint to get all trusts
app.get('/api/trusts', async (req, res) => {
try {
const client = await pool.connect();
const result = await client.query('SELECT * FROM trusts ORDER BY name');
const trusts = result.rows;
client.release();
res.json(trusts);
} catch (err) {
console.error('Error executing query', err);
res.status(500).json({ error: 'Database error' });
}
});
// API endpoint to get a specific trust by ID
app.get('/api/trusts/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
const client = await pool.connect();
const result = await client.query('SELECT * FROM trusts WHERE id = $1', [id]);
if (result.rows.length === 0) {
res.status(404).json({ error: 'Trust not found' });
} else {
res.json(result.rows[0]);
}
client.release();
} catch (err) {
console.error('Error executing query', err);
res.status(500).json({ error: 'Database error' });
}
});
// API endpoint to get trusts by role
app.get('/api/trusts/role/:role', async (req, res) => {
try {
const role = req.params.role;
const client = await pool.connect();
const result = await client.query('SELECT * FROM trusts WHERE role = $1 ORDER BY name', [role]);
const trusts = result.rows;
client.release();
res.json(trusts);
} catch (err) {
console.error('Error executing query', err);
res.status(500).json({ error: 'Database error' });
}
});
// API endpoint to search trusts
app.get('/api/trusts/search/:query', async (req, res) => {
try {
const query = req.params.query;
const client = await pool.connect();
const result = await client.query(`
SELECT * FROM trusts
WHERE
name ILIKE $1 OR
role ILIKE $1 OR
job ILIKE $1 OR
spells ILIKE $1 OR
abilities ILIKE $1
ORDER BY name
`, [`%${query}%`]);
const trusts = result.rows;
client.release();
res.json(trusts);
} catch (err) {
console.error('Error executing query', err);
res.status(500).json({ error: 'Database error' });
}
});
// API endpoint to toggle the acquired status of a trust
app.put('/api/trusts/:id/toggle-acquired', async (req, res) => {
try {
const id = parseInt(req.params.id);
const client = await pool.connect();
// Get the current acquired status
const getResult = await client.query('SELECT acquired FROM trusts WHERE id = $1', [id]);
if (getResult.rows.length === 0) {
client.release();
return res.status(404).json({ error: 'Trust not found' });
}
// Toggle the acquired status
const currentStatus = getResult.rows[0].acquired;
const newStatus = !currentStatus;
// Update the acquired status
await client.query('UPDATE trusts SET acquired = $1 WHERE id = $2', [newStatus, id]);
// Get the updated trust
const result = await client.query('SELECT * FROM trusts WHERE id = $1', [id]);
client.release();
res.json(result.rows[0]);
} catch (err) {
console.error('Error executing query', err);
res.status(500).json({ error: 'Database error' });
}
});
// Health check endpoint
app.get('/health', async (req, res) => {
try {
// Check database connection
const client = await pool.connect();
await client.query('SELECT 1');
client.release();
res.status(200).json({
status: 'ok',
message: 'Service is healthy',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '1.0.0',
database: 'connected'
});
} catch (err) {
console.error('Health check failed:', err);
res.status(500).json({
status: 'error',
message: 'Service is unhealthy',
timestamp: new Date().toISOString(),
error: err.message,
database: 'disconnected'
});
}
});
// Start the server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Access the web app at http://localhost:${PORT}`);
console.log(`API available at http://localhost:${PORT}/api/trusts`);
});

43
single_entry.txt Normal file
View File

@@ -0,0 +1,43 @@
Ark Angel EV
Job
Paladin / White Mage
Spells
Flash, Phalanx, Cure I - IV, Enlight, Reprisal
Abilities
Chivalry, Palisade, Sentinel, Divine Emblem, Rampart, Shield Strike
Weapon Skills
Chant du Cygne Light SC Icon.png/Distortion SC Icon.png, Vorpal Blade Scission SC Icon.png/Impaction SC Icon.png, Dominion Slash (AoE) None, Arrogance Incarnate (AoE Spirits Within) None
Acquisition
Trade the Cipher: Ark EV item to one of the beginning Trust quest NPCs, which may be acquired via:
Complete Dawn and all its cutscenes.
Be in possession of the Bundle of scrolls key item.
Have obtained the additional following Trust spells: Rainemard, & Ark Angel GK
Speak with Jamal in Ru'Lude Gardens (H-5).
After trading Ark Angel GK 's cipher, you must speak to Jamal, zone, and speak to Jamal again to unlock the Objective for Cipher: Ark EV. Be sure before zoning Jamal asks you to "Please come by again later."
Complete the Records of Eminence Objective: Temper Your Arrogance
Warp to Tu'Lia → Ru'Aun Gardens #4 to battle the Ark Angel with a Phantom gem of arrogance on any difficulty.
Players will be unable to receive the alter ego when trading the Cipher if they have not completed the Rhapsodies of Vana'diel mission "Exploring the Ruins." Completing that cutscene/mission will allow the cipher to be traded.
Special Features
Possesses Fast Cast, Cure Potency Bonus+50%, Damage Taken-10%, HP+20%, MP+50%, Converts 5% of Damage Taken to MP.
Lacks Provoke; however, AAEV's additional Fast Cast trait reduces the recast time on Flash and Reprisal for solid enmity control.
Recast times can be improved further by providing Haste.
AAEV has improved Shield stats compared to other trusts, implied by the [Nov 2021 Patch Notes]. This would also make her Reprisal better.
Ark Angel Elvaan doesn't use any WHM-only abilities or spells, but /WHM has Auto-Regen and Magic Defense Bonus, making her a good physical/magical hybrid tank.
Uses Rampart when her target is under the effects of Chainspell, Manafont, or Astral Flow.
Uses Shield Strike to interrupt enemies casting high tier spells.
Holds up to 2000 TP to try to close skillchains.
With two weapon skills that have no skillchain properties, tends to not interrupt skillchains performed by other party members.
Trust Synergy
ArkEV / ArkHM / ArkMR / ArkGK / ArkTT: When all 5 Ark Angels are summoned, they possess a Magic Evasion bonus (see: Resist).
Estimated 240 Magic Evasion Verification Needed, a 50% increase.
The bonus is a reference to the Accumulative Magic Resistance that was added to counter the strategy of clearing Divine Might with an alliance of Black Mages. Official Synergy Hint

406
styles.css Normal file
View File

@@ -0,0 +1,406 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f4f4f4;
}
header {
background-color: #2c3e50;
color: #fff;
padding: 1rem;
text-align: center;
}
.search-container {
margin-top: 1rem;
}
#search-input {
padding: 0.5rem;
width: 80%;
max-width: 500px;
border: none;
border-radius: 4px;
font-size: 1rem;
}
.view-toggle-container {
margin-top: 1rem;
display: flex;
justify-content: center;
}
.view-toggle {
display: inline-flex;
background-color: #ecf0f1;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.view-toggle-btn {
padding: 0.5rem 1rem;
border: none;
background-color: transparent;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s, color 0.2s;
}
.view-toggle-btn:hover {
background-color: #d6dbdf;
}
.view-toggle-btn.active {
background-color: #3498db;
color: #fff;
}
.container {
display: grid;
grid-template-columns: 1fr 2fr 3fr;
gap: 1rem;
padding: 1rem;
max-width: 1400px;
margin: 0 auto;
}
@media (max-width: 1024px) {
.container {
grid-template-columns: 1fr 2fr;
}
.character-details {
grid-column: span 2;
}
}
.character-details {
flex: 2;
padding: 1rem;
overflow-y: auto;
max-height: calc(100vh - 150px);
}
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
}
.character-details {
grid-column: span 1;
}
}
.roles-container, .characters-container, .character-details {
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
padding: 1rem;
}
h2 {
margin-bottom: 0.5rem;
color: #2c3e50;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 0.5rem;
}
.alt-name {
font-size: 1rem;
font-style: italic;
color: #7f8c8d;
margin-top: 0;
margin-bottom: 1rem;
}
#roles-list {
list-style: none;
}
#roles-list li {
padding: 0.5rem;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
}
#roles-list li:hover {
background-color: #ecf0f1;
}
#roles-list li.active {
background-color: #3498db;
color: #fff;
}
#characters-grid {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.role-section-header {
display: flex;
align-items: center;
padding: 0.75rem;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
color: #fff;
margin-top: 0.5rem;
}
.role-section-header:first-child {
margin-top: 0;
}
.expand-icon {
margin-right: 0.5rem;
font-size: 1.2rem;
width: 20px;
text-align: center;
}
.character-count {
margin-left: 0.5rem;
font-size: 0.9rem;
opacity: 0.8;
}
.role-characters-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 0.5rem;
padding: 0.5rem;
transition: max-height 0.5s ease-in-out, opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
max-height: 5000px; /* Increased to accommodate more characters */
opacity: 1;
overflow: hidden;
visibility: visible;
}
.role-characters-container.collapsed {
max-height: 0;
opacity: 0;
padding: 0;
margin: 0;
overflow: hidden;
visibility: hidden; /* Hide completely when collapsed */
}
.character-card {
background-color: #ecf0f1;
border-radius: 4px;
padding: 0.5rem;
text-align: center;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.character-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.character-card.active {
background-color: #3498db;
color: #fff;
}
#character-info {
min-height: 300px;
}
.placeholder-message {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-height: 100px;
color: #7f8c8d;
font-style: italic;
width: 100%;
text-align: center;
padding: 2rem;
background-color: #f9f9f9;
border-radius: 4px;
margin-top: 1rem;
}
.character-section {
margin-bottom: 1rem;
}
.character-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.acquired-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
}
.character-section h3 {
color: #2c3e50;
margin-bottom: 0.5rem;
}
.character-section ul {
list-style-position: inside;
padding-left: 1rem;
}
.character-section p {
text-align: justify;
white-space: pre-line; /* Preserve newlines */
overflow-wrap: break-word; /* Break long words */
word-wrap: break-word;
max-width: 100%;
}
/* Grid items for spells, abilities, and weapon skills */
.grid-items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 0.5rem;
margin-top: 0.5rem;
}
.grid-item {
background-color: #f8f9fa;
border-radius: 4px;
padding: 0.5rem;
text-align: center;
border: 1px solid #e9ecef;
font-size: 0.9rem;
}
/* Bullet lists for acquisition, special features, and trust synergy */
.character-section ul {
margin-top: 0.5rem;
padding-left: 1.5rem;
}
.character-section li {
margin-bottom: 0.5rem;
line-height: 1.4;
}
/* Synergy characters section */
.synergy-characters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.synergy-character {
padding: 0.5rem 0.75rem;
border-radius: 4px;
color: #fff;
font-size: 0.9rem;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.synergy-character:hover {
transform: translateY(-2px);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
}
/* Synergy Sets section */
.synergy-sets-container {
margin-top: 2rem;
padding-top: 1rem;
border-top: 2px solid #ecf0f1;
}
.synergy-sets-container h2 {
margin-bottom: 1rem;
}
#synergy-sets {
display: flex;
flex-direction: column;
gap: 1rem;
}
.synergy-set {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.synergy-set-title {
font-weight: bold;
margin-bottom: 0.5rem;
color: #2c3e50;
}
.synergy-set-characters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.synergy-set-character {
padding: 0.5rem 0.75rem;
border-radius: 4px;
color: #fff;
font-size: 0.9rem;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.synergy-set-character:hover {
transform: translateY(-2px);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
}
footer {
background-color: #2c3e50;
color: #fff;
text-align: center;
padding: 1rem;
margin-top: 1rem;
}
/* Role-specific colors */
.Tank { background-color: #e74c3c; }
.Melee { background-color: #e67e22; }
.Ranged { background-color: #f1c40f; }
.Offensive { background-color: #9b59b6; }
.Healer { background-color: #2ecc71; }
.Support { background-color: #3498db; }
.Special { background-color: #1abc9c; }
.Unity { background-color: #34495e; }
.Synergy { background-color: #7f8c8d; }
.Tank, .Melee, .Ranged, .Offensive, .Healer, .Support, .Special, .Unity, .Synergy {
color: #fff;
}
/* Acquired character styling */
.character-card.acquired {
border: 2px solid #27ae60;
position: relative;
}
.character-card.acquired::after {
content: "✓";
position: absolute;
top: 5px;
right: 5px;
color: #27ae60;
font-weight: bold;
}

75
test-docker.sh Executable file
View File

@@ -0,0 +1,75 @@
#!/bin/bash
# Script to test the Docker setup for the FFXI Trust Characters web app
echo "Testing Docker setup for FFXI Trust Characters web app..."
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
echo "Error: Docker is not installed. Please install Docker first."
exit 1
fi
# Check if Docker Compose is installed
if ! command -v docker-compose &> /dev/null; then
echo "Error: Docker Compose is not installed. Please install Docker Compose first."
exit 1
fi
# Build the Docker image
echo "Building Docker image..."
docker-compose build
# Start the Docker container
echo "Starting Docker container..."
docker-compose up -d
# Wait for the container to start
echo "Waiting for the container to start..."
sleep 5
# Check if the container is running
if [ "$(docker-compose ps -q | wc -l)" -eq 0 ]; then
echo "Error: Docker container failed to start."
docker-compose logs
exit 1
fi
# Test the API
echo "Testing API..."
response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/trusts)
if [ "$response" -eq 200 ]; then
echo "API test successful!"
else
echo "Error: API test failed with status code $response."
docker-compose logs
exit 1
fi
# Test the web app
echo "Testing web app..."
response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000)
if [ "$response" -eq 200 ]; then
echo "Web app test successful!"
else
echo "Error: Web app test failed with status code $response."
docker-compose logs
exit 1
fi
echo "All tests passed! The Docker setup is working correctly."
echo "You can access the web app at http://localhost:3000"
echo "You can access the API at http://localhost:3000/api/trusts"
# Ask if the user wants to stop the container
read -p "Do you want to stop the Docker container? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Stopping Docker container..."
docker-compose down
echo "Docker container stopped."
fi
exit 0

1254
trusts.json Normal file

File diff suppressed because it is too large Load Diff

3915
trusts.txt Normal file

File diff suppressed because it is too large Load Diff

342
uc.txt Normal file
View File

@@ -0,0 +1,342 @@
Unity Concord
Aldo (UC)
Job
Thief / Ninja
Spells
Abilities
Bully, Sneak Attack
Weapon Skills
(50)Sarva's Storm Dark SC Icon.png/Distortion SC Icon.png
Acquisition
Be a member of the Aldo Unity Concord.
Obtain 5000 Unity Accolades through Records of Eminence objectives for a Partial Personal Evaluation of 5pt.
Special Features
Possesses 5/5 Triple Attack Rate merits at lv75.
Excellent skillchain partner with thieves using Rudra's Storm.
Uses Sneak Attack when behind the target or after using Bully, but does not try to combine it with weapon skills.
Uses Sarva's Storm whenever another party member has 1000 TP in order to open skillchains.
If no other party members gain TP, will use Sarva's Storm at 3000 TP.
Apururu (UC)
Job
White Mage / Red Mage
Spells
Cure I - VI, Curaga I - V, Protect/ra I - V, Shell/ra I - V, -na Spells, Erase, Stoneskin, Haste
Abilities
Martyr, Devotion, Convert
Weapon Skills
(50)Nott None
Acquisition
Be a member of the Apururu Unity Concord.
Obtain 5000 Unity Accolades through Records of Eminence objectives for a Partial Personal Evaluation of 5pt (this is also how you re-obtain it in the event you've lost it).
Special Features
Possesses Regain (75 TP/tick).
Does not melee or cast spells on enemies, relying on Regain for TP.
Uses TP to recover MP. It is a low priority, and at 3000 TP she may continue casting Cure until she is out of MP.
The really high Regain compared to other trusts is balanced by the high MP cost of Curaga.
Will use Curaga spells when 3 or more party members are in yellow HP (<75%) or asleep.
Casts Haste on the player, herself, and melee damage dealers in the party (with the exception of NIN).
Uses Convert and Martyr only at very low MP (<10%).
Casts Stoneskin on herself and tries to keep it applied.
Uses Devotion on party members that have <20% MP.
Tries to stay a distance (~15') away from the monster when she doesn't have hate.
Devotion and Martyr have a shorter range (10.6'), so pay attention to her movements if you need those effects.
Trust Synergy
Apururu will prioritize supporting her brother Ajido-Marujido.
Status Removal and Haste priority changes to Ajido-Marujido > Player > Herself > Others.
If multiple party members meet the conditions for Devotion, will use it on Ajido-Marujido preferentially.
Apururu gains +25% Cure Potency Bonus.
Ayame (UC)
Job
Samurai / Warrior
Spells
Abilities
Blade Bash, Sengikori, Hasso, Third Eye, Shikikoyo, Meditate
Weapon Skills
(5)Tachi: Jinpu Scission SC Icon.png/Eks.gif, (25)Tachi: Koki Reverberation SC Icon.png/Impaction SC Icon.png, (50)Tachi: Mudo Eks.gif/Distortion SC Icon.png, (60)Tachi: Kasha Fusion SC Icon.png/Eks.gif, (70)Tachi: Ageha Compression SC Icon.png/Eks.gif
Acquisition
Be a member of the Ayame Unity Concord.
Obtain 5000 Unity Accolades through Records of Eminence objectives for a Partial Personal Evaluation of 5pt.
Special Features
Possesses 5/5 Shikikoyo merits at level 75 (shared TP +48%)
Skillchains
Specializes in two-person 3-step or 5-step skillchains with the player, resulting in higher Skillchain damage and Magic Burst Damage Bonus with each step.
Completes skillchains while using Sengikori if ready.
Only closes skillchains started by the player (or their pet). Waits at 3000 TP until conditions are met.
Always closes a higher level skillchain, ignores the skillchain if she can't.
Due to the weapon skills available, cannot close skillchains started with Distortion or Fusion.
Does not close Level 4 Light skillchains, Tachi: Mudo is only used to close Darkness.
Always chooses Tachi: Ageha following a Detonation opener.
Level 1 Chainbound (Status) will be closed by Tachi: Koki for Fragmentation.
Abilities are used based on skillchain level and battle condition, so she may Meditate before a weaponskill instead of waiting for you to reach 1000 TP.
Ayame's consistent weapon skill choice and timing make her a good choice for parties who want to set up magic bursts of a specific element boosted by Sengikori (+25% Magic Burst Damage).
High TP gain rate with Hasso/Zanshin and >250 TP per hit depending on level of SAM Store TP trait.
Tachi: Ageha is the 2015 version.
Defense Down could need more TP and Magic Accuracy than the player's version to land on high-level enemies.
Trust weapon skills have different IDs than the ones players use and have a separate implementation.
When she has 2000+ TP, she uses Shikikoyo on the party leader after they use a weapon skill.
After summoning Ayame, if she reaches 2000 TP before the party leader has used a weapon skill, uses Shikikoyo immediately.
If she gets hate, uses Third Eye.
Stuns enemies with Blade Bash, only to interrupt spellcasting.
Flaviria (UC)
Job
Dragoon / Warrior
Spells
Abilities
Jump, High Jump, Super Jump, Angon, Berserk
Weapon Skills
(5)Skewer Transfixion SC Icon.png/Impaction SC Icon.png, (25)Impulse Drive Gravitation SC Icon.png/Induration SC Icon.png, (50)Celidon's Torment Light SC Icon.png/Fragmentation SC Icon.png
Acquisition
Be a member of the Flaviria Unity Concord
Obtain 5000 Unity Accolades through Records of Eminence objectives for a Partial Personal Evaluation of 5pt.
Special Features
Possesses merits in Jump recast down, High Jump recast down, and Angon at level 75.
Uses weapon skills at 1000 TP. Does not try to skillchain.
Celidon's Torment is a Unity Leader version of Camlann's Torment which has a similar Ignores Defense property.
Aggressive weapon skill usage and Jumps enhanced by Berserk, boosted Unity Leader stats (and Flaviria Unity Shirt), and early access to higher level weapon skills make Flaviria a strong physical damage dealer to have while leveling.
An advantage of the Piercing Damage Type, is that enemies weak to it like Mandragora, Birds, and Flys are common across Vana'diel.
Invincible Shield (UC)
Job
Warrior / Corsair
Spells
Abilities
Provoke, Aggressor, Restraint, Retaliation, Warcry, Blood Rage, Tomahawk, Savagery
Weapon Skills
(5)Raging Rush Induration SC Icon.png/Reverberation SC Icon.png, (25)Steel Cyclone Distortion SC Icon.png/Detonation SC Icon.png, (50)Soturi's Fury Light SC Icon.png/Fragmentation SC Icon.png
Acquisition
Be a member of the Invincible Shield Unity Concord.
Obtain 5000 Unity Accolades through Records of Eminence objectives for a Partial Personal Evaluation of 5pt.
Special Features
Possesses Damage Taken -20%. WAR/traits.
Depending on Unity Ranking: HP+20%~+30%.
Uses Warcry then Blood Rage as soon as Warcry ends.
Uses Tomahawk on Skeletons, Slimes, and Elementals.
He is a damage dealer who Provokes.
When you don't need a tank, he'll do more damage with Retaliation.
To get the most out of Retaliation, this version of Ginuva does not have his shield.
Holds up to 1500 TP to close skillchains.
At item level, he gets the same ilvl stat increase as the tanks and Monberaux.
Jakoh Wahcondalo (UC)
Job
Thief / Warrior
Spells
Abilities
Conspirator, Trick Attack, Sneak Attack, Feint
Weapon Skills
(5)Dancing Edge Scission SC Icon.png/Detonation SC Icon.png, (25)Evisceration Gravitation SC Icon.png/Transfixion SC Icon.png, (50)Sarva's Storm Dark SC Icon.png/Distortion SC Icon.png
Acquisition
Be a member of the Jakoh Wahcondalo Unity Concord.
Obtain 5000 Unity Accolades through Records of Eminence objectives for a Partial Personal Evaluation of 5pt.
Special Features
Uses weapon skills at >2000 TP with Trick Attack and/or Sneak Attack.
Holds up to 3000 TP to wait for positioning.
Weapon skill used is random. Does not try to close skillchains.
Will open with Feint and uses it on cooldown.
Wields a knife. Gains 55 TP on hit.
Maat (UC)
Job
Monk / Warrior
Spells
Abilities
Chakra, Counterstance, Impetus
Weapon Skills
(50)Hollow Smite Light SC Icon.png/Fragmentation SC Icon.png
Acquisition
Be a member of the Maat Unity Concord.
Obtain 5000 Unity Accolades through Records of Eminence objectives for a Partial Personal Evaluation of 5pt.
Special Features
Possesses increased Kick Attacks rate.
Exclusively uses Hollow Smite as his weaponskill. If called below level 50, he will not have a way to spend TP.
Uses Hollow Smite under any the following conditions:
To open skillchains for the player when they have 1000 TP.
To close a skillchain started by other party members if possible.
When Maat (UC) has 3000 TP.
Naja Salaheem (UC)
Job
Monk / Warrior
Spells
Abilities
Weapon Skills
(5)Peacebreaker Distortion SC Icon.png/Reverberation SC Icon.png, (25)Hexa Strike Fusion SC Icon.png, (50)Nott None, (60)Black Halo Fragmentation SC Icon.png/Compression SC Icon.png, (70)Justicebreaker Dark SC Icon.png/Gravitation SC Icon.png
Acquisition
Be a member of the Naja Salaheem Unity Concord.
Obtain 5000 Unity Accolades through Records of Eminence objectives for a Partial Personal Evaluation of 5pt.
Special Features
Possesses Quadruple Attack, Triple Attack, Store TP, and Gilfinder traits.
On summoning will pick a club weapon skill from her list and use that exclusively, re-summoning her will randomize the choice of weapon skill again, in this way you can "pick" her weapon skill.
200 TP per hit.
Uses weapon skills when another party member has 1000 TP, otherwise holds TP indefinitely.
Her Multi-Attack rate is very high: can a great skillchain partner for other trusts or yourself.
But the damage is low to balance out the number of swings, and you may worry about feeding TP (Monster TP Gain).
Peacebreaker applies a 20% Defense Down and 20% Magic Defense Down to the target for up to 30 seconds.
Justicebreaker applies a 10% Defense Down and 10% Magic Defense Down to the target for up to 60 seconds.
She has no MP so Nott only serves to restore her HP, which can make her very survivable if she has the required accuracy.
With another party member like Ajido-Marujido who builds but doesn't spend TP, you may get Naja to self-skillchain Darkness with Justicebreaker.
Pieuje (UC)
Job
White Mage / Paladin
Spells
Cure I - VI, -na Spells, Erase, Esuna, Protect/ra I - V, Shell/ra I - V, Auspice, Haste
Abilities
Afflatus Misery, Sacrosanctity
Weapon Skills
(5)Starlight None, (25)Moonlight None, (50)Nott None
Acquisition
Be a member of the Pieuje Unity Concord.
Obtain 5000 Unity Accolades through Records of Eminence objectives for a Partial Personal Evaluation of 5pt.
Special Features
Possesses Regain (34tp/tick), WHM/PLD traits including Auto Refresh and Resist Sleep.
Stays in place after engaging, but will attack with a club if the enemy is nearby.
When positioned in attack range, he'll be able to use MP recovery moves more often, making up for his lower max MP compared to Tarutaru trusts.
Afflatus Misery and Auspice together give him an Enlight effect, increasing his accuracy.
Will use Esuna in Misery stance, removing 2 debuffs from himself and any other party members in range with the same debuff.
Casts Haste on players regardless of job.
Sacrosanctity defends your party from enemy SP abilities like Manafont, Chainspell, and Astral Flow.
Uses TP for MP recovery. Prefers Nott.
Trust Synergy
Trion: Pieuje only uses Regen on Trion. Pieuje prioritizes Trion > Player > Others when casting Haste and -na Spells
Sylvie (UC)
Job
Geomancer / White Mage
Spells
Cure I - IV, -na Spells, Erase, Haste, Indi-Haste, Indi-Fury, Indi-Precision, Indi-Refresh, Indi-Regen, Indi-Acumen, Indi-Languor
Abilities
Entrust
Weapon Skills
(50)Nott None
Acquisition
Be a member of the Sylvie Unity Concord.
Obtain 5000 Unity Accolades through Records of Eminence objectives for a Partial Personal Evaluation of 5pt.
Special Features
Possesses Regain+50, Damage Taken-25%, Enhanced Indicolure duration (6 minutes total, includes Entrust effects).
Will change Indicolure spells based on accuracy requirements, and the main job of the player.
Does not melee or cast spells on enemies, relying on Regain for TP.
Follows the player or trust in front of her in the party lineup.
Casts Haste on the player who summoned her (regardless of job) and any physical melee damage dealers in the party.
Uses Entrust on the player unless their main job is GEO where she will Entrust the first PLD, RUN, or NIN in the party instead.
Even if the player's Entrust is on that target, Sylvie will overwrite it with her Entrust.
Will cast Indicolure spells based on the player's job and hit rate, ranged hit rate, or item level:
Indi-Fury (+37.5% Attack/Ranged Attack) or Indi-Precision (+56 Accuracy/Ranged Accuracy) and Entrust Indi-Frailty (-12.5% Defense):
WAR, MNK, THF, BST, DRK, DRG, SAM, BLU, PUP, DNC based on hit rate
RNG, COR based on ranged hit rate
Indi-Haste (+28.8% haste) and Entrust Indi-Refresh (+5/tick): PLD and RUN
Indi-Haste (+28.8% haste) and Entrust Indi-Regen (+30/tick): NIN
Indi-Acumen (+21 Magic Attack) or Indi-Focus (+55 Magic Accuracy Verification Needed) and Entrust Indi-Refresh (+5/tick): BLM, RDM, SCH based on the difference between your level or item level and the enemy's level. Indi-Focus is used when the enemy's level is higher than the player's level or item level by 5 or more.
Indi-Refresh (+8/tick) and Entrust Indi-Acumen (+12 Magic Attack): WHM, BRD, SMN
Indi-Refresh (+8/tick) and Entrust Indi-Languor (-41 Magic Evasion Verification Needed): GEO
*Sylvie only uses Entrust with a player GEO who does not have an Indicolure on themself, but Sylvie's Indi-Languor will go on the first PLD, RUN, or NIN in the party.
Below level 93, Sylvie won't use any of the above Indi- spells until everything's available, regardless of your job. (i.e., she is waiting until Indi-Haste's level, whether you need it or not)
Indi-Regen available at Level 20.
Provides about level÷3 hp/tick, reaching the maximum +30hp @ 89. About equivalent potency to the highest level Regen Spells available.
Indi-Refresh available at Level 30. Used instead of Indi-Regen if your main job has MP: WHM, RDM, BLM, SCH, SMN, GEO, PLD, RUN, BLU, DRK.
Provides +2mp @ 32, +3mp @ 58, +4mp @ 84, +5mp @ 98.
Gains Geomancy+3 at level 99.
Uses TP to recover MP.
Yoran-Oran (UC)
Job
White Mage / Black Mage
Spells
Protectra I - V, Shellra I - V, -na spells, Cure I - VI, Erase, Stoneskin
Abilities
Afflatus Solace
Weapon Skills
(50)Nott None
Acquisition
Be a member of the Yoran-Oran Unity Concord.
Obtain 5000 Unity Accolades through Records of Eminence objectives for a Partial Personal Evaluation of 5pt.
Special Features
Possesses Cure Potency Bonus+50%, Fast Cast, Regain (50/tick).
Depending on Unity rank: MP+15%~+25%
Does not melee or cast spells on enemies, relying on Regain for TP.
Uses TP to recover MP. It is a low priority, and at 3000 TP he may continue casting Cure until he is out of MP.
Very high magic evasion for a trust. Will occasionally sleep but avoids silence and other enfeebles with a very high success rate.
Is very mana efficient thanks to Afflatus Solace, Conserve MP, and capped Cure Potency.
Tries to keep himself buffed with Stoneskin.
Stays at a distance from enemies while healing.

93
update_db.js Normal file
View File

@@ -0,0 +1,93 @@
/**
* Script to update the database schema and reload data
*/
const fs = require('fs');
const { Pool } = require('pg');
const { exec } = require('child_process');
const path = require('path');
// Read database configuration
const dbConfFile = fs.readFileSync('db.conf', 'utf8');
const dbConfig = {};
// Parse the db.conf file
dbConfFile.split('\n').forEach(line => {
if (line.trim() === '') return;
const [key, value] = line.split('=');
if (key && value) {
// Remove quotes if present
const cleanValue = value.replace(/^['"]|['"]$/g, '');
dbConfig[key] = cleanValue;
}
});
// Configure PostgreSQL connection
const pool = new Pool({
user: dbConfig.PSQL_USER,
host: dbConfig.PSQL_HOST,
database: dbConfig.PSQL_DBNAME,
password: dbConfig.PSQL_PASSWORD,
port: dbConfig.PSQL_PORT,
});
// Function to execute SQL from a file
async function executeSqlFile(filePath) {
const client = await pool.connect();
try {
const sql = fs.readFileSync(filePath, 'utf8');
await client.query('BEGIN');
await client.query(sql);
await client.query('COMMIT');
console.log(`SQL file ${filePath} executed successfully`);
} catch (e) {
await client.query('ROLLBACK');
console.error(`Error executing SQL file ${filePath}:`, e);
throw e;
} finally {
client.release();
}
}
// Function to run a Node.js script
function runScript(scriptPath) {
return new Promise((resolve, reject) => {
console.log(`Running script: ${scriptPath}`);
const process = exec(`node ${scriptPath}`, (error, stdout, stderr) => {
if (error) {
console.error(`Error executing ${scriptPath}:`, error);
return reject(error);
}
console.log(stdout);
if (stderr) {
console.error(stderr);
}
resolve();
});
});
}
// Main function
async function main() {
try {
// 1. Alter the table structure
console.log('Altering table structure...');
await executeSqlFile('alter_table.sql');
// 2. Reload the data
console.log('Reloading data...');
await runScript('load_to_db.js');
// 3. Close the pool
await pool.end();
console.log('Database update completed successfully');
} catch (e) {
console.error('Error updating database:', e);
process.exit(1);
}
}
// Run the main function
main();

222
update_synergy_names.js Normal file
View File

@@ -0,0 +1,222 @@
/**
* Script to update trust_synergy_names column with character names extracted from trust_synergy
*/
const fs = require('fs');
const { Pool } = require('pg');
const path = require('path');
// Read database configuration
const dbConfFile = fs.readFileSync('db.conf', 'utf8');
const dbConfig = {};
// Parse the db.conf file
dbConfFile.split('\n').forEach(line => {
if (line.trim() === '') return;
const [key, value] = line.split('=');
if (key && value) {
// Remove quotes if present
const cleanValue = value.replace(/^['"]|['"]$/g, '');
dbConfig[key] = cleanValue;
}
});
// Configure PostgreSQL connection
const pool = new Pool({
user: dbConfig.PSQL_USER,
host: dbConfig.PSQL_HOST,
database: dbConfig.PSQL_DBNAME,
password: dbConfig.PSQL_PASSWORD,
port: dbConfig.PSQL_PORT,
});
// Function to execute SQL from a file
async function executeSqlFile(filePath) {
const client = await pool.connect();
try {
const sql = fs.readFileSync(filePath, 'utf8');
await client.query('BEGIN');
await client.query(sql);
await client.query('COMMIT');
console.log(`SQL file ${filePath} executed successfully`);
} catch (e) {
await client.query('ROLLBACK');
console.error(`Error executing SQL file ${filePath}:`, e);
throw e;
} finally {
client.release();
}
}
// Function to get all characters from the database
async function getAllCharacters() {
const client = await pool.connect();
try {
const result = await client.query('SELECT id, name, alt_name, trust_synergy FROM trusts');
return result.rows;
} catch (e) {
console.error('Error fetching characters:', e);
throw e;
} finally {
client.release();
}
}
// Function to extract character names from trust_synergy text
function extractCharacterNames(synergyText, currentCharName) {
if (!synergyText) return [];
// Get all character names and alt_names from the database to use as a reference
const allCharNames = allCharacters.map(char => char.name);
// Create a map of alt_names to their corresponding character names
const altNameMap = new Map();
allCharacters.forEach(char => {
if (char.alt_name) {
altNameMap.set(char.alt_name, char.name);
}
});
// Create a set to store unique character names
const synergyNames = new Set();
// Common patterns in trust synergy text
const patterns = [
// Pattern: Name/Name/Name: description
/([A-Za-z\s\-']+(?:\([A-Z]\))?(?:\/[A-Za-z\s\-']+(?:\([A-Z]\))?)+):/g,
// Pattern: Name, Name, and Name
/([A-Za-z\s\-']+(?:\([A-Z]\))?)(?:,\s+([A-Za-z\s\-']+(?:\([A-Z]\))?))(?:,?\s+and\s+([A-Za-z\s\-']+(?:\([A-Z]\))?))/g,
// Pattern: Name and Name
/([A-Za-z\s\-']+(?:\([A-Z]\))?)\s+and\s+([A-Za-z\s\-']+(?:\([A-Z]\))?)/g
];
// Apply each pattern
for (const pattern of patterns) {
const matches = synergyText.matchAll(pattern);
for (const match of matches) {
// Process the first match which might contain multiple names separated by '/'
if (match[1] && match[1].includes('/')) {
const names = match[1].split('/').map(name => name.trim());
names.forEach(name => {
if (name !== currentCharName && allCharNames.includes(name)) {
synergyNames.add(name);
}
});
}
// Process individual names from the match groups
else {
for (let i = 1; i < match.length; i++) {
const name = match[i]?.trim();
if (name && name !== currentCharName && allCharNames.includes(name)) {
synergyNames.add(name);
}
}
}
}
}
// Also check for direct mentions of character names
allCharNames.forEach(name => {
// Skip the current character's name
if (name === currentCharName) return;
// Only consider names with 4 or more characters to avoid false positives
if (name.length < 4) return;
// Check if the name appears as a whole word in the synergy text
const nameRegex = new RegExp(`\\b${name}\\b`, 'g');
if (nameRegex.test(synergyText)) {
synergyNames.add(name);
}
});
// Check for alt_names in the synergy text
altNameMap.forEach((charName, altName) => {
// Skip the current character's alt_name
if (charName === currentCharName) return;
// Only consider alt_names with 4 or more characters to avoid false positives
if (altName.length < 4) return;
// Check if the alt_name appears as a whole word in the synergy text
const altNameRegex = new RegExp(`\\b${altName}\\b`, 'g');
if (altNameRegex.test(synergyText)) {
synergyNames.add(charName);
}
});
return Array.from(synergyNames);
}
// Function to update trust_synergy_names for a character
async function updateSynergyNames(id, synergyNames) {
const client = await pool.connect();
try {
// Convert array to PostgreSQL array format
const pgArray = `{${synergyNames.map(name => `"${name}"`).join(',')}}`;
await client.query(
'UPDATE trusts SET trust_synergy_names = $1 WHERE id = $2',
[pgArray, id]
);
return true;
} catch (e) {
console.error(`Error updating synergy names for character ID ${id}:`, e);
return false;
} finally {
client.release();
}
}
// Main function
async function main() {
try {
// Skip adding the column since it already exists
console.log('Column trust_synergy_names already exists, skipping creation...');
// 2. Get all characters
console.log('Fetching all characters...');
global.allCharacters = await getAllCharacters();
// 3. Process each character
console.log('Processing characters...');
let successCount = 0;
let failCount = 0;
for (const char of allCharacters) {
console.log(`Processing ${char.name}...`);
// Extract character names from trust_synergy
const synergyNames = extractCharacterNames(char.trust_synergy, char.name);
// Update the database
const success = await updateSynergyNames(char.id, synergyNames);
if (success) {
console.log(`Updated ${char.name} with synergy names: [${synergyNames.join(', ')}]`);
successCount++;
} else {
console.error(`Failed to update ${char.name}`);
failCount++;
}
}
console.log(`\nProcessing complete!`);
console.log(`Successfully updated: ${successCount} characters`);
console.log(`Failed to update: ${failCount} characters`);
// 4. Close the pool
await pool.end();
} catch (e) {
console.error('Error in main process:', e);
process.exit(1);
}
}
// Run the main function
main();

92
update_weapon_skills.js Normal file
View File

@@ -0,0 +1,92 @@
/**
* Script to update weapon skills in the database by removing "SC Icon.png" strings
*/
const { Pool } = require('pg');
const fs = require('fs');
// Read database configuration
const dbConfFile = fs.readFileSync('db.conf', 'utf8');
const dbConfig = {};
// Parse the db.conf file
dbConfFile.split('\n').forEach(line => {
if (line.trim() === '') return;
const [key, value] = line.split('=');
if (key && value) {
// Remove quotes if present
const cleanValue = value.replace(/^['"]|['"]$/g, '');
dbConfig[key] = cleanValue;
}
});
// Configure PostgreSQL connection
const pool = new Pool({
user: dbConfig.PSQL_USER,
host: dbConfig.PSQL_HOST,
database: dbConfig.PSQL_DBNAME,
password: dbConfig.PSQL_PASSWORD,
port: dbConfig.PSQL_PORT,
});
// Function to update weapon skills
async function updateWeaponSkills() {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Get all trusts with weapon skills
const result = await client.query('SELECT id, weapon_skills FROM trusts WHERE weapon_skills IS NOT NULL AND weapon_skills != \'\'');
let updatedCount = 0;
// Update each trust's weapon skills
for (const row of result.rows) {
const originalWeaponSkills = row.weapon_skills;
// Remove "SC Icon.png" from weapon skills
const updatedWeaponSkills = originalWeaponSkills.replace(/SC Icon\.png/g, '');
// Only update if there was a change
if (updatedWeaponSkills !== originalWeaponSkills) {
await client.query(
'UPDATE trusts SET weapon_skills = $1 WHERE id = $2',
[updatedWeaponSkills, row.id]
);
updatedCount++;
console.log(`Updated weapon skills for trust ID ${row.id}`);
}
}
await client.query('COMMIT');
console.log(`Updated weapon skills for ${updatedCount} trusts`);
} catch (e) {
await client.query('ROLLBACK');
console.error('Error updating weapon skills:', e);
throw e;
} finally {
client.release();
}
}
// Main function
async function main() {
try {
// Update weapon skills
await updateWeaponSkills();
// Close the pool
await pool.end();
console.log('Database update completed successfully');
} catch (e) {
console.error('Error updating database:', e);
process.exit(1);
}
}
// Run the main function
main();