Compare commits
12 Commits
| 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",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@xyflow/react": "^12.9.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
@@ -90,6 +91,7 @@
|
|||||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
@@ -1444,6 +1446,55 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -1468,8 +1519,9 @@
|
|||||||
"version": "19.1.9",
|
"version": "19.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
||||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -1530,6 +1582,7 @@
|
|||||||
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
|
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.39.1",
|
"@typescript-eslint/scope-manager": "8.39.1",
|
||||||
"@typescript-eslint/types": "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"
|
"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": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -1974,6 +2060,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001733",
|
"caniuse-lite": "^1.0.30001733",
|
||||||
"electron-to-chromium": "^1.5.199",
|
"electron-to-chromium": "^1.5.199",
|
||||||
@@ -2080,6 +2167,12 @@
|
|||||||
"node": ">= 6"
|
"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": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -2156,9 +2249,115 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -2191,7 +2390,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -2298,6 +2496,7 @@
|
|||||||
"integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==",
|
"integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -2896,18 +3095,6 @@
|
|||||||
"@pkgjs/parseargs": "^0.11.0"
|
"@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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -3018,37 +3205,6 @@
|
|||||||
"immediate": "~3.0.5"
|
"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": {
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
|
||||||
@@ -3062,7 +3218,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -3084,7 +3239,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -3106,7 +3260,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -3128,7 +3281,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -3150,7 +3302,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -3172,7 +3323,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -3194,7 +3344,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -3216,7 +3365,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -3238,7 +3386,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -3260,7 +3407,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -3645,6 +3791,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -3819,6 +3966,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3828,6 +3976,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@@ -4220,6 +4369,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@@ -4320,6 +4470,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -4377,6 +4528,7 @@
|
|||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -4450,6 +4602,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@@ -4462,6 +4623,7 @@
|
|||||||
"integrity": "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==",
|
"integrity": "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.6",
|
"fdir": "^6.4.6",
|
||||||
@@ -4552,6 +4714,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -4703,6 +4866,34 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"dependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@xyflow/react": "^12.9.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
|
|||||||
224
src/App.tsx
224
src/App.tsx
@@ -1,20 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { Settings, MessageSquare, Sword, Users, Code, FileText, Upload } from 'lucide-react';
|
import { Sparkles, Grid } from 'lucide-react';
|
||||||
import type { NPCConfiguration, DialogueConfiguration } from './types/npc';
|
import type { NPCConfiguration } from './types/npc';
|
||||||
import { NPCBasicSettings } from './components/NPCBasicSettings';
|
import { NodeCanvas } from './components/NodeCanvas';
|
||||||
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';
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('basic');
|
const [viewMode, setViewMode] = useState<'modular' | 'classic'>('modular');
|
||||||
|
|
||||||
const [npcConfig, setNpcConfig] = useState<NPCConfiguration>({
|
const [npcConfig, setNpcConfig] = useState<NPCConfiguration>({
|
||||||
id: 'my_npc',
|
id: 'my_npc',
|
||||||
name: 'My NPC',
|
name: 'My NPC',
|
||||||
@@ -26,173 +17,74 @@ function App() {
|
|||||||
resourceIdentifier: 'cobblemon:my_npc'
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* Header */}
|
{/* 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="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 justify-between h-16">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<h1 className="text-xl font-bold text-gray-900">Cobblemon NPC Creator</h1>
|
<h1 className="text-xl font-bold text-gray-900">Cobblemon NPC Creator</h1>
|
||||||
</div>
|
</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>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="flex items-center gap-4">
|
||||||
Create and customize NPCs for your Cobblemon mod
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
{/* Main Content - Full Screen Canvas */}
|
||||||
<div className="lg:grid lg:grid-cols-12 lg:gap-8">
|
{viewMode === 'modular' ? (
|
||||||
{/* Sidebar Navigation */}
|
<NodeCanvas
|
||||||
<div className="lg:col-span-3">
|
npcConfig={npcConfig}
|
||||||
<nav className="space-y-1">
|
onConfigChange={setNpcConfig}
|
||||||
{tabs.map(tab => {
|
/>
|
||||||
const Icon = tab.icon;
|
) : (
|
||||||
const isActive = activeTab === tab.id;
|
<div className="flex items-center justify-center h-screen">
|
||||||
|
<div className="text-center">
|
||||||
return (
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">Classic View</h2>
|
||||||
<button
|
<p className="text-gray-600">Coming soon...</p>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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