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
403 changes: 0 additions & 403 deletions package-lock.json

Large diffs are not rendered by default.

606 changes: 567 additions & 39 deletions src/Layer.css

Large diffs are not rendered by default.

123 changes: 103 additions & 20 deletions src/Layer.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,57 @@
import React, { useState, useEffect } from 'react';
import React, {useEffect, useState} from 'react';
import FloatingWindow from './components/FloatingWindow';
import LoadingProgress from './components/LoadingProgress';
import LoadingScreen from './components/LoadingScreen';
import ModuleHeader from './components/ModuleHeader';
import ModuleList from './components/ModuleList';
import ModuleContent from './components/ModuleContent';
import { razorIsPro } from "./util/pro.ts";
import SettingsPanel from './components/SettingsPanel';
import {applySettings, loadSettings} from './settings';
import {razorIsPro} from "./util/pro.ts";
import type {ModuleConfig} from "./modules.ts";
import razorModSdk from "./razor-wings";
import main from "./main.tsx";
import { useGameState } from './hooks/useGameState';
import { useModuleManager } from './hooks/useModuleManager';
import {useGameState} from './hooks/useGameState';
import {useModuleManager} from './hooks/useModuleManager';

const Layer: React.FC = () => {
const [isExpanded, setIsExpanded] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [showLoading, setShowLoading] = useState(false);

type NavDir = 'forward' | 'backward' | null;
const [navDir, setNavDir] = useState<NavDir>(null);

// Track animation setting from saved config
const [enableAnimations, setEnableAnimations] = useState(() => loadSettings().enableAnimations);

// Use custom hooks for game state and module management
const gameState = useGameState();
const {
availableModules,
loadingState,
currentOpenModule,
openModule,
closeModule
openModule: _openModule,
closeModule: _closeModule
} = useModuleManager(gameState);

// Setup keyboard hook on mount
// Wrap open/close to inject direction tracking (only when animations enabled)
const handleOpenModule = (m: ModuleConfig) => {
if (enableAnimations) setNavDir('forward');
_openModule(m);
};
const handleCloseModule = () => {
if (enableAnimations) setNavDir('backward');
_closeModule();
};
// Clear direction after animation completes
useEffect(() => {
if (navDir) {
const t = setTimeout(() => setNavDir(null), 250);
return () => clearTimeout(t);
}
}, [navDir, currentOpenModule]);

// Setup keyboard hook
useEffect(() => {
razorModSdk.hookFunction('ChatRoomKeyDown', 10, (args, next) => {
if (document.activeElement && main.overlay && main.overlay.contains(document.activeElement)) {
Expand All @@ -33,38 +61,93 @@ const Layer: React.FC = () => {
});
}, []);

// Toggle expand/collapse state
// Apply saved settings on mount (target the shadow host so CSS vars reach inside Shadow DOM)
useEffect(() => {
const settings = loadSettings();
applySettings(settings, main.overlay!);
}, []);

const toggleExpanded = () => {
if (!isExpanded) {
setShowLoading(true);
}
setIsExpanded(!isExpanded);
};
const toggleSettings = () => {
if (enableAnimations) setNavDir('forward');
setShowSettings(!showSettings);
};
const closeSettings = () => {
if (enableAnimations) setNavDir('backward');
setShowSettings(false);
// Refresh animation setting after settings panel closes
const s = loadSettings();
setEnableAnimations(s.enableAnimations);
};
const finishLoading = () => setShowLoading(false);

// Derive animation classes — only when animations are enabled
const animForward = enableAnimations && navDir === 'forward' ? 'view-enter-forward' : '';
const animBackward = enableAnimations && navDir === 'backward' ? 'view-enter-backward' : '';
const animFade = enableAnimations ? 'view-enter-fade' : '';

const headerConfig = {
title: "Razor Wings" + (razorIsPro() ? ' Pro' : ''),
actions: (
<button className="settings-btn" onClick={toggleSettings} title="Settings">
</button>
),
};

return (
<FloatingWindow
isExpanded={isExpanded}
onToggleExpanded={toggleExpanded}
header={"Razor Wings" + (razorIsPro() ? ' Pro' : '')}
header={headerConfig}
collapsed="R"
initialSize={{ width: 400, height: 300 }}
minSize={{ width: 300, height: 200 }}
initialSize={{width: 520, height: 400}}
minSize={{width: 320, height: 240}}
maxSize={{ width: 1152, height: 864 }}
resizable={true}
>
{/* Sci-fi boot animation */}
{showLoading && <LoadingScreen onComplete={finishLoading}/>}

<LoadingProgress loadingState={loadingState} />

<ModuleHeader
currentModule={gameState.currentModule}
currentScreen={gameState.currentScreen}
showBackButton={currentOpenModule !== null}
onBack={closeModule}
onBack={handleCloseModule}
/>

{currentOpenModule === null ? (
<ModuleList
modules={availableModules}
onModuleClick={openModule}
/>
) : (
<ModuleContent Component={currentOpenModule} />
{/* ── Animated content area ── */}
<div className="view-transition-wrap">
{currentOpenModule === null ? (
<div className={`view-panel ${animForward} ${animBackward}`} key="list">
<ModuleList
modules={availableModules}
onModuleClick={handleOpenModule}
/>
</div>
) : (
<div className={`view-panel ${animForward}`} key="content">
<ModuleContent Component={currentOpenModule}/>
</div>
)}
</div>

{/* Settings overlay with fade animation */}
{showSettings && (
<div className={`view-panel ${animFade}`} key="settings"
style={{position: 'absolute', inset: 0, zIndex: 'var(--rw-z-settings)'}}>
<SettingsPanel
onClose={closeSettings}
styleRoot={main.overlay!}
/>
</div>
)}
</FloatingWindow>
);
Expand Down
72 changes: 72 additions & 0 deletions src/components/LoadingScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useState, useEffect, useRef } from 'react';

interface Props {
onComplete: () => void;
duration?: number;
}

/** Cyber sci-fi boot animation — shows once per expand cycle */
const LoadingScreen: React.FC<Props> = ({ onComplete, duration = 700 }) => {
const [phase, setPhase] = useState(0);
const timerRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);

useEffect(() => {
const step = duration / 5;
let i = 0;
timerRef.current = setInterval(() => {
i++;
setPhase(i);
if (i >= 5) {
clearInterval(timerRef.current);
setTimeout(onComplete, 150);
}
}, step);
return () => { if (timerRef.current !== undefined) clearInterval(timerRef.current); };
}, []);
Comment thread
InkerBot marked this conversation as resolved.

const phases = [
'INITIALIZING_KERNEL',
'LOADING_MODULES',
'ESTABLISHING_HANDSHAKE',
'CALIBRATING_RENDERER',
'SYSTEM_READY',
];

return (
<div className="loading-screen">
<div className="loading-screen-inner">
{/* Decorative cyber grid */}
<div className="loading-grid" />

{/* Central content */}
<div className="loading-center">
<div className="loading-logo">
<span className="loading-bracket">{'['}</span>
<span className="loading-rw">RW</span>
<span className="loading-bracket">{']'}</span>
</div>

<div className="loading-bar-track">
<div
className="loading-bar-fill"
style={{ width: `${(phase / 5) * 100}%` }}
/>
</div>

<div className="loading-status">
<span className="loading-cursor">▌</span>
{phase < 5 ? phases[phase] : phases[4]}
</div>
</div>

{/* Corner decorations */}
<div className="loading-corner tl" />
<div className="loading-corner tr" />
<div className="loading-corner bl" />
<div className="loading-corner br" />
</div>
</div>
);
};

export default LoadingScreen;
8 changes: 5 additions & 3 deletions src/components/ModuleHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ const ModuleHeader: React.FC<ModuleHeaderProps> = ({
}) => {
return (
<div className="module-header">
Current location: {currentModule ?? 'unknown'} - {currentScreen ?? 'unknown'}
<span className="location-info">
Current location: {currentModule ?? 'unknown'} - {currentScreen ?? 'unknown'}
</span>
{showBackButton && (
<button onClick={onBack}>
Back
<button className="back-btn" onClick={onBack}>
Back
</button>
)}
</div>
Expand Down
111 changes: 101 additions & 10 deletions src/components/ModuleList.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,117 @@
import React from 'react';
import type { ModuleConfig } from '../modules.ts';
import type {ModuleConfig} from '../modules.ts';

interface ModuleListProps {
modules: ModuleConfig[];
onModuleClick: (module: ModuleConfig) => void;
}

/* ── Sci-fi linear SVG icons (16x16 viewBox, stroke-based) ── */
const SVGIcon: React.FC<{ d: string }> = ({d}) => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"
strokeLinecap="round" strokeLinejoin="round">
<path d={d}/>
</svg>
);

const ICONS: Record<string, string> = {
'翻译': 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM2 12h20M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10M12 2a15 15 0 0 0-4 10 15 15 0 0 0 4 10',
'隐私设置': 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
'辅助_退出房间': 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4m7 14 5-5-5-5m5 5H9',
'辅助_解锁': 'M19 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2zM7 11V7a5 5 0 0 1 10 0',
'辅助_上锁': 'M19 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2zM7 11V7a5 5 0 0 1 9.9-1',
'辅助_外观编辑器': 'M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z',
'辅助_解除限制': 'M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09zM12 15l-3-3m0 0a22 22 0 0 1 4-4m-4 4 4 4',
'辅助_移除奴隶': 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2m16 0v-2M22 11l-4-4m0 0-4 4m4-4v13m-14-1a4 4 0 0 1 0-8',
'工具_陷阱': 'M22 2 11 13m11-11-7 7M22 2l-4.5 16.5L14 14 2 8.5Z',
'聊天导出': 'M22 12h-4l-3 9L9 3l-3 9H2',
'地图脚本': 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z',
'外观记录': 'M12 8v4l3 3m6-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0z',
'郊狼': 'M13 2 3 14h9l-1 8 10-12h-9l1-8z',
'作弊_获得所有物品': 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z',
'强制开启绒语翻译器': 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z',
};

function getIconId(displayName: string): string | null {
if (ICONS[displayName]) return displayName;
for (const prefix of Object.keys(ICONS)) {
if (displayName.startsWith(prefix)) return prefix;
}
return null;
}

const CATEGORIES: Record<string, string> = {
'辅助_': 'AUX',
'工具_': 'TOOLS',
'作弊_': 'CHEAT',
};

function getCategory(displayName: string): string | null {
for (const [prefix, label] of Object.entries(CATEGORIES)) {
if (displayName.startsWith(prefix)) return label;
}
return null;
}

function getCleanName(displayName: string): string {
for (const prefix of Object.keys(CATEGORIES)) {
if (displayName.startsWith(prefix)) return displayName.slice(prefix.length);
}
return displayName;
}

interface GroupedModules {
category: string | null;
items: ModuleConfig[];
}

function groupModules(modules: ModuleConfig[]): GroupedModules[] {
const groups: GroupedModules[] = [];
let currentCategory: string | null = null;
let currentItems: ModuleConfig[] = [];

for (const mod of modules) {
const cat = getCategory(mod.displayName);
if (cat !== currentCategory) {
if (currentItems.length > 0) groups.push({category: currentCategory, items: currentItems});
currentCategory = cat;
currentItems = [];
}
currentItems.push(mod);
}
if (currentItems.length > 0) groups.push({category: currentCategory, items: currentItems});
return groups;
}

const ModuleList: React.FC<ModuleListProps> = ({ modules, onModuleClick }) => {
if (modules.length === 0) {
return (
<div className="module-list-empty">
No modules available in current context
</div>
);
return <div className="module-list-empty">NO MODULES AVAILABLE IN CURRENT CONTEXT</div>;
}

const groups = groupModules(modules);
const hasMultipleGroups = groups.length > 1;

return (
<ul className="module-list">
{modules.map((module, index) => (
<li key={index} onClick={() => onModuleClick(module)}>
{module.displayName}
</li>
{groups.map((group, groupIndex) => (
<React.Fragment key={groupIndex}>
{hasMultipleGroups && group.category && (
<li className="module-category">
[{group.category}]
</li>
)}
{group.items.map((module, index) => {
const iconId = getIconId(module.displayName);
return (
<li key={`${groupIndex}-${index}`} onClick={() => onModuleClick(module)}>
<span className="module-icon">
{iconId && <SVGIcon d={ICONS[iconId]}/>}
</span>
<span>{getCleanName(module.displayName)}</span>
</li>
);
})}
</React.Fragment>
))}
</ul>
);
Expand Down
Loading