initial commit
This commit is contained in:
10
.env.example
Normal file
10
.env.example
Normal 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
63
.gitignore
vendored
Normal 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
29
Dockerfile
Normal 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
21
LICENSE
Normal 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
139
README.md
Normal 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
86
add_acquired_column.js
Normal 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
76
add_alt_name.js
Normal 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
89
add_uc_to_json.js
Normal 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`);
|
||||||
16
char_list.txt
Normal file
16
char_list.txt
Normal 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
114
convert_to_json.js
Normal 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
259
db_parser.js
Normal 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
18
docker-compose.yml
Normal 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
74
index.ejs
Normal 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
161
load_to_db.js
Normal 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
18
package.json
Normal 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
132
parser.js
Normal 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
228
server.js
Normal 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
43
single_entry.txt
Normal 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
406
styles.css
Normal 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
75
test-docker.sh
Executable 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
1254
trusts.json
Normal file
File diff suppressed because it is too large
Load Diff
3915
trusts.txt
Normal file
3915
trusts.txt
Normal file
File diff suppressed because it is too large
Load Diff
342
uc.txt
Normal file
342
uc.txt
Normal 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
93
update_db.js
Normal 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
222
update_synergy_names.js
Normal 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
92
update_weapon_skills.js
Normal 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();
|
||||||
Reference in New Issue
Block a user