primeiro commit
This commit is contained in:
144
internal/vn/script.go
Normal file
144
internal/vn/script.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package vn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// MaxImportNesting defines the maximum recursive import depth.
|
||||
const MaxImportNesting = 8
|
||||
|
||||
// Script is the root structure for a visual novel script file.
|
||||
type Script struct {
|
||||
Title string `yaml:"title"`
|
||||
Start string `yaml:"start"`
|
||||
Imports []string `yaml:"imports"`
|
||||
Scenes []Scene `yaml:"scenes"`
|
||||
BaseDir string `yaml:"-"`
|
||||
LoadedFiles []string `yaml:"-"`
|
||||
}
|
||||
|
||||
// Scene is a labeled sequence of commands.
|
||||
type Scene struct {
|
||||
ID string `yaml:"id"`
|
||||
Commands []Command `yaml:"commands"`
|
||||
}
|
||||
|
||||
// Command is a single script instruction.
|
||||
type Command struct {
|
||||
Type string `yaml:"type"`
|
||||
Speaker string `yaml:"speaker,omitempty"`
|
||||
Text string `yaml:"text,omitempty"`
|
||||
Background string `yaml:"background,omitempty"`
|
||||
BackgroundImage string `yaml:"background_image,omitempty"`
|
||||
Character string `yaml:"character,omitempty"`
|
||||
Emotion string `yaml:"emotion,omitempty"`
|
||||
Sprite string `yaml:"sprite,omitempty"`
|
||||
Key string `yaml:"key,omitempty"`
|
||||
Value any `yaml:"value,omitempty"`
|
||||
Target string `yaml:"target,omitempty"`
|
||||
If string `yaml:"if,omitempty"`
|
||||
Then []Command `yaml:"then,omitempty"`
|
||||
Else []Command `yaml:"else,omitempty"`
|
||||
Choices []Choice `yaml:"choices,omitempty"`
|
||||
MenuItems []MenuItem `yaml:"menu_items,omitempty"`
|
||||
}
|
||||
|
||||
// Choice is a branching option shown to the player.
|
||||
type Choice struct {
|
||||
Text string `yaml:"text"`
|
||||
Target string `yaml:"target"`
|
||||
If string `yaml:"if,omitempty"`
|
||||
}
|
||||
|
||||
// MenuItem is a positioned interactive sprite option for a menu command.
|
||||
type MenuItem struct {
|
||||
ID string `yaml:"id,omitempty"`
|
||||
Sprite string `yaml:"sprite"`
|
||||
X float64 `yaml:"x"`
|
||||
Y float64 `yaml:"y"`
|
||||
If string `yaml:"if,omitempty"`
|
||||
Selectable *bool `yaml:"selectable,omitempty"`
|
||||
Commands []Command `yaml:"commands"`
|
||||
}
|
||||
|
||||
func LoadScript(path string) (*Script, error) {
|
||||
rootPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve script path: %w", err)
|
||||
}
|
||||
|
||||
visited := map[string]bool{}
|
||||
stack := map[string]bool{}
|
||||
loaded := []string{}
|
||||
root, err := loadScriptRecursive(rootPath, 0, visited, stack, &loaded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if root.Start == "" {
|
||||
return nil, fmt.Errorf("script missing start scene")
|
||||
}
|
||||
if len(root.Scenes) == 0 {
|
||||
return nil, fmt.Errorf("script has no scenes")
|
||||
}
|
||||
root.BaseDir = filepath.Dir(rootPath)
|
||||
root.LoadedFiles = loaded
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func loadScriptRecursive(path string, depth int, visited, stack map[string]bool, loaded *[]string) (*Script, error) {
|
||||
if depth > MaxImportNesting {
|
||||
return nil, fmt.Errorf("import depth exceeded max (%d) at %s", MaxImportNesting, path)
|
||||
}
|
||||
|
||||
if stack[path] {
|
||||
return nil, fmt.Errorf("cyclic import detected at %s", path)
|
||||
}
|
||||
|
||||
if visited[path] {
|
||||
return &Script{}, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read script %s: %w", path, err)
|
||||
}
|
||||
|
||||
var current Script
|
||||
if err := yaml.Unmarshal(data, ¤t); err != nil {
|
||||
return nil, fmt.Errorf("parse yaml %s: %w", path, err)
|
||||
}
|
||||
|
||||
visited[path] = true
|
||||
stack[path] = true
|
||||
*loaded = append(*loaded, path)
|
||||
|
||||
mergedScenes := make([]Scene, 0, len(current.Scenes))
|
||||
for _, imp := range current.Imports {
|
||||
resolved := imp
|
||||
if !filepath.IsAbs(resolved) {
|
||||
resolved = filepath.Join(filepath.Dir(path), imp)
|
||||
}
|
||||
resolved, err = filepath.Abs(resolved)
|
||||
if err != nil {
|
||||
delete(stack, path)
|
||||
return nil, fmt.Errorf("resolve import %s from %s: %w", imp, path, err)
|
||||
}
|
||||
|
||||
child, err := loadScriptRecursive(resolved, depth+1, visited, stack, loaded)
|
||||
if err != nil {
|
||||
delete(stack, path)
|
||||
return nil, err
|
||||
}
|
||||
mergedScenes = append(mergedScenes, child.Scenes...)
|
||||
}
|
||||
|
||||
delete(stack, path)
|
||||
mergedScenes = append(mergedScenes, current.Scenes...)
|
||||
current.Scenes = mergedScenes
|
||||
return ¤t, nil
|
||||
}
|
||||
Reference in New Issue
Block a user