Compare commits
12 Commits
trunk
...
feature/IS
| Author | SHA1 | Date | |
|---|---|---|---|
| 28868c24ca | |||
| d5e2e1559d | |||
| 0283b5173b | |||
| 51babf770f | |||
| b04f22ea22 | |||
| 48934f0ffa | |||
| 41bbe00369 | |||
| a5efbd3824 | |||
| f604099a9b | |||
| d7a997519d | |||
| 60fdb95ce7 | |||
| 3d5a819512 |
401
MODULAR_ARCHITECTURE.md
Normal file
401
MODULAR_ARCHITECTURE.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# Modular NPC Builder - Architecture Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Cobblemon NPC Creator has been refactored from a traditional tab-based form interface to a modern, modular node-based system inspired by tools like n8n, ComfyUI, and Unreal Engine's Blueprint system. This new approach provides a more intuitive, visual way to create and customize NPCs through connectable modules.
|
||||
|
||||
## Why This Refactor?
|
||||
|
||||
### Problems with the Old System
|
||||
- Complex, multi-tab forms were overwhelming for users
|
||||
- Hard to see the complete NPC configuration at a glance
|
||||
- Difficult to understand relationships between different components
|
||||
- Linear workflow didn't match the non-linear nature of NPC creation
|
||||
|
||||
### Benefits of the New System
|
||||
- Visual representation of NPC structure
|
||||
- Drag-and-drop modular components
|
||||
- Clear data flow through connections
|
||||
- Easier to add/remove features on demand
|
||||
- More scalable and extensible architecture
|
||||
- Better separation of concerns
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Core Library: React Flow (@xyflow/react)
|
||||
|
||||
**Why React Flow?**
|
||||
- Production-ready, battle-tested in many applications
|
||||
- Excellent TypeScript support
|
||||
- Highly customizable node and edge rendering
|
||||
- Built-in features: minimap, controls, background grid
|
||||
- Great performance with many nodes
|
||||
- Active development and community
|
||||
- MIT licensed
|
||||
|
||||
### Other Options Considered:
|
||||
- **jsPlumb**: Older library, less React-friendly
|
||||
- **Vue Flow**: Excellent but Vue-specific
|
||||
- **React Diagrams**: Less maintained, fewer features
|
||||
- **Rete.js**: Good but more complex API
|
||||
|
||||
React Flow was chosen for its perfect balance of features, performance, and ease of use.
|
||||
|
||||
## Architecture
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── NodeCanvas.tsx # Main workspace component
|
||||
│ └── nodes/ # Individual node modules
|
||||
│ ├── BasicInfoNode.tsx # Basic NPC settings module
|
||||
│ ├── BattleConfigNode.tsx # Battle configuration module
|
||||
│ ├── PartyNode.tsx # Pokemon party builder module
|
||||
│ ├── InteractionNode.tsx # Interaction system module
|
||||
│ ├── VariablesNode.tsx # MoLang variables module
|
||||
│ └── OutputNode.tsx # JSON preview and export module
|
||||
├── types/
|
||||
│ ├── npc.ts # Original NPC type definitions
|
||||
│ └── nodes.ts # New node-based type definitions
|
||||
└── App.tsx # Updated main app with modular view
|
||||
```
|
||||
|
||||
### Module Types
|
||||
|
||||
#### 1. BasicInfoNode (Blue)
|
||||
**Purpose:** Configure core NPC properties
|
||||
**Inputs:** None (root node)
|
||||
**Outputs:** Basic info data
|
||||
**Fields:**
|
||||
- Resource Identifier
|
||||
- Names (comma-separated)
|
||||
- Model Scale
|
||||
- Aspects
|
||||
- Behavior flags (invulnerable, persistent, movable, etc.)
|
||||
|
||||
#### 2. BattleConfigNode (Red)
|
||||
**Purpose:** Set up battle-related configurations
|
||||
**Inputs:** Connection from any module
|
||||
**Outputs:** Battle config data
|
||||
**Fields:**
|
||||
- Can Battle / Can Challenge toggles
|
||||
- Battle Theme
|
||||
- Victory Theme
|
||||
- Defeat Theme
|
||||
- Simultaneous Battles option
|
||||
- Heal After Battle option
|
||||
|
||||
#### 3. PartyNode (Green)
|
||||
**Purpose:** Configure Pokemon party
|
||||
**Inputs:** Connection from any module
|
||||
**Outputs:** Party data
|
||||
**Fields:**
|
||||
- Party Type selector (Simple/Pool/Script)
|
||||
- Pokemon list with add/remove
|
||||
- Static Party toggle
|
||||
**Features:**
|
||||
- Add/remove Pokemon
|
||||
- Inline Pokemon string editing
|
||||
- Support for different party provider types
|
||||
|
||||
#### 4. InteractionNode (Purple)
|
||||
**Purpose:** Define NPC interaction behavior
|
||||
**Inputs:** Connection from any module
|
||||
**Outputs:** Interaction data
|
||||
**Fields:**
|
||||
- Interaction Type (None/Dialogue/Script/Shop/Heal)
|
||||
- Dialogue Reference (for dialogue type)
|
||||
- Script Reference (for script type)
|
||||
|
||||
#### 5. VariablesNode (Yellow)
|
||||
**Purpose:** Manage MoLang configuration variables
|
||||
**Inputs:** Connection from any module
|
||||
**Outputs:** Variables data
|
||||
**Fields:**
|
||||
- Variable list with add/remove
|
||||
- Per-variable configuration:
|
||||
- Variable Name
|
||||
- Display Name
|
||||
- Type (Number/Text/Boolean)
|
||||
- Default Value
|
||||
|
||||
#### 6. OutputNode (Indigo)
|
||||
**Purpose:** Preview and export final NPC JSON
|
||||
**Inputs:** Collects data from all modules
|
||||
**Outputs:** None (terminal node)
|
||||
**Features:**
|
||||
- NPC summary view
|
||||
- Live JSON preview
|
||||
- Download JSON button
|
||||
- Character count display
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
BasicInfoNode ─┐
|
||||
├─► OutputNode (JSON Preview)
|
||||
BattleNode ────┤
|
||||
├─► [Collects all module data]
|
||||
PartyNode ─────┤
|
||||
├─► [Generates final NPC config]
|
||||
InteractionNode┤
|
||||
│
|
||||
VariablesNode ─┘
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
#### NodeCanvas.tsx
|
||||
The main workspace component that:
|
||||
- Manages the React Flow instance
|
||||
- Handles node and edge state
|
||||
- Provides "Add Module" functionality
|
||||
- Aggregates node data into final NPC configuration
|
||||
- Updates OutputNode preview in real-time
|
||||
- Renders control panels and minimap
|
||||
|
||||
**Key Features:**
|
||||
- Drag-and-drop node positioning
|
||||
- Visual connections between modules
|
||||
- Module palette for adding new nodes
|
||||
- Real-time configuration updates
|
||||
- Responsive design
|
||||
|
||||
#### Individual Node Components
|
||||
Each node module follows a consistent pattern:
|
||||
- Collapsible header with icon and color coding
|
||||
- Input/output handles for connections
|
||||
- Expandable/collapsible content area
|
||||
- Form fields for configuration
|
||||
- Real-time data updates
|
||||
|
||||
**Design Principles:**
|
||||
- Self-contained logic
|
||||
- Minimal external dependencies
|
||||
- Consistent UI patterns
|
||||
- Color-coded by category
|
||||
- Icon-based identification
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Basic Workflow
|
||||
|
||||
1. **Start with BasicInfoNode** (automatically created)
|
||||
- Set NPC name and identifier
|
||||
- Configure basic properties
|
||||
|
||||
2. **Add Required Modules** via "Add Module" button
|
||||
- Click desired module from palette
|
||||
- Module appears on canvas
|
||||
|
||||
3. **Configure Each Module**
|
||||
- Click to expand/collapse
|
||||
- Fill in relevant fields
|
||||
- Data updates automatically
|
||||
|
||||
4. **Connect Modules** (optional, visual only)
|
||||
- Drag from output handle to input handle
|
||||
- Creates visual relationship
|
||||
|
||||
5. **Preview Output** in OutputNode
|
||||
- View NPC summary
|
||||
- Check JSON preview
|
||||
- Download when ready
|
||||
|
||||
### Adding New Module Types
|
||||
|
||||
To add a new module type:
|
||||
|
||||
1. **Define types** in `src/types/nodes.ts`:
|
||||
```typescript
|
||||
export interface NewModuleNodeData {
|
||||
type: 'newModule';
|
||||
field1: string;
|
||||
field2: number;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Create component** in `src/components/nodes/NewModuleNode.tsx`:
|
||||
```typescript
|
||||
export const NewModuleNode = memo(({ data }: NodeProps<NewModuleNodeData>) => {
|
||||
// Implementation
|
||||
});
|
||||
```
|
||||
|
||||
3. **Register in NodeCanvas.tsx**:
|
||||
```typescript
|
||||
const nodeTypes: NodeTypes = {
|
||||
// ... existing types
|
||||
newModule: NewModuleNode,
|
||||
};
|
||||
```
|
||||
|
||||
4. **Add to palette** in NodeCanvas.tsx:
|
||||
```typescript
|
||||
{
|
||||
type: 'newModule',
|
||||
label: 'New Module',
|
||||
description: 'Description here',
|
||||
color: 'bg-color-500',
|
||||
icon: '🎨',
|
||||
}
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### Current Approach
|
||||
- React Flow manages node positions and connections
|
||||
- Each node stores its own data in `node.data`
|
||||
- NodeCanvas aggregates node data into final NPC config
|
||||
- Parent App.tsx maintains final state
|
||||
|
||||
### Data Updates
|
||||
- Node changes trigger immediate updates
|
||||
- NodeCanvas effect listens for node changes
|
||||
- Aggregates all node data into NPCConfiguration
|
||||
- Updates OutputNode preview automatically
|
||||
- Parent receives final config via callback
|
||||
|
||||
## Extensibility
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
1. **Dialogue Editor Module**
|
||||
- Visual dialogue tree within a node
|
||||
- Inline page editing
|
||||
- Speaker management
|
||||
|
||||
2. **Advanced Party Builder**
|
||||
- Pokemon selector modal
|
||||
- Visual Pokemon cards
|
||||
- Type-based team generation
|
||||
|
||||
3. **Quest Module**
|
||||
- Quest objectives
|
||||
- Reward configuration
|
||||
- Quest chain visualization
|
||||
|
||||
4. **AI Behavior Module**
|
||||
- Pathfinding settings
|
||||
- Custom behaviors
|
||||
- Schedule configuration
|
||||
|
||||
5. **Import/Export Module**
|
||||
- Load from datapack
|
||||
- Save workflow templates
|
||||
- Share configurations
|
||||
|
||||
### API Integration Opportunities
|
||||
- Pokemon data from PokeAPI
|
||||
- Cobblemon mod documentation links
|
||||
- Community preset sharing
|
||||
- Validation against mod version
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- React Flow handles virtualization automatically
|
||||
- Memoized components prevent unnecessary re-renders
|
||||
- Debounced updates for text inputs (can be added)
|
||||
- Lazy loading for heavy modules (can be added)
|
||||
|
||||
## Styling
|
||||
|
||||
### Design System
|
||||
- Tailwind CSS for utility-first styling
|
||||
- Color-coded modules for easy identification:
|
||||
- Blue: Core settings
|
||||
- Red: Battle/Combat
|
||||
- Green: Pokemon/Party
|
||||
- Purple: Interactions
|
||||
- Yellow: Configuration/Variables
|
||||
- Indigo: Output/Export
|
||||
|
||||
### Responsive Design
|
||||
- Nodes have minimum widths
|
||||
- Collapsible sections for small screens
|
||||
- Touch-friendly controls
|
||||
- Mobile-optimized panels
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Manual Testing Checklist
|
||||
- [ ] Can add all module types
|
||||
- [ ] Can delete nodes
|
||||
- [ ] Can connect nodes (visual)
|
||||
- [ ] Can collapse/expand nodes
|
||||
- [ ] Data persists when nodes are moved
|
||||
- [ ] OutputNode shows correct preview
|
||||
- [ ] Can download valid JSON
|
||||
- [ ] All form fields work correctly
|
||||
- [ ] Checkbox states persist
|
||||
- [ ] Number inputs validate properly
|
||||
|
||||
### Automated Testing (Future)
|
||||
- Unit tests for node components
|
||||
- Integration tests for data flow
|
||||
- E2E tests for complete workflows
|
||||
- Visual regression tests for UI
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### For Users
|
||||
- Old tab-based view is still accessible (Classic mode)
|
||||
- Existing save files remain compatible
|
||||
- New modular view is opt-in
|
||||
- Can switch between modes
|
||||
|
||||
### For Developers
|
||||
- Old components still exist in codebase
|
||||
- New modular system is additive
|
||||
- Can maintain both systems
|
||||
- Gradual migration path
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Connection Logic**: Connections are currently visual only and don't enforce data flow
|
||||
2. **Undo/Redo**: Not yet implemented
|
||||
3. **Copy/Paste Nodes**: Not yet implemented
|
||||
4. **Node Templates**: Can't save/load node configurations
|
||||
5. **Mobile Experience**: Best on desktop/tablet
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Nodes not appearing:**
|
||||
- Check if nodeTypes are registered in NodeCanvas
|
||||
- Verify component import paths
|
||||
- Check for TypeScript errors
|
||||
|
||||
**Data not updating:**
|
||||
- Ensure node data is being mutated correctly
|
||||
- Check useEffect dependencies in NodeCanvas
|
||||
- Verify onChange callbacks are called
|
||||
|
||||
**Styling issues:**
|
||||
- Ensure @xyflow/react styles are imported
|
||||
- Check Tailwind CSS configuration
|
||||
- Verify color classes exist
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features:
|
||||
1. Follow existing node component patterns
|
||||
2. Use TypeScript for type safety
|
||||
3. Add appropriate color coding
|
||||
4. Include descriptions in module palette
|
||||
5. Test with various configurations
|
||||
6. Update this documentation
|
||||
|
||||
## Resources
|
||||
|
||||
- [React Flow Documentation](https://reactflow.dev/)
|
||||
- [Cobblemon Mod Documentation](https://wiki.cobblemon.com/)
|
||||
- [Tailwind CSS Documentation](https://tailwindcss.com/)
|
||||
- [TypeScript Documentation](https://www.typescriptlang.org/)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The modular node-based architecture provides a more intuitive and scalable approach to NPC creation. By leveraging React Flow's powerful features and maintaining clean separation of concerns, the system is both user-friendly and developer-friendly. The visual nature of the interface makes complex configurations more approachable while maintaining the full power of the underlying Cobblemon NPC system.
|
||||
303
package-lock.json
generated
303
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@xyflow/react": "^12.9.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.539.0",
|
||||
@@ -90,6 +91,7 @@
|
||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -1444,6 +1446,55 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-drag": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-selection": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-transition": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-zoom": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -1468,8 +1519,9 @@
|
||||
"version": "19.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -1530,6 +1582,7 @@
|
||||
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
@@ -1776,12 +1829,45 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.9.1.tgz",
|
||||
"integrity": "sha512-JRPCT5p7NnPdVSIh15AFvUSSm+8GUyz2I6iuBEC1LG2lKgig/L48AM/ImMHCc3ZUCg+AgTOJDaX2fcRyPA9BTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.72",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.72",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.72.tgz",
|
||||
"integrity": "sha512-WBI5Aau0fXTXwxHPzceLNS6QdXggSWnGjDtj/gG669crApN8+SCmEtkBth1m7r6pStNo/5fI9McEi7Dk0ymCLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-drag": "^3.0.7",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-selection": "^3.0.10",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/d3-zoom": "^3.0.8",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1974,6 +2060,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001733",
|
||||
"electron-to-chromium": "^1.5.199",
|
||||
@@ -2080,6 +2167,12 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/classcat": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2156,9 +2249,115 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
@@ -2191,7 +2390,6 @@
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -2298,6 +2496,7 @@
|
||||
"integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -2896,18 +3095,6 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -3018,37 +3205,6 @@
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-darwin-arm64": "1.30.1",
|
||||
"lightningcss-darwin-x64": "1.30.1",
|
||||
"lightningcss-freebsd-x64": "1.30.1",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.30.1",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.1",
|
||||
"lightningcss-linux-arm64-musl": "1.30.1",
|
||||
"lightningcss-linux-x64-gnu": "1.30.1",
|
||||
"lightningcss-linux-x64-musl": "1.30.1",
|
||||
"lightningcss-win32-arm64-msvc": "1.30.1",
|
||||
"lightningcss-win32-x64-msvc": "1.30.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
|
||||
@@ -3062,7 +3218,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -3084,7 +3239,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -3106,7 +3260,6 @@
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -3128,7 +3281,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -3150,7 +3302,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -3172,7 +3323,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -3194,7 +3344,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -3216,7 +3365,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -3238,7 +3386,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -3260,7 +3407,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -3645,6 +3791,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -3819,6 +3966,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -3828,6 +3976,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -4220,6 +4369,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -4320,6 +4470,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4377,6 +4528,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -4450,6 +4602,15 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -4462,6 +4623,7 @@
|
||||
"integrity": "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.6",
|
||||
@@ -4552,6 +4714,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4703,6 +4866,34 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@xyflow/react": "^12.9.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.539.0",
|
||||
|
||||
224
src/App.tsx
224
src/App.tsx
@@ -1,20 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Settings, MessageSquare, Sword, Users, Code, FileText, Upload } from 'lucide-react';
|
||||
import type { NPCConfiguration, DialogueConfiguration } from './types/npc';
|
||||
import { NPCBasicSettings } from './components/NPCBasicSettings';
|
||||
import { NPCBattleConfiguration } from './components/NPCBattleConfiguration';
|
||||
import { NPCPartyBuilder } from './components/NPCPartyBuilder';
|
||||
import { NPCInteractionEditor } from './components/NPCInteractionEditor';
|
||||
import { ConfigVariablesEditor } from './components/ConfigVariablesEditor';
|
||||
import { DialogueEditor } from './components/DialogueEditor';
|
||||
import { JSONPreview } from './components/JSONPreview';
|
||||
import { ImportExport } from './components/ImportExport';
|
||||
|
||||
type Tab = 'basic' | 'battle' | 'party' | 'interaction' | 'variables' | 'dialogue' | 'preview' | 'import';
|
||||
import { useState } from 'react';
|
||||
import { Sparkles, Grid } from 'lucide-react';
|
||||
import type { NPCConfiguration } from './types/npc';
|
||||
import { NodeCanvas } from './components/NodeCanvas';
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('basic');
|
||||
|
||||
const [viewMode, setViewMode] = useState<'modular' | 'classic'>('modular');
|
||||
|
||||
const [npcConfig, setNpcConfig] = useState<NPCConfiguration>({
|
||||
id: 'my_npc',
|
||||
name: 'My NPC',
|
||||
@@ -26,173 +17,74 @@ function App() {
|
||||
resourceIdentifier: 'cobblemon:my_npc'
|
||||
});
|
||||
|
||||
const [dialogueConfig, setDialogueConfig] = useState<DialogueConfiguration | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (npcConfig.interactions && npcConfig.interactions.length > 0 && npcConfig.interactions[0].type === 'dialogue' && !dialogueConfig) {
|
||||
const newDialogue: DialogueConfiguration = {
|
||||
speakers: {
|
||||
npc: {
|
||||
name: { type: 'expression', expression: 'q.npc.name' },
|
||||
face: 'q.npc.face(false);'
|
||||
},
|
||||
player: {
|
||||
name: { type: 'expression', expression: 'q.player.username' },
|
||||
face: 'q.player.face();'
|
||||
}
|
||||
},
|
||||
pages: [{
|
||||
id: 'greeting',
|
||||
speaker: 'npc',
|
||||
lines: ['Hello there!'],
|
||||
input: 'q.dialogue.close();'
|
||||
}]
|
||||
};
|
||||
setDialogueConfig(newDialogue);
|
||||
}
|
||||
}, [npcConfig.interactions, dialogueConfig]);
|
||||
|
||||
const tabs = [
|
||||
{ id: 'basic', name: 'Basic Settings', icon: Settings },
|
||||
{ id: 'battle', name: 'Battle Config', icon: Sword },
|
||||
{ id: 'party', name: 'Pokemon Party', icon: Users },
|
||||
{ id: 'interaction', name: 'Interaction', icon: MessageSquare },
|
||||
{ id: 'variables', name: 'Variables', icon: Code },
|
||||
{ id: 'dialogue', name: 'Dialogue', icon: MessageSquare },
|
||||
{ id: 'preview', name: 'JSON Preview', icon: FileText },
|
||||
{ id: 'import', name: 'Import/Export', icon: Upload }
|
||||
];
|
||||
|
||||
const renderActiveTab = () => {
|
||||
switch (activeTab) {
|
||||
case 'basic':
|
||||
return <NPCBasicSettings config={npcConfig} onChange={setNpcConfig} />;
|
||||
case 'battle':
|
||||
return <NPCBattleConfiguration config={npcConfig} onChange={setNpcConfig} />;
|
||||
case 'party':
|
||||
return <NPCPartyBuilder config={npcConfig} onChange={setNpcConfig} />;
|
||||
case 'interaction':
|
||||
return <NPCInteractionEditor config={npcConfig} onChange={setNpcConfig} />;
|
||||
case 'variables':
|
||||
return <ConfigVariablesEditor config={npcConfig} onChange={setNpcConfig} />;
|
||||
case 'dialogue':
|
||||
return <DialogueEditor dialogue={dialogueConfig} onChange={setDialogueConfig} />;
|
||||
case 'preview':
|
||||
return <JSONPreview npcConfig={npcConfig} dialogueConfig={dialogueConfig} />;
|
||||
case 'import':
|
||||
return (
|
||||
<ImportExport
|
||||
npcConfigs={[npcConfig]}
|
||||
dialogueConfiguration={dialogueConfig}
|
||||
onImport={(configs) => {
|
||||
if (configs.length > 0) {
|
||||
setNpcConfig(configs[0]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm border-b border-gray-200">
|
||||
<header className="bg-white shadow-sm border-b border-gray-200 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<h1 className="text-xl font-bold text-gray-900">Cobblemon NPC Creator</h1>
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center gap-2 px-3 py-1 bg-gradient-to-r from-indigo-500 to-purple-500 text-white text-xs font-medium rounded-full">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
Modular Edition
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
Create and customize NPCs for your Cobblemon mod
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-sm text-gray-500 hidden md:block">
|
||||
Build NPCs with connectable modules
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setViewMode('modular')}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
||||
viewMode === 'modular'
|
||||
? 'bg-white text-indigo-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
Modular
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('classic')}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
||||
viewMode === 'classic'
|
||||
? 'bg-white text-indigo-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
disabled
|
||||
title="Classic view coming soon"
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<Grid className="h-3 w-3" />
|
||||
Classic
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="lg:grid lg:grid-cols-12 lg:gap-8">
|
||||
{/* Sidebar Navigation */}
|
||||
<div className="lg:col-span-3">
|
||||
<nav className="space-y-1">
|
||||
{tabs.map(tab => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as Tab)}
|
||||
className={`w-full group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
isActive
|
||||
? 'bg-indigo-100 text-indigo-700 border-r-2 border-indigo-500'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
className={`flex-shrink-0 -ml-1 mr-3 h-5 w-5 ${
|
||||
isActive ? 'text-indigo-500' : 'text-gray-400 group-hover:text-gray-500'
|
||||
}`}
|
||||
/>
|
||||
{tab.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Quick Info Panel */}
|
||||
<div className="mt-8 bg-white rounded-lg shadow p-4">
|
||||
<h3 className="text-sm font-medium text-gray-900 mb-3">Current NPC</h3>
|
||||
<dl className="space-y-2 text-xs">
|
||||
<div>
|
||||
<dt className="font-medium text-gray-700">Name</dt>
|
||||
<dd className="text-gray-600">{npcConfig.names[0] || 'Unnamed'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-700">Type</dt>
|
||||
<dd className="text-gray-600 capitalize">
|
||||
{npcConfig.battleConfiguration?.canChallenge ? 'Trainer' : 'NPC'}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-700">Interaction</dt>
|
||||
<dd className="text-gray-600 capitalize">
|
||||
{npcConfig.interactions && npcConfig.interactions.length > 0
|
||||
? npcConfig.interactions[0].type
|
||||
: 'none'}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-700">Variables</dt>
|
||||
<dd className="text-gray-600">{npcConfig.config?.length || 0}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="mt-8 lg:mt-0 lg:col-span-9">
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-6 py-8">
|
||||
{renderActiveTab()}
|
||||
</div>
|
||||
</div>
|
||||
{/* Main Content - Full Screen Canvas */}
|
||||
{viewMode === 'modular' ? (
|
||||
<NodeCanvas
|
||||
npcConfig={npcConfig}
|
||||
onConfigChange={setNpcConfig}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Classic View</h2>
|
||||
<p className="text-gray-600">Coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-white border-t border-gray-200 mt-12">
|
||||
<div className="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
Built for the Cobblemon Minecraft mod - Create amazing NPCs for your world!
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
388
src/components/NodeCanvas.tsx
Normal file
388
src/components/NodeCanvas.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
addEdge,
|
||||
Panel,
|
||||
} from '@xyflow/react';
|
||||
import type { Connection, NodeTypes } from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { Plus, Sparkles } from 'lucide-react';
|
||||
import type { NPCConfiguration } from '../types/npc';
|
||||
import type { ModularNode, NodeType } from '../types/nodes';
|
||||
import { BasicInfoNode } from './nodes/BasicInfoNode';
|
||||
import { BattleConfigNode } from './nodes/BattleConfigNode';
|
||||
import { PartyNode } from './nodes/PartyNode';
|
||||
import { InteractionNode } from './nodes/InteractionNode';
|
||||
import { VariablesNode } from './nodes/VariablesNode';
|
||||
import { OutputNode } from './nodes/OutputNode';
|
||||
|
||||
interface NodeCanvasProps {
|
||||
npcConfig: NPCConfiguration;
|
||||
onConfigChange: (config: NPCConfiguration) => void;
|
||||
}
|
||||
|
||||
// Define custom node types for React Flow
|
||||
const nodeTypes: NodeTypes = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
basicInfo: BasicInfoNode as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
battleConfig: BattleConfigNode as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
party: PartyNode as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
interaction: InteractionNode as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
variables: VariablesNode as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
output: OutputNode as any,
|
||||
};
|
||||
|
||||
export function NodeCanvas({
|
||||
npcConfig,
|
||||
onConfigChange,
|
||||
}: NodeCanvasProps) {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<ModularNode>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const [showNodeMenu, setShowNodeMenu] = useState(false);
|
||||
|
||||
// Initialize with a basic setup
|
||||
useEffect(() => {
|
||||
const initialNodes: ModularNode[] = [
|
||||
{
|
||||
id: 'basic-1',
|
||||
type: 'basicInfo',
|
||||
position: { x: 50, y: 100 },
|
||||
data: {
|
||||
type: 'basicInfo',
|
||||
resourceIdentifier: npcConfig.resourceIdentifier || 'cobblemon:my_npc',
|
||||
names: npcConfig.names || ['My NPC'],
|
||||
hitbox: npcConfig.hitbox || 'player',
|
||||
modelScale: npcConfig.modelScale,
|
||||
aspects: npcConfig.aspects,
|
||||
isInvulnerable: npcConfig.isInvulnerable,
|
||||
canDespawn: npcConfig.canDespawn,
|
||||
isMovable: npcConfig.isMovable,
|
||||
isLeashable: npcConfig.isLeashable,
|
||||
allowProjectileHits: npcConfig.allowProjectileHits,
|
||||
hideNameTag: npcConfig.hideNameTag,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'output-1',
|
||||
type: 'output',
|
||||
position: { x: 800, y: 250 },
|
||||
data: {
|
||||
type: 'output',
|
||||
previewData: npcConfig,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
setNodes(initialNodes);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Update NPC config when nodes change
|
||||
useEffect(() => {
|
||||
// Skip if no nodes yet
|
||||
if (nodes.length === 0) return;
|
||||
|
||||
const newConfig = { ...npcConfig };
|
||||
|
||||
nodes.forEach((node) => {
|
||||
switch (node.data.type) {
|
||||
case 'basicInfo': {
|
||||
const data = node.data;
|
||||
newConfig.resourceIdentifier = data.resourceIdentifier;
|
||||
newConfig.names = data.names;
|
||||
if (typeof data.hitbox === 'string' || (typeof data.hitbox === 'object' && data.hitbox !== null)) {
|
||||
newConfig.hitbox = data.hitbox as "player" | { width: number; height: number };
|
||||
}
|
||||
newConfig.modelScale = data.modelScale;
|
||||
newConfig.aspects = data.aspects;
|
||||
newConfig.isInvulnerable = data.isInvulnerable;
|
||||
newConfig.canDespawn = data.canDespawn;
|
||||
newConfig.isMovable = data.isMovable;
|
||||
newConfig.isLeashable = data.isLeashable;
|
||||
newConfig.allowProjectileHits = data.allowProjectileHits;
|
||||
newConfig.hideNameTag = data.hideNameTag;
|
||||
break;
|
||||
}
|
||||
case 'battleConfig': {
|
||||
const data = node.data;
|
||||
newConfig.battleConfiguration = {
|
||||
canBattle: data.canBattle,
|
||||
canChallenge: data.canChallenge,
|
||||
battleTheme: data.battleTheme,
|
||||
victoryTheme: data.victoryTheme,
|
||||
defeatTheme: data.defeatTheme,
|
||||
simultaneousBattles: data.simultaneousBattles,
|
||||
healAfterwards: data.healAfterwards,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'party': {
|
||||
const data = node.data;
|
||||
newConfig.party = data.partyConfig;
|
||||
break;
|
||||
}
|
||||
case 'interaction': {
|
||||
const data = node.data;
|
||||
newConfig.interactions = [{
|
||||
type: data.interactionType,
|
||||
data: data.interactionData,
|
||||
dialogue: data.dialogueReference,
|
||||
script: data.scriptReference,
|
||||
}];
|
||||
break;
|
||||
}
|
||||
case 'variables': {
|
||||
const data = node.data;
|
||||
newConfig.config = data.configVariables;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update parent config
|
||||
onConfigChange(newConfig);
|
||||
|
||||
// Update output node preview with the new config
|
||||
// Check if output node exists and if its preview data needs updating
|
||||
const outputNode = nodes.find(n => n.data.type === 'output');
|
||||
if (outputNode && JSON.stringify(outputNode.data.previewData) !== JSON.stringify(newConfig)) {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) =>
|
||||
node.data.type === 'output'
|
||||
? { ...node, data: { ...node.data, previewData: newConfig } }
|
||||
: node
|
||||
)
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodes]);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const addNode = (type: NodeType) => {
|
||||
const newNodeId = `${type}-${Date.now()}`;
|
||||
const position = {
|
||||
x: Math.random() * 400 + 100,
|
||||
y: Math.random() * 300 + 100,
|
||||
};
|
||||
|
||||
let newNode: ModularNode;
|
||||
|
||||
switch (type) {
|
||||
case 'basicInfo':
|
||||
newNode = {
|
||||
id: newNodeId,
|
||||
type: 'basicInfo',
|
||||
position,
|
||||
data: {
|
||||
type: 'basicInfo',
|
||||
resourceIdentifier: 'cobblemon:my_npc',
|
||||
names: ['My NPC'],
|
||||
hitbox: 'player',
|
||||
},
|
||||
};
|
||||
break;
|
||||
case 'battleConfig':
|
||||
newNode = {
|
||||
id: newNodeId,
|
||||
type: 'battleConfig',
|
||||
position,
|
||||
data: {
|
||||
type: 'battleConfig',
|
||||
canBattle: false,
|
||||
canChallenge: false,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case 'party':
|
||||
newNode = {
|
||||
id: newNodeId,
|
||||
type: 'party',
|
||||
position,
|
||||
data: {
|
||||
type: 'party',
|
||||
partyConfig: { type: 'simple', pokemon: [''] },
|
||||
},
|
||||
};
|
||||
break;
|
||||
case 'interaction':
|
||||
newNode = {
|
||||
id: newNodeId,
|
||||
type: 'interaction',
|
||||
position,
|
||||
data: {
|
||||
type: 'interaction',
|
||||
interactionType: 'none',
|
||||
},
|
||||
};
|
||||
break;
|
||||
case 'variables':
|
||||
newNode = {
|
||||
id: newNodeId,
|
||||
type: 'variables',
|
||||
position,
|
||||
data: {
|
||||
type: 'variables',
|
||||
configVariables: [],
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
setNodes((nds) => [...nds, newNode]);
|
||||
setShowNodeMenu(false);
|
||||
};
|
||||
|
||||
const nodeCategories = [
|
||||
{
|
||||
name: 'Core',
|
||||
nodes: [
|
||||
{
|
||||
type: 'basicInfo' as NodeType,
|
||||
label: 'Basic Info',
|
||||
description: 'NPC name, model, and basic properties',
|
||||
color: 'bg-blue-500',
|
||||
icon: '⚙️',
|
||||
},
|
||||
{
|
||||
type: 'battleConfig' as NodeType,
|
||||
label: 'Battle Config',
|
||||
description: 'Battle settings and themes',
|
||||
color: 'bg-red-500',
|
||||
icon: '⚔️',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Content',
|
||||
nodes: [
|
||||
{
|
||||
type: 'party' as NodeType,
|
||||
label: 'Pokemon Party',
|
||||
description: 'Configure NPC Pokemon team',
|
||||
color: 'bg-green-500',
|
||||
icon: '👥',
|
||||
},
|
||||
{
|
||||
type: 'interaction' as NodeType,
|
||||
label: 'Interaction',
|
||||
description: 'Set up NPC interactions',
|
||||
color: 'bg-purple-500',
|
||||
icon: '💬',
|
||||
},
|
||||
{
|
||||
type: 'variables' as NodeType,
|
||||
label: 'Variables',
|
||||
description: 'MoLang config variables',
|
||||
color: 'bg-yellow-500',
|
||||
icon: '🔢',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen bg-gray-50">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
className="bg-gray-50"
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<MiniMap />
|
||||
|
||||
{/* Top control panel */}
|
||||
<Panel position="top-left" className="bg-white rounded-lg shadow-lg p-3 m-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-indigo-600" />
|
||||
<h2 className="font-semibold text-gray-900">Modular NPC Builder</h2>
|
||||
</div>
|
||||
<div className="h-6 w-px bg-gray-300" />
|
||||
<button
|
||||
onClick={() => setShowNodeMenu(!showNodeMenu)}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 transition-colors"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Module
|
||||
</button>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
{/* Node menu */}
|
||||
{showNodeMenu && (
|
||||
<Panel position="top-center" className="bg-white rounded-lg shadow-xl p-4 m-4 max-w-2xl">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold text-gray-900">Add Module</h3>
|
||||
<button
|
||||
onClick={() => setShowNodeMenu(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{nodeCategories.map((category) => (
|
||||
<div key={category.name}>
|
||||
<h4 className="text-xs font-semibold text-gray-500 uppercase mb-2">
|
||||
{category.name}
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{category.nodes.map((node) => (
|
||||
<button
|
||||
key={node.type}
|
||||
onClick={() => addNode(node.type)}
|
||||
className="flex items-start gap-3 p-3 text-left border rounded-lg hover:border-indigo-500 hover:bg-indigo-50 transition-all"
|
||||
>
|
||||
<div className={`w-8 h-8 ${node.color} rounded-lg flex items-center justify-center text-white text-lg flex-shrink-0`}>
|
||||
{node.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900 text-sm">
|
||||
{node.label}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{node.description}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{/* Info panel */}
|
||||
<Panel position="bottom-right" className="bg-white rounded-lg shadow-lg p-3 m-4 text-xs text-gray-600">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
<span>{nodes.length} modules connected</span>
|
||||
</div>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
src/components/nodes/BasicInfoNode.tsx
Normal file
148
src/components/nodes/BasicInfoNode.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import type { NodeProps } from '@xyflow/react';
|
||||
import { Settings, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { BasicInfoNodeData } from '../../types/nodes';
|
||||
|
||||
export const BasicInfoNode = memo(({ data }: NodeProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const nodeData = data as BasicInfoNodeData;
|
||||
|
||||
const handleChange = (field: keyof BasicInfoNodeData, value: string | number | boolean | string[] | undefined | { width: number; height: number }) => {
|
||||
// Update node data - in a real implementation, this would use a store or context
|
||||
// For now, we'll use data mutation which React Flow handles
|
||||
Object.assign(nodeData, { [field]: value });
|
||||
};
|
||||
|
||||
const handleNamesChange = (names: string) => {
|
||||
handleChange('names', names.split(',').map(name => name.trim()).filter(name => name));
|
||||
};
|
||||
|
||||
const handleAspectsChange = (aspects: string) => {
|
||||
handleChange('aspects', aspects ? aspects.split(',').map(aspect => aspect.trim()).filter(aspect => aspect) : undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-xl border-2 border-blue-500 min-w-[350px]">
|
||||
{/* Node Header */}
|
||||
<div className="bg-blue-500 text-white px-4 py-2 rounded-t-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="font-semibold text-sm">Basic Info</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-white hover:bg-blue-600 rounded p-1 transition-colors"
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Node Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Resource Identifier
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nodeData.resourceIdentifier}
|
||||
onChange={(e) => handleChange('resourceIdentifier', e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="cobblemon:npc_name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Names (comma-separated)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nodeData.names.join(', ')}
|
||||
onChange={(e) => handleNamesChange(e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="NPC Name 1, NPC Name 2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Model Scale</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={nodeData.modelScale || 0.9375}
|
||||
onChange={(e) => handleChange('modelScale', parseFloat(e.target.value))}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Aspects (comma-separated)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nodeData.aspects?.join(', ') || ''}
|
||||
onChange={(e) => handleAspectsChange(e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="aspect1, aspect2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-3">
|
||||
<div className="text-xs font-medium text-gray-700 mb-2">Behavior</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<label className="flex items-center text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={nodeData.isInvulnerable || false}
|
||||
onChange={(e) => handleChange('isInvulnerable', e.target.checked)}
|
||||
className="mr-1.5 h-3 w-3 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
Invulnerable
|
||||
</label>
|
||||
<label className="flex items-center text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!(nodeData.canDespawn ?? true)}
|
||||
onChange={(e) => handleChange('canDespawn', !e.target.checked)}
|
||||
className="mr-1.5 h-3 w-3 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
Persistent
|
||||
</label>
|
||||
<label className="flex items-center text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={nodeData.isMovable ?? true}
|
||||
onChange={(e) => handleChange('isMovable', e.target.checked)}
|
||||
className="mr-1.5 h-3 w-3 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
Movable
|
||||
</label>
|
||||
<label className="flex items-center text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={nodeData.hideNameTag || false}
|
||||
onChange={(e) => handleChange('hideNameTag', e.target.checked)}
|
||||
className="mr-1.5 h-3 w-3 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
Hide Nametag
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-blue-500 border-2 border-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
BasicInfoNode.displayName = 'BasicInfoNode';
|
||||
137
src/components/nodes/BattleConfigNode.tsx
Normal file
137
src/components/nodes/BattleConfigNode.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import type { NodeProps } from '@xyflow/react';
|
||||
import { Sword, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { BattleConfigNodeData } from '../../types/nodes';
|
||||
|
||||
export const BattleConfigNode = memo(({ data }: NodeProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const nodeData = data as BattleConfigNodeData;
|
||||
|
||||
const handleChange = (field: keyof BattleConfigNodeData, value: string | boolean | undefined) => {
|
||||
Object.assign(nodeData, { [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-xl border-2 border-red-500 min-w-[350px]">
|
||||
{/* Node Header */}
|
||||
<div className="bg-red-500 text-white px-4 py-2 rounded-t-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sword className="h-4 w-4" />
|
||||
<span className="font-semibold text-sm">Battle Configuration</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-white hover:bg-red-600 rounded p-1 transition-colors"
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-red-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Node Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={nodeData.canBattle || false}
|
||||
onChange={(e) => handleChange('canBattle', e.target.checked)}
|
||||
className="mr-2 h-4 w-4 text-red-600 rounded focus:ring-red-500"
|
||||
/>
|
||||
<span className="font-medium">Can Battle</span>
|
||||
</label>
|
||||
<label className="flex items-center text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={nodeData.canChallenge || false}
|
||||
onChange={(e) => handleChange('canChallenge', e.target.checked)}
|
||||
className="mr-2 h-4 w-4 text-red-600 rounded focus:ring-red-500"
|
||||
/>
|
||||
<span className="font-medium">Can Challenge</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Battle Theme
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nodeData.battleTheme || ''}
|
||||
onChange={(e) => handleChange('battleTheme', e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
placeholder="cobblemon:battle_theme"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Victory Theme
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nodeData.victoryTheme || ''}
|
||||
onChange={(e) => handleChange('victoryTheme', e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
placeholder="cobblemon:victory_theme"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Defeat Theme
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nodeData.defeatTheme || ''}
|
||||
onChange={(e) => handleChange('defeatTheme', e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
placeholder="cobblemon:defeat_theme"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-3">
|
||||
<div className="text-xs font-medium text-gray-700 mb-2">Options</div>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={nodeData.simultaneousBattles || false}
|
||||
onChange={(e) => handleChange('simultaneousBattles', e.target.checked)}
|
||||
className="mr-1.5 h-3 w-3 text-red-600 rounded focus:ring-red-500"
|
||||
/>
|
||||
Simultaneous Battles
|
||||
</label>
|
||||
<label className="flex items-center text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={nodeData.healAfterwards || false}
|
||||
onChange={(e) => handleChange('healAfterwards', e.target.checked)}
|
||||
className="mr-1.5 h-3 w-3 text-red-600 rounded focus:ring-red-500"
|
||||
/>
|
||||
Heal Party After Battle
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-red-500 border-2 border-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
BattleConfigNode.displayName = 'BattleConfigNode';
|
||||
120
src/components/nodes/InteractionNode.tsx
Normal file
120
src/components/nodes/InteractionNode.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import type { NodeProps } from '@xyflow/react';
|
||||
import { MessageSquare, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { InteractionNodeData } from '../../types/nodes';
|
||||
|
||||
export const InteractionNode = memo(({ data }: NodeProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const nodeData = data as InteractionNodeData;
|
||||
|
||||
const handleChange = (field: keyof InteractionNodeData, value: string | undefined) => {
|
||||
Object.assign(nodeData, { [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-xl border-2 border-purple-500 min-w-[350px]">
|
||||
{/* Node Header */}
|
||||
<div className="bg-purple-500 text-white px-4 py-2 rounded-t-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
<span className="font-semibold text-sm">Interaction</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-white hover:bg-purple-600 rounded p-1 transition-colors"
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-purple-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Node Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-2">
|
||||
Interaction Type
|
||||
</label>
|
||||
<select
|
||||
value={nodeData.interactionType}
|
||||
onChange={(e) => handleChange('interactionType', e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
>
|
||||
<option value="none">None</option>
|
||||
<option value="dialogue">Dialogue</option>
|
||||
<option value="script">Script</option>
|
||||
<option value="shop">Shop</option>
|
||||
<option value="heal">Heal</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{nodeData.interactionType === 'dialogue' && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Dialogue Reference
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nodeData.dialogueReference || ''}
|
||||
onChange={(e) => handleChange('dialogueReference', e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="cobblemon:my_dialogue"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Reference to a dialogue configuration
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeData.interactionType === 'script' && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Script Reference
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nodeData.scriptReference || ''}
|
||||
onChange={(e) => handleChange('scriptReference', e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="cobblemon:interaction_script"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
MoLang script for custom interactions
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeData.interactionType === 'none' && (
|
||||
<div className="text-xs text-gray-500 italic py-4 text-center">
|
||||
No interaction configured
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(nodeData.interactionType === 'shop' || nodeData.interactionType === 'heal') && (
|
||||
<div className="text-xs text-gray-500 italic py-2">
|
||||
{nodeData.interactionType === 'shop'
|
||||
? 'Shop interaction - configure items in data'
|
||||
: 'Heal interaction - heals player Pokemon'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-purple-500 border-2 border-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
InteractionNode.displayName = 'InteractionNode';
|
||||
124
src/components/nodes/OutputNode.tsx
Normal file
124
src/components/nodes/OutputNode.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import type { NodeProps } from '@xyflow/react';
|
||||
import { FileText, ChevronDown, ChevronUp, Download } from 'lucide-react';
|
||||
import type { OutputNodeData } from '../../types/nodes';
|
||||
|
||||
export const OutputNode = memo(({ data }: NodeProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const nodeData = data as OutputNodeData;
|
||||
|
||||
const downloadJSON = () => {
|
||||
if (!nodeData.previewData) return;
|
||||
|
||||
const jsonString = JSON.stringify(nodeData.previewData, null, 2);
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${nodeData.previewData.id || 'npc'}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-xl border-2 border-indigo-500 min-w-[400px] max-w-[500px]">
|
||||
{/* Node Header */}
|
||||
<div className="bg-indigo-500 text-white px-4 py-2 rounded-t-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
<span className="font-semibold text-sm">Output Preview</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={downloadJSON}
|
||||
className="text-white hover:bg-indigo-600 rounded p-1 transition-colors"
|
||||
title="Download JSON"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-white hover:bg-indigo-600 rounded p-1 transition-colors"
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-indigo-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Node Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200">
|
||||
<h4 className="text-xs font-semibold text-gray-700 mb-2">NPC Summary</h4>
|
||||
<dl className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600">Name:</dt>
|
||||
<dd className="font-medium text-gray-900">
|
||||
{nodeData.previewData?.names?.[0] || 'Unnamed'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600">ID:</dt>
|
||||
<dd className="font-medium text-gray-900">
|
||||
{nodeData.previewData?.id || 'N/A'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600">Battle:</dt>
|
||||
<dd className="font-medium text-gray-900">
|
||||
{nodeData.previewData?.battleConfiguration?.canChallenge
|
||||
? 'Enabled'
|
||||
: 'Disabled'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600">Party:</dt>
|
||||
<dd className="font-medium text-gray-900">
|
||||
{nodeData.previewData?.party?.type || 'None'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600">Variables:</dt>
|
||||
<dd className="font-medium text-gray-900">
|
||||
{nodeData.previewData?.config?.length || 0}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-xs font-semibold text-gray-700">JSON Preview</h4>
|
||||
<span className="text-xs text-gray-500">
|
||||
{JSON.stringify(nodeData.previewData).length} chars
|
||||
</span>
|
||||
</div>
|
||||
<pre className="bg-gray-900 text-green-400 rounded-lg p-3 text-xs overflow-x-auto max-h-96 overflow-y-auto font-mono">
|
||||
{JSON.stringify(nodeData.previewData, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={downloadJSON}
|
||||
className="w-full inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg text-white bg-indigo-600 hover:bg-indigo-700 transition-colors"
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Download JSON
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
OutputNode.displayName = 'OutputNode';
|
||||
184
src/components/nodes/PartyNode.tsx
Normal file
184
src/components/nodes/PartyNode.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import type { NodeProps } from '@xyflow/react';
|
||||
import { Users, Plus, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { PartyNodeData } from '../../types/nodes';
|
||||
import type { SimplePartyProvider, PartyProvider } from '../../types/npc';
|
||||
|
||||
export const PartyNode = memo(({ data }: NodeProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const nodeData = data as PartyNodeData;
|
||||
|
||||
const handleChange = (newPartyConfig: PartyProvider) => {
|
||||
Object.assign(nodeData, { partyConfig: newPartyConfig });
|
||||
};
|
||||
|
||||
const renderSimpleParty = () => {
|
||||
const party = nodeData.partyConfig as SimplePartyProvider;
|
||||
|
||||
if (!party || !party.pokemon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const addPokemon = () => {
|
||||
handleChange({
|
||||
...party,
|
||||
pokemon: [...party.pokemon, ''],
|
||||
});
|
||||
};
|
||||
|
||||
const removePokemon = (index: number) => {
|
||||
handleChange({
|
||||
...party,
|
||||
pokemon: party.pokemon.filter((_, i) => i !== index),
|
||||
});
|
||||
};
|
||||
|
||||
const updatePokemon = (index: number, value: string) => {
|
||||
const newPokemon = [...party.pokemon];
|
||||
newPokemon[index] = value;
|
||||
handleChange({
|
||||
...party,
|
||||
pokemon: newPokemon,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-700">
|
||||
Pokemon ({party.pokemon.length}/6)
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addPokemon}
|
||||
className="p-1 text-green-600 hover:bg-green-50 rounded transition-colors"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{party.pokemon.map((pokemon, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={pokemon}
|
||||
onChange={(e) => updatePokemon(index, e.target.value)}
|
||||
className="flex-1 px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||||
placeholder="pikachu level=50 shiny=true"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removePokemon(index)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label className="flex items-center text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={party.isStatic || false}
|
||||
onChange={(e) =>
|
||||
handleChange({ ...party, isStatic: e.target.checked })
|
||||
}
|
||||
className="mr-1.5 h-3 w-3 text-green-600 rounded focus:ring-green-500"
|
||||
/>
|
||||
Static Party
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-xl border-2 border-green-500 min-w-[350px]">
|
||||
{/* Node Header */}
|
||||
<div className="bg-green-500 text-white px-4 py-2 rounded-t-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="h-4 w-4" />
|
||||
<span className="font-semibold text-sm">Pokemon Party</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-white hover:bg-green-600 rounded p-1 transition-colors"
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-green-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Node Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-2">
|
||||
Party Type
|
||||
</label>
|
||||
<select
|
||||
value={nodeData.partyConfig?.type || 'simple'}
|
||||
onChange={(e) => {
|
||||
const type = e.target.value as 'simple' | 'pool' | 'script';
|
||||
if (type === 'simple') {
|
||||
handleChange({ type: 'simple', pokemon: [''] });
|
||||
} else if (type === 'pool') {
|
||||
handleChange({ type: 'pool', pool: [{ pokemon: '', weight: 1 }] });
|
||||
} else {
|
||||
handleChange({ type: 'script', script: '' });
|
||||
}
|
||||
}}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||||
>
|
||||
<option value="simple">Simple</option>
|
||||
<option value="pool">Pool</option>
|
||||
<option value="script">Script</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{nodeData.partyConfig?.type === 'simple' && renderSimpleParty()}
|
||||
|
||||
{nodeData.partyConfig?.type === 'pool' && (
|
||||
<div className="text-xs text-gray-500 italic">
|
||||
Pool configuration available - add Pokemon to the pool
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeData.partyConfig?.type === 'script' && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Script Resource
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={(nodeData.partyConfig as { type: 'script'; script: string }).script || ''}
|
||||
onChange={(e) =>
|
||||
handleChange({ type: 'script', script: e.target.value })
|
||||
}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||||
placeholder="cobblemon:party_script"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-green-500 border-2 border-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
PartyNode.displayName = 'PartyNode';
|
||||
211
src/components/nodes/VariablesNode.tsx
Normal file
211
src/components/nodes/VariablesNode.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import type { NodeProps } from '@xyflow/react';
|
||||
import { Code, Plus, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { VariablesNodeData } from '../../types/nodes';
|
||||
import type { MoLangConfigVariable } from '../../types/npc';
|
||||
|
||||
export const VariablesNode = memo(({ data }: NodeProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const nodeData = data as VariablesNodeData;
|
||||
|
||||
const handleChange = (variables: MoLangConfigVariable[]) => {
|
||||
Object.assign(nodeData, { configVariables: variables });
|
||||
};
|
||||
|
||||
const addVariable = () => {
|
||||
const newVariable: MoLangConfigVariable = {
|
||||
variableName: `var_${Date.now()}`,
|
||||
displayName: 'New Variable',
|
||||
description: 'Description',
|
||||
type: 'NUMBER',
|
||||
defaultValue: 0,
|
||||
};
|
||||
handleChange([...(nodeData.configVariables || []), newVariable]);
|
||||
};
|
||||
|
||||
const removeVariable = (index: number) => {
|
||||
const newVariables = [...(nodeData.configVariables || [])];
|
||||
newVariables.splice(index, 1);
|
||||
handleChange(newVariables);
|
||||
};
|
||||
|
||||
const updateVariable = (
|
||||
index: number,
|
||||
field: keyof MoLangConfigVariable,
|
||||
value: string | number | boolean
|
||||
) => {
|
||||
const newVariables = [...(nodeData.configVariables || [])];
|
||||
newVariables[index] = { ...newVariables[index], [field]: value };
|
||||
handleChange(newVariables);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-xl border-2 border-yellow-500 min-w-[350px]">
|
||||
{/* Node Header */}
|
||||
<div className="bg-yellow-500 text-white px-4 py-2 rounded-t-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="h-4 w-4" />
|
||||
<span className="font-semibold text-sm">Config Variables</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-white hover:bg-yellow-600 rounded p-1 transition-colors"
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-yellow-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Node Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-700">
|
||||
Variables ({nodeData.configVariables?.length || 0})
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addVariable}
|
||||
className="inline-flex items-center px-2 py-1 text-xs font-medium rounded text-yellow-700 bg-yellow-100 hover:bg-yellow-200 transition-colors"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-80 overflow-y-auto">
|
||||
{(!nodeData.configVariables || nodeData.configVariables.length === 0) && (
|
||||
<div className="text-xs text-gray-500 italic py-4 text-center">
|
||||
No variables defined
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeData.configVariables?.map((variable, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200 rounded-lg p-3 space-y-2 bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-gray-700">
|
||||
Variable {index + 1}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeVariable(index)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
||||
Variable Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={variable.variableName}
|
||||
onChange={(e) =>
|
||||
updateVariable(index, 'variableName', e.target.value)
|
||||
}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500"
|
||||
placeholder="my_variable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
||||
Display Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={variable.displayName}
|
||||
onChange={(e) =>
|
||||
updateVariable(index, 'displayName', e.target.value)
|
||||
}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500"
|
||||
placeholder="My Variable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
||||
Type
|
||||
</label>
|
||||
<select
|
||||
value={variable.type}
|
||||
onChange={(e) =>
|
||||
updateVariable(
|
||||
index,
|
||||
'type',
|
||||
e.target.value as 'NUMBER' | 'TEXT' | 'BOOLEAN'
|
||||
)
|
||||
}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500"
|
||||
>
|
||||
<option value="NUMBER">Number</option>
|
||||
<option value="TEXT">Text</option>
|
||||
<option value="BOOLEAN">Boolean</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
||||
Default Value
|
||||
</label>
|
||||
{variable.type === 'BOOLEAN' ? (
|
||||
<select
|
||||
value={variable.defaultValue.toString()}
|
||||
onChange={(e) =>
|
||||
updateVariable(
|
||||
index,
|
||||
'defaultValue',
|
||||
e.target.value === 'true'
|
||||
)
|
||||
}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500"
|
||||
>
|
||||
<option value="true">True</option>
|
||||
<option value="false">False</option>
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type={variable.type === 'NUMBER' ? 'number' : 'text'}
|
||||
value={String(variable.defaultValue)}
|
||||
onChange={(e) =>
|
||||
updateVariable(
|
||||
index,
|
||||
'defaultValue',
|
||||
variable.type === 'NUMBER'
|
||||
? parseFloat(e.target.value) || 0
|
||||
: e.target.value
|
||||
)
|
||||
}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-yellow-500 border-2 border-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
VariablesNode.displayName = 'VariablesNode';
|
||||
98
src/types/nodes.ts
Normal file
98
src/types/nodes.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { Node, Edge } from '@xyflow/react';
|
||||
import type { NPCConfiguration, DialogueConfiguration } from './npc';
|
||||
|
||||
// Define the types of nodes available in the modular system
|
||||
export type NodeType =
|
||||
| 'basicInfo'
|
||||
| 'battleConfig'
|
||||
| 'party'
|
||||
| 'interaction'
|
||||
| 'variables'
|
||||
| 'dialogue'
|
||||
| 'output';
|
||||
|
||||
// Data structure for each node type
|
||||
export interface BasicInfoNodeData extends Record<string, unknown> {
|
||||
type: 'basicInfo';
|
||||
resourceIdentifier: string;
|
||||
names: string[];
|
||||
hitbox: string | { width: number; height: number };
|
||||
modelScale?: number;
|
||||
aspects?: string[];
|
||||
isInvulnerable?: boolean;
|
||||
canDespawn?: boolean;
|
||||
isMovable?: boolean;
|
||||
isLeashable?: boolean;
|
||||
allowProjectileHits?: boolean;
|
||||
hideNameTag?: boolean;
|
||||
}
|
||||
|
||||
export interface BattleConfigNodeData extends Record<string, unknown> {
|
||||
type: 'battleConfig';
|
||||
canBattle?: boolean;
|
||||
canChallenge?: boolean;
|
||||
battleTheme?: string;
|
||||
victoryTheme?: string;
|
||||
defeatTheme?: string;
|
||||
simultaneousBattles?: boolean;
|
||||
healAfterwards?: boolean;
|
||||
}
|
||||
|
||||
export interface PartyNodeData extends Record<string, unknown> {
|
||||
type: 'party';
|
||||
partyConfig: NPCConfiguration['party'];
|
||||
}
|
||||
|
||||
export interface InteractionNodeData extends Record<string, unknown> {
|
||||
type: 'interaction';
|
||||
interactionType: string;
|
||||
interactionData?: Record<string, unknown>;
|
||||
dialogueReference?: string;
|
||||
scriptReference?: string;
|
||||
}
|
||||
|
||||
export interface VariablesNodeData extends Record<string, unknown> {
|
||||
type: 'variables';
|
||||
configVariables: NPCConfiguration['config'];
|
||||
}
|
||||
|
||||
export interface DialogueNodeData extends Record<string, unknown> {
|
||||
type: 'dialogue';
|
||||
dialogueConfig: DialogueConfiguration | null;
|
||||
}
|
||||
|
||||
export interface OutputNodeData extends Record<string, unknown> {
|
||||
type: 'output';
|
||||
previewData?: NPCConfiguration;
|
||||
}
|
||||
|
||||
// Union type for all node data types
|
||||
export type ModularNodeData =
|
||||
| BasicInfoNodeData
|
||||
| BattleConfigNodeData
|
||||
| PartyNodeData
|
||||
| InteractionNodeData
|
||||
| VariablesNodeData
|
||||
| DialogueNodeData
|
||||
| OutputNodeData;
|
||||
|
||||
// Extended Node type with our custom data
|
||||
export type ModularNode = Node<ModularNodeData>;
|
||||
|
||||
// Workflow state that contains all nodes and edges
|
||||
export interface WorkflowState {
|
||||
nodes: ModularNode[];
|
||||
edges: Edge[];
|
||||
npcConfig: NPCConfiguration;
|
||||
dialogueConfig: DialogueConfiguration | null;
|
||||
}
|
||||
|
||||
// Node template for creating new nodes
|
||||
export interface NodeTemplate {
|
||||
type: NodeType;
|
||||
label: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
color: string;
|
||||
defaultData: Partial<ModularNodeData>;
|
||||
}
|
||||
Reference in New Issue
Block a user