Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,12 @@ Choose the exact profile for your display when possible, or the nearest validate

Download SpinFOURKAYYY separately from its [latest GitHub release](https://github.com/itsspin/SPINFOURKAYYY/releases/latest), extract the complete ZIP into a normal user-writable folder, choose the percentage **before** launching, click **Start EverQuest for me**, and keep the scaler open until the game exits. EverQuest, SpinFOURKAYYY, and companion overlays should run at the same Windows privilege level.

## Sharper world textures with SpinTexture

[**SpinTexture**](https://itsspin.github.io/spintexture/) is a separate companion project for improving EverQuest Legends textures. SpinUI modernizes the native interface while SpinTexture enhances the world around it, so players can use either project independently or combine them for a more complete visual upgrade.

Visit the [SpinTexture project site](https://itsspin.github.io/spintexture/) for previews, downloads, and installation guidance.

## A compact tour of the rest

| Feature | Distinguishing behavior |
Expand Down
28 changes: 28 additions & 0 deletions loremaster-desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,34 @@ health. The parser engine remains authoritative for combat attribution,
charmed pets, mez/lull evidence and D0–D4 weekly raid events. The narrow preload
exposes only versioned state, folder selection, reset, and window commands.

## Native Loremaster themes

Loremaster includes two complete presentation systems using the same parser,
layout and accessibility behavior:

- **Vellum & Ember** is the default and matches `spinui_reloaded` with oiled
leather surfaces, parchment text, brass edges and restrained spirit-blue
selections.
- **Midnight Frost Glass** matches `spinui_glass` with deep translucent panes,
ice-blue edges, mint actions and violet selections.

The theme picker in Settings applies immediately to the Seed, expanded HUD,
Settings, alerts, crowd-control timers and Combat Archive. The selection is
stored with the other desktop settings and restored before windows are shown,
so changing themes never restarts the parser or moves an overlay.

Alert Sound Studio includes four locally generated cues plus Silent, with a
separate selection for charm breaks, tells, summons, deaths, big hits, name
calls, mez warnings and lull warnings. Each event may instead use a local WAV,
MP3, OGG or M4A file selected through the native file picker. Custom audio is
validated and size-limited by Electron's main process; the sandboxed renderer
never receives general filesystem access.

Weekly D0–D4 progress comes from explicit raid difficulty plus combat-log boss
evidence, with a confirmation prompt when the difficulty is not known and a
manual correction grid. Loremaster deliberately does not scrape EverQuest's
Instance Information window or reserve a global lockout-screen hotkey.

The Gear Path surface imports EQ Legends Tools' version-1 character-sheet JSON
and EverQuest's `/outputfile inventory` TXT locally. It identifies goal items
already equipped or held in bags/bank and groups missing pieces by source zone.
Expand Down
181 changes: 136 additions & 45 deletions loremaster-desktop/electron/main.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion loremaster-desktop/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ contextBridge.exposeInMainWorld("loremasterDesktop", {
openExternal: (value: string) => ipcRenderer.invoke("external:open", value),
checkForUpdates: () => ipcRenderer.invoke("updates:check"),
resetEngine: () => ipcRenderer.send("engine:reset"),
scanLockouts: () => ipcRenderer.send("engine:scan-lockouts"),
testAlert: () => ipcRenderer.send("alerts:test"),
chooseAlertSound: (kind: string) => ipcRenderer.invoke("alerts:choose-sound", kind),
readAlertSound: (kind: string) => ipcRenderer.invoke("alerts:read-sound", kind),
onSnapshot: (callback: (event: unknown) => void) => {
const listener = (_event: Electron.IpcRendererEvent, value: unknown) => callback(value);
ipcRenderer.on("engine:snapshot", listener);
Expand Down
4 changes: 2 additions & 2 deletions loremaster-desktop/index.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<!doctype html>
<html lang="en">
<html lang="en" data-theme="vellum">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' http://127.0.0.1:5173 ws://127.0.0.1:5173"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'self' data: blob:; connect-src 'self' http://127.0.0.1:5173 ws://127.0.0.1:5173"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
Expand Down
288 changes: 242 additions & 46 deletions loremaster-desktop/src/App.tsx

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions loremaster-desktop/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DesktopSettings, EngineHealth, EngineSnapshotEvent, GearPlanView } from "./protocol";
import type { AlertSoundKind, DesktopSettings, EngineHealth, EngineSnapshotEvent, GearPlanView } from "./protocol";

export {};

Expand Down Expand Up @@ -34,8 +34,13 @@ declare global {
detail: string;
}>;
resetEngine: () => void;
scanLockouts: () => void;
testAlert: () => void;
chooseAlertSound: (kind: AlertSoundKind) => Promise<DesktopSettings | null>;
readAlertSound: (kind: AlertSoundKind) => Promise<{
bytes: Uint8Array;
extension: string;
name: string;
} | null>;
onSnapshot: (callback: (event: unknown) => void) => () => void;
onHealth: (callback: (health: unknown) => void) => () => void;
onGearPlan: (callback: (gearPlan: unknown) => void) => () => void;
Expand Down
4 changes: 4 additions & 0 deletions loremaster-desktop/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./styles.css";
import "./themes.css";

const requestedTheme = new URLSearchParams(window.location.search).get("theme");
document.documentElement.dataset.theme = requestedTheme === "glass" ? "glass" : "vellum";

createRoot(document.getElementById("root")!).render(
<StrictMode>
Expand Down
32 changes: 12 additions & 20 deletions loremaster-desktop/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,24 +158,6 @@ export interface WeeklyRaidRowView {
bestSeconds: readonly (number | null)[];
}

export interface AltZLockoutView {
target: string;
difficulty: number;
remainingSeconds: number;
instanceName: string;
eventName: string;
expiresAt: string;
}

export interface AltZScanView {
status: "idle" | "scanning" | "success" | "error";
detail: string;
scannedAt: string;
importedCount: number;
timedCount?: number;
hotkey: string;
}

export interface WeeklyProgressView {
weekStart: string;
nextReset: string;
Expand All @@ -189,8 +171,6 @@ export interface WeeklyProgressView {
pendingRaidTarget?: string;
/** Every kill awaiting a difficulty. pendingRaidTarget names only the first. */
pendingRaidTargets?: string[];
altZLockouts?: readonly AltZLockoutView[];
altZScan?: AltZScanView;
}

export interface GearGoalView {
Expand Down Expand Up @@ -248,6 +228,16 @@ export interface EngineHealth {
}

export type AlertAnchor = "auto" | "above" | "below" | "left" | "right";
export type LoremasterTheme = "vellum" | "glass";
export type AlertSoundKind = "default" | "charmBreak" | "tell" | "summon" | "death" | "bigHit" | "nameCalled" | "mez" | "lull";
export type AlertSoundPreset = "rune" | "crystal" | "ember" | "bell" | "custom" | "silent";

export interface AlertSoundProfile {
preset: AlertSoundPreset;
customPath: string;
}

export type AlertSoundProfiles = Record<AlertSoundKind, AlertSoundProfile>;

export interface AlertSettings {
alertsEnabled: boolean;
Expand All @@ -267,13 +257,15 @@ export interface AlertSettings {
lullTimersEnabled: boolean;
lullTimerSound: boolean;
lullWarningSeconds: number;
soundProfiles: AlertSoundProfiles;
}

export interface DesktopSettings {
logPath: string;
raidDifficulty: number | null;
bisBuildPath: string;
inventoryPath: string;
uiTheme: LoremasterTheme;
alwaysOnTop: boolean;
fontScale: number;
composition: string;
Expand Down
42 changes: 21 additions & 21 deletions loremaster-desktop/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -262,26 +262,6 @@ body.control-window { background: transparent; }
.weekly-card summary > span.tier-needed { color: #f2c96f; }
.weekly-card > p { color: var(--dim); font-size: 9px; line-height: 1.4; margin: 8px 0 1px; }
.weekly-card .raid-pending { color: #f2c96f; border-left: 2px solid #f2c96f; padding: 5px 7px; background: rgba(242,201,111,.06); }
.altz-sync { display: grid; gap: 6px; margin-top: 9px; border: 1px solid rgba(97,215,208,.2); border-radius: 8px; padding: 8px; background: linear-gradient(135deg, rgba(17,35,43,.58), rgba(9,13,19,.68)); }
.altz-sync.error { border-color: rgba(255,118,89,.34); background: rgba(62,25,23,.2); }
.altz-sync.scanning { border-color: rgba(242,201,111,.42); box-shadow: inset 0 0 18px rgba(242,201,111,.04); }
.altz-sync header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.altz-sync header > div { display: grid; gap: 2px; min-width: 0; }
.altz-sync header small { color: var(--cyan); font-size: 7px; letter-spacing: .11em; }
.altz-sync.error header small { color: var(--ember); }
.altz-sync header b { color: #e9f1f4; font: 700 10px Georgia, serif; letter-spacing: .04em; }
.altz-sync .altz-scan { flex: 0 0 auto; cursor: pointer; border: 1px solid rgba(214,173,103,.34); border-radius: 5px; padding: 4px 7px; color: var(--gold); background: rgba(8,10,14,.78); font: 700 8px Georgia, serif; letter-spacing: .06em; box-shadow: inset 0 1px rgba(255,255,255,.04); transition: border-color .12s, color .12s; }
.altz-sync .altz-scan:hover:not(:disabled) { border-color: rgba(214,173,103,.62); color: #f0dcae; }
.altz-sync .altz-scan:disabled { cursor: default; opacity: .55; }
.altz-sync kbd { flex: 0 0 auto; border: 1px solid rgba(214,173,103,.34); border-radius: 5px; padding: 4px 6px; color: var(--gold); background: rgba(8,10,14,.78); font: 700 8px Georgia, serif; box-shadow: inset 0 1px rgba(255,255,255,.04); }
.weekly-card .altz-sync > p { margin: 0; color: #91a5b4; font-size: 8px; line-height: 1.4; }
.altz-lockouts { display: grid; gap: 3px; }
.altz-lockouts article { display: flex; align-items: center; gap: 8px; border-top: 1px solid rgba(91,123,151,.18); padding-top: 5px; }
.altz-lockouts article > span { display: grid; gap: 1px; min-width: 0; }
.altz-lockouts article b { color: #dce9f1; font-size: 9px; }
.altz-lockouts article small { color: var(--dim); font-size: 7px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.altz-lockouts article strong { margin-left: auto; flex: 0 0 auto; color: #45e3a1; font: 700 9px Georgia, serif; }
.altz-sync footer { color: #617b8e; font-size: 7px; line-height: 1.35; }
.raid-grid { display: grid; gap: 3px; margin-top: 9px; }
.raid-grid-head, .raid-grid-row { display: grid; grid-template-columns: minmax(116px, 1fr) repeat(5, 30px); align-items: center; gap: 3px; }
.raid-grid-head { color: var(--dim); font-size: 8px; letter-spacing: .08em; padding: 0 2px 2px; }
Expand Down Expand Up @@ -362,6 +342,26 @@ body.control-window { background: transparent; }
.settings-panel button:disabled { opacity: .55; cursor: wait; }
.settings-card.update-card .update-ready { color: #08110f; border-color: #45e3a1; background: #45e3a1; }

.sound-studio { display:grid; gap:8px; }
.sound-profile-list { display:grid; gap:5px; }
.sound-profile { display:grid; grid-template-columns:minmax(0,1fr) 125px 28px; align-items:center; gap:7px; min-height:42px; border-top:1px solid var(--soft-line); padding:6px 0; }
.sound-profile>span { min-width:0; display:grid; gap:2px; }
.sound-profile>span b { color:#dce9f1; font:600 10px "Segoe UI",sans-serif; }
.sound-profile>span small,.sound-note { color:var(--dim); font:400 8px/1.35 "Segoe UI",sans-serif; }
.sound-preset-trigger { min-width:0; width:100%; height:28px; display:flex; align-items:center; justify-content:space-between; gap:6px; border:1px solid var(--line); border-radius:6px; padding:0 7px; color:#dce9f1; background:#080c12; font:600 9px "Segoe UI",sans-serif; text-align:left; }
.sound-preset-trigger span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sound-preset-trigger>i { width:6px; height:6px; flex:0 0 auto; border-right:1px solid currentColor; border-bottom:1px solid currentColor; transform:translateY(-2px) rotate(45deg); transition:transform .14s ease; }
.sound-preset-trigger.open>i { transform:translateY(2px) rotate(225deg); }
.sound-profile .sound-preview { width:28px; height:28px; padding:0; color:var(--cyan); }
.sound-profile .sound-file { grid-column:2/4; min-width:0; overflow:hidden; padding:5px 7px; color:var(--gold); font-size:8px; text-align:left; text-overflow:ellipsis; white-space:nowrap; }
.sound-preset-menu { grid-column:1/4; display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:5px; border:1px solid var(--line); border-radius:8px; padding:6px; background:#080c12; box-shadow:0 9px 22px rgba(0,0,0,.38),inset 0 1px rgba(255,255,255,.04); }
.sound-preset-menu button { min-width:0; min-height:27px; display:flex; align-items:center; gap:6px; padding:5px 7px; border-color:var(--soft-line); text-align:left; }
.sound-preset-menu button>i { width:6px; height:6px; flex:0 0 auto; border:1px solid currentColor; border-radius:50%; opacity:.48; }
.sound-preset-menu button>span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sound-preset-menu button.selected { color:var(--gold); border-color:var(--line); background:rgba(214,173,103,.09); }
.sound-preset-menu button.selected>i { border-color:currentColor; background:currentColor; box-shadow:0 0 6px currentColor; opacity:1; }
.sound-note { display:block; padding-top:2px; }

.settings-toggle { position: relative; display: flex; align-items: center; gap: 12px; min-height: 39px; border-top: 1px solid rgba(91,123,151,.13); cursor: pointer; }
.settings-toggle > span { min-width: 0; display: grid; gap: 2px; }
.settings-toggle > span b { color: #dce9f1; font: 600 10px "Segoe UI", sans-serif; letter-spacing: 0; }
Expand Down Expand Up @@ -575,7 +575,7 @@ body.alert-window { background: transparent; }
.archive-table>article>span b { font-size:11px; }.archive-table>article>span small { font-size:8px; }.archive-table>article>strong { font-size:10px; }
.archive-insights article small { font-size:8px; }.archive-insights article b { font-size:11px; }.archive-insights article span,.archive-insights footer { font-size:9px; }
.archive-timeline header,.archive-timeline footer { font-size:9px; }
.encounter-nav>span small,.fight-facts small,.actor-tabs button small,.actor-tabs button span,.actor-drilldown>header small,.actor-facts small,.actor-identities small,.breakdown-columns article small,.stance-advisor small,.stance-advisor>span,.altz-sync header small,.altz-lockouts article small,.altz-sync footer,.seed-control-time small,.number-setting b { font-family:"Segoe UI Variable Text","Segoe UI",sans-serif; font-size:8px; }
.encounter-nav>span small,.fight-facts small,.actor-tabs button small,.actor-tabs button span,.actor-drilldown>header small,.actor-facts small,.actor-identities small,.breakdown-columns article small,.stance-advisor small,.stance-advisor>span,.seed-control-time small,.number-setting b { font-family:"Segoe UI Variable Text","Segoe UI",sans-serif; font-size:8px; }

@media (max-width:920px) {
.archive-layout { grid-template-columns:236px minmax(0,1fr); }
Expand Down
Loading