Docs: expand help and README with params, platform notes; add simple SDFH logo [cai]
This commit is contained in:
parent
f1e1c37b59
commit
eb6454d11f
3 changed files with 173 additions and 38 deletions
80
README.md
80
README.md
|
|
@ -1,29 +1,79 @@
|
|||
Stella Deus Fuse Helper (SDFH)
|
||||
|
||||
Interactive CLI to browse Stella Deus items and their fusion recipes using section-typed data.
|
||||
Interactive CLI to browse items and compute fusion plans with cost analysis.
|
||||
|
||||
What’s included
|
||||
- sdfh.js: single-file CLI (pure JS)
|
||||
- sdfh_item_data.json: supplied data (items include name, rank, section, type, optional stats, and recipes)
|
||||
- sdfh.js: single-file CLI
|
||||
- sdfh_item_data.json: item database
|
||||
|
||||
How to run
|
||||
Run
|
||||
- node sdfh.js
|
||||
- Or use the SEA executables in dist/ (no Node required)
|
||||
|
||||
How to search
|
||||
- Type-only rank: "katana r7", "axe r6", "hat r3"
|
||||
- Exact item name: "battle axe"
|
||||
- Item name + rank: "battle axe r7", "golden apple r5"
|
||||
- Suggestions: top 5 by default; add "--full" to show more
|
||||
Search examples
|
||||
- katana r7
|
||||
- battle axe
|
||||
- battle axe r7
|
||||
- golden apple r5 --full
|
||||
|
||||
Rules
|
||||
- Types come from guide sections (e.g., 1.6 knife, 4.2 glove, 5.2 scroll, 5.3 rankup)
|
||||
- No aliasing or cross-type mapping
|
||||
- Rank queries use explicit types or exact item names only
|
||||
What does r7 mean?
|
||||
- r7 (or rank 7) is a rank filter. It limits matches to items of rank 7.
|
||||
- With a type word (e.g., "katana r7"), it finds all items of that type and rank.
|
||||
- With an exact item name + rank (e.g., "battle axe r7"), it searches within that item’s family/type.
|
||||
|
||||
Output
|
||||
- Shows item header, optional stats, and an aligned ASCII table of all fusion recipes that create the item
|
||||
Types vs item names
|
||||
- You can search by type/category (e.g., "katana", "axe", "bow", "glove", "robe", etc.) to browse groups.
|
||||
- You can search by exact item name (e.g., "Battle Axe").
|
||||
- For rank filters:
|
||||
- Type + rank is required unless you give an exact full item name.
|
||||
- Examples:
|
||||
- OK: "axe r5" → all axes of rank 5
|
||||
- OK: "battle axe r5" → rank 5 items in the Battle Axe line/type
|
||||
- Not OK: "battle r5" (ambiguous; add a type or full name)
|
||||
|
||||
Partial and typos
|
||||
- The search is typo-tolerant and token-aware. "bttle axe" or "btl ax" can still find "Battle Axe".
|
||||
- Suggestions show the top matches; add --full to see more.
|
||||
|
||||
Fusion and pricing
|
||||
- golden apple --fuse Show default recipe’s fusion tree
|
||||
- golden apple --fuse 2 Use recipe index 2
|
||||
- golden apple --fuse --depth 3 Resolve to leaves with rank <= 3
|
||||
- golden apple --fuse --store 3 Stop at purchasable leaves in store 3; show prices and total
|
||||
- golden apple --fuse --depth 3 --store 3 Treat non-buyable leaves as owned; price the rest
|
||||
|
||||
Options
|
||||
- --full Show up to 50 suggestions instead of 5
|
||||
- --fuse [N] Enter fusion mode; optional N is recipe index (1-based)
|
||||
- --recipe [N] Alias of --fuse [N]
|
||||
- --depth N Alias of --fuse-rank N; leaves must be rank <= N
|
||||
- --fuse-rank N Leaves must be rank <= N
|
||||
- --store N Price analysis at store level N (1..5)
|
||||
|
||||
Notes
|
||||
- Types derive from guide sections; rank filters require explicit type or exact item name
|
||||
- Data file is discovered beside the executable or in data/, then SEA-embedded asset
|
||||
|
||||
Platform notes
|
||||
- macOS first run (ad‑hoc signing and quarantine removal):
|
||||
- For Apple Silicon use `sdfh-sea-macos-arm64`, for Intel use `sdfh-sea-macos-x64`.
|
||||
- Then run:
|
||||
```bash
|
||||
xattr -p com.apple.quarantine ./sdfh-sea-macos-arm64 || true
|
||||
xattr -dr com.apple.quarantine ./sdfh-sea-macos-arm64
|
||||
codesign --force --sign - ./sdfh-sea-macos-arm64
|
||||
```
|
||||
- If Finder blocks: System Settings → Privacy & Security → Allow Anyway, then Control‑click → Open.
|
||||
- Windows on Wine: ensure Wine prefix reports Windows 10+ via winecfg so the exe launches by double‑click.
|
||||
|
||||
License
|
||||
MIT
|
||||
|
||||
Acknowledgments
|
||||
- Data and fusion references are derived from community guides on GameFAQs:
|
||||
- Dragen’s Fusion/Item FAQ: [link](https://gamefaqs.gamespot.com/ps2/921280-stella-deus-the-gate-of-eternity/faqs/36685)
|
||||
- Gundam4fun’s Fusion FAQ: [link](https://gamefaqs.gamespot.com/ps2/921280-stella-deus-the-gate-of-eternity/faqs/37029)
|
||||
- While those guides are copyrighted, the factual data about in-game items and recipes is available to anyone who plays the game.
|
||||
- Many thanks to the authors for their meticulous work collecting and presenting the data. This project acknowledges that processing the raw facts into a comprehensive, searchable format and building this fuse-helper app is a separate effort built on top of their contributions.
|
||||
|
||||
|
||||
|
|
|
|||
30
assets/sdfh-logo.svg
Normal file
30
assets/sdfh-logo.svg
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#0f1a2a"/>
|
||||
<stop offset="100%" stop-color="#1f3a5a"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="6" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="512" height="512" fill="url(#bg)"/>
|
||||
<g opacity="0.22" fill="none" stroke="#89a7c2" stroke-width="2">
|
||||
<circle cx="256" cy="256" r="180" />
|
||||
<circle cx="256" cy="256" r="140" />
|
||||
<circle cx="256" cy="256" r="100" />
|
||||
</g>
|
||||
<g transform="translate(0,8)" fill="#cfe0f0" opacity="0.18">
|
||||
<path d="M256 120c-44 0-80 36-80 80v24c0 18-10 34-26 42l-14 7c-6 3-10 9-10 16v11c0 7 4 13 10 16l36 16c18 8 38 12 58 12h52c20 0 40-4 58-12l36-16c6-3 10-9 10-16v-11c0-7-4-13-10-16l-14-7c-16-8-26-24-26-42v-24c0-44-36-80-80-80z"/>
|
||||
</g>
|
||||
<g filter="url(#glow)">
|
||||
<text x="50%" y="380" text-anchor="middle" font-family="Trebuchet MS, Segoe UI, sans-serif" font-size="132" font-weight="900" letter-spacing="8" fill="#e8f2fb">SDFH</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
99
sdfh.js
99
sdfh.js
|
|
@ -220,11 +220,14 @@ function parseFlags(input) {
|
|||
m = p.match(/^--recipe=(\d+)$/i);
|
||||
if (m) { flags.doFuse = true; flags.recipeIndex = parseInt(m[1], 10); continue; }
|
||||
if (/^--recipe$/i.test(p) && parts[i + 1] && /^\d+$/.test(parts[i + 1])) { flags.doFuse = true; flags.recipeIndex = parseInt(parts[i + 1], 10); i += 1; continue; }
|
||||
const mRank = p.match(/^--(?:fuse-rank)=(\d+)$/i)?.[1] || (p.match(/^--fuse-rank$/i) && parts[i + 1] && /^\d+$/.test(parts[i + 1]) ? parts[++i] : null);
|
||||
let mTmp = p.match(/^--(?:fuse-rank)=(\d+)$/i);
|
||||
const mRank = (mTmp && mTmp[1]) || (p.match(/^--fuse-rank$/i) && parts[i + 1] && /^\d+$/.test(parts[i + 1]) ? parts[++i] : null);
|
||||
if (mRank) { flags.fuseRankLimit = parseInt(mRank, 10); continue; }
|
||||
const mDepthAsRank = p.match(/^--(?:depth)=(\d+)$/i)?.[1] || (p.match(/^--depth$/i) && parts[i + 1] && /^\d+$/.test(parts[i + 1]) ? parts[++i] : null);
|
||||
mTmp = p.match(/^--(?:depth)=(\d+)$/i);
|
||||
const mDepthAsRank = (mTmp && mTmp[1]) || (p.match(/^--depth$/i) && parts[i + 1] && /^\d+$/.test(parts[i + 1]) ? parts[++i] : null);
|
||||
if (mDepthAsRank) { flags.fuseRankLimit = parseInt(mDepthAsRank, 10); continue; }
|
||||
const mStore = p.match(/^--store=(\d+)$/i)?.[1] || (p.match(/^--store$/i) && parts[i + 1] && /^\d+$/.test(parts[i + 1]) ? parts[++i] : null);
|
||||
mTmp = p.match(/^--store=(\d+)$/i);
|
||||
const mStore = (mTmp && mTmp[1]) || (p.match(/^--store$/i) && parts[i + 1] && /^\d+$/.test(parts[i + 1]) ? parts[++i] : null);
|
||||
if (mStore) { flags.storeLevel = Math.max(1, Math.min(5, parseInt(mStore, 10))); continue; }
|
||||
kept.push(p);
|
||||
}
|
||||
|
|
@ -528,35 +531,68 @@ function summarizeItem(item) {
|
|||
|
||||
function sortRecipes(recipes) {
|
||||
return [...(recipes || [])].sort((a, b) => {
|
||||
const ra = (a.ingredients || []).map((x) => x.rank || 0);
|
||||
const rb = (b.ingredients || []).map((x) => x.rank || 0);
|
||||
const ia = (a.ingredients || []);
|
||||
const ib = (b.ingredients || []);
|
||||
const ra = ia.map((x) => x && x.rank ? x.rank : 0);
|
||||
const rb = ib.map((x) => x && x.rank ? x.rank : 0);
|
||||
const maxA = Math.max(...ra, 0);
|
||||
const maxB = Math.max(...rb, 0);
|
||||
if (maxA !== maxB) return maxA - maxB;
|
||||
const sumA = ra.reduce((p, c) => p + c, 0);
|
||||
const sumB = rb.reduce((p, c) => p + c, 0);
|
||||
if (sumA !== sumB) return sumA - sumB;
|
||||
const aStr = `${a.ingredients?.[0]?.name || ''} + ${a.ingredients?.[1]?.name || ''}`;
|
||||
const bStr = `${b.ingredients?.[0]?.name || ''} + ${b.ingredients?.[1]?.name || ''}`;
|
||||
const aStr = `${(ia[0] && ia[0].name) ? ia[0].name : ''} + ${(ia[1] && ia[1].name) ? ia[1].name : ''}`;
|
||||
const bStr = `${(ib[0] && ib[0].name) ? ib[0].name : ''} + ${(ib[1] && ib[1].name) ? ib[1].name : ''}`;
|
||||
return aStr.localeCompare(bStr);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------- CLI ----------------
|
||||
|
||||
function loadData(dataFile) {
|
||||
if (!fs.existsSync(dataFile)) {
|
||||
console.error(`Data file not found: ${dataFile}`);
|
||||
process.exit(1);
|
||||
function loadDataAuto() {
|
||||
function tryReadJSON(p) {
|
||||
try {
|
||||
const txt = fs.readFileSync(p, 'utf8');
|
||||
const obj = JSON.parse(txt);
|
||||
if (obj && Array.isArray(obj.items)) return { items: obj.items.map((it) => ({ ...it })), source: p };
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
const raw = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
|
||||
if (!raw || !Array.isArray(raw.items)) {
|
||||
console.error('Invalid data format: expected { items: [] }');
|
||||
process.exit(1);
|
||||
function tryReadSEAAsset(key) {
|
||||
try {
|
||||
const sea = require('node:sea');
|
||||
if (sea && typeof sea.isSea === 'function' && sea.isSea()) {
|
||||
const txt = sea.getAsset(key, 'utf8');
|
||||
if (txt) {
|
||||
const obj = JSON.parse(txt);
|
||||
if (obj && Array.isArray(obj.items)) return { items: obj.items.map((it) => ({ ...it })), source: 'embedded-asset' };
|
||||
}
|
||||
const items = raw.items.map((it) => ({ ...it }));
|
||||
// Do not infer or override types here. Trust the data file.
|
||||
return items;
|
||||
}
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
const execDir = (process && process.execPath) ? path.dirname(process.execPath) : __dirname;
|
||||
const candidates = [
|
||||
path.resolve(execDir, 'sdfh_item_data.json'),
|
||||
path.resolve(execDir, 'data', 'sdfh_item_data.json'),
|
||||
path.resolve(__dirname, 'sdfh_item_data.json'),
|
||||
path.resolve(__dirname, 'data', 'sdfh_item_data.json'),
|
||||
];
|
||||
for (let i = 0; i < candidates.length; i += 1) {
|
||||
const found = tryReadJSON(candidates[i]);
|
||||
if (found) return found;
|
||||
}
|
||||
// Fallbacks: try SEA asset first, then bundled require
|
||||
const fromSEA = tryReadSEAAsset('sdfh_item_data.json');
|
||||
if (fromSEA) return fromSEA;
|
||||
try {
|
||||
const embedded = require('./sdfh_item_data.json');
|
||||
if (embedded && Array.isArray(embedded.items)) {
|
||||
return { items: embedded.items.map((it) => ({ ...it })), source: 'embedded-require' };
|
||||
}
|
||||
} catch (e) {}
|
||||
console.error('Data file not found: tried', candidates.join(', '));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
|
|
@ -564,15 +600,34 @@ function printHelp() {
|
|||
console.log(' help Show this help');
|
||||
console.log(' quit / exit Exit the app');
|
||||
console.log(' <query> Search items by name (partial match, typo-tolerant)');
|
||||
console.log(' Filters: append "r8" or "rank 8" or trailing number to constrain rank; prefix type, e.g., "katana r7"');
|
||||
console.log('');
|
||||
console.log('Filters:');
|
||||
console.log(' - Append rank: "r8" or "rank 8" or trailing number (e.g., "katana r7")');
|
||||
console.log(' - Exact item name: "battle axe"; name + rank: "battle axe r7"');
|
||||
console.log('');
|
||||
console.log('Options:');
|
||||
console.log(' --full Show up to 50 suggestions instead of top 5');
|
||||
console.log(' --fuse [N] Enter fusion mode. Optional N selects recipe index (1-based)');
|
||||
console.log(' --recipe [N] Same as --fuse [N]');
|
||||
console.log(' --depth N Alias of --fuse-rank N; leaves must be rank <= N');
|
||||
console.log(' --fuse-rank N Leaves must be rank <= N');
|
||||
console.log(' --store N Price analysis at store level N (1..5).');
|
||||
console.log(' With --store, totals table shows Price and Total price.');
|
||||
console.log(' With --depth and --store, items without prices are treated as owned');
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
console.log(' golden apple --fuse');
|
||||
console.log(' golden apple --fuse 2');
|
||||
console.log(' golden apple --fuse --depth 3');
|
||||
console.log(' golden apple --fuse --store 3');
|
||||
console.log(' golden apple --fuse --depth 3 --store 3');
|
||||
}
|
||||
|
||||
function start() {
|
||||
const DATA_FILE = path.resolve(__dirname, 'sdfh_item_data.json');
|
||||
const items = loadData(DATA_FILE);
|
||||
const loaded = loadDataAuto();
|
||||
const items = loaded.items;
|
||||
const index = buildIndex(items);
|
||||
console.log(`Loaded ${items.length} items from ${path.basename(DATA_FILE)}`);
|
||||
console.log(`Loaded ${items.length} items from ${path.basename(loaded && loaded.source ? loaded.source : 'sdfh_item_data.json')}`);
|
||||
console.log('Type a name to search (partial allowed). Type "help" for help.');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue