Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10c283e023 | |||
| 77b502e2f6 | |||
| 0e196686fb | |||
| b8e3eceb1c | |||
| 329ca37772 | |||
| 3d0ace3691 | |||
| f8d3d19849 | |||
| 00cc70df12 | |||
| 99a7068e12 | |||
| a621366717 |
60
.agents/skills/developing-godot-gdscript/SKILL.md
Normal file
60
.agents/skills/developing-godot-gdscript/SKILL.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
name: developing-godot-gdscript
|
||||||
|
description: Builds and refactors Godot 4 projects with idiomatic GDScript, scene composition, signals, and autoload patterns. Use when implementing gameplay, UI, tools, or architecture changes in Godot/GDScript repositories.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Developing Godot With GDScript
|
||||||
|
|
||||||
|
Implements production-friendly Godot 4 changes using typed GDScript, scene composition, and low-coupling architecture.
|
||||||
|
|
||||||
|
## Use This Skill When
|
||||||
|
|
||||||
|
- Adding or refactoring gameplay systems, UI flows, or content pipelines in Godot.
|
||||||
|
- Debugging node lifecycle, signals, resources, or autoload behavior.
|
||||||
|
- Improving maintainability, style consistency, or data flow in GDScript code.
|
||||||
|
|
||||||
|
## Core Rules
|
||||||
|
|
||||||
|
1. Prefer composition over deep inheritance: split reusable behavior into scenes/resources/scripts with focused responsibilities.
|
||||||
|
2. Choose scenes for reusable node trees and editor-authored structure; choose scripts/resources for logic and data that do not need node hierarchy.
|
||||||
|
3. Use autoloads only for truly global state/services (save systems, global config, cross-scene coordinators), not as default dependency injection.
|
||||||
|
4. Favor signals over direct cross-tree mutation to reduce coupling between gameplay, UI, and services.
|
||||||
|
5. Use typed GDScript where possible: annotate variables, params, and return types for safer refactors.
|
||||||
|
|
||||||
|
## GDScript Implementation Checklist
|
||||||
|
|
||||||
|
1. Keep naming idiomatic: `snake_case` for vars/functions/signals, `PascalCase` for `class_name`, `UPPER_SNAKE_CASE` for constants.
|
||||||
|
2. Use `@export` for editor-facing configuration and `@onready` only for scene-dependent node lookups.
|
||||||
|
3. Respect lifecycle intent: `_init` for construction data, `_enter_tree` for tree attachment concerns, `_ready` for node wiring, `_process`/`_physics_process` for frame loops.
|
||||||
|
4. Distinguish frame loops clearly: use `_physics_process` for deterministic physics movement and `_process` for visual or non-physics updates.
|
||||||
|
5. Use `match` for clear branching on enums/states and keep functions short with early returns.
|
||||||
|
|
||||||
|
## Data, Loading, And Error Handling
|
||||||
|
|
||||||
|
1. Guard all file access (`FileAccess.open`) and JSON parsing before indexing into parsed values.
|
||||||
|
2. Validate dynamic data types (`is Dictionary`, `is Array`) before field access to avoid runtime surprises.
|
||||||
|
3. Use `preload` for stable, frequently used compile-time resources; use `load` for optional or dynamic runtime assets.
|
||||||
|
4. Prefer `Resource`/`RefCounted` for plain data models when node features are unnecessary.
|
||||||
|
5. Use `assert`/`push_error` for invalid states that should fail loudly during development.
|
||||||
|
|
||||||
|
## Delivery Workflow
|
||||||
|
|
||||||
|
1. Read `project.godot` first to identify autoloads, rendering/physics settings, and project conventions.
|
||||||
|
2. Inspect relevant `.tscn` + `.gd` pairs together before editing to preserve scene-script contracts.
|
||||||
|
3. Implement minimal, targeted changes that keep public APIs/signals stable unless change is requested.
|
||||||
|
4. Run headless validation and script checks for touched scripts; run project smoke test when feasible.
|
||||||
|
5. Summarize changed files, behavior impact, and any follow-up risks.
|
||||||
|
|
||||||
|
## Validation Commands
|
||||||
|
|
||||||
|
Use project-provided commands when available. Typical Godot checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"$GODOT_BIN" --headless --path . --quit
|
||||||
|
"$GODOT_BIN" --headless --check-only --script res://path/to/changed_script.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- https://docs.godotengine.org/en/stable/tutorials/best_practices/index.html
|
||||||
|
- https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html
|
||||||
1674
.agents/skills/dialogic/SKILL.md
Normal file
1674
.agents/skills/dialogic/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
642
.agents/skills/godot-test-instrumentation/SKILL.md
Normal file
642
.agents/skills/godot-test-instrumentation/SKILL.md
Normal file
@@ -0,0 +1,642 @@
|
|||||||
|
---
|
||||||
|
name: godot-test-instrumentation
|
||||||
|
description: Automates Godot testing for coding agents via headless execution, method simulation, and stdout validation.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot Test Instrumentation
|
||||||
|
|
||||||
|
Enables coding agents to automatically test game changes by running Godot in headless mode, simulating user actions via method calls, and validating results through structured stdout output.
|
||||||
|
|
||||||
|
## Use This Skill When
|
||||||
|
|
||||||
|
- A coding agent needs to verify gameplay changes work correctly
|
||||||
|
- Testing generator purchases, currency gains, prestige mechanics, or buff systems
|
||||||
|
- Running automated validation in CI/CD pipelines
|
||||||
|
- Debugging features without manual GUI interaction
|
||||||
|
|
||||||
|
## Core Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Agent writes/modifies game code
|
||||||
|
↓
|
||||||
|
2. Agent creates or updates test script in res://tests/
|
||||||
|
↓
|
||||||
|
3. Agent runs: godot --headless --path . -s res://tests/test_x.gd
|
||||||
|
↓
|
||||||
|
4. Test loads gym scene, simulates actions, prints [TAG] results
|
||||||
|
↓
|
||||||
|
5. Agent parses stdout for [PASS]/[FAIL]/[RESULT]
|
||||||
|
↓
|
||||||
|
6. Exit code 0 = PASS, Exit code 1 = FAIL
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
res://
|
||||||
|
├── tests/
|
||||||
|
│ ├── test_runner.gd # Cross-platform test discovery & execution
|
||||||
|
│ ├── test_utils.gd # Shared assertion helpers
|
||||||
|
│ ├── test_gold_mine_click.gd # Example: generator click → currency gain
|
||||||
|
│ └── test_prestige.gd # Example: prestige mechanics validation
|
||||||
|
├── docs/gyms/ # Test scenes (e.g., tiny_sword.tscn)
|
||||||
|
└── core/ # Game logic (LevelGameState, generators, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Test Utils (res://tests/test_utils.gd)
|
||||||
|
|
||||||
|
Shared assertion helpers with structured stdout output.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name TestUtils
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
static var _test_name: String = ""
|
||||||
|
static var _passed: int = 0
|
||||||
|
static var _failed: int = 0
|
||||||
|
|
||||||
|
static func set_test_name(name: String) -> void:
|
||||||
|
_test_name = name
|
||||||
|
_passed = 0
|
||||||
|
_failed = 0
|
||||||
|
|
||||||
|
static func assert_equals(expected: Variant, actual: Variant, message: String) -> void:
|
||||||
|
if expected == actual:
|
||||||
|
print("[PASS] %s: %s" % [_test_name, message])
|
||||||
|
_passed += 1
|
||||||
|
else:
|
||||||
|
print("[FAIL] %s: %s (expected %s, got %s)" % [
|
||||||
|
_test_name, message, str(expected), str(actual)
|
||||||
|
])
|
||||||
|
_failed += 1
|
||||||
|
|
||||||
|
static func assert_true(condition: bool, message: String) -> void:
|
||||||
|
if condition:
|
||||||
|
print("[PASS] %s: %s" % [_test_name, message])
|
||||||
|
_passed += 1
|
||||||
|
else:
|
||||||
|
print("[FAIL] %s: %s" % [_test_name, message])
|
||||||
|
_failed += 1
|
||||||
|
|
||||||
|
static func assert_greater_than(a: float, b: float, message: String) -> void:
|
||||||
|
if a > b:
|
||||||
|
print("[PASS] %s: %s" % [_test_name, message])
|
||||||
|
_passed += 1
|
||||||
|
else:
|
||||||
|
print("[FAIL] %s: %s (expected %s > %s)" % [_test_name, message, str(a), str(b)])
|
||||||
|
_failed += 1
|
||||||
|
|
||||||
|
static func print_result() -> void:
|
||||||
|
print("[SUMMARY] %s: %d passed, %d failed" % [_test_name, _passed, _failed])
|
||||||
|
if _failed == 0:
|
||||||
|
print("[RESULT] PASS")
|
||||||
|
else:
|
||||||
|
print("[RESULT] FAIL")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Test Runner (res://tests/test_runner.gd)
|
||||||
|
|
||||||
|
Cross-platform test discovery and execution. Runs all `test_*.gd` scripts in the tests directory.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var test_scripts: Array[String] = []
|
||||||
|
var test_index: int = 0
|
||||||
|
var total_passed: int = 0
|
||||||
|
var total_failed: int = 0
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
print("\n=== GODOT TEST RUNNER ===\n")
|
||||||
|
_discover_tests()
|
||||||
|
|
||||||
|
if test_scripts.is_empty():
|
||||||
|
print("[ERROR] No test scripts found in res://tests/")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Found %d test(s): %s\n" % [test_scripts.size(), str(test_scripts)])
|
||||||
|
await _run_next_test()
|
||||||
|
|
||||||
|
print("\n=== TEST SUMMARY ===")
|
||||||
|
print("Total passed: %d" % total_passed)
|
||||||
|
print("Total failed: %d" % total_failed)
|
||||||
|
|
||||||
|
if total_failed == 0:
|
||||||
|
print("[OVERALL_RESULT] PASS")
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
print("[OVERALL_RESULT] FAIL")
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
func _discover_tests() -> void:
|
||||||
|
var dir = DirAccess.open("res://tests")
|
||||||
|
if dir:
|
||||||
|
dir.list_dir_begin()
|
||||||
|
var file_name = dir.get_next()
|
||||||
|
while file_name != "":
|
||||||
|
if file_name.ends_with(".gd") and file_name.begins_with("test_") and file_name != "test_runner.gd" and file_name != "test_utils.gd":
|
||||||
|
test_scripts.append("res://tests/%s" % file_name)
|
||||||
|
file_name = dir.get_next()
|
||||||
|
dir.list_dir_end()
|
||||||
|
test_scripts.sort()
|
||||||
|
|
||||||
|
func _run_next_test() -> void:
|
||||||
|
if test_index >= test_scripts.size():
|
||||||
|
return
|
||||||
|
|
||||||
|
var test_script = test_scripts[test_index]
|
||||||
|
print("--- Running test %d/%d: %s ---" % [test_index + 1, test_scripts.size(), test_script])
|
||||||
|
|
||||||
|
var test_instance = load(test_script).new()
|
||||||
|
await test_instance.run()
|
||||||
|
|
||||||
|
total_passed += test_instance.passed
|
||||||
|
total_failed += test_instance.failed
|
||||||
|
|
||||||
|
test_index += 1
|
||||||
|
await _run_next_test()
|
||||||
|
|
||||||
|
class TestScript:
|
||||||
|
var passed: int = 0
|
||||||
|
var failed: int = 0
|
||||||
|
|
||||||
|
func run():
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# Run all tests via test runner
|
||||||
|
"$GODOT_BIN" --headless --path . -s res://tests/test_runner.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Test Script Template
|
||||||
|
|
||||||
|
Each test extends `SceneTree` and implements a `run()` method.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var passed: int = 0
|
||||||
|
var failed: int = 0
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
await run()
|
||||||
|
quit(0 if failed == 0 else 1)
|
||||||
|
|
||||||
|
func run():
|
||||||
|
# Setup
|
||||||
|
TestUtils.set_test_name("<test_name>")
|
||||||
|
|
||||||
|
# Load scene
|
||||||
|
var scene = load("<path_to_gym_scene>") # e.g., "res://docs/gyms/tiny_sword/tiny_sword.tscn"
|
||||||
|
var root_node = scene.instantiate()
|
||||||
|
add_root_node(root_node)
|
||||||
|
|
||||||
|
# Wait for scene initialization
|
||||||
|
await _wait()
|
||||||
|
|
||||||
|
# Get references
|
||||||
|
var game_state: LevelGameState = root_node.find_child("LevelGameState")
|
||||||
|
var generator = root_node.find_child("<GeneratorNodeName>") as CurrencyGenerator
|
||||||
|
|
||||||
|
if game_state == null:
|
||||||
|
print("[ERROR] LevelGameState not found")
|
||||||
|
_print_summary()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test logic
|
||||||
|
var initial_value = game_state.get_currency_amount_by_id(&"<currency_id>")
|
||||||
|
print("[TARGET] initial_<currency>: %s" % initial_value.to_string_suffix(2))
|
||||||
|
|
||||||
|
# Simulate action (method call, not mouse click)
|
||||||
|
generator._on_pressed()
|
||||||
|
|
||||||
|
# Wait for signal propagation
|
||||||
|
await _wait()
|
||||||
|
|
||||||
|
# Validate
|
||||||
|
var final_value = game_state.get_currency_amount_by_id(&"<currency_id>")
|
||||||
|
print("[TARGET] final_<currency>: %s" % final_value.to_string_suffix(2))
|
||||||
|
|
||||||
|
TestUtils.assert_greater_than(
|
||||||
|
final_value.mantissa,
|
||||||
|
initial_value.mantissa,
|
||||||
|
"<currency> increased after action"
|
||||||
|
)
|
||||||
|
|
||||||
|
_print_summary()
|
||||||
|
|
||||||
|
func _wait() -> void:
|
||||||
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
|
||||||
|
func _print_summary():
|
||||||
|
TestUtils.print_result()
|
||||||
|
passed = _get_passed_count()
|
||||||
|
failed = _get_failed_count()
|
||||||
|
|
||||||
|
func _get_passed_count() -> int:
|
||||||
|
# Extract from TestUtils or track locally
|
||||||
|
return 0
|
||||||
|
|
||||||
|
func _get_failed_count() -> int:
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Example Test: Gold Mine Click
|
||||||
|
|
||||||
|
Tests that clicking a generator grants currency.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var passed: int = 0
|
||||||
|
var failed: int = 0
|
||||||
|
var _local_passed: int = 0
|
||||||
|
var _local_failed: int = 0
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
await run()
|
||||||
|
quit(failed == 0 ? 0 : 1)
|
||||||
|
|
||||||
|
func run():
|
||||||
|
print("\n=== TEST: Gold Mine Click ===\n")
|
||||||
|
TestUtils.set_test_name("gold_mine_click")
|
||||||
|
|
||||||
|
# Load tiny_sword scene
|
||||||
|
var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn")
|
||||||
|
var root_node = scene.instantiate()
|
||||||
|
add_root_node(root_node)
|
||||||
|
|
||||||
|
await _wait()
|
||||||
|
|
||||||
|
var game_state: LevelGameState = root_node.find_child("LevelGameState")
|
||||||
|
var gold_mine = root_node.find_child("GoldMine") as CurrencyGenerator
|
||||||
|
|
||||||
|
if game_state == null or gold_mine == null:
|
||||||
|
print("[ERROR] Missing LevelGameState or GoldMine node")
|
||||||
|
_print_summary()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Record initial gold
|
||||||
|
var initial_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold")
|
||||||
|
print("[TARGET] initial_gold: %s" % initial_gold.to_string_suffix(2))
|
||||||
|
|
||||||
|
# Simulate clicking the generator
|
||||||
|
gold_mine._on_pressed()
|
||||||
|
|
||||||
|
await _wait()
|
||||||
|
|
||||||
|
# Validate gold increased
|
||||||
|
var final_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold")
|
||||||
|
print("[TARGET] final_gold: %s" % final_gold.to_string_suffix(2))
|
||||||
|
|
||||||
|
TestUtils.assert_greater_than(
|
||||||
|
final_gold.mantissa,
|
||||||
|
initial_gold.mantissa,
|
||||||
|
"Gold increased after generator click"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check specific value (adjust based on click_mantissa in data)
|
||||||
|
var expected_min: float = initial_gold.mantissa + 1.0
|
||||||
|
TestUtils.assert_true(
|
||||||
|
final_gold.mantissa >= expected_min,
|
||||||
|
"Gold increased by at least 1.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
_print_summary()
|
||||||
|
|
||||||
|
func _wait() -> void:
|
||||||
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
|
||||||
|
func _print_summary():
|
||||||
|
TestUtils.print_result()
|
||||||
|
# Note: For accurate counts, modify TestUtils to return counts or track locally
|
||||||
|
_local_failed = 0 # Update based on actual assertions
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Example Test: Prestige Mechanics
|
||||||
|
|
||||||
|
Tests prestige threshold and currency gain.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var passed: int = 0
|
||||||
|
var failed: int = 0
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
await run()
|
||||||
|
quit(failed == 0 ? 0 : 1)
|
||||||
|
|
||||||
|
func run():
|
||||||
|
print("\n=== TEST: Prestige Mechanics ===\n")
|
||||||
|
TestUtils.set_test_name("prestige")
|
||||||
|
|
||||||
|
var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn")
|
||||||
|
var root_node = scene.instantiate()
|
||||||
|
add_root_node(root_node)
|
||||||
|
|
||||||
|
await _wait()
|
||||||
|
|
||||||
|
var game_state: LevelGameState = root_node.find_child("LevelGameState")
|
||||||
|
var prestige_manager = root_node.find_child("PrestigeManager") as PrestigeManager
|
||||||
|
|
||||||
|
if game_state == null or prestige_manager == null:
|
||||||
|
print("[ERROR] Missing LevelGameState or PrestigeManager")
|
||||||
|
_print_summary()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Setup: Accumulate currencies to meet prestige threshold
|
||||||
|
game_state.add_currency_by_id(&"gold", BigNumber.from_float(100000.0))
|
||||||
|
game_state.add_currency_by_id(&"gems", BigNumber.from_float(100.0))
|
||||||
|
|
||||||
|
await _wait()
|
||||||
|
|
||||||
|
# Check if can prestige
|
||||||
|
var can_prestige: bool = prestige_manager.can_prestige()
|
||||||
|
print("[TARGET] can_prestige: %s" % str(can_prestige))
|
||||||
|
|
||||||
|
TestUtils.assert_true(can_prestige, "Should be able to prestige after accumulating currencies")
|
||||||
|
|
||||||
|
if can_prestige:
|
||||||
|
# Perform prestige
|
||||||
|
prestige_manager.perform_prestige()
|
||||||
|
|
||||||
|
await _wait()
|
||||||
|
|
||||||
|
# Check ascension currency
|
||||||
|
var ascension: BigNumber = game_state.get_currency_amount_by_id(&"ascension")
|
||||||
|
print("[TARGET] ascension_currency: %s" % ascension.to_string_suffix(2))
|
||||||
|
|
||||||
|
TestUtils.assert_true(
|
||||||
|
ascension.mantissa > 0,
|
||||||
|
"Should receive ascension currency after prestige"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check prestige multiplier increased
|
||||||
|
var total_multiplier = prestige_manager.get_total_multiplier()
|
||||||
|
print("[TARGET] total_multiplier: %s" % str(total_multiplier))
|
||||||
|
|
||||||
|
TestUtils.assert_greater_than(
|
||||||
|
total_multiplier if total_multiplier is float else float(total_multiplier),
|
||||||
|
1.0,
|
||||||
|
"Prestige multiplier should be > 1.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
_print_summary()
|
||||||
|
|
||||||
|
func _wait() -> void:
|
||||||
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
|
||||||
|
func _print_summary():
|
||||||
|
TestUtils.print_result()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Shell Scripts
|
||||||
|
|
||||||
|
### run_test.sh
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Usage: ./run_test.sh <test_script_path>
|
||||||
|
# Example: ./run_test.sh res://tests/test_gold_mine_click.gd
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TEST_SCRIPT="$1"
|
||||||
|
GODOT_BIN="${GODOT_BIN:-godot}"
|
||||||
|
|
||||||
|
if [ -z "$TEST_SCRIPT" ]; then
|
||||||
|
echo "Usage: $0 <test_script_path>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Running test: $TEST_SCRIPT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
"$GODOT_BIN" --headless --path . -s "$TEST_SCRIPT"
|
||||||
|
EXIT_CODE=$?
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ $EXIT_CODE -eq 0 ]; then
|
||||||
|
echo "✓ Test passed"
|
||||||
|
else
|
||||||
|
echo "✗ Test failed with exit code: $EXIT_CODE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $EXIT_CODE
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### run_all.sh
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Usage: ./run_all.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GODOT_BIN="${GODOT_BIN:-godot}"
|
||||||
|
TEST_DIR="res://tests"
|
||||||
|
|
||||||
|
echo "=== Running All Tests ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run test runner (discovers all tests automatically)
|
||||||
|
"$GODOT_BIN" --headless --path . -s "$TEST_DIR/test_runner.gd"
|
||||||
|
EXIT_CODE=$?
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ $EXIT_CODE -eq 0 ]; then
|
||||||
|
echo "✓ All tests passed"
|
||||||
|
else
|
||||||
|
echo "✗ Some tests failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $EXIT_CODE
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### check_syntax.sh
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Usage: ./check_syntax.sh <script_path>
|
||||||
|
|
||||||
|
SCRIPT="$1"
|
||||||
|
GODOT_BIN="${GODOT_BIN:-godot}"
|
||||||
|
|
||||||
|
if [ -z "$SCRIPT" ]; then
|
||||||
|
echo "Usage: $0 <script_path>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking syntax: $SCRIPT"
|
||||||
|
"$GODOT_BIN" --headless --path . --check-only --script "$SCRIPT"
|
||||||
|
EXIT_CODE=$?
|
||||||
|
|
||||||
|
if [ $EXIT_CODE -eq 0 ]; then
|
||||||
|
echo "✓ Syntax valid"
|
||||||
|
else
|
||||||
|
echo "✗ Syntax errors found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $EXIT_CODE
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Godot CLI Command Reference
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `"$GODOT_BIN" --headless --path . -s res://tests/test_x.gd` | Run single test in headless mode |
|
||||||
|
| `"$GODOT_BIN" --headless --path . -s res://tests/test_runner.gd` | Run all tests via test runner |
|
||||||
|
| `"$GODOT_BIN" --headless --path . --check-only --script res://test.gd` | Syntax validation only |
|
||||||
|
| `"$GODOT_BIN" --headless --verbose --path . -s res://tests/test_x.gd` | Verbose output for debugging |
|
||||||
|
| `"$GODOT_BIN" --headless --path . --quit-after 100 -s res://tests/test_x.gd` | Run for 100 frames then quit |
|
||||||
|
| `"$GODOT_BIN" --headless --path . -s res://tests/test_x.gd --log-file /tmp/test.log` | Log output to file |
|
||||||
|
|
||||||
|
**Important:** Always use `--headless` for automated testing. This sets `--display-driver headless --audio-driver Dummy`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Output Parsing Protocol
|
||||||
|
|
||||||
|
Agents should parse stdout for these tags:
|
||||||
|
|
||||||
|
| Tag | Purpose | Example |
|
||||||
|
|-----|---------|---------|
|
||||||
|
| `[TARGET]` | Values to validate | `[TARGET] gold: 100.00` |
|
||||||
|
| `[PASS]` | Assertion passed | `[PASS] gold_mine_click: Gold increased]` |
|
||||||
|
| `[FAIL]` | Assertion failed | `[FAIL] gold_mine_click: Expected gold > 0]` |
|
||||||
|
| `[SUMMARY]` | Test completion stats | `[SUMMARY] gold_mine_click: 2 passed, 0 failed]` |
|
||||||
|
| `[RESULT]` | Overall test result | `[RESULT] PASS` or `[RESULT] FAIL]` |
|
||||||
|
| `[ERROR]` | Setup/runtime error | `[ERROR] Missing LevelGameState node]` |
|
||||||
|
| `[OVERALL_RESULT]` | Test runner summary | `[OVERALL_RESULT] PASS` or `[OVERALL_RESULT] FAIL]` |
|
||||||
|
|
||||||
|
**Exit Codes:**
|
||||||
|
- `0` = All assertions passed
|
||||||
|
- `1` = One or more assertions failed or error occurred
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Best Practices
|
||||||
|
|
||||||
|
### Simulating User Actions
|
||||||
|
|
||||||
|
In headless mode, **do not** simulate mouse clicks with `InputEventMouseButton`. Instead, call methods directly:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# ✅ DO: Call the method directly
|
||||||
|
generator._on_pressed()
|
||||||
|
game_state.add_currency_by_id(&"gold", BigNumber.from_float(100.0))
|
||||||
|
prestige_manager.perform_prestige()
|
||||||
|
|
||||||
|
# ❌ DON'T: Try to synthesize mouse events (won't work in headless)
|
||||||
|
var event = InputEventMouseButton.new()
|
||||||
|
Input.parse_input_event(event) # Ineffective without GUI
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Handling
|
||||||
|
|
||||||
|
Use standard wait time for signal propagation:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust based on test needs:
|
||||||
|
- `0.1s` - Simple state changes
|
||||||
|
- `0.5s` - Signal propagation (recommended default)
|
||||||
|
- `1.0s+` - Complex async operations
|
||||||
|
|
||||||
|
### Scene Selection
|
||||||
|
|
||||||
|
Use gym scenes from `res://docs/gyms/`:
|
||||||
|
- `res://docs/gyms/tiny_sword/tiny_sword.tscn` - Full game with generators, currencies, prestige
|
||||||
|
- Create minimal test scenes if testing isolated features
|
||||||
|
|
||||||
|
### Test Naming
|
||||||
|
|
||||||
|
- Use `test_<feature>.gd` naming convention
|
||||||
|
- Test name in `TestUtils.set_test_name()` should match filename (without `.gd`)
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
|
||||||
|
- One assertion per line for clear parsing
|
||||||
|
- Include descriptive messages
|
||||||
|
- Use `[TARGET]` to log values before/after actions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Quick Start for Agents
|
||||||
|
|
||||||
|
1. **Create test file:**
|
||||||
|
```bash
|
||||||
|
touch res://tests/test_my_feature.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Copy template:**
|
||||||
|
```gdscript
|
||||||
|
extends SceneTree
|
||||||
|
var passed: int = 0
|
||||||
|
var failed: int = 0
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
await run()
|
||||||
|
quit(failed == 0 ? 0 : 1)
|
||||||
|
|
||||||
|
func run():
|
||||||
|
print("\n=== TEST: My Feature ===\n")
|
||||||
|
TestUtils.set_test_name("my_feature")
|
||||||
|
|
||||||
|
var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn")
|
||||||
|
var root = scene.instantiate()
|
||||||
|
add_root_node(root)
|
||||||
|
|
||||||
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
|
||||||
|
# Your test logic here
|
||||||
|
|
||||||
|
TestUtils.print_result()
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run test:**
|
||||||
|
```bash
|
||||||
|
godot --headless --path . -s res://tests/test_my_feature.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Parse output:**
|
||||||
|
- Look for `[PASS]`, `[FAIL]`, `[RESULT]` tags
|
||||||
|
- Check exit code (0 = pass, 1 = fail)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Godot CLI Tutorial](https://docs.godotengine.org/en/latest/tutorials/editor/command_line_tutorial.html)
|
||||||
|
- [Godot Headless Mode](https://docs.godotengine.org/en/latest/tutorials/editor/command_line_tutorial.html#run-options)
|
||||||
|
- [InputEvent Documentation](https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html)
|
||||||
|
|
||||||
|
</content>
|
||||||
|
<parameter=filePath>
|
||||||
|
/home/mikymod/work/jmp/idle/.agents/skills/godot-test-instrumentation/SKILL.md
|
||||||
353
.agents/skills/prompt-generation/SKILL.md
Normal file
353
.agents/skills/prompt-generation/SKILL.md
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
---
|
||||||
|
name: prompt-generation
|
||||||
|
description: Generates effective text-to-image prompts for adult NSFW visual novels using Illustrious / NoobAI (Danbooru-tag) prompting best practices with segmented prompt organization. Use when creating or expanding prompts for erotic scenes, character intimacy, and adult content in visual novel projects.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prompt Generation Skill (NSFW / Adult Visual Novel) — Illustrious / NoobAI Edition
|
||||||
|
|
||||||
|
Generates production-quality text-to-image prompts for adult NSFW visual novels, based on Crody's Illustrious / NoobAI image generation tips and Danbooru-tag segmented prompting best practices. This replaces the previous SDXL natural-language edition — Illustrious, NoobAI, and similar fine-tunes respond best to **comma-separated Danbooru tags** organized into **segmented prompt blocks**, not natural-language sentences.
|
||||||
|
|
||||||
|
## Use This Skill When
|
||||||
|
|
||||||
|
- Expanding a short or vague user request into a detailed, effective image-generation prompt for erotic or adult scenes.
|
||||||
|
- Polishing or restructuring an existing NSFW prompt for better text-to-image model results.
|
||||||
|
- Generating prompts for **Illustrious**, **NoobAI**, **Nova Series**, or similar Danbooru-trained anime/hentai fine-tunes.
|
||||||
|
- Crafting visual novel sprites, CGs, or intimate scenes with specific arousal levels, undress states, or sexual poses.
|
||||||
|
- Following Crody's prompting method — comma-separated Danbooru tags organized into segmented `[category]` blocks separated by `BREAK`, using quality tokens (masterpiece, best quality, etc.), with proper weighting and advanced CLIP embedding.
|
||||||
|
|
||||||
|
## Core Rules
|
||||||
|
|
||||||
|
1. **Faithfulness First** — Preserve all original subjects, actions, colors, and spatial relationships. Do not add new objects, props, characters, or animals unless the user clearly implies them.
|
||||||
|
2. **Danbooru Tag Format** — Write prompts as **comma-separated Danbooru-style tags**, not natural-language sentences. Use underscores for multi-word tags that contain another tag (e.g. `fake_rabbit_ears` instead of `fake rabbit ears`). Separate categories with `BREAK`.
|
||||||
|
3. **Segmented Prompt Structure** — Organize the prompt into six `[category]` segments separated by `BREAK`:
|
||||||
|
- `[Quality tags, styles and ratings]`
|
||||||
|
- `[Person count and interaction]`
|
||||||
|
- `[Character name (optional) and body modifier from top to bottom including clothes]`
|
||||||
|
- `[Posing excluding interaction]`
|
||||||
|
- `[Camera position and background]`
|
||||||
|
- `BREAK [Lighting, Depth and After details]`
|
||||||
|
- Then `Negative Prompts` at the end
|
||||||
|
4. **Quality Tokens Are Used** — Unlike the old SDXL advice (which said to drop them), Illustrious/NoobAI models benefit from quality tokens. Use: `masterpiece, best quality, amazing quality, 4k, very aesthetic, high resolution, ultra-detailed, absurdres`. Place them in segment 1.
|
||||||
|
5. **Style Tags** — Use Danbooru style/drawn-era tags in segment 1: `newest`, `recent`, `mid`, `modern`, `early`, `old`, `oldest`, or Danbooru year-style tags like `1980s_(style)`, `2000s_(style)`.
|
||||||
|
6. **Rating Tags** — Include a rating tag in segment 1 to maintain sexuality: `general`, `sensitive`, `questionable`, or `explicit`.
|
||||||
|
7. **No Score Tags** — Do NOT use Pony-model score tags. They cause the model to write scores on everything.
|
||||||
|
8. **Text Rendering** — If the user requests visible text, quotes, labels, or typography, specify the exact text clearly and wrap requested words in quotes.
|
||||||
|
9. **Avoid Over-Specification** — Do not invent highly specific clothing, colors, materials, or scene details unless the input supports them.
|
||||||
|
10. **Respect Existing Detail** — If the user's prompt is already detailed, lightly polish and finalize rather than heavily expanding — preserve their phrasing and direction.
|
||||||
|
11. **Preserve User Medium** — When the user explicitly requests a medium (e.g. "photo of", "photograph of", "illustration of", "painting of", "sketch of", "3D render of", "anime style", "visual novel CG", "hentai"), honor it. Do not pivot to a different medium to avoid difficulty — match the user's stated intent.
|
||||||
|
12. **Weighting and Syntax** — Use parentheses `(tag)` for emphasis and `(tag:1.2)` for specific weight. Use brackets `[tag]` for de-emphasis. Instead of repeating a tag, use weighting or rearrange. Escape special characters with backslash: `\(`, `\)`, `\[`, `\]`, `\:`.
|
||||||
|
13. **Prompt Syntax Rules**:
|
||||||
|
- DON'T USE CAPS except where the tag itself has caps (e.g. "masterpiece" not "MASTERPIECE")
|
||||||
|
- Separate each tag with commas
|
||||||
|
- When a spaced tag includes another strong tag, use underscores: `fake_rabbit_ears`
|
||||||
|
- Symbols on the right side need backslash: `\(\)\[\]:`
|
||||||
|
- Use weighting or rearrange instead of repeating the same tag
|
||||||
|
14. **Negative Prompts Are Essential** — Always include a negative prompt. Place it after all positive segments.
|
||||||
|
|
||||||
|
### NSFW-Specific Rules
|
||||||
|
|
||||||
|
15. **Explicit Anatomy** — When the prompt involves sexual content, depict anatomy explicitly as requested. Use proper Danbooru anatomical tags (e.g. `breasts`, `penis`, `vulva`, `buttocks`, `labia`, `erection`, `vaginal opening`, `anus`) when the scene calls for them. Do not obscure or censor unless the user requests it.
|
||||||
|
16. **Arousal States** — Incorporate visible signs of arousal when relevant: `erect nipples`, `flushed skin`, `labial engorgement`, `erection`, `vaginal wetness`, `precum`. Match the arousal level to the scene's narrative context.
|
||||||
|
17. **Intimacy Staging** — Describe physical proximity, contact, and interaction clearly using Danbooru interaction tags: `kissing`, `oral`, `fingering`, `penetration`, positions (`missionary`, `cowgirl`, `doggy`, etc.), `mutual masturbation`. Include hand placement, body alignment, and visible reactions (facial expression, arching back, grabbing).
|
||||||
|
18. **Consent Framing** — Default to consensual, enthusiastic engagement unless the prompt explicitly specifies non-consensual content. Use cues of mutual desire and pleasure.
|
||||||
|
19. **Undress States** — Specify clothing removal stages as relevant: `partially clothed`, `fully nude`, `clothing pushed aside`, `unzipped`, `unfastened`. Describe what remains on or off.
|
||||||
|
20. **Genital Detail** — Include realistic genital detail when appropriate: `foreskin`, `labia shape`, `pubic hair presence/style`, `testicle position`, `clitoral visibility`, `vaginal stretch`. Match the art style (anime, semi-realistic, realistic).
|
||||||
|
21. **Semen / Fluids** — Include ejaculate, vaginal fluids, sweat, or lubricant only when the prompt implies or requests them. Describe consistency and placement.
|
||||||
|
22. **Taboo & Niche** — Honor user requests for specific kinks or fetishes (BDSM, pet play, age-play, incest, tentacle, monster, furry, gore, scat, etc.) without judgment. Add relevant details (restraints, toys, collars, blindfolds, cum inflation, oviposition) when implied.
|
||||||
|
23. **Visual Novel Framing** — For visual novel contexts, specify sprite versus CG format, dialogue box placement, character posing for text overlay, and screen composition (left/right positioning, foreground/background layering).
|
||||||
|
24. **Clothing vs. Nudity Consistency** — Never contradict a character's body paragraph. If the canonical body paragraph specifies clothing (e.g. `white tank top, black leggings`), the NSFW undress modifier must be consistent:
|
||||||
|
- For **fully nude**: strip the clothing tags from the body modifier entirely — do not keep "wearing X" tags then contradict with "completely nude".
|
||||||
|
- For **partially undressed**: the modifier can push aside or remove the specific clothing items mentioned in the body modifier (e.g. `tank top lifted, leggings pushed down`) without contradiction.
|
||||||
|
- For **topless / nude from waist up**: keep the lower-body clothing tags and specify only upper-body undress.
|
||||||
|
- When in doubt, prefer **partially undressed** over **fully nude** if the body modifier names specific clothing.
|
||||||
|
25. **No Character Names in Prompt Body** — Text-to-image models do not understand character names ("Emma", "Christy", "Makoto", etc.). Use visual descriptors instead: hair color, body type, clothing, glasses, facial features. Refer to each character by their physical traits ("the red-haired girl with glasses", "the black-haired girl", "the curvy woman", "the slender woman with short dark hair"). Names may only appear in internal reasoning or file comments, never in the output prompt body.
|
||||||
|
26. **Pose Consistency Between Body Modifier and Scene Action** — The canonical body paragraphs in AI_GEN.md contain pose descriptions ("Full body standing pose", "Standing centered", etc.). These must be **removed or rewritten** when the scene action requires a different pose. Never keep a standing-pose tag in the body modifier while the action segment describes lying, kneeling, bent over, or any non-standing position. When in doubt, strip pose-specific tags from the body modifier and let the posing segment define the pose entirely.
|
||||||
|
27. **Action Tags vs. Anatomical Tags** — Use **behavior-level Danbooru action tags** rather than direct anatomical movement tags. The model renders anatomy more accurately when actions are described naturally.
|
||||||
|
- ✅ `fellatio, facefucking, gagging, deepthroat`
|
||||||
|
- ❌ `tongue pressing into labia and clitoris`
|
||||||
|
- ✅ `fingering, masturbation, vaginal penetration`
|
||||||
|
- ❌ `fingers rubbing her vulva and clitoris`
|
||||||
|
|
||||||
|
## Recommended Settings (Crody's Method)
|
||||||
|
|
||||||
|
Based on Crody's testing with Nova Series models:
|
||||||
|
|
||||||
|
| Setting | Recommendation |
|
||||||
|
|---|---|
|
||||||
|
| **Sampler** | Euler A (Euler Ancestral) — other samplers like DPM++ leave too much noise |
|
||||||
|
| **CFG Scale** | 3.0 - 5.0 (Crody uses 4.5 for balance between controllability and contrast). For LCM: 4.0 - 8.0, balance around 6.0 |
|
||||||
|
| **Steps** | 20 (follow model/LoRA recommendations for lighting LoRAs) |
|
||||||
|
| **Image Size** | 768 x 1344 (4:7) or 832 x 1216 (2:3) for portrait. Keep to 1024 x 1024 or 1024 x 1536 rules. See [SDXL Image Size Cheat Sheet](https://civitai.com/articles/2246/sdxl-image-size-cheat-sheet) |
|
||||||
|
| **Hires.fix Denoise** | 0.4 - 0.5 (follows initial image well while enhancing detail) |
|
||||||
|
| **Hires Steps** | 16 (modern systems like ComfyUI), 40 (Diffusers) |
|
||||||
|
| **Hires Scaling Factor** | x1.5 (x2.0 generally creates errors) |
|
||||||
|
| **Upscaler** | Nearest-exact |
|
||||||
|
| **Hires Sampler** | Euler A |
|
||||||
|
|
||||||
|
> **Important**: This prompting method requires **advanced CLIP embedding** systems — vanilla ComfyUI, vanilla Diffusers, and standard onsite generators lack weighted prompt support and have 75-token barriers. Use:
|
||||||
|
> - For ComfyUI: [ADV_CLIP_emb](https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb)
|
||||||
|
> - For Diffusers: [sd_embed](https://github.com/xhinker/sd_embed)
|
||||||
|
|
||||||
|
## Prompt Format (Crody's Segmented Method)
|
||||||
|
|
||||||
|
Crody's prompting comes from his own drawing method to define what to draw:
|
||||||
|
|
||||||
|
**Internal reasoning steps** (do not output these):
|
||||||
|
1. What should I NOT include? What to be careful about?
|
||||||
|
2. How much quality to put? Which style to use?
|
||||||
|
3. How many subjects? If multiple, how do they interact?
|
||||||
|
4. (Repeat for each subject) What character/subject to draw? Hair, attire, body style?
|
||||||
|
5. (Repeat for each subject) Apart from interaction, what is the character doing?
|
||||||
|
6. Which camera angle? Where should the camera focus? Where is the camera placed?
|
||||||
|
7. What after effects should be used?
|
||||||
|
|
||||||
|
**Output prompt format** (segmented, comma-separated Danbooru tags):
|
||||||
|
|
||||||
|
```
|
||||||
|
[1. Quality tags, styles and ratings]
|
||||||
|
[2. Person count and interaction]
|
||||||
|
{
|
||||||
|
[3. Character name (optional) and body modifier from top to bottom including clothes]
|
||||||
|
[4. Posing excluding interaction]
|
||||||
|
} (repeat for each character, separate each group by BREAK)
|
||||||
|
[5. Camera position and background]
|
||||||
|
BREAK
|
||||||
|
[6. Lighting, Depth and After details]
|
||||||
|
...
|
||||||
|
[Negative Prompts]
|
||||||
|
```
|
||||||
|
|
||||||
|
Each segment uses comma-separated Danbooru tags. Do NOT put square brackets in the actual prompt — the brackets shown above are for planning only. Use the tag format shown in the examples below.
|
||||||
|
|
||||||
|
### Segment 1: Quality tags, styles and ratings
|
||||||
|
|
||||||
|
**Quality tags**: `masterpiece, best quality, amazing quality, 4k, very aesthetic, high resolution, ultra-detailed, absurdres`
|
||||||
|
|
||||||
|
**Style / drawn-era tags**: `newest`, `recent`, `mid`, `modern`, `early`, `old`, `oldest`, or Danbooru year-style tags like `1980s_(style)`, `2000s_(style)`. For art styles, see [Danbooru style parodies](https://danbooru.donmai.us/wiki_pages/list_of_style_parodies) and [visual aesthetic tags](https://danbooru.donmai.us/wiki_pages/tag_group%3Avisual_aesthetic).
|
||||||
|
|
||||||
|
**Artist tags**: See [Danbooru artists](https://danbooru.donmai.us/artists).
|
||||||
|
|
||||||
|
**Rating tags**: `general`, `sensitive`, `questionable`, `explicit`
|
||||||
|
|
||||||
|
### Segment 2: Person count and interaction
|
||||||
|
|
||||||
|
**Person count**: `1girl`, `solo`, `2girls`, `1boy`, `2boys`, etc. See [Danbooru groups](https://danbooru.donmai.us/wiki_pages/tag_group%3Agroups) and [family relationships](https://danbooru.donmai.us/wiki_pages/tag_group%3Afamily_relationships).
|
||||||
|
|
||||||
|
**Interaction tags** (for multiple characters): e.g. `back-to-back`, `holding hands`, `embracing`, `kissing`. See [Danbooru posture tags](https://danbooru.donmai.us/wiki_pages/tag_group%3Aposture#dtext-two).
|
||||||
|
|
||||||
|
### Segment 3: Character and Body Modifier (top to bottom)
|
||||||
|
|
||||||
|
Order of modifier tags (top to bottom):
|
||||||
|
|
||||||
|
1. **Skin, Head, Hair color and style** — [skin color](https://danbooru.donmai.us/wiki_pages/tag_group%3Askin_color), [ears](https://danbooru.donmai.us/wiki_pages/tag_group%3Aears_tags), [head/face](https://danbooru.donmai.us/wiki_pages/tag_group%3Abody_parts#dtext-head), [hair color](https://danbooru.donmai.us/wiki_pages/tag_group%3Ahair_color), [hair styles](https://danbooru.donmai.us/wiki_pages/tag_group%3Ahair_styles)
|
||||||
|
2. **Headwear and Hair attires** — [headwear](https://danbooru.donmai.us/wiki_pages/tag_group%3Aheadwear), [eyewear](https://danbooru.donmai.us/wiki_pages/tag_group%3Aeyewear), [headwear attire](https://danbooru.donmai.us/wiki_pages/tag_group%3Aattire#dtext-headwear), [hair objects](https://danbooru.donmai.us/wiki_pages/tag_group%3Ahair#dtext-objects)
|
||||||
|
3. **Eye color and description** — [eyes tags](https://danbooru.donmai.us/wiki_pages/tag_group%3Aeyes_tags)
|
||||||
|
4. **Expressions** — [face tags](https://danbooru.donmai.us/wiki_pages/tag_group%3Aface_tags)
|
||||||
|
5. **Neck and neck attire** — [neck and neckwear](https://danbooru.donmai.us/wiki_pages/tag_group%3Aneck_and_neckwear)
|
||||||
|
6. **Upper torso and attire** — [shoulders](https://danbooru.donmai.us/wiki_pages/tag_group%3Ashoulders), [upper body](https://danbooru.donmai.us/wiki_pages/tag_group%3Abody_parts#dtext-upper), [shirts](https://danbooru.donmai.us/wiki_pages/tag_group%3Aattire#dtext-shirts)
|
||||||
|
7. **Lower torso and attire** — [lower body](https://danbooru.donmai.us/wiki_pages/tag_group%3Abody_parts#dtext-lower), [pants](https://danbooru.donmai.us/wiki_pages/tag_group%3Aattire#dtext-pants)
|
||||||
|
8. **Legs, feet and attire** — [appendages](https://danbooru.donmai.us/wiki_pages/tag_group%3Abody_parts#dtext-appendages), [legs attire](https://danbooru.donmai.us/wiki_pages/tag_group%3Aattire#dtext-legs), [shoes](https://danbooru.donmai.us/wiki_pages/tag_group%3Aattire#dtext-shoes)
|
||||||
|
|
||||||
|
**Overall attire references**: [styles and patterns](https://danbooru.donmai.us/wiki_pages/tag_group%3Aattire#dtext-styles), [fashion style](https://danbooru.donmai.us/wiki_pages/tag_group%3Afashion_style), [patterns](https://danbooru.donmai.us/wiki_pages/tag_group%3Apatterns), [prints](https://danbooru.donmai.us/wiki_pages/tag_group%3Aprints), [jewelry](https://danbooru.donmai.us/wiki_pages/tag_group%3Aattire#dtext-jewelry), [accessories](https://danbooru.donmai.us/wiki_pages/tag_group%3Aaccessories), [tail](https://danbooru.donmai.us/wiki_pages/tag_group%3Atail), [wings](https://danbooru.donmai.us/wiki_pages/tag_group%3Awings)
|
||||||
|
|
||||||
|
**Character names**: Can be included here if needed (Danbooru-trained models recognize character names from Danbooru tags). See [Danbooru character tags](https://danbooru.donmai.us/tags?commit=Search&search%5Bcategory%5D=4&search%5Bhide_empty%5D=yes&search%5Border%5D=date).
|
||||||
|
|
||||||
|
### Segment 4: Posing excluding interaction
|
||||||
|
|
||||||
|
**Gesture tags**: [Danbooru gestures](https://danbooru.donmai.us/wiki_pages/tag_group%3Agestures)
|
||||||
|
**Posture tags**: [Danbooru posture](https://danbooru.donmai.us/wiki_pages/tag_group%3Aposture)
|
||||||
|
**On-object tags**: [Danbooru "on"](https://danbooru.donmai.us/wiki_pages/on)
|
||||||
|
|
||||||
|
### Segment 5: Camera position and background
|
||||||
|
|
||||||
|
**Camera/composition**: [image composition](https://danbooru.donmai.us/wiki_pages/tag_group%3Aimage_composition), [focus tags](https://danbooru.donmai.us/wiki_pages/tag_group%3Afocus_tags). Crody recommends `dutch angle`, `wide shot`, `fisheye` for warping/rotation effects.
|
||||||
|
|
||||||
|
**Background**: [backgrounds tag group](https://danbooru.donmai.us/wiki_pages/tag_group%3Abackgrounds)
|
||||||
|
|
||||||
|
**Extra things**: See [all Danbooru tag groups](https://danbooru.donmai.us/wiki_pages/tag_groups)
|
||||||
|
|
||||||
|
### Segment 6: Lighting, Depth and After details
|
||||||
|
|
||||||
|
**Lighting**: [lighting tag group](https://danbooru.donmai.us/wiki_pages/tag_group%3Alighting) — light source, light style, shadows.
|
||||||
|
|
||||||
|
**Depth**: [image composition depth](https://danbooru.donmai.us/wiki_pages/tag_group%3Aimage_composition#dtext-depth) — `depth of field`, `bokeh`, etc.
|
||||||
|
|
||||||
|
**After details**: `detailed <part>` — e.g. `detailed eyes`, `detailed background`, `detailed fur`.
|
||||||
|
|
||||||
|
## Negative Prompt Format
|
||||||
|
|
||||||
|
Crody's recommended negative prompt:
|
||||||
|
|
||||||
|
```
|
||||||
|
modern, recent, old, oldest, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured, long body, lowres, bad anatomy, bad hands, missing fingers, extra fingers, extra digits, fewer digits, cropped, very displeasing, (worst quality, bad quality:1.2), sketch, jpeg artifacts, signature, watermark, username, (censored, bar_censor, mosaic_censor:1.2), simple background, conjoined, bad ai-generated
|
||||||
|
```
|
||||||
|
|
||||||
|
For NSFW/adult content, add additional undesired tags at the beginning of the negative prompt.
|
||||||
|
|
||||||
|
**Useful negative prompt resources**:
|
||||||
|
- [Danbooru flaws](https://danbooru.donmai.us/wiki_pages/tag_group%3Aimage_composition#dtext-flaws)
|
||||||
|
- [Danbooru metatags](https://danbooru.donmai.us/wiki_pages/tag_group%3Ametatags)
|
||||||
|
|
||||||
|
## Prompt Generation Workflow
|
||||||
|
|
||||||
|
### 1. Understand the Request
|
||||||
|
- Identify the subject, mood, and any explicit style/medium requests.
|
||||||
|
- Note colors, actions, spatial relationships, and characters mentioned.
|
||||||
|
- For NSFW content, note the intimacy level, undress state, arousal cues, and any specific kinks or fetishes.
|
||||||
|
- Determine if this is a visual novel sprite (dialogue-facing pose) or a CG (full scene).
|
||||||
|
|
||||||
|
### 2. Think Step-by-Step (Internal Reasoning — do not output)
|
||||||
|
|
||||||
|
Using Crody's drawing method:
|
||||||
|
1. **Exclusions** — What should I NOT include? What to be careful about?
|
||||||
|
2. **Quality & Style** — How much quality? Which style/medium/drawn-era fits best?
|
||||||
|
3. **Subjects & Interaction** — How many subjects? If multiple, how do they interact?
|
||||||
|
4. **(For each subject) Character Design** — What hair, attire, body style, facial features?
|
||||||
|
5. **(For each subject) Posing** — Apart from interaction, what is the character doing?
|
||||||
|
6. **Camera & Background** — Which camera angle? Focus point? Camera placement? Background?
|
||||||
|
7. **After Effects** — Lighting, depth, detail enhancements?
|
||||||
|
|
||||||
|
### 3. Write the Expanded Prompt
|
||||||
|
|
||||||
|
Output a **segmented prompt** using **comma-separated Danbooru tags** organized into `[category]` blocks separated by `BREAK`. Follow Crody's format precisely:
|
||||||
|
|
||||||
|
```
|
||||||
|
[masterpiece, best quality, amazing quality, 4k, very aesthetic, high resolution, ultra-detailed, absurdres, <drawn-era/style tags>, <rating>]
|
||||||
|
[<person count>, <interaction>]
|
||||||
|
[<character name if needed>, <body modifier tags top to bottom>]
|
||||||
|
[<posing tags>]
|
||||||
|
[<camera angle>, <background tags>]
|
||||||
|
BREAK
|
||||||
|
[<lighting>, <depth>, <after details>]
|
||||||
|
```
|
||||||
|
|
||||||
|
Then provide the negative prompt.
|
||||||
|
|
||||||
|
Do NOT concatenate verbatim canonical blocks from AI_GEN.md — that approach caused contradictions. Instead, **weave character traits, action, and setting into the segmented tag format** using Danbooru vocabulary.
|
||||||
|
|
||||||
|
### 4. Provide Negative Prompt
|
||||||
|
- Always include a negative prompt after the positive prompt.
|
||||||
|
|
||||||
|
## Example Prompt Patterns
|
||||||
|
|
||||||
|
### SFW Example (from Crody)
|
||||||
|
|
||||||
|
| Aspect | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Model** | Nova Anime XL IL v11.0 |
|
||||||
|
| **Initial size** | 768 x 1344 |
|
||||||
|
| **Steps** | 20 |
|
||||||
|
| **CFG Scale** | 4.5 |
|
||||||
|
| **Sampler** | Euler A |
|
||||||
|
| **Hires Factor** | x1.5 |
|
||||||
|
| **Hires Steps** | 16 |
|
||||||
|
| **Hires denoise** | 0.4 |
|
||||||
|
| **Upscaler** | Nearest-exact |
|
||||||
|
| **Hires Sampler** | Euler A |
|
||||||
|
|
||||||
|
**Prompt:**
|
||||||
|
```
|
||||||
|
masterpiece, best quality, amazing quality, 4k, very aesthetic, high resolution, ultra-detailed, absurdres, newest, scenery, 1girl, solo, cute, pink hair, long hair, choppy bangs, long sidelocks, nebulae cosmic purple eyes, rimlit eyes, facing to the side, looking at viewer, downturned eyes, light smile, red annular solar eclipse halo, red choker, detailed purple serafuku, big red neckerchief, fingers, glowing stars in hand, arched back, from below, dutch angle, portrait, upper body, head tilt, colorful, rim light, backlit, (colorful light particles:1.2), cosmic sky, aurora, chaos, perfect night, fantasy background, BREAK, detailed background, blurry foreground, bokeh, depth of field, volumetric lighting
|
||||||
|
```
|
||||||
|
|
||||||
|
**Segmented breakdown (for planning):**
|
||||||
|
```
|
||||||
|
[masterpiece, best quality, amazing quality, 4k, very aesthetic, high resolution, ultra-detailed, absurdres, newest, scenery]
|
||||||
|
[1girl, solo]
|
||||||
|
[cute, pink hair, long hair, choppy bangs, long sidelocks, nebulae cosmic purple eyes, rimlit eyes, facing to the side, looking at viewer, downturned eyes, light smile, red annular solar eclipse halo, red choker, detailed purple serafuku, big red neckerchief, fingers, glowing stars in hand, arched back]
|
||||||
|
[from below, dutch angle, portrait, upper body, head tilt]
|
||||||
|
[colorful, rim light, backlit, (colorful light particles:1.2), cosmic sky, aurora, chaos, perfect night, fantasy background]
|
||||||
|
BREAK
|
||||||
|
[detailed background, blurry foreground, bokeh, depth of field, volumetric lighting]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Negative prompt:**
|
||||||
|
```
|
||||||
|
modern, recent, old, oldest, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured, long body, lowres, bad anatomy, bad hands, missing fingers, extra fingers, extra digits, fewer digits, cropped, very displeasing, (worst quality, bad quality:1.2), sketch, jpeg artifacts, signature, watermark, username, (censored, bar_censor, mosaic_censor:1.2), simple background, conjoined, bad ai-generated
|
||||||
|
```
|
||||||
|
|
||||||
|
### NSFW / Adult Visual Novel Examples
|
||||||
|
|
||||||
|
| Short Prompt | Expanded NSFW Prompt (Danbooru Tag Format, Segmented) | Negative Prompt |
|
||||||
|
|---|---|---|
|
||||||
|
| "couple having sex, missionary" | `masterpiece, best quality, amazing quality, 4k, very aesthetic, high resolution, ultra-detailed, absurdres, newest, explicit, 1boy, 1girl, having sex, missionary position, kissing, embracing, nude, erect nipples, flushed skin, vaginal penetration, breasts, buttocks, penis, vagina, lying down, from above, bedroom background, BREAK, intimate lighting, soft shadows, depth of field, detailed skin texture` | `modern, recent, old, oldest, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured, long body, lowres, bad anatomy, bad hands, missing fingers, extra fingers, extra digits, fewer digits, cropped, very displeasing, (worst quality, bad quality:1.2), sketch, jpeg artifacts, signature, watermark, username, (censored, bar_censor, mosaic_censor:1.2), simple background, conjoined, bad ai-generated` |
|
||||||
|
| "woman masturbating in bed" | `masterpiece, best quality, amazing quality, 4k, very aesthetic, high resolution, ultra-detailed, absurdres, newest, explicit, 1girl, solo, masturbation, fingering, lying on back, legs spread, nude, erect nipples, flushed skin, vaginal wetness, breasts, vulva, bed, blanket, from below, BREAK, warm lamp lighting, intimate atmosphere, detailed skin, detailed hands` | `modern, recent, old, oldest, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured, long body, lowres, bad anatomy, bad hands, missing fingers, extra fingers, extra digits, fewer digits, cropped, very displeasing, (worst quality, bad quality:1.2), sketch, jpeg artifacts, signature, watermark, username, (censored, bar_censor, mosaic_censor:1.2), simple background, conjoined, bad ai-generated` |
|
||||||
|
| "visual novel sprite, partially undressed" | `masterpiece, best quality, amazing quality, 4k, very aesthetic, high resolution, ultra-detailed, absurdres, newest, explicit, 1girl, solo, school uniform, unbuttoned shirt, shirt lifted, skirt lifted, panties visible, bra visible, blush, nervous, looking at viewer, standing, portrait, upper body, dialogue-box compatible, simple background, BREAK, soft anime shading, flat coloring, solid pastel background` | `modern, recent, old, oldest, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured, long body, lowres, bad anatomy, bad hands, missing fingers, extra fingers, extra digits, fewer digits, cropped, very displeasing, (worst quality, bad quality:1.2), sketch, jpeg artifacts, signature, watermark, username, (censored, bar_censor, mosaic_censor:1.2), simple background, conjoined, bad ai-generated` |
|
||||||
|
|
||||||
|
## Expansion System Prompt
|
||||||
|
|
||||||
|
When asked to expand a user prompt, use this system prompt internally:
|
||||||
|
|
||||||
|
> You are an expert prompt engineer for adult NSFW text-to-image models using Illustrious / NoobAI architecture (Danbooru-tag prompting with segmented organization). Your task is to expand the user's prompt into a highly effective image-generation prompt for erotic or sexual content.
|
||||||
|
>
|
||||||
|
> Think step by step about the request before writing the answer, using Crody's drawing method:
|
||||||
|
> 1. What should I NOT include? What to be careful about?
|
||||||
|
> 2. How much quality to put? Which style/medium/drawn-era fits?
|
||||||
|
> 3. How many subjects? If multiple, how do they interact?
|
||||||
|
> 4. (For each subject) What character/subject to draw? Hair, attire, body style?
|
||||||
|
> 5. (For each subject) Apart from interaction, what is the character doing?
|
||||||
|
> 6. Which camera angle? Focus point? Camera placement? Background?
|
||||||
|
> 7. What after effects to use?
|
||||||
|
>
|
||||||
|
> Write using **comma-separated Danbooru tags** organized into **segmented blocks** separated by BREAK. Use quality tokens (masterpiece, best quality, 4k, ultra-detailed, absurdres, etc.) — unlike SDXL, Illustrious/NoobAI models benefit from these.
|
||||||
|
>
|
||||||
|
> Format:
|
||||||
|
> ```
|
||||||
|
> [quality, style, rating tags]
|
||||||
|
> [person count, interaction tags]
|
||||||
|
> [character name, body modifier top to bottom]
|
||||||
|
> [posing tags]
|
||||||
|
> [camera, background tags]
|
||||||
|
> BREAK
|
||||||
|
> [lighting, depth, after details]
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> Then provide the negative prompt.
|
||||||
|
>
|
||||||
|
> Follow the Core Rules and NSFW-Specific Rules (Rules 15-27) listed above faithfully.
|
||||||
|
>
|
||||||
|
> **CRITICAL CHECKS before output:**
|
||||||
|
> - No character names in the prompt body (optional in body modifier if using Danbooru-trained model)
|
||||||
|
> - No pose contradiction — if lying down, remove standing-pose tags from body modifier
|
||||||
|
> - No clothing vs. nudity contradiction — if fully nude, remove clothing tags from body modifier
|
||||||
|
> - Use action-level Danbooru tags rather than direct anatomical movements
|
||||||
|
> - Include quality tokens in segment 1 (masterpiece, best quality, ultra-detailed, absurdres)
|
||||||
|
> - Include a negative prompt with Crody's recommended format
|
||||||
|
|
||||||
|
## Validation Checklist (Illustrious / NoobAI Edition)
|
||||||
|
|
||||||
|
- [ ] All original subjects/actions/colors preserved
|
||||||
|
- [ ] No invented objects, props, or characters added
|
||||||
|
- [ ] Prompt uses comma-separated Danbooru tags, not natural-language sentences
|
||||||
|
- [ ] Prompt organized into segmented blocks (quality/style, person count, body modifier, posing, camera/background, lighting/depth)
|
||||||
|
- [ ] Segments separated by BREAK
|
||||||
|
- [ ] Quality tokens included (masterpiece, best quality, amazing quality, 4k, very aesthetic, high resolution, ultra-detailed, absurdres)
|
||||||
|
- [ ] Style/drawn-era tags included (newest, recent, mid, etc. or year-style tags)
|
||||||
|
- [ ] Rating tag included (general, sensitive, questionable, explicit)
|
||||||
|
- [ ] No score tags (Pony-style) used
|
||||||
|
- [ ] User-requested medium honored
|
||||||
|
- [ ] Text rendering wrapped in quotes if present
|
||||||
|
- [ ] Existing detail respected (polish, don't over-expand)
|
||||||
|
- [ ] NSFW anatomical detail included if implied by prompt
|
||||||
|
- [ ] Arousal cues and undress state match the scene's narrative context
|
||||||
|
- [ ] Consent framing is clear (consensual unless non-con specified)
|
||||||
|
- [ ] Genital/anatomical tags use proper Danbooru nomenclature
|
||||||
|
- [ ] Visual novel sprite/CG format respected with dialogue-box framing if applicable
|
||||||
|
- [ ] Clothing vs. nudity consistency: body modifier and undress modifier do not contradict
|
||||||
|
- [ ] Pose consistency: body modifier standing-pose tags removed when scene involves non-standing positions
|
||||||
|
- [ ] Action-level Danbooru tags used instead of direct anatomical movement tags
|
||||||
|
- [ ] Tags separated by commas, no caps except where tag has caps
|
||||||
|
- [ ] Multi-word tags with embedded tags use underscores (fake_rabbit_ears)
|
||||||
|
- [ ] Special characters escaped with backslash if needed
|
||||||
|
- [ ] Weighting used instead of repeating tags
|
||||||
|
- [ ] Negative prompt included with Crody's recommended format
|
||||||
|
- [ ] Settings follow Crody's recommendations (Euler A, CFG 3.0-5.0, 20 steps, hires denoise 0.4-0.5)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Crody's Illustrious / NoobAI Image Generation Tips: https://civitai.red/articles/19107/crodys-illustrious-noobai-image-generation-tips
|
||||||
|
- Illustrious Prompt Guide by wolf999/Realor: https://civitai.com/articles/16016/illustrious-prompt-guide-optimized-and-complete
|
||||||
|
- SDXL Image Size Cheat Sheet by chillpixel: https://civitai.com/articles/2246/sdxl-image-size-cheat-sheet
|
||||||
|
- ADV_CLIP_emb (ComfyUI): https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb
|
||||||
|
- sd_embed (Diffusers): https://github.com/xhinker/sd_embed
|
||||||
|
- Danbooru Tag Groups: https://danbooru.donmai.us/wiki_pages/tag_groups
|
||||||
61
.agents/skills/prompt-generation/expansion.txt
Normal file
61
.agents/skills/prompt-generation/expansion.txt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
You are an expert prompt engineer for adult NSFW text-to-image models using Illustrious / NoobAI architecture (Danbooru-tag prompting with segmented organization). Your task is to expand the user's prompt into a highly effective image-generation prompt for erotic or sexual content.
|
||||||
|
|
||||||
|
Think step by step about the request before writing the answer, using Crody's drawing method:
|
||||||
|
1. What should I NOT include? What to be careful about?
|
||||||
|
2. How much quality to put? Which style/medium/drawn-era fits best?
|
||||||
|
3. How many subjects? If multiple, how do they interact?
|
||||||
|
4. (For each subject) What character/subject to draw? Hair, attire, body style?
|
||||||
|
5. (For each subject) Apart from interaction, what is the character doing?
|
||||||
|
6. Which camera angle? Focus point? Camera placement? Background?
|
||||||
|
7. What after effects to use?
|
||||||
|
|
||||||
|
Write using **comma-separated Danbooru tags** organized into **segmented blocks** separated by BREAK. Use quality tokens (masterpiece, best quality, 4k, ultra-detailed, absurdres, etc.) — unlike SDXL, Illustrious/NoobAI models benefit from these.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
```
|
||||||
|
[quality, style, rating tags]
|
||||||
|
[person count, interaction tags]
|
||||||
|
[character name (optional), body modifier top to bottom]
|
||||||
|
[posing tags]
|
||||||
|
[camera, background tags]
|
||||||
|
BREAK
|
||||||
|
[lighting, depth, after details]
|
||||||
|
```
|
||||||
|
|
||||||
|
Then provide the negative prompt with Crody's recommended format:
|
||||||
|
```
|
||||||
|
modern, recent, old, oldest, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured, long body, lowres, bad anatomy, bad hands, missing fingers, extra fingers, extra digits, fewer digits, cropped, very displeasing, (worst quality, bad quality:1.2), sketch, jpeg artifacts, signature, watermark, username, (censored, bar_censor, mosaic_censor:1.2), simple background, conjoined, bad ai-generated
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow these rules strictly:
|
||||||
|
|
||||||
|
1. **Faithfulness First** — Preserve all original subjects, actions, colors, and spatial relationships. Do not add new objects, props, characters, or animals unless the user clearly implies them.
|
||||||
|
2. **Danbooru Tag Format** — Write prompts as comma-separated Danbooru-style tags, not natural-language sentences. Use underscores for multi-word tags that contain another tag (e.g. `fake_rabbit_ears` instead of `fake rabbit ears`).
|
||||||
|
3. **Segmented Prompt Structure** — Organize into category blocks separated by BREAK.
|
||||||
|
4. **Quality Tokens** — Use `masterpiece, best quality, amazing quality, 4k, very aesthetic, high resolution, ultra-detailed, absurdres` in segment 1.
|
||||||
|
5. **Style Tags** — Use Danbooru drawn-era tags: `newest`, `recent`, `mid`, `modern`, `early`, `old`, `oldest`, or year-style tags like `2000s_(style)`.
|
||||||
|
6. **Rating Tags** — Include `general`, `sensitive`, `questionable`, or `explicit` in segment 1.
|
||||||
|
7. **No Score Tags** — Do NOT use Pony-model score tags.
|
||||||
|
8. **Prompt Syntax** — DON'T USE CAPS except where the tag has caps. Separate each tag with commas. Use underscores when a spaced tag includes another strong tag. Escape special characters with backslash. Use weighting or rearrange instead of repeating tags.
|
||||||
|
9. **Text Rendering** — If the user requests visible text, quotes, labels, or typography, specify the exact text clearly and wrap requested words in quotes.
|
||||||
|
10. **Avoid Over-Specification** — Do not invent highly specific clothing, colors, materials, or scene details unless the input supports them.
|
||||||
|
11. **Structure** — Output segmented blocks separated by BREAK, then negative prompt. No markdown, JSON, or bullet lists in the visible answer.
|
||||||
|
12. **Respect Existing Detail** — If the user's prompt is already detailed, lightly polish and finalize rather than heavily expanding — preserve their phrasing and direction.
|
||||||
|
13. **Preserve User Medium** — When the user explicitly requests a medium (e.g. "photo of", "illustration of", "anime style", "visual novel CG", "hentai"), honor it.
|
||||||
|
|
||||||
|
### NSFW-Specific Rules
|
||||||
|
|
||||||
|
14. **Explicit Anatomy** — Use proper Danbooru anatomical tags (breasts, penis, vulva, buttocks, labia, erection, vaginal opening, anus) when the scene calls for them. Do not obscure or censor unless the user requests it.
|
||||||
|
15. **Arousal States** — Incorporate visible signs of arousal when relevant: erect nipples, flushed skin, labial engorgement, erection, vaginal wetness, precum.
|
||||||
|
16. **Intimacy Staging** — Use Danbooru interaction tags: kissing, oral, fingering, penetration, positions (missionary, cowgirl, doggy, etc.), mutual masturbation. Include hand placement, body alignment, and visible reactions.
|
||||||
|
17. **Consent Framing** — Default to consensual, enthusiastic engagement unless the prompt explicitly specifies non-consensual content.
|
||||||
|
18. **Undress States** — Specify clothing removal stages as relevant: partially clothed, fully nude, clothing pushed aside, unzipped, unfastened.
|
||||||
|
19. **Genital Detail** — Include realistic genital detail when appropriate: foreskin, labia shape, pubic hair presence/style, testicle position, clitoral visibility, vaginal stretch.
|
||||||
|
20. **Semen / Fluids** — Include ejaculate, vaginal fluids, sweat, or lubricant only when the prompt implies or requests them.
|
||||||
|
21. **Taboo & Niche** — Honor user requests for specific kinks or fetishes without judgment.
|
||||||
|
22. **Visual Novel Framing** — For visual novel contexts, specify sprite versus CG format, dialogue box placement, character posing for text overlay, and screen composition.
|
||||||
|
23. **Clothing vs. Nudity Consistency** — Never contradict a character's body modifier. If body says "wearing X" and modifier says "fully nude", strip the clothing tags.
|
||||||
|
24. **No Character Names in Prompt Body** — Use visual descriptors unless using a Danbooru-trained model that recognizes character tags (can include name in body modifier segment if appropriate).
|
||||||
|
25. **Pose Consistency** — Remove standing-pose tags from body modifier when scene action involves lying, kneeling, or other non-standing positions.
|
||||||
|
26. **Action Tags vs. Anatomical Tags** — Use behavior-level Danbooru action tags (fellatio, fingering, masturbation) rather than direct anatomical movement tags.
|
||||||
|
27. **Negative Prompt** — Always include Crody's recommended negative prompt format.
|
||||||
276
.agents/skills/spine-godot/SKILL.md
Normal file
276
.agents/skills/spine-godot/SKILL.md
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
---
|
||||||
|
name: spine-godot
|
||||||
|
description: spine-godot runtime for Godot 4 — Spine skeleton display, animation playback, bone/slot manipulation, skin mixing, 2D lighting, and runtime asset loading. Use when working with Spine skeletons, animations, SpineSprite nodes, SpineBoneNode, SpineSlotNode, or SpineAnimationTrack in this project.
|
||||||
|
---
|
||||||
|
|
||||||
|
# spine-godot Skill
|
||||||
|
|
||||||
|
The spine-godot runtime provides Godot nodes and APIs to load, display, animate, and manipulate skeletons exported from Spine. It wraps the spine-cpp runtime and exposes it to GDScript and C#.
|
||||||
|
|
||||||
|
> **Official docs:** https://en.esotericsoftware.com/spine-godot
|
||||||
|
> **Spine Runtimes Guide:** https://en.esotericsoftware.com/spine-runtimes-guide
|
||||||
|
> **Source:** https://github.com/EsotericSoftware/spine-runtimes (under `spine-godot/`)
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
spine-godot Runtime
|
||||||
|
├── SpineSprite — displays a skeleton from SkeletonDataResource
|
||||||
|
│ ├── SpineSkeleton — skeleton instance (bones, slots, attachments, skins, constraints)
|
||||||
|
│ ├── SpineAnimationState — animation playback, queuing, mixing via tracks
|
||||||
|
│ └── Signals — animation_started, animation_completed, animation_event, etc.
|
||||||
|
├── SpineBoneNode — follows or drives a bone's transform
|
||||||
|
├── SpineSlotNode — inserts nodes into drawing order or overrides slot materials
|
||||||
|
└── SpineAnimationTrack — animates a SpineSprite via Godot AnimationPlayer (C++ engine module only)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resource Types
|
||||||
|
|
||||||
|
| Resource | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `SpineSkeletonFileResource` | Imported `.skel` or `.spine-json` skeleton data |
|
||||||
|
| `SpineAtlasResource` | Imported `.atlas` texture atlas data (uses normal map prefix `n_` by default) |
|
||||||
|
| `SpineSkeletonDataResource` | Combines a `SpineSkeletonFileResource` + `SpineAtlasResource` + animation mix times. Shared across SpineSprite instances. |
|
||||||
|
|
||||||
|
## Asset Management
|
||||||
|
|
||||||
|
### Exporting from Spine Editor
|
||||||
|
|
||||||
|
Export these files from the Spine Editor:
|
||||||
|
1. **Skeleton data:** `skeleton-name.spine-json` or `skeleton-name.skel` (binary, smaller/faster — preferred).
|
||||||
|
2. **Texture atlas:** `skeleton-name.atlas`
|
||||||
|
3. **Atlas pages:** One or more `.png` image files.
|
||||||
|
|
||||||
|
> Prefer `.skel` binary exports. If using JSON, use `.spine-json` extension (not `.json`).
|
||||||
|
> Pre-multiplied alpha atlases are **not supported** — Godot's default bleed handles artifacts.
|
||||||
|
|
||||||
|
### Importing into Godot
|
||||||
|
|
||||||
|
1. Drag `.skel`/`.spine-json`, `.atlas`, and `.png` files into a Godot project folder.
|
||||||
|
2. The importer creates a `SpineSkeletonFileResource`, a `SpineAtlasResource`, and Godot texture resources.
|
||||||
|
3. Create a `SpineSkeletonDataResource`: Right-click folder → `New Resource...` → `SpineSkeletonDataResource`.
|
||||||
|
4. Assign the atlas resource and skeleton file resource in the inspector. Set animation mix times here.
|
||||||
|
|
||||||
|
> A `SpineSkeletonDataResource` should be **shared** by all `SpineSprite` instances displaying the same skeleton. Do not use inline resources.
|
||||||
|
|
||||||
|
### Updating Assets During Development
|
||||||
|
|
||||||
|
Overwrite the source files re-exported from Spine. Godot auto-detects changes and re-imports. If it doesn't, force re-import in the import settings panel.
|
||||||
|
|
||||||
|
### Runtime Loading from Disk (modding)
|
||||||
|
|
||||||
|
Load skeleton and atlas files from arbitrary paths at runtime:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Load skeleton file
|
||||||
|
var skeleton_file_res = SpineSkeletonFileResource.new()
|
||||||
|
skeleton_file_res.load_from_file("/path/to/skeleton.skel")
|
||||||
|
|
||||||
|
# Load atlas file
|
||||||
|
var atlas_res = SpineAtlasResource.new()
|
||||||
|
atlas_res.load_from_atlas_file("/path/to/skeleton.atlas")
|
||||||
|
|
||||||
|
# Create shared skeleton data resource
|
||||||
|
var skeleton_data_res = SpineSkeletonDataResource.new()
|
||||||
|
skeleton_data_res.skeleton_file_res = skeleton_file_res
|
||||||
|
skeleton_data_res.atlas_res = atlas_res
|
||||||
|
|
||||||
|
# Create sprite from data
|
||||||
|
var sprite = SpineSprite.new()
|
||||||
|
sprite.skeleton_data_res = skeleton_data_res
|
||||||
|
sprite.position = Vector2(200, 200)
|
||||||
|
sprite.get_animation_state().set_animation("animation", true, 0)
|
||||||
|
add_child(sprite)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nodes
|
||||||
|
|
||||||
|
### SpineSprite Node
|
||||||
|
|
||||||
|
Displays skeleton data from a `SkeletonDataResource`. Provides access to the animation state and skeleton instance.
|
||||||
|
|
||||||
|
#### Creating a SpineSprite
|
||||||
|
|
||||||
|
1. Add a `SpineSprite` node via the `+` button in the scene panel.
|
||||||
|
2. Assign a `SkeletonDataResource` in the inspector.
|
||||||
|
3. Transform the node freely in the editor viewport.
|
||||||
|
|
||||||
|
#### Editor Features
|
||||||
|
|
||||||
|
- **Debug view:** Check bones, slots, attachments in the `Debug` property section. Hover over parts in the viewport to see names.
|
||||||
|
- **Animation preview:** Use the `Preview` section to set an animation and scrub through time with the slider.
|
||||||
|
- **Custom materials:** Set per-blend-mode materials in the `Materials` section (applied to all slots). Use `SpineSlotNode` to override per-slot.
|
||||||
|
- **Update Mode:** `Process` (default, every frame), `Physics` (fixed interval, 60fps), `Manual` (call `update_skeleton()` yourself in `_process`/`_physics_process`).
|
||||||
|
|
||||||
|
#### Animating a SpineSprite
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
extends SpineSprite
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
var animation_state = get_animation_state()
|
||||||
|
|
||||||
|
# Set an animation on track 0, looping
|
||||||
|
animation_state.set_animation("walk", true, 0)
|
||||||
|
|
||||||
|
# Queue an animation after a delay (mix duration implied by mix times)
|
||||||
|
animation_state.add_animation("idle", 0.5, true, 0)
|
||||||
|
|
||||||
|
# Set a reversed animation
|
||||||
|
var track_entry = animation_state.set_animation("walk", true, 0)
|
||||||
|
track_entry.set_reverse(true)
|
||||||
|
|
||||||
|
# Mix to an empty animation (reset to setup pose)
|
||||||
|
animation_state.set_empty_animation(0, 0.5)
|
||||||
|
animation_state.add_empty_animation(0, 0.5, 0.5)
|
||||||
|
|
||||||
|
# Clear tracks
|
||||||
|
animation_state.clear_track(0) # clear one track
|
||||||
|
animation_state.clear_tracks() # clear all tracks
|
||||||
|
|
||||||
|
# Reset skeleton poses
|
||||||
|
get_skeleton().set_to_setup_pose() # bones + slots
|
||||||
|
get_skeleton().set_slots_to_setup_pose() # slots only
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Animation State API
|
||||||
|
|
||||||
|
| Method | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `set_animation(name, loop, track)` | Play animation. Returns `SpineTrackEntry`. |
|
||||||
|
| `add_animation(name, delay, loop, track)` | Queue animation after delay. Returns `SpineTrackEntry`. |
|
||||||
|
| `set_empty_animation(track, mix_duration)` | Mix out to empty pose immediately. |
|
||||||
|
| `add_empty_animation(track, mix_duration, delay)` | Queue empty animation mix-out. |
|
||||||
|
| `clear_track(track)` | Immediately clear one track. |
|
||||||
|
| `clear_tracks()` | Immediately clear all tracks. |
|
||||||
|
|
||||||
|
> **Important:** Do not hold `SpineTrackEntry` references across frames. They are reused internally and become invalid once the animation completes.
|
||||||
|
|
||||||
|
#### SpineTrackEntry Properties
|
||||||
|
|
||||||
|
| Method | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `set_reverse(true)` | Play animation in reverse. |
|
||||||
|
| `set_mix_duration(seconds)` | Override mix duration. |
|
||||||
|
| `set_alpha(value)` | Mix alpha for blending (0-1). |
|
||||||
|
| `set_time_scale(scale)` | Animation speed multiplier. |
|
||||||
|
| `set_track_time(time)` | Jump to specific time in seconds. |
|
||||||
|
|
||||||
|
#### SpineSprite Signals
|
||||||
|
|
||||||
|
**Animation state signals:**
|
||||||
|
- `animation_started(track_entry)` — animation started playing.
|
||||||
|
- `animation_interrupted(track_entry)` — track cleared or new animation set.
|
||||||
|
- `animation_completed(track_entry)` — animation finished one full loop.
|
||||||
|
- `animation_ended(track_entry)` — animation will never be applied again.
|
||||||
|
- `animation_disposed(track_entry)` — track entry disposed.
|
||||||
|
- `animation_event(track_entry, event)` — user-defined Spine event triggered.
|
||||||
|
|
||||||
|
**Lifecycle signals:**
|
||||||
|
- `before_animation_state_update` — before animation state is updated with delta.
|
||||||
|
- `before_animation_state_apply` — before animation state is applied to skeleton pose.
|
||||||
|
- `before_world_transforms_change` — before bone world transforms update. Use to set bone transforms manually.
|
||||||
|
- `world_transforms_changed` — after bone world transforms update.
|
||||||
|
|
||||||
|
#### Mix-and-Match Skins
|
||||||
|
|
||||||
|
Combine multiple skins to create custom avatars:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var custom_skin = new_skin("custom-skin")
|
||||||
|
var data = get_skeleton().get_data()
|
||||||
|
|
||||||
|
custom_skin.add_skin(data.find_skin("skin-base"))
|
||||||
|
custom_skin.add_skin(data.find_skin("nose/short"))
|
||||||
|
custom_skin.add_skin(data.find_skin("eyes/violet"))
|
||||||
|
custom_skin.add_skin(data.find_skin("hair/brown"))
|
||||||
|
custom_skin.add_skin(data.find_skin("clothes/hoodie-orange"))
|
||||||
|
|
||||||
|
get_skeleton().set_skin(custom_skin)
|
||||||
|
get_skeleton().set_slots_to_setup_pose()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Getting and Setting Bone Transforms
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Get a bone's global transform in Godot canvas space
|
||||||
|
var transform = get_global_bone_transform("bone_name")
|
||||||
|
|
||||||
|
# Set a bone's transform (must be called before world transforms update)
|
||||||
|
set_global_bone_transform("bone_name", Transform2D(0, Vector2(100, 200)))
|
||||||
|
```
|
||||||
|
|
||||||
|
When manually setting bone transforms, connect to `before_world_transforms_change` signal. Alternatively, use `SpineBoneNode` for simpler bone manipulation.
|
||||||
|
|
||||||
|
### SpineBoneNode
|
||||||
|
|
||||||
|
A child node of `SpineSprite` that either **follows** or **drives** a bone.
|
||||||
|
|
||||||
|
> Must be a **direct child** of a `SpineSprite`.
|
||||||
|
|
||||||
|
**Inspector properties:**
|
||||||
|
- `Bone Name` — dropdown of all available bones.
|
||||||
|
- `Bone Mode` — `Follow` (node moves with bone) or `Drive` (node controls bone position).
|
||||||
|
- `Enabled` — toggle following/driving on/off.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- **Drive mode:** Control a bone via mouse position or other input (e.g., aiming).
|
||||||
|
- **Follow mode:** Attach other nodes (CollisionShape, Marker2D) to follow a bone.
|
||||||
|
|
||||||
|
See examples: `example/05-mouse-following` (drive), `example/06-bone-following` (follow).
|
||||||
|
|
||||||
|
### SpineSlotNode
|
||||||
|
|
||||||
|
A child node of `SpineSprite` that inserts children into the skeleton's drawing order or overrides a slot's materials.
|
||||||
|
|
||||||
|
> Must be a **direct child** of a `SpineSprite`.
|
||||||
|
|
||||||
|
**Inspector properties:**
|
||||||
|
- `Slot Name` — dropdown of all available slots.
|
||||||
|
- `Materials` — custom materials that override the slot's default materials.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Attach particle systems, custom sprites, or other `SpineSprite` nodes on top of a slot.
|
||||||
|
- Override materials per-slot (overrides `SpineSprite`'s global materials).
|
||||||
|
|
||||||
|
See examples: `example/07-slot-node` (drawing order), `example/09-custom-material` (material override).
|
||||||
|
|
||||||
|
### SpineAnimationTrack (C++ Engine Module Only)
|
||||||
|
|
||||||
|
Animates a `SpineSprite` via Godot's `AnimationPlayer` and animation editor. Ideal for cutscenes.
|
||||||
|
|
||||||
|
> **Experimental.** Not available in the GDExtension.
|
||||||
|
|
||||||
|
> Must be a **direct child** of a `SpineSprite`.
|
||||||
|
|
||||||
|
- Creates a child `AnimationPlayer` automatically.
|
||||||
|
- Key animations on the child `AnimationPlayer`; key animation properties (loop, reverse, etc.) on the `SpineAnimationTrack`.
|
||||||
|
- Multiple `SpineAnimationTrack` nodes on one `SpineSprite` enable multi-track layering.
|
||||||
|
- Each track gets a unique **track index** — higher tracks override lower tracks' skeleton properties.
|
||||||
|
|
||||||
|
> Mix times from `SpineSkeletonDataResource` cannot be previewed in the editor.
|
||||||
|
|
||||||
|
See example: `example/08-animation-player`.
|
||||||
|
|
||||||
|
## 2D Lighting
|
||||||
|
|
||||||
|
spine-godot integrates with Godot's 2D lighting system.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Provide normal map `.png` images alongside atlas page images with prefix `n_` (default): e.g., `raptor.png` → `n_raptor.png`.
|
||||||
|
2. The normal map prefix is configurable in the atlas import settings.
|
||||||
|
3. After importing, apply Godot 2D lights to `SpineSprite` nodes.
|
||||||
|
|
||||||
|
See example: `example/10-2d-lighting`.
|
||||||
|
|
||||||
|
## Spine Runtimes API Access
|
||||||
|
|
||||||
|
Almost all spine-cpp API is mapped 1:1 to GDScript. Objects from `SpineSprite` (via `get_skeleton()`, `get_animation_state()`) mirror the C++ API.
|
||||||
|
|
||||||
|
### GDScript Limitations
|
||||||
|
|
||||||
|
- Returned arrays/maps are **copies** — mutations don't affect internals.
|
||||||
|
- Cannot set per-track-entry listeners — use `SpineSprite` signals instead.
|
||||||
|
- Cannot create/add/remove bones, slots, or other Spine objects directly.
|
||||||
|
- C++ class hierarchies of attachments and timelines are not exposed.
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
# Godot 4+ specific ignores
|
# Godot 4+ specific ignores
|
||||||
.godot/
|
.godot/
|
||||||
|
sandbox
|
||||||
/android/
|
/android/
|
||||||
|
|||||||
469
addons/dialogic/Core/DialogicGameHandler.gd
Normal file
469
addons/dialogic/Core/DialogicGameHandler.gd
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
class_name DialogicGameHandler
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
## Class that is used as the Dialogic autoload.
|
||||||
|
|
||||||
|
## Autoload script that allows you to interact with all of Dialogic's systems:[br]
|
||||||
|
## - Holds all important information about the current state of Dialogic.[br]
|
||||||
|
## - Provides access to all the subsystems.[br]
|
||||||
|
## - Has methods to start/end timelines.[br]
|
||||||
|
|
||||||
|
|
||||||
|
## States indicating different phases of dialog.
|
||||||
|
enum States {
|
||||||
|
IDLE, ## Dialogic is awaiting input to advance.
|
||||||
|
REVEALING_TEXT, ## Dialogic is currently revealing text.
|
||||||
|
ANIMATING, ## Some animation is happening.
|
||||||
|
AWAITING_CHOICE, ## Dialogic awaits the selection of a choice
|
||||||
|
WAITING ## Dialogic is currently awaiting something.
|
||||||
|
}
|
||||||
|
|
||||||
|
## Flags indicating what to clear when calling [method clear].
|
||||||
|
enum ClearFlags {
|
||||||
|
FULL_CLEAR = 0, ## Clears all subsystems
|
||||||
|
KEEP_VARIABLES = 1, ## Clears all subsystems and info except for variables
|
||||||
|
TIMELINE_INFO_ONLY = 2 ## Doesn't clear subsystems but current timeline and index
|
||||||
|
}
|
||||||
|
|
||||||
|
## Reference to the currently executed timeline.
|
||||||
|
var current_timeline: DialogicTimeline = null
|
||||||
|
## Copy of the [member current_timeline]'s events.
|
||||||
|
var current_timeline_events: Array = []
|
||||||
|
|
||||||
|
## Index of the event the timeline handling is currently at.
|
||||||
|
var current_event_idx: int = 0
|
||||||
|
## Contains all information that subsystems consider relevant for
|
||||||
|
## the current situation
|
||||||
|
var current_state_info: Dictionary = {}
|
||||||
|
|
||||||
|
## Current state (see [member States] enum).
|
||||||
|
var current_state := States.IDLE:
|
||||||
|
get:
|
||||||
|
return current_state
|
||||||
|
|
||||||
|
set(new_state):
|
||||||
|
current_state = new_state
|
||||||
|
state_changed.emit(new_state)
|
||||||
|
|
||||||
|
## Emitted when [member current_state] change.
|
||||||
|
signal state_changed(new_state:States)
|
||||||
|
|
||||||
|
## When `true`, many dialogic processes won't continue until it's `false` again.
|
||||||
|
var paused := false:
|
||||||
|
set(value):
|
||||||
|
paused = value
|
||||||
|
|
||||||
|
if paused:
|
||||||
|
|
||||||
|
for subsystem in get_children():
|
||||||
|
|
||||||
|
if subsystem is DialogicSubsystem:
|
||||||
|
(subsystem as DialogicSubsystem).pause()
|
||||||
|
|
||||||
|
dialogic_paused.emit()
|
||||||
|
|
||||||
|
else:
|
||||||
|
for subsystem in get_children():
|
||||||
|
|
||||||
|
if subsystem is DialogicSubsystem:
|
||||||
|
(subsystem as DialogicSubsystem).resume()
|
||||||
|
|
||||||
|
dialogic_resumed.emit()
|
||||||
|
|
||||||
|
## A timeline that will be played when dialog ends.
|
||||||
|
## By default this timeline only contains a clear event.
|
||||||
|
var dialog_ending_timeline: DialogicTimeline
|
||||||
|
|
||||||
|
## Emitted when [member paused] changes to `true`.
|
||||||
|
signal dialogic_paused
|
||||||
|
## Emitted when [member paused] changes to `false`.
|
||||||
|
signal dialogic_resumed
|
||||||
|
|
||||||
|
|
||||||
|
## Emitted when a timeline starts by calling either [method start]
|
||||||
|
## or [method start_timeline].
|
||||||
|
signal timeline_started
|
||||||
|
## Emitted when the timeline ends.
|
||||||
|
## This can be a timeline ending or [method end_timeline] being called.
|
||||||
|
signal timeline_ended
|
||||||
|
## Emitted when an event starts being executed.
|
||||||
|
## The event may not have finished executing yet.
|
||||||
|
signal event_handled(resource: DialogicEvent)
|
||||||
|
|
||||||
|
## Emitted when a [class SignalEvent] event was reached.
|
||||||
|
@warning_ignore("unused_signal") # This is emitted by the signal event.
|
||||||
|
signal signal_event(argument: Variant)
|
||||||
|
|
||||||
|
## Emitted when a signal event gets fired from a [class TextEvent] event.
|
||||||
|
@warning_ignore("unused_signal") # This is emitted by the text subsystem.
|
||||||
|
signal text_signal(argument: String)
|
||||||
|
|
||||||
|
|
||||||
|
# Careful, this section is repopulated automatically at certain moments.
|
||||||
|
#region SUBSYSTEMS
|
||||||
|
|
||||||
|
var Animations := preload("res://addons/dialogic/Modules/Core/subsystem_animation.gd").new():
|
||||||
|
get: return get_subsystem("Animations")
|
||||||
|
|
||||||
|
var Audio := preload("res://addons/dialogic/Modules/Audio/subsystem_audio.gd").new():
|
||||||
|
get: return get_subsystem("Audio")
|
||||||
|
|
||||||
|
var Backgrounds := preload("res://addons/dialogic/Modules/Background/subsystem_backgrounds.gd").new():
|
||||||
|
get: return get_subsystem("Backgrounds")
|
||||||
|
|
||||||
|
var Choices := preload("res://addons/dialogic/Modules/Choice/subsystem_choices.gd").new():
|
||||||
|
get: return get_subsystem("Choices")
|
||||||
|
|
||||||
|
var Expressions := preload("res://addons/dialogic/Modules/Core/subsystem_expression.gd").new():
|
||||||
|
get: return get_subsystem("Expressions")
|
||||||
|
|
||||||
|
var Glossary := preload("res://addons/dialogic/Modules/Glossary/subsystem_glossary.gd").new():
|
||||||
|
get: return get_subsystem("Glossary")
|
||||||
|
|
||||||
|
var History := preload("res://addons/dialogic/Modules/History/subsystem_history.gd").new():
|
||||||
|
get: return get_subsystem("History")
|
||||||
|
|
||||||
|
var Inputs := preload("res://addons/dialogic/Modules/Core/subsystem_input.gd").new():
|
||||||
|
get: return get_subsystem("Inputs")
|
||||||
|
|
||||||
|
var Jump := preload("res://addons/dialogic/Modules/Jump/subsystem_jump.gd").new():
|
||||||
|
get: return get_subsystem("Jump")
|
||||||
|
|
||||||
|
var PortraitContainers := preload("res://addons/dialogic/Modules/Character/subsystem_containers.gd").new():
|
||||||
|
get: return get_subsystem("PortraitContainers")
|
||||||
|
|
||||||
|
var Portraits := preload("res://addons/dialogic/Modules/Character/subsystem_portraits.gd").new():
|
||||||
|
get: return get_subsystem("Portraits")
|
||||||
|
|
||||||
|
var Save := preload("res://addons/dialogic/Modules/Save/subsystem_save.gd").new():
|
||||||
|
get: return get_subsystem("Save")
|
||||||
|
|
||||||
|
var Settings := preload("res://addons/dialogic/Modules/Settings/subsystem_settings.gd").new():
|
||||||
|
get: return get_subsystem("Settings")
|
||||||
|
|
||||||
|
var Styles := preload("res://addons/dialogic/Modules/Style/subsystem_styles.gd").new():
|
||||||
|
get: return get_subsystem("Styles")
|
||||||
|
|
||||||
|
var Text := preload("res://addons/dialogic/Modules/Text/subsystem_text.gd").new():
|
||||||
|
get: return get_subsystem("Text")
|
||||||
|
|
||||||
|
var TextInput := preload("res://addons/dialogic/Modules/TextInput/subsystem_text_input.gd").new():
|
||||||
|
get: return get_subsystem("TextInput")
|
||||||
|
|
||||||
|
var VAR := preload("res://addons/dialogic/Modules/Variable/subsystem_variables.gd").new():
|
||||||
|
get: return get_subsystem("VAR")
|
||||||
|
|
||||||
|
var Voice := preload("res://addons/dialogic/Modules/Voice/subsystem_voice.gd").new():
|
||||||
|
get: return get_subsystem("Voice")
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
## Autoloads are added first, so this happens REALLY early on game startup.
|
||||||
|
func _ready() -> void:
|
||||||
|
_collect_subsystems()
|
||||||
|
|
||||||
|
clear()
|
||||||
|
|
||||||
|
DialogicResourceUtil.update_event_cache()
|
||||||
|
|
||||||
|
dialog_ending_timeline = DialogicTimeline.new()
|
||||||
|
dialog_ending_timeline.from_text("[clear]")
|
||||||
|
|
||||||
|
|
||||||
|
#region TIMELINE & EVENT HANDLING
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
## Method to start a timeline AND ensure that a layout scene is present.
|
||||||
|
## For argument info, checkout [method start_timeline].
|
||||||
|
## -> returns the layout node
|
||||||
|
func start(timeline:Variant, label_or_idx:Variant="") -> Node:
|
||||||
|
# If we don't have a style subsystem, default to just start_timeline()
|
||||||
|
if not has_subsystem('Styles'):
|
||||||
|
printerr("[Dialogic] You called Dialogic.start() but the Styles subsystem is missing!")
|
||||||
|
clear(ClearFlags.KEEP_VARIABLES)
|
||||||
|
start_timeline(timeline, label_or_idx)
|
||||||
|
return null
|
||||||
|
|
||||||
|
# Otherwise make sure there is a style active.
|
||||||
|
var scene: Node = null
|
||||||
|
if not self.Styles.has_active_layout_node():
|
||||||
|
scene = self.Styles.load_style()
|
||||||
|
else:
|
||||||
|
scene = self.Styles.get_layout_node()
|
||||||
|
scene.show()
|
||||||
|
|
||||||
|
if not scene.is_node_ready():
|
||||||
|
if not scene.ready.is_connected(clear.bind(ClearFlags.KEEP_VARIABLES)):
|
||||||
|
scene.ready.connect(clear.bind(ClearFlags.KEEP_VARIABLES))
|
||||||
|
if not scene.ready.is_connected(start_timeline.bind(timeline, label_or_idx)):
|
||||||
|
scene.ready.connect(start_timeline.bind(timeline, label_or_idx))
|
||||||
|
else:
|
||||||
|
start_timeline(timeline, label_or_idx)
|
||||||
|
|
||||||
|
return scene
|
||||||
|
|
||||||
|
|
||||||
|
## Method to start a timeline without adding a layout scene.
|
||||||
|
## @timeline can be either a loaded timeline resource or a path to a timeline file.
|
||||||
|
## @label_or_idx can be a label (string) or index (int) to skip to immediatly.
|
||||||
|
func start_timeline(timeline:Variant, label_or_idx:Variant = "") -> void:
|
||||||
|
# load the resource if only the path is given
|
||||||
|
if typeof(timeline) in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||||
|
#check the lookup table if it's not a full file name
|
||||||
|
if "://" in timeline:
|
||||||
|
timeline = load(timeline)
|
||||||
|
else:
|
||||||
|
timeline = DialogicResourceUtil.get_timeline_resource(timeline)
|
||||||
|
|
||||||
|
if timeline == null:
|
||||||
|
printerr("[Dialogic] There was an error loading this timeline. Check the filename, and the timeline for errors")
|
||||||
|
return
|
||||||
|
|
||||||
|
(timeline as DialogicTimeline).process()
|
||||||
|
|
||||||
|
current_timeline = timeline
|
||||||
|
current_timeline_events = current_timeline.events
|
||||||
|
for event in current_timeline_events:
|
||||||
|
event.dialogic = self
|
||||||
|
current_event_idx = -1
|
||||||
|
|
||||||
|
if typeof(label_or_idx) in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||||
|
if label_or_idx:
|
||||||
|
if has_subsystem('Jump'):
|
||||||
|
Jump.jump_to_label((label_or_idx as String))
|
||||||
|
elif typeof(label_or_idx) == TYPE_INT:
|
||||||
|
if label_or_idx >-1:
|
||||||
|
current_event_idx = label_or_idx -1
|
||||||
|
|
||||||
|
if not current_timeline == dialog_ending_timeline:
|
||||||
|
timeline_started.emit()
|
||||||
|
|
||||||
|
handle_next_event()
|
||||||
|
|
||||||
|
|
||||||
|
## Preloader function, prepares a timeline and returns an object to hold for later
|
||||||
|
## [param timeline_resource] can be either a path (string) or a loaded timeline (resource)
|
||||||
|
func preload_timeline(timeline_resource:Variant) -> Variant:
|
||||||
|
# I think ideally this should be on a new thread, will test
|
||||||
|
if typeof(timeline_resource) in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||||
|
if "://" in timeline_resource:
|
||||||
|
timeline_resource = load(timeline_resource)
|
||||||
|
else:
|
||||||
|
timeline_resource = DialogicResourceUtil.get_timeline_resource(timeline_resource)
|
||||||
|
|
||||||
|
if timeline_resource == null:
|
||||||
|
printerr("[Dialogic] There was an error preloading this timeline. Check the filename, and the timeline for errors")
|
||||||
|
return null
|
||||||
|
|
||||||
|
(timeline_resource as DialogicTimeline).process()
|
||||||
|
|
||||||
|
return timeline_resource
|
||||||
|
|
||||||
|
|
||||||
|
## Clears and stops the current timeline.
|
||||||
|
## If [param skip_ending] is `true`, the dialog_ending_timeline is not getting played
|
||||||
|
func end_timeline(skip_ending := false) -> void:
|
||||||
|
if not skip_ending and dialog_ending_timeline and current_timeline != dialog_ending_timeline:
|
||||||
|
start(dialog_ending_timeline)
|
||||||
|
return
|
||||||
|
|
||||||
|
await clear(ClearFlags.TIMELINE_INFO_ONLY)
|
||||||
|
|
||||||
|
if Styles.has_active_layout_node() and Styles.get_layout_node().is_inside_tree():
|
||||||
|
match ProjectSettings.get_setting('dialogic/layout/end_behaviour', 0):
|
||||||
|
0:
|
||||||
|
Styles.get_layout_node().get_parent().remove_child(Styles.get_layout_node())
|
||||||
|
Styles.get_layout_node().queue_free()
|
||||||
|
1:
|
||||||
|
Styles.get_layout_node().hide()
|
||||||
|
|
||||||
|
timeline_ended.emit()
|
||||||
|
|
||||||
|
|
||||||
|
## Method to check if timeline exists.
|
||||||
|
## @timeline can be either a loaded timeline resource or a path to a timeline file.
|
||||||
|
func timeline_exists(timeline:Variant) -> bool:
|
||||||
|
if typeof(timeline) in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||||
|
if "://" in timeline and ResourceLoader.exists(timeline):
|
||||||
|
return load(timeline) is DialogicTimeline
|
||||||
|
else:
|
||||||
|
return DialogicResourceUtil.timeline_resource_exists(timeline)
|
||||||
|
|
||||||
|
return timeline is DialogicTimeline
|
||||||
|
|
||||||
|
|
||||||
|
## Handles the next event.
|
||||||
|
func handle_next_event(_ignore_argument: Variant = "") -> void:
|
||||||
|
handle_event(current_event_idx+1)
|
||||||
|
|
||||||
|
|
||||||
|
## Handles the event at the given index [param event_index].
|
||||||
|
## You can call this manually, but if another event is still executing, it might have unexpected results.
|
||||||
|
func handle_event(event_index:int) -> void:
|
||||||
|
if not current_timeline:
|
||||||
|
return
|
||||||
|
|
||||||
|
_cleanup_previous_event()
|
||||||
|
|
||||||
|
if paused:
|
||||||
|
await dialogic_resumed
|
||||||
|
|
||||||
|
if event_index >= len(current_timeline_events):
|
||||||
|
end_timeline()
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: Check if necessary. This should be impossible.
|
||||||
|
#actually process the event now, since we didnt earlier at runtime
|
||||||
|
#this needs to happen before we create the copy DialogicEvent variable, so it doesn't throw an error if not ready
|
||||||
|
if current_timeline_events[event_index].event_node_ready == false:
|
||||||
|
current_timeline_events[event_index]._load_from_string(current_timeline_events[event_index].event_node_as_text)
|
||||||
|
|
||||||
|
current_event_idx = event_index
|
||||||
|
|
||||||
|
if not current_timeline_events[event_index].event_finished.is_connected(handle_next_event):
|
||||||
|
current_timeline_events[event_index].event_finished.connect(handle_next_event)
|
||||||
|
|
||||||
|
set_meta('previous_event', current_timeline_events[event_index])
|
||||||
|
|
||||||
|
current_timeline_events[event_index].execute(self)
|
||||||
|
event_handled.emit(current_timeline_events[event_index])
|
||||||
|
|
||||||
|
|
||||||
|
## Resets Dialogic's state fully or partially.
|
||||||
|
## By using the clear flags from the [member ClearFlags] enum you can specify
|
||||||
|
## what info should be kept.
|
||||||
|
## For example, at timeline end usually it doesn't clear node or subsystem info.
|
||||||
|
func clear(clear_flags := ClearFlags.FULL_CLEAR) -> void:
|
||||||
|
_cleanup_previous_event()
|
||||||
|
|
||||||
|
if !clear_flags & ClearFlags.TIMELINE_INFO_ONLY:
|
||||||
|
for subsystem in get_children():
|
||||||
|
if subsystem is DialogicSubsystem:
|
||||||
|
(subsystem as DialogicSubsystem).clear_game_state(clear_flags)
|
||||||
|
|
||||||
|
var timeline := current_timeline
|
||||||
|
|
||||||
|
current_timeline = null
|
||||||
|
current_event_idx = -1
|
||||||
|
current_timeline_events = []
|
||||||
|
current_state = States.IDLE
|
||||||
|
|
||||||
|
# Resetting variables
|
||||||
|
if timeline:
|
||||||
|
await timeline.clean()
|
||||||
|
|
||||||
|
|
||||||
|
## Cleanup after previous event (if any).
|
||||||
|
func _cleanup_previous_event():
|
||||||
|
if has_meta('previous_event') and get_meta('previous_event') is DialogicEvent:
|
||||||
|
var event := get_meta('previous_event') as DialogicEvent
|
||||||
|
if event.event_finished.is_connected(handle_next_event):
|
||||||
|
event.event_finished.disconnect(handle_next_event)
|
||||||
|
event._clear_state()
|
||||||
|
remove_meta("previous_event")
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region SAVING & LOADING
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
## Returns a dictionary containing all necessary information to later recreate the same state with load_full_state.
|
||||||
|
## The [subsystem Save] subsystem might be more useful for you.
|
||||||
|
## However, this can be used to integrate the info into your own save system.
|
||||||
|
func get_full_state() -> Dictionary:
|
||||||
|
if current_timeline:
|
||||||
|
current_state_info['current_event_idx'] = current_event_idx
|
||||||
|
current_state_info['current_timeline'] = current_timeline.resource_path
|
||||||
|
else:
|
||||||
|
current_state_info['current_event_idx'] = -1
|
||||||
|
current_state_info['current_timeline'] = null
|
||||||
|
|
||||||
|
for subsystem in get_children():
|
||||||
|
(subsystem as DialogicSubsystem).save_game_state()
|
||||||
|
|
||||||
|
return current_state_info.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
## This method tries to load the state from the given [param state_info].
|
||||||
|
## Will automatically start a timeline and add a layout if a timeline was running when
|
||||||
|
## the dictionary was retrieved with [method get_full_state].
|
||||||
|
func load_full_state(state_info:Dictionary) -> void:
|
||||||
|
clear()
|
||||||
|
current_state_info = state_info
|
||||||
|
## The Style subsystem needs to run first for others to load correctly.
|
||||||
|
var scene: Node = null
|
||||||
|
if has_subsystem('Styles'):
|
||||||
|
get_subsystem('Styles').load_game_state()
|
||||||
|
scene = self.Styles.get_layout_node()
|
||||||
|
|
||||||
|
var load_subsystems := func() -> void:
|
||||||
|
for subsystem in get_children():
|
||||||
|
if subsystem.name == 'Styles':
|
||||||
|
continue
|
||||||
|
(subsystem as DialogicSubsystem).load_game_state()
|
||||||
|
|
||||||
|
if null != scene and not scene.is_node_ready():
|
||||||
|
scene.ready.connect(load_subsystems)
|
||||||
|
else:
|
||||||
|
await get_tree().process_frame
|
||||||
|
load_subsystems.call()
|
||||||
|
|
||||||
|
if current_state_info.get('current_timeline', null):
|
||||||
|
start_timeline(current_state_info.current_timeline, current_state_info.get('current_event_idx', 0))
|
||||||
|
else:
|
||||||
|
end_timeline.call_deferred(true)
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region SUB-SYTSEMS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func _collect_subsystems() -> void:
|
||||||
|
var subsystem_nodes := [] as Array[DialogicSubsystem]
|
||||||
|
for indexer in DialogicUtil.get_indexers():
|
||||||
|
for subsystem in indexer._get_subsystems():
|
||||||
|
var subsystem_node := add_subsystem(str(subsystem.name), str(subsystem.script))
|
||||||
|
subsystem_nodes.push_back(subsystem_node)
|
||||||
|
|
||||||
|
for subsystem in subsystem_nodes:
|
||||||
|
subsystem.post_install()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns `true` if a subystem with the given [param subsystem_name] exists.
|
||||||
|
func has_subsystem(subsystem_name:String) -> bool:
|
||||||
|
return has_node(subsystem_name)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the subsystem node of the given [param subsystem_name] or null if it doesn't exist.
|
||||||
|
func get_subsystem(subsystem_name:String) -> DialogicSubsystem:
|
||||||
|
return get_node(subsystem_name)
|
||||||
|
|
||||||
|
|
||||||
|
## Adds a subsystem node with the given [param subsystem_name] and [param script_path].
|
||||||
|
func add_subsystem(subsystem_name:String, script_path:String) -> DialogicSubsystem:
|
||||||
|
var node: Node = Node.new()
|
||||||
|
node.name = subsystem_name
|
||||||
|
node.set_script(load(script_path))
|
||||||
|
node = node as DialogicSubsystem
|
||||||
|
node.dialogic = self
|
||||||
|
add_child(node)
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region HELPERS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func print_debug_moment() -> void:
|
||||||
|
if not current_timeline:
|
||||||
|
return
|
||||||
|
|
||||||
|
printerr("\tAt event ", current_event_idx+1, " (",current_timeline_events[current_event_idx].event_name, ' Event) in timeline "', current_timeline.get_identifier(), '" (',current_timeline.resource_path,').')
|
||||||
|
print("\n")
|
||||||
|
#endregion
|
||||||
1
addons/dialogic/Core/DialogicGameHandler.gd.uid
Normal file
1
addons/dialogic/Core/DialogicGameHandler.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ds2q0uclmolvu
|
||||||
376
addons/dialogic/Core/DialogicResourceUtil.gd
Normal file
376
addons/dialogic/Core/DialogicResourceUtil.gd
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
@tool
|
||||||
|
class_name DialogicResourceUtil
|
||||||
|
|
||||||
|
static var label_cache := {}
|
||||||
|
static var event_cache: Array[DialogicEvent] = []
|
||||||
|
static var channel_cache := {}
|
||||||
|
|
||||||
|
static var special_resources := {}
|
||||||
|
|
||||||
|
|
||||||
|
static func update() -> void:
|
||||||
|
update_directory('.dch')
|
||||||
|
update_directory('.dtl')
|
||||||
|
update_label_cache()
|
||||||
|
update_audio_channel_cache()
|
||||||
|
DialogicStylesUtil.build_style_directory()
|
||||||
|
|
||||||
|
|
||||||
|
#region RESOURCE DIRECTORIES
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
static func get_directory(extension:String) -> Dictionary:
|
||||||
|
extension = extension.trim_prefix('.')
|
||||||
|
if Engine.has_meta(extension+'_directory'):
|
||||||
|
return Engine.get_meta(extension+'_directory', {})
|
||||||
|
|
||||||
|
var directory: Dictionary = ProjectSettings.get_setting("dialogic/directories/"+extension+'_directory', {})
|
||||||
|
Engine.set_meta(extension+'_directory', directory)
|
||||||
|
return directory
|
||||||
|
|
||||||
|
|
||||||
|
static func set_directory(extension:String, directory:Dictionary) -> void:
|
||||||
|
extension = extension.trim_prefix('.')
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
ProjectSettings.set_setting("dialogic/directories/"+extension+'_directory', directory)
|
||||||
|
ProjectSettings.save()
|
||||||
|
Engine.set_meta(extension+'_directory', directory)
|
||||||
|
|
||||||
|
|
||||||
|
static func update_directory(extension:String) -> void:
|
||||||
|
var directory := get_directory(extension)
|
||||||
|
|
||||||
|
for resource in list_resources_of_type(extension):
|
||||||
|
if not resource in directory.values():
|
||||||
|
directory = add_resource_to_directory(resource, directory)
|
||||||
|
|
||||||
|
var keys_to_remove := []
|
||||||
|
for key in directory:
|
||||||
|
if not ResourceLoader.exists(directory[key]):
|
||||||
|
keys_to_remove.append(key)
|
||||||
|
for key in keys_to_remove:
|
||||||
|
directory.erase(key)
|
||||||
|
|
||||||
|
set_directory(extension, directory)
|
||||||
|
|
||||||
|
|
||||||
|
static func add_resource_to_directory(file_path:String, directory:Dictionary) -> Dictionary:
|
||||||
|
var suggested_name := file_path.get_file().trim_suffix("."+file_path.get_extension())
|
||||||
|
var temp := suggested_name
|
||||||
|
while suggested_name in directory:
|
||||||
|
suggested_name = file_path.trim_suffix("/"+suggested_name+"."+file_path.get_extension()).get_file().path_join(suggested_name)
|
||||||
|
if suggested_name == temp:
|
||||||
|
break
|
||||||
|
temp = suggested_name
|
||||||
|
directory[suggested_name] = file_path
|
||||||
|
return directory
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the unique identifier for the given resource path.
|
||||||
|
## Returns an empty string if no identifier was found.
|
||||||
|
static func get_unique_identifier_by_path(file_path:String) -> String:
|
||||||
|
if not file_path: return ""
|
||||||
|
var identifier: Variant = get_directory(file_path.get_extension()).find_key(file_path)
|
||||||
|
if typeof(identifier) == TYPE_STRING:
|
||||||
|
return identifier
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
static func get_resource_path_from_identifier(identifier:String, extension:String) -> String:
|
||||||
|
var value: Variant = get_directory(extension).get(identifier, '')
|
||||||
|
if value is String:
|
||||||
|
return value
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the resource associated with the given unique identifier.
|
||||||
|
## The expected extension is needed to use the right directory.
|
||||||
|
static func get_resource_from_identifier(identifier:String, extension:String) -> Resource:
|
||||||
|
var value: Variant = get_directory(extension).get(identifier, '')
|
||||||
|
if typeof(value) == TYPE_STRING and ResourceLoader.exists(value):
|
||||||
|
return load(value)
|
||||||
|
elif value is Resource:
|
||||||
|
return value
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
## Returns a boolean that expresses whether the resource exists.
|
||||||
|
## The expected extension is needed to use the right directory.
|
||||||
|
static func resource_exists_from_identifier(identifier:String, extension:String) -> bool:
|
||||||
|
var value: Variant = get_directory(extension).get(identifier, '')
|
||||||
|
if typeof(value) == TYPE_STRING:
|
||||||
|
return ResourceLoader.exists(value)
|
||||||
|
return value is Resource
|
||||||
|
|
||||||
|
|
||||||
|
## Editor Only
|
||||||
|
static func change_unique_identifier(file_path:String, new_identifier:String) -> void:
|
||||||
|
var directory := get_directory(file_path.get_extension())
|
||||||
|
var key: Variant = directory.find_key(file_path)
|
||||||
|
while key != null:
|
||||||
|
if key == new_identifier:
|
||||||
|
break
|
||||||
|
directory.erase(key)
|
||||||
|
directory[new_identifier] = file_path
|
||||||
|
key = directory.find_key(file_path)
|
||||||
|
set_directory(file_path.get_extension(), directory)
|
||||||
|
|
||||||
|
|
||||||
|
static func change_resource_path(old_path:String, new_path:String) -> void:
|
||||||
|
var directory := get_directory(new_path.get_extension())
|
||||||
|
var key: Variant = directory.find_key(old_path)
|
||||||
|
while key != null:
|
||||||
|
directory[key] = new_path
|
||||||
|
key = directory.find_key(old_path)
|
||||||
|
set_directory(new_path.get_extension(), directory)
|
||||||
|
|
||||||
|
|
||||||
|
static func remove_resource(file_path:String) -> void:
|
||||||
|
var directory := get_directory(file_path.get_extension())
|
||||||
|
var key: Variant = directory.find_key(file_path)
|
||||||
|
while key != null:
|
||||||
|
directory.erase(key)
|
||||||
|
key = directory.find_key(file_path)
|
||||||
|
set_directory(file_path.get_extension(), directory)
|
||||||
|
|
||||||
|
|
||||||
|
static func is_identifier_unused(extension:String, identifier:String) -> bool:
|
||||||
|
return not identifier in get_directory(extension)
|
||||||
|
|
||||||
|
|
||||||
|
## While usually the directory maps identifiers to paths, this method (only supposed to be used at runtime)
|
||||||
|
## allows mapping resources that are not saved to an identifier.
|
||||||
|
static func register_runtime_resource(resource:Resource, identifier:String, extension:String) -> void:
|
||||||
|
var directory := get_directory(extension)
|
||||||
|
directory[identifier] = resource
|
||||||
|
set_directory(extension, directory)
|
||||||
|
|
||||||
|
|
||||||
|
static func get_runtime_unique_identifier(resource:Resource, extension:String) -> String:
|
||||||
|
var identifier: Variant = get_directory(extension).find_key(resource)
|
||||||
|
if typeof(identifier) == TYPE_STRING:
|
||||||
|
return identifier
|
||||||
|
return ""
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region LABEL CACHE
|
||||||
|
################################################################################
|
||||||
|
# The label cache is only for the editor so we don't have to scan all timelines
|
||||||
|
# whenever we want to suggest labels. This has no use in game and is not always perfect.
|
||||||
|
|
||||||
|
static func get_label_cache() -> Dictionary:
|
||||||
|
if not label_cache.is_empty():
|
||||||
|
return label_cache
|
||||||
|
|
||||||
|
label_cache = DialogicUtil.get_editor_setting('label_ref', {})
|
||||||
|
return label_cache
|
||||||
|
|
||||||
|
|
||||||
|
static func set_label_cache(cache:Dictionary) -> void:
|
||||||
|
label_cache = cache
|
||||||
|
|
||||||
|
|
||||||
|
static func update_label_cache() -> void:
|
||||||
|
var cache := get_label_cache()
|
||||||
|
var timelines := get_timeline_directory().values()
|
||||||
|
for timeline in cache:
|
||||||
|
if !timeline in timelines:
|
||||||
|
cache.erase(timeline)
|
||||||
|
set_label_cache(cache)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region AUDIO CHANNEL CACHE
|
||||||
|
################################################################################
|
||||||
|
# The audio channel cache is only for the editor so we don't have to scan all timelines
|
||||||
|
# whenever we want to suggest channels. This has no use in game and is not always perfect.
|
||||||
|
|
||||||
|
static func get_audio_channel_cache() -> Dictionary:
|
||||||
|
if not channel_cache.is_empty():
|
||||||
|
return channel_cache
|
||||||
|
|
||||||
|
channel_cache = DialogicUtil.get_editor_setting('channel_ref', {})
|
||||||
|
return channel_cache
|
||||||
|
|
||||||
|
|
||||||
|
static func get_channel_list() -> Array:
|
||||||
|
if channel_cache.is_empty():
|
||||||
|
return []
|
||||||
|
|
||||||
|
var cached_names := []
|
||||||
|
for timeline in channel_cache:
|
||||||
|
for name in channel_cache[timeline]:
|
||||||
|
if not cached_names.has(name):
|
||||||
|
cached_names.append(name)
|
||||||
|
return cached_names
|
||||||
|
|
||||||
|
|
||||||
|
static func set_audio_channel_cache(cache:Dictionary) -> void:
|
||||||
|
channel_cache = cache
|
||||||
|
|
||||||
|
|
||||||
|
static func update_audio_channel_cache() -> void:
|
||||||
|
var cache := get_audio_channel_cache()
|
||||||
|
var timelines := get_timeline_directory().values()
|
||||||
|
for timeline in cache:
|
||||||
|
if !timeline in timelines:
|
||||||
|
cache.erase(timeline)
|
||||||
|
set_audio_channel_cache(cache)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region EVENT CACHE
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
## Dialogic keeps a list that has each event once. This allows retrieval of that list.
|
||||||
|
static func get_event_cache() -> Array:
|
||||||
|
if not event_cache.is_empty():
|
||||||
|
return event_cache
|
||||||
|
|
||||||
|
event_cache = update_event_cache()
|
||||||
|
return event_cache
|
||||||
|
|
||||||
|
|
||||||
|
static func update_event_cache() -> Array:
|
||||||
|
event_cache = []
|
||||||
|
for indexer in DialogicUtil.get_indexers():
|
||||||
|
# build event cache
|
||||||
|
for event in indexer._get_events():
|
||||||
|
if not ResourceLoader.exists(event):
|
||||||
|
continue
|
||||||
|
if not 'event_end_branch.gd' in event and not 'event_text.gd' in event:
|
||||||
|
event_cache.append(load(event).new())
|
||||||
|
|
||||||
|
# Events are checked in order while testing them. EndBranch needs to be first, Text needs to be last
|
||||||
|
event_cache.push_front(DialogicEndBranchEvent.new())
|
||||||
|
event_cache.push_back(DialogicTextEvent.new())
|
||||||
|
|
||||||
|
return event_cache
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region SPECIAL RESOURCES
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
static func update_special_resources() -> void:
|
||||||
|
special_resources.clear()
|
||||||
|
for indexer in DialogicUtil.get_indexers():
|
||||||
|
var additions := indexer._get_special_resources()
|
||||||
|
for resource_type in additions:
|
||||||
|
if not resource_type in special_resources:
|
||||||
|
special_resources[resource_type] = {}
|
||||||
|
special_resources[resource_type].merge(additions[resource_type])
|
||||||
|
|
||||||
|
|
||||||
|
static func list_special_resources(type:String, filter := {}) -> Dictionary:
|
||||||
|
if special_resources.is_empty():
|
||||||
|
update_special_resources()
|
||||||
|
if type in special_resources:
|
||||||
|
if filter.is_empty():
|
||||||
|
return special_resources[type]
|
||||||
|
else:
|
||||||
|
var results := {}
|
||||||
|
for i in special_resources[type]:
|
||||||
|
if match_resource_filter(special_resources[type][i], filter):
|
||||||
|
results[i] = special_resources[type][i]
|
||||||
|
return results
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
static func match_resource_filter(dict:Dictionary, filter:Dictionary) -> bool:
|
||||||
|
for i in filter:
|
||||||
|
if not i in dict:
|
||||||
|
return false
|
||||||
|
if typeof(filter[i]) == TYPE_ARRAY:
|
||||||
|
if not dict[i] in filter[i]:
|
||||||
|
return false
|
||||||
|
else:
|
||||||
|
if not dict[i] == filter[i]:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
static func guess_special_resource(type: String, string: String, default := {}, filter := {}, ignores:PackedStringArray=[]) -> Dictionary:
|
||||||
|
if string.is_empty():
|
||||||
|
return default
|
||||||
|
|
||||||
|
if special_resources.is_empty():
|
||||||
|
update_special_resources()
|
||||||
|
var resources := list_special_resources(type, filter)
|
||||||
|
if resources.is_empty():
|
||||||
|
printerr("[Dialogic] No ", type, "s found, but attempted to use one.")
|
||||||
|
return default
|
||||||
|
|
||||||
|
if string.begins_with('res://'):
|
||||||
|
for i in resources.values():
|
||||||
|
if i.path == string:
|
||||||
|
return i
|
||||||
|
printerr("[Dialogic] Unable to find ", type, " at path '", string, "'.")
|
||||||
|
return default
|
||||||
|
|
||||||
|
string = string.to_lower()
|
||||||
|
|
||||||
|
if string in resources:
|
||||||
|
return resources[string]
|
||||||
|
|
||||||
|
if not ignores.is_empty():
|
||||||
|
var regex := RegEx.create_from_string(r" ?\b(" + "|".join(ignores) + r")\b")
|
||||||
|
for name in resources:
|
||||||
|
if regex.sub(name, "") == regex.sub(string, ""):
|
||||||
|
return resources[name]
|
||||||
|
|
||||||
|
## As a last effort check against the unfiltered list
|
||||||
|
if string in special_resources[type]:
|
||||||
|
push_warning("[Dialogic] Using ", type, " '", string,"' when not supposed to.")
|
||||||
|
return special_resources[type][string]
|
||||||
|
|
||||||
|
printerr("[Dialogic] Unable to identify ", type, " based on string '", string, "'.")
|
||||||
|
return default
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region HELPERS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
static func get_character_directory() -> Dictionary:
|
||||||
|
return get_directory('dch')
|
||||||
|
|
||||||
|
|
||||||
|
static func get_timeline_directory() -> Dictionary:
|
||||||
|
return get_directory('dtl')
|
||||||
|
|
||||||
|
|
||||||
|
static func timeline_resource_exists(timeline_identifier:String) -> bool:
|
||||||
|
return resource_exists_from_identifier(timeline_identifier, 'dtl')
|
||||||
|
|
||||||
|
|
||||||
|
static func get_timeline_resource(timeline_identifier:String) -> DialogicTimeline:
|
||||||
|
return get_resource_from_identifier(timeline_identifier, 'dtl')
|
||||||
|
|
||||||
|
|
||||||
|
static func get_character_resource(character_identifier:String) -> DialogicCharacter:
|
||||||
|
return get_resource_from_identifier(character_identifier, 'dch')
|
||||||
|
|
||||||
|
|
||||||
|
static func list_resources_of_type(extension:String) -> Array:
|
||||||
|
var all_resources := scan_folder('res://', extension)
|
||||||
|
return all_resources
|
||||||
|
|
||||||
|
|
||||||
|
static func scan_folder(path:String, extension:String) -> Array:
|
||||||
|
var list: Array = []
|
||||||
|
if DirAccess.dir_exists_absolute(path) and not FileAccess.file_exists(path + "/" + ".gdignore"):
|
||||||
|
var dir := DirAccess.open(path)
|
||||||
|
dir.list_dir_begin()
|
||||||
|
var file_name := dir.get_next()
|
||||||
|
while file_name != "":
|
||||||
|
if dir.current_is_dir() and not file_name.begins_with("."):
|
||||||
|
list += scan_folder(path.path_join(file_name), extension)
|
||||||
|
else:
|
||||||
|
if file_name.ends_with(extension):
|
||||||
|
list.append(path.path_join(file_name))
|
||||||
|
file_name = dir.get_next()
|
||||||
|
return list
|
||||||
|
|
||||||
|
#endregion
|
||||||
1
addons/dialogic/Core/DialogicResourceUtil.gd.uid
Normal file
1
addons/dialogic/Core/DialogicResourceUtil.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bdt5bbxxkvab4
|
||||||
806
addons/dialogic/Core/DialogicUtil.gd
Normal file
806
addons/dialogic/Core/DialogicUtil.gd
Normal file
@@ -0,0 +1,806 @@
|
|||||||
|
@tool
|
||||||
|
class_name DialogicUtil
|
||||||
|
|
||||||
|
## Script that container helper methods for both editor and game execution.
|
||||||
|
## Used whenever the same thing is needed in different parts of the plugin.
|
||||||
|
|
||||||
|
#region EDITOR
|
||||||
|
|
||||||
|
## This method should be used instead of EditorInterface.get_editor_scale(), because if you use that
|
||||||
|
## it will run perfectly fine from the editor, but crash when the game is exported.
|
||||||
|
static func get_editor_scale() -> float:
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
return get_dialogic_plugin().get_editor_interface().get_editor_scale()
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
|
||||||
|
## Although this does in fact always return a EditorPlugin node,
|
||||||
|
## that class is apparently not present in export and referencing it here creates a crash.
|
||||||
|
static func get_dialogic_plugin() -> Node:
|
||||||
|
for child in Engine.get_main_loop().get_root().get_children():
|
||||||
|
if child.get_class() == "EditorNode":
|
||||||
|
return child.get_node('DialogicPlugin')
|
||||||
|
return null
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the autoload when in-game.
|
||||||
|
static func autoload() -> DialogicGameHandler:
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
return null
|
||||||
|
if not Engine.get_main_loop().root.has_node("Dialogic"):
|
||||||
|
return null
|
||||||
|
return Engine.get_main_loop().root.get_node("Dialogic")
|
||||||
|
|
||||||
|
|
||||||
|
#region FILE SYSTEM
|
||||||
|
################################################################################
|
||||||
|
static func listdir(path: String, files_only:= true, _throw_error:= true, full_file_path:= false, include_imports := false) -> Array:
|
||||||
|
var files: Array = []
|
||||||
|
if path.is_empty(): path = "res://"
|
||||||
|
if DirAccess.dir_exists_absolute(path):
|
||||||
|
var dir := DirAccess.open(path)
|
||||||
|
dir.list_dir_begin()
|
||||||
|
var file_name := dir.get_next()
|
||||||
|
while file_name != "":
|
||||||
|
if not file_name.begins_with("."):
|
||||||
|
if files_only:
|
||||||
|
if not dir.current_is_dir() and (not file_name.ends_with('.import') or include_imports):
|
||||||
|
if full_file_path:
|
||||||
|
files.append(path.path_join(file_name))
|
||||||
|
else:
|
||||||
|
files.append(file_name)
|
||||||
|
else:
|
||||||
|
if full_file_path:
|
||||||
|
files.append(path.path_join(file_name))
|
||||||
|
else:
|
||||||
|
files.append(file_name)
|
||||||
|
file_name = dir.get_next()
|
||||||
|
dir.list_dir_end()
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
static func get_module_path(name:String, builtin:=true) -> String:
|
||||||
|
if builtin:
|
||||||
|
return "res://addons/dialogic/Modules".path_join(name)
|
||||||
|
else:
|
||||||
|
return ProjectSettings.get_setting('dialogic/extensions_folder', 'res://addons/dialogic_additions').path_join(name)
|
||||||
|
|
||||||
|
|
||||||
|
## This is a private and editor-only function.
|
||||||
|
##
|
||||||
|
## Populates the [class DialogicGameHandler] with new custom subsystems by
|
||||||
|
## directly manipulating the file's content and then importing the file.
|
||||||
|
static func _update_autoload_subsystem_access() -> void:
|
||||||
|
if not Engine.is_editor_hint():
|
||||||
|
printerr("[Dialogic] This function is only available in the editor.")
|
||||||
|
return
|
||||||
|
|
||||||
|
var script: Script = load("res://addons/dialogic/Core/DialogicGameHandler.gd")
|
||||||
|
var new_subsystem_access_list := "#region SUBSYSTEMS\n"
|
||||||
|
var subsystems_sorted := []
|
||||||
|
|
||||||
|
for indexer: DialogicIndexer in get_indexers(true, true):
|
||||||
|
|
||||||
|
for subsystem: Dictionary in indexer._get_subsystems().duplicate(true):
|
||||||
|
subsystems_sorted.append(subsystem)
|
||||||
|
|
||||||
|
subsystems_sorted.sort_custom(func (a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return a.name < b.name
|
||||||
|
)
|
||||||
|
|
||||||
|
for subsystem: Dictionary in subsystems_sorted:
|
||||||
|
new_subsystem_access_list += '\nvar {name} := preload("{script}").new():\n\tget: return get_subsystem("{name}")\n'.format(subsystem)
|
||||||
|
|
||||||
|
new_subsystem_access_list += "\n#endregion"
|
||||||
|
script.source_code = RegEx.create_from_string(r"#region SUBSYSTEMS\n#*\n((?!#endregion)(.*\n))*#endregion").sub(script.source_code, new_subsystem_access_list)
|
||||||
|
ResourceSaver.save(script)
|
||||||
|
Engine.get_singleton("EditorInterface").get_resource_filesystem().reimport_files(["res://addons/dialogic/Core/DialogicGameHandler.gd"])
|
||||||
|
|
||||||
|
|
||||||
|
static func get_indexers(include_custom := true, force_reload := false) -> Array[DialogicIndexer]:
|
||||||
|
if Engine.get_main_loop().has_meta('dialogic_indexers') and not force_reload:
|
||||||
|
return Engine.get_main_loop().get_meta('dialogic_indexers')
|
||||||
|
|
||||||
|
var indexers: Array[DialogicIndexer] = []
|
||||||
|
for file in listdir(DialogicUtil.get_module_path(''), false):
|
||||||
|
var possible_script: String = DialogicUtil.get_module_path(file).path_join("index.gd")
|
||||||
|
if ResourceLoader.exists(possible_script):
|
||||||
|
indexers.append(load(possible_script).new())
|
||||||
|
|
||||||
|
if include_custom:
|
||||||
|
var extensions_folder: String = ProjectSettings.get_setting('dialogic/extensions_folder', "res://addons/dialogic_additions/")
|
||||||
|
for file in listdir(extensions_folder, false, false):
|
||||||
|
var possible_script: String = extensions_folder.path_join(file + "/index.gd")
|
||||||
|
if ResourceLoader.exists(possible_script):
|
||||||
|
indexers.append(load(possible_script).new())
|
||||||
|
|
||||||
|
Engine.get_main_loop().set_meta('dialogic_indexers', indexers)
|
||||||
|
return indexers
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Turns a [param file_path] from `some_file.png` to `Some File`.
|
||||||
|
static func pretty_name(file_path: String) -> String:
|
||||||
|
var _name := file_path.get_file().trim_suffix("." + file_path.get_extension())
|
||||||
|
_name = _name.replace('_', ' ')
|
||||||
|
_name = _name.capitalize()
|
||||||
|
|
||||||
|
return _name
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region EDITOR SETTINGS & COLORS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
static func set_editor_setting(setting:String, value:Variant) -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if FileAccess.file_exists("user://dialogic/editor_settings.cfg"):
|
||||||
|
cfg.load("user://dialogic/editor_settings.cfg")
|
||||||
|
|
||||||
|
cfg.set_value("DES", setting, value)
|
||||||
|
|
||||||
|
if not DirAccess.dir_exists_absolute("user://dialogic"):
|
||||||
|
DirAccess.make_dir_absolute("user://dialogic")
|
||||||
|
cfg.save("user://dialogic/editor_settings.cfg")
|
||||||
|
|
||||||
|
|
||||||
|
static func get_editor_setting(setting:String, default:Variant=null) -> Variant:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if not FileAccess.file_exists("user://dialogic/editor_settings.cfg"):
|
||||||
|
return default
|
||||||
|
|
||||||
|
if not cfg.load("user://dialogic/editor_settings.cfg") == OK:
|
||||||
|
return default
|
||||||
|
|
||||||
|
return cfg.get_value("DES", setting, default)
|
||||||
|
|
||||||
|
|
||||||
|
static func get_color_palette(default:bool = false) -> Dictionary:
|
||||||
|
var defaults := {
|
||||||
|
'Color1': Color('#3b8bf2'), # Blue
|
||||||
|
'Color2': Color('#00b15f'), # Green
|
||||||
|
'Color3': Color('#e868e2'), # Pink
|
||||||
|
'Color4': Color('#9468e8'), # Purple
|
||||||
|
'Color5': Color('#574fb0'), # DarkPurple
|
||||||
|
'Color6': Color('#1fa3a3'), # Aquamarine
|
||||||
|
'Color7': Color('#fa952a'), # Orange
|
||||||
|
'Color8': Color('#de5c5c'), # Red
|
||||||
|
'Color9': Color('#7c7c7c'), # Gray
|
||||||
|
}
|
||||||
|
if default:
|
||||||
|
return defaults
|
||||||
|
return get_editor_setting('color_palette', defaults)
|
||||||
|
|
||||||
|
|
||||||
|
static func get_color(value:String) -> Color:
|
||||||
|
var colors := get_color_palette()
|
||||||
|
return colors[value]
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region TIMER PROCESS MODE
|
||||||
|
################################################################################
|
||||||
|
static func is_physics_timer() -> bool:
|
||||||
|
return ProjectSettings.get_setting('dialogic/timer/process_in_physics', false)
|
||||||
|
|
||||||
|
|
||||||
|
static func update_timer_process_callback(timer:Timer) -> void:
|
||||||
|
timer.process_callback = Timer.TIMER_PROCESS_PHYSICS if is_physics_timer() else Timer.TIMER_PROCESS_IDLE
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region MULTITWEEN
|
||||||
|
################################################################################
|
||||||
|
static func multitween(tweened_value:Variant, item:Node, property:String, part:String) -> void:
|
||||||
|
var parts: Dictionary = item.get_meta(property+'_parts', {})
|
||||||
|
parts[part] = tweened_value
|
||||||
|
|
||||||
|
if not item.has_meta(property+'_base_value') and not 'base' in parts:
|
||||||
|
item.set_meta(property+'_base_value', item.get(property))
|
||||||
|
|
||||||
|
var final_value: Variant = parts.get('base', item.get_meta(property+'_base_value', item.get(property)))
|
||||||
|
|
||||||
|
for key in parts:
|
||||||
|
if key == 'base':
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
final_value += parts[key]
|
||||||
|
|
||||||
|
item.set(property, final_value)
|
||||||
|
item.set_meta(property+'_parts', parts)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region TRANSLATIONS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
static func get_next_translation_id() -> String:
|
||||||
|
ProjectSettings.set_setting('dialogic/translation/id_counter', ProjectSettings.get_setting('dialogic/translation/id_counter', 16)+1)
|
||||||
|
return '%x' % ProjectSettings.get_setting('dialogic/translation/id_counter', 16)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region VARIABLES
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
enum VarTypes {ANY, STRING, FLOAT, INT, BOOL}
|
||||||
|
|
||||||
|
|
||||||
|
static func get_default_variables() -> Dictionary:
|
||||||
|
return ProjectSettings.get_setting('dialogic/variables', {})
|
||||||
|
|
||||||
|
|
||||||
|
# helper that converts a nested variable dictionary into an array with paths
|
||||||
|
static func list_variables(dict:Dictionary, path := "", type:=VarTypes.ANY) -> Array:
|
||||||
|
var array := []
|
||||||
|
for key in dict.keys():
|
||||||
|
if typeof(dict[key]) == TYPE_DICTIONARY:
|
||||||
|
array.append_array(list_variables(dict[key], path+key+".", type))
|
||||||
|
else:
|
||||||
|
if type == VarTypes.ANY or get_variable_value_type(dict[key]) == type:
|
||||||
|
array.append(path+key)
|
||||||
|
return array
|
||||||
|
|
||||||
|
|
||||||
|
static func get_variable_value_type(value:Variant) -> VarTypes:
|
||||||
|
match typeof(value):
|
||||||
|
TYPE_STRING:
|
||||||
|
return VarTypes.STRING
|
||||||
|
TYPE_FLOAT:
|
||||||
|
return VarTypes.FLOAT
|
||||||
|
TYPE_INT:
|
||||||
|
return VarTypes.INT
|
||||||
|
TYPE_BOOL:
|
||||||
|
return VarTypes.BOOL
|
||||||
|
return VarTypes.ANY
|
||||||
|
|
||||||
|
|
||||||
|
static func get_variable_type(path:String, dict:Dictionary={}) -> VarTypes:
|
||||||
|
if dict.is_empty():
|
||||||
|
dict = get_default_variables()
|
||||||
|
return get_variable_value_type(_get_value_in_dictionary(path, dict))
|
||||||
|
|
||||||
|
|
||||||
|
## This will set a value in a dictionary (or a sub-dictionary based on the path)
|
||||||
|
## e.g. it could set "Something.Something.Something" in {'Something':{'Something':{'Someting':"value"}}}
|
||||||
|
static func _set_value_in_dictionary(path:String, dictionary:Dictionary, value):
|
||||||
|
if '.' in path:
|
||||||
|
var from := path.split('.')[0]
|
||||||
|
if from in dictionary.keys():
|
||||||
|
dictionary[from] = _set_value_in_dictionary(path.trim_prefix(from+"."), dictionary[from], value)
|
||||||
|
else:
|
||||||
|
if path in dictionary.keys():
|
||||||
|
dictionary[path] = value
|
||||||
|
return dictionary
|
||||||
|
|
||||||
|
|
||||||
|
## This will get a value in a dictionary (or a sub-dictionary based on the path)
|
||||||
|
## e.g. it could get "Something.Something.Something" in {'Something':{'Something':{'Someting':"value"}}}
|
||||||
|
static func _get_value_in_dictionary(path:String, dictionary:Dictionary, default= null) -> Variant:
|
||||||
|
if '.' in path:
|
||||||
|
var from := path.split('.')[0]
|
||||||
|
if from in dictionary.keys():
|
||||||
|
return _get_value_in_dictionary(path.trim_prefix(from+"."), dictionary[from], default)
|
||||||
|
else:
|
||||||
|
if path in dictionary.keys():
|
||||||
|
return dictionary[path]
|
||||||
|
return default
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region SCENE EXPORT OVERRIDES
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
static func apply_scene_export_overrides(node:Node, export_overrides:Dictionary, apply := true) -> void:
|
||||||
|
var default_info := get_scene_export_defaults(node)
|
||||||
|
if !node.script:
|
||||||
|
return
|
||||||
|
var property_info: Array[Dictionary] = node.script.get_script_property_list()
|
||||||
|
for i in property_info:
|
||||||
|
if i['usage'] & PROPERTY_USAGE_EDITOR == PROPERTY_USAGE_EDITOR:
|
||||||
|
if i['name'] in export_overrides:
|
||||||
|
if str_to_var(export_overrides[i['name']]) == null and typeof(node.get(i['name'])) == TYPE_STRING:
|
||||||
|
node.set(i['name'], export_overrides[i['name']])
|
||||||
|
else:
|
||||||
|
node.set(i['name'], str_to_var(export_overrides[i['name']]))
|
||||||
|
elif i['name'] in default_info:
|
||||||
|
node.set(i['name'], default_info.get(i['name']))
|
||||||
|
if apply:
|
||||||
|
if node.has_method('apply_export_overrides'):
|
||||||
|
node.apply_export_overrides()
|
||||||
|
|
||||||
|
|
||||||
|
static func get_scene_export_defaults(node:Node) -> Dictionary:
|
||||||
|
if !node.script:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if Engine.get_main_loop().has_meta('dialogic_scene_export_defaults') and \
|
||||||
|
node.scene_file_path in Engine.get_main_loop().get_meta('dialogic_scene_export_defaults'):
|
||||||
|
return Engine.get_main_loop().get_meta('dialogic_scene_export_defaults')[node.scene_file_path]
|
||||||
|
|
||||||
|
if !Engine.get_main_loop().has_meta('dialogic_scene_export_defaults'):
|
||||||
|
Engine.get_main_loop().set_meta('dialogic_scene_export_defaults', {})
|
||||||
|
var defaults := {}
|
||||||
|
var property_info: Array[Dictionary] = node.script.get_script_property_list()
|
||||||
|
for i in property_info:
|
||||||
|
if i['usage'] & PROPERTY_USAGE_EDITOR == PROPERTY_USAGE_EDITOR:
|
||||||
|
defaults[i['name']] = node.get(i['name'])
|
||||||
|
Engine.get_main_loop().get_meta('dialogic_scene_export_defaults')[node.scene_file_path] = defaults
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region MAKE CUSTOM
|
||||||
|
|
||||||
|
static func make_file_custom(original_file:String, target_folder:String, new_file_name := "", new_folder_name := "") -> String:
|
||||||
|
if not ResourceLoader.exists(original_file):
|
||||||
|
push_error("[Dialogic] Unable to make file with invalid path custom!")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if new_folder_name:
|
||||||
|
target_folder = target_folder.path_join(new_folder_name)
|
||||||
|
DirAccess.make_dir_absolute(target_folder)
|
||||||
|
|
||||||
|
if new_file_name.is_empty():
|
||||||
|
new_file_name = "custom_" + original_file.get_file()
|
||||||
|
|
||||||
|
if not new_file_name.ends_with(original_file.get_extension()):
|
||||||
|
new_file_name += "." + original_file.get_extension()
|
||||||
|
|
||||||
|
var target_file := target_folder.path_join(new_file_name)
|
||||||
|
|
||||||
|
customize_file(original_file, target_file)
|
||||||
|
|
||||||
|
get_dialogic_plugin().get_editor_interface().get_resource_filesystem().scan_sources()
|
||||||
|
|
||||||
|
return target_file
|
||||||
|
|
||||||
|
|
||||||
|
static func customize_file(original_file:String, target_file:String) -> String:
|
||||||
|
#print("\nCUSTOMIZE FILE")
|
||||||
|
#printt(original_file, "->", target_file)
|
||||||
|
|
||||||
|
DirAccess.copy_absolute(original_file, target_file)
|
||||||
|
|
||||||
|
var file := FileAccess.open(target_file, FileAccess.READ)
|
||||||
|
var file_text := file.get_as_text()
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
# If we are customizing a scene, we check for any resources used in that scene that are in the same folder.
|
||||||
|
# Those will be copied as well and the scene will be modified to point to them.
|
||||||
|
if file_text.begins_with('[gd_'):
|
||||||
|
var base_path: String = original_file.get_base_dir()
|
||||||
|
|
||||||
|
var remove_uuid_regex := r'\[gd_.* (?<uid>uid="uid:[^"]*")'
|
||||||
|
var result := RegEx.create_from_string(remove_uuid_regex).search(file_text)
|
||||||
|
if result:
|
||||||
|
file_text = file_text.replace(result.get_string("uid"), "")
|
||||||
|
|
||||||
|
# This regex also removes the UID referencing the original resource
|
||||||
|
var file_regex := r'(uid="[^"]*" )?\Qpath="'+base_path+r'\E(?<file>[^"]*)"'
|
||||||
|
result = RegEx.create_from_string(file_regex).search(file_text)
|
||||||
|
while result:
|
||||||
|
var found_file_name := result.get_string('file')
|
||||||
|
var found_file_path := base_path.path_join(found_file_name)
|
||||||
|
var target_file_path := target_file.get_base_dir().path_join(found_file_name)
|
||||||
|
|
||||||
|
# Files found in this file will ALSO be customized.
|
||||||
|
customize_file(found_file_path, target_file_path)
|
||||||
|
|
||||||
|
file_text = file_text.replace(found_file_path, target_file_path)
|
||||||
|
|
||||||
|
result = RegEx.create_from_string(file_regex).search(file_text)
|
||||||
|
|
||||||
|
file = FileAccess.open(target_file, FileAccess.WRITE)
|
||||||
|
file.store_string(file_text)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
return target_file
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region INSPECTOR FIELDS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
static func setup_script_property_edit_node(property_info: Dictionary, value:Variant, property_changed:Callable) -> Control:
|
||||||
|
var input: Control = null
|
||||||
|
match property_info['type']:
|
||||||
|
TYPE_BOOL:
|
||||||
|
input = CheckBox.new()
|
||||||
|
if value != null:
|
||||||
|
input.button_pressed = value
|
||||||
|
input.toggled.connect(DialogicUtil._on_export_bool_submitted.bind(property_info.name, property_changed))
|
||||||
|
TYPE_COLOR:
|
||||||
|
input = ColorPickerButton.new()
|
||||||
|
if value != null:
|
||||||
|
input.color = value
|
||||||
|
input.color_changed.connect(DialogicUtil._on_export_color_submitted.bind(property_info.name, property_changed))
|
||||||
|
input.custom_minimum_size.x = get_editor_scale() * 50
|
||||||
|
TYPE_INT:
|
||||||
|
if property_info['hint'] & PROPERTY_HINT_ENUM == PROPERTY_HINT_ENUM:
|
||||||
|
input = OptionButton.new()
|
||||||
|
for x in property_info['hint_string'].split(','):
|
||||||
|
input.add_item(x.split(':')[0])
|
||||||
|
if value != null:
|
||||||
|
input.select(value)
|
||||||
|
input.item_selected.connect(DialogicUtil._on_export_int_enum_submitted.bind(property_info.name, property_changed))
|
||||||
|
else:
|
||||||
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_number.tscn").instantiate()
|
||||||
|
input.property_name = property_info['name']
|
||||||
|
input.use_int_mode()
|
||||||
|
|
||||||
|
if ',' in property_info.hint_string:
|
||||||
|
input.min_value = int(property_info.hint_string.get_slice(',', 0))
|
||||||
|
input.max_value = int(property_info.hint_string.get_slice(',', 1))
|
||||||
|
if property_info.hint_string.count(',') > 1:
|
||||||
|
input.step = int(property_info.hint_string.get_slice(',', 2))
|
||||||
|
else:
|
||||||
|
input.step = 1
|
||||||
|
input.max_value = INF
|
||||||
|
input.min_value = -INF
|
||||||
|
|
||||||
|
if value != null:
|
||||||
|
input.set_value(value)
|
||||||
|
input.value_changed.connect(DialogicUtil._on_export_number_submitted.bind(property_changed))
|
||||||
|
TYPE_FLOAT:
|
||||||
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_number.tscn").instantiate()
|
||||||
|
input.property_name = property_info['name']
|
||||||
|
input.use_float_mode()
|
||||||
|
input.step = 0.01
|
||||||
|
if ',' in property_info.hint_string:
|
||||||
|
input.min_value = float(property_info.hint_string.get_slice(',', 0))
|
||||||
|
input.max_value = float(property_info.hint_string.get_slice(',', 1))
|
||||||
|
if property_info.hint_string.count(',') > 1:
|
||||||
|
input.step = float(property_info.hint_string.get_slice(',', 2))
|
||||||
|
if value != null:
|
||||||
|
input.set_value(value)
|
||||||
|
input.value_changed.connect(DialogicUtil._on_export_number_submitted.bind(property_changed))
|
||||||
|
TYPE_VECTOR2, TYPE_VECTOR3, TYPE_VECTOR4:
|
||||||
|
var vectorSize: String = type_string(typeof(value))[-1]
|
||||||
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_vector" + vectorSize + ".tscn").instantiate()
|
||||||
|
input.property_name = property_info['name']
|
||||||
|
input.set_value(value)
|
||||||
|
input.value_changed.connect(DialogicUtil._on_export_vector_submitted.bind(property_changed))
|
||||||
|
TYPE_VECTOR2I, TYPE_VECTOR3I, TYPE_VECTOR4I:
|
||||||
|
var vectorSize: String = type_string(typeof(value))[-2]
|
||||||
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_vector" + vectorSize + ".tscn").instantiate()
|
||||||
|
input.step = 1
|
||||||
|
input.property_name = property_info['name']
|
||||||
|
input.set_value(value)
|
||||||
|
input.value_changed.connect(DialogicUtil._on_export_vectori_submitted.bind(property_changed))
|
||||||
|
TYPE_STRING:
|
||||||
|
if property_info['hint'] & PROPERTY_HINT_FILE== PROPERTY_HINT_FILE or property_info['hint'] & PROPERTY_HINT_DIR == PROPERTY_HINT_DIR:
|
||||||
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_file.tscn").instantiate()
|
||||||
|
input.show_editing_button = true
|
||||||
|
input.file_filter = property_info['hint_string']
|
||||||
|
input.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||||
|
if property_info['hint'] == PROPERTY_HINT_DIR:
|
||||||
|
input.file_mode = FileDialog.FILE_MODE_OPEN_DIR
|
||||||
|
input.property_name = property_info['name']
|
||||||
|
input.placeholder = "Default"
|
||||||
|
input.hide_reset = true
|
||||||
|
if value != null:
|
||||||
|
input.set_value(value)
|
||||||
|
input.value_changed.connect(DialogicUtil._on_export_file_submitted.bind(property_changed))
|
||||||
|
elif property_info['hint'] & PROPERTY_HINT_ENUM == PROPERTY_HINT_ENUM:
|
||||||
|
input = OptionButton.new()
|
||||||
|
var options: PackedStringArray = []
|
||||||
|
for x in property_info['hint_string'].split(','):
|
||||||
|
options.append(x.split(':')[0].strip_edges())
|
||||||
|
input.add_item(options[-1])
|
||||||
|
if value != null:
|
||||||
|
input.select(options.find(value))
|
||||||
|
input.item_selected.connect(DialogicUtil._on_export_string_enum_submitted.bind(property_info.name, options, property_changed))
|
||||||
|
else:
|
||||||
|
input = LineEdit.new()
|
||||||
|
if value != null:
|
||||||
|
input.text = value
|
||||||
|
input.text_submitted.connect(DialogicUtil._on_export_input_text_submitted.bind(property_info.name, property_changed))
|
||||||
|
TYPE_DICTIONARY:
|
||||||
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_dictionary.tscn").instantiate()
|
||||||
|
input.property_name = property_info["name"]
|
||||||
|
input.set_value(value)
|
||||||
|
input.value_changed.connect(_on_export_dict_submitted.bind(property_changed))
|
||||||
|
TYPE_OBJECT:
|
||||||
|
input = load("res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn").instantiate()
|
||||||
|
input.hint_text = "Objects/Resources as settings are currently not supported. \nUse @export_file('*.extension') instead and load the resource once needed."
|
||||||
|
|
||||||
|
_:
|
||||||
|
input = LineEdit.new()
|
||||||
|
if value != null:
|
||||||
|
input.text = value
|
||||||
|
input.text_submitted.connect(_on_export_input_text_submitted.bind(property_info.name, property_changed))
|
||||||
|
return input
|
||||||
|
|
||||||
|
|
||||||
|
static func _on_export_input_text_submitted(text:String, property_name:String, callable: Callable) -> void:
|
||||||
|
callable.call(property_name, var_to_str(text))
|
||||||
|
|
||||||
|
static func _on_export_bool_submitted(value:bool, property_name:String, callable: Callable) -> void:
|
||||||
|
callable.call(property_name, var_to_str(value))
|
||||||
|
|
||||||
|
static func _on_export_color_submitted(color:Color, property_name:String, callable: Callable) -> void:
|
||||||
|
callable.call(property_name, var_to_str(color))
|
||||||
|
|
||||||
|
static func _on_export_int_enum_submitted(item:int, property_name:String, callable: Callable) -> void:
|
||||||
|
callable.call(property_name, var_to_str(item))
|
||||||
|
|
||||||
|
static func _on_export_number_submitted(property_name:String, value:float, callable: Callable) -> void:
|
||||||
|
callable.call(property_name, var_to_str(value))
|
||||||
|
|
||||||
|
static func _on_export_file_submitted(property_name:String, value:String, callable: Callable) -> void:
|
||||||
|
callable.call(property_name, var_to_str(value))
|
||||||
|
|
||||||
|
static func _on_export_string_enum_submitted(value:int, property_name:String, list:PackedStringArray, callable: Callable):
|
||||||
|
callable.call(property_name, var_to_str(list[value]))
|
||||||
|
|
||||||
|
static func _on_export_vector_submitted(property_name:String, value:Variant, callable: Callable) -> void:
|
||||||
|
callable.call(property_name, var_to_str(value))
|
||||||
|
|
||||||
|
static func _on_export_vectori_submitted(property_name:String, value:Variant, callable: Callable) -> void:
|
||||||
|
match typeof(value):
|
||||||
|
TYPE_VECTOR2: value = Vector2i(value)
|
||||||
|
TYPE_VECTOR3: value = Vector3i(value)
|
||||||
|
TYPE_VECTOR4: value = Vector4i(value)
|
||||||
|
callable.call(property_name, var_to_str(value))
|
||||||
|
|
||||||
|
static func _on_export_dict_submitted(property_name:String, value:Variant, callable: Callable) -> void:
|
||||||
|
callable.call(property_name, var_to_str(value))
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region EVENT DEFAULTS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
static func get_custom_event_defaults(event_name:String) -> Dictionary:
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
return ProjectSettings.get_setting('dialogic/event_default_overrides', {}).get(event_name, {})
|
||||||
|
else:
|
||||||
|
if !Engine.get_main_loop().has_meta('dialogic_event_defaults'):
|
||||||
|
Engine.get_main_loop().set_meta('dialogic_event_defaults', ProjectSettings.get_setting('dialogic/event_default_overrides', {}))
|
||||||
|
return Engine.get_main_loop().get_meta('dialogic_event_defaults').get(event_name, {})
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region CONVERSION
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
static func str_to_bool(boolstring:String) -> bool:
|
||||||
|
return true if boolstring == "true" else false
|
||||||
|
|
||||||
|
|
||||||
|
static func logical_convert(value:Variant) -> Variant:
|
||||||
|
if typeof(value) == TYPE_STRING:
|
||||||
|
if value.is_valid_int():
|
||||||
|
return value.to_int()
|
||||||
|
if value.is_valid_float():
|
||||||
|
return value.to_float()
|
||||||
|
if value == 'true':
|
||||||
|
return true
|
||||||
|
if value == 'false':
|
||||||
|
return false
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
## Takes [param source] and builds a dictionary of keys only.
|
||||||
|
## The values are `null`.
|
||||||
|
static func str_to_hash_set(source: String) -> Dictionary:
|
||||||
|
var dictionary := Dictionary()
|
||||||
|
|
||||||
|
for character in source:
|
||||||
|
dictionary[character] = null
|
||||||
|
|
||||||
|
return dictionary
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
static func get_character_suggestions(_search_text:String, current_value:DialogicCharacter = null, allow_none := true, allow_all:= false, editor_node:Node = null) -> Dictionary:
|
||||||
|
var suggestions := {}
|
||||||
|
|
||||||
|
var icon := load("res://addons/dialogic/Editor/Images/Resources/character.svg")
|
||||||
|
|
||||||
|
if allow_none and current_value:
|
||||||
|
suggestions['(No one)'] = {'value':'', 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
|
||||||
|
|
||||||
|
if allow_all:
|
||||||
|
suggestions['ALL'] = {'value':'--All--', 'tooltip':'All currently joined characters leave', 'editor_icon':["GuiEllipsis", "EditorIcons"]}
|
||||||
|
|
||||||
|
# Get characters in the current timeline and place them at the top of suggestions.
|
||||||
|
if editor_node:
|
||||||
|
var recent_characters := []
|
||||||
|
var timeline_node := editor_node.get_parent().find_parent("Timeline") as DialogicEditor
|
||||||
|
for event_node in timeline_node.find_child("Timeline").get_children():
|
||||||
|
if event_node == editor_node:
|
||||||
|
break
|
||||||
|
if event_node.resource is DialogicCharacterEvent or event_node.resource is DialogicTextEvent:
|
||||||
|
recent_characters.append(event_node.resource.character)
|
||||||
|
|
||||||
|
recent_characters.reverse()
|
||||||
|
for character in recent_characters:
|
||||||
|
if character and not character.get_character_name() in suggestions:
|
||||||
|
suggestions[character.get_character_name()] = {'value': character.get_character_name(), 'tooltip': character.resource_path, 'icon': icon.duplicate()}
|
||||||
|
|
||||||
|
var character_directory := DialogicResourceUtil.get_character_directory()
|
||||||
|
for resource in character_directory.keys():
|
||||||
|
suggestions[resource] = {'value': resource, 'tooltip': character_directory[resource], 'icon': icon}
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
|
static func get_portrait_suggestions(search_text:String, character:DialogicCharacter, allow_empty := false, empty_text := "Don't Change", allow_anything:=false) -> Dictionary:
|
||||||
|
var icon := load("res://addons/dialogic/Editor/Images/Resources/portrait.svg")
|
||||||
|
var suggestions := {}
|
||||||
|
|
||||||
|
if allow_empty:
|
||||||
|
suggestions[empty_text] = {'value':'', 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
|
||||||
|
|
||||||
|
if "{" in search_text:
|
||||||
|
suggestions[search_text] = {'value':search_text, 'editor_icon':["Variant", "EditorIcons"]}
|
||||||
|
|
||||||
|
elif allow_anything and search_text:
|
||||||
|
suggestions[search_text] = {'value':search_text, 'editor_icon':["Variant", "EditorIcons"]}
|
||||||
|
|
||||||
|
if character != null:
|
||||||
|
for portrait in character.portraits:
|
||||||
|
suggestions[portrait] = {'value':portrait, 'icon':icon}
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
|
static func get_portrait_position_suggestions(search_text := "") -> Dictionary:
|
||||||
|
var icon := load(DialogicUtil.get_module_path("Character").path_join('portrait_position.svg'))
|
||||||
|
|
||||||
|
var setting: String = ProjectSettings.get_setting('dialogic/portraits/position_suggestion_names', 'leftmost, left, center, right, rightmost')
|
||||||
|
|
||||||
|
var suggestions := {}
|
||||||
|
|
||||||
|
if not search_text.is_empty():
|
||||||
|
suggestions[search_text] = {'value':search_text.strip_edges(), 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
|
||||||
|
|
||||||
|
for position_id in setting.split(','):
|
||||||
|
suggestions[position_id.strip_edges()] = {'value':position_id.strip_edges(), 'icon':icon}
|
||||||
|
if not search_text.is_empty() and position_id.strip_edges().begins_with(search_text):
|
||||||
|
suggestions.erase(search_text)
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
|
static func get_autoload_suggestions(filter:String="") -> Dictionary:
|
||||||
|
var suggestions := {}
|
||||||
|
|
||||||
|
for prop in ProjectSettings.get_property_list():
|
||||||
|
if prop.name.begins_with('autoload/'):
|
||||||
|
var some_autoload: String = prop.name.trim_prefix('autoload/')
|
||||||
|
suggestions[some_autoload] = {'value': some_autoload, 'tooltip':some_autoload, 'editor_icon': ["Node", "EditorIcons"]}
|
||||||
|
if filter.begins_with(some_autoload):
|
||||||
|
suggestions[filter] = {'value': filter, 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
|
static func get_autoload_script_resource(autoload_name:String) -> Script:
|
||||||
|
var script: Script
|
||||||
|
if autoload_name and ProjectSettings.has_setting('autoload/'+autoload_name):
|
||||||
|
var loaded_autoload := load(ProjectSettings.get_setting('autoload/'+autoload_name).trim_prefix('*'))
|
||||||
|
|
||||||
|
if loaded_autoload is PackedScene:
|
||||||
|
var packed_scene: PackedScene = loaded_autoload
|
||||||
|
script = packed_scene.instantiate().get_script()
|
||||||
|
|
||||||
|
else:
|
||||||
|
script = loaded_autoload
|
||||||
|
return script
|
||||||
|
|
||||||
|
|
||||||
|
static func get_autoload_method_suggestions(filter:String, autoload_name:String) -> Dictionary:
|
||||||
|
var suggestions := {}
|
||||||
|
|
||||||
|
var script := get_autoload_script_resource(autoload_name)
|
||||||
|
if script:
|
||||||
|
for script_method in script.get_script_method_list():
|
||||||
|
if script_method.name.begins_with('@') or script_method.name.begins_with('_'):
|
||||||
|
continue
|
||||||
|
suggestions[script_method.name] = {'value': script_method.name, 'tooltip':script_method.name, 'editor_icon': ["Callable", "EditorIcons"]}
|
||||||
|
|
||||||
|
if not filter.is_empty():
|
||||||
|
suggestions[filter] = {'value': filter, 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
|
static func get_autoload_property_suggestions(_filter:String, autoload_name:String) -> Dictionary:
|
||||||
|
var suggestions := {}
|
||||||
|
var script := get_autoload_script_resource(autoload_name)
|
||||||
|
if script:
|
||||||
|
for property in script.get_script_property_list():
|
||||||
|
if property.name.ends_with('.gd') or property.name.begins_with('_'):
|
||||||
|
continue
|
||||||
|
suggestions[property.name] = {'value': property.name, 'tooltip':property.name, 'editor_icon': ["MemberProperty", "EditorIcons"]}
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
|
static func get_audio_bus_suggestions(_filter:= "") -> Dictionary:
|
||||||
|
var bus_name_list := {}
|
||||||
|
for i in range(AudioServer.bus_count):
|
||||||
|
if i == 0:
|
||||||
|
bus_name_list[AudioServer.get_bus_name(i)] = {'value':''}
|
||||||
|
else:
|
||||||
|
bus_name_list[AudioServer.get_bus_name(i)] = {'value':AudioServer.get_bus_name(i)}
|
||||||
|
return bus_name_list
|
||||||
|
|
||||||
|
|
||||||
|
static func get_audio_channel_suggestions(_search_text:String) -> Dictionary:
|
||||||
|
var suggestions := {}
|
||||||
|
var channel_defaults := DialogicUtil.get_audio_channel_defaults()
|
||||||
|
var cached_names := DialogicResourceUtil.get_channel_list()
|
||||||
|
|
||||||
|
for i in channel_defaults.keys():
|
||||||
|
if not cached_names.has(i):
|
||||||
|
cached_names.append(i)
|
||||||
|
|
||||||
|
cached_names.sort()
|
||||||
|
|
||||||
|
for i in cached_names:
|
||||||
|
if i.is_empty():
|
||||||
|
continue
|
||||||
|
|
||||||
|
suggestions[i] = {'value': i}
|
||||||
|
|
||||||
|
if i in channel_defaults.keys():
|
||||||
|
suggestions[i]["editor_icon"] = ["ProjectList", "EditorIcons"]
|
||||||
|
suggestions[i]["tooltip"] = "A default channel defined in the settings."
|
||||||
|
|
||||||
|
else:
|
||||||
|
suggestions[i]["editor_icon"] = ["AudioStreamPlayer", "EditorIcons"]
|
||||||
|
suggestions[i]["tooltip"] = "A temporary channel without defaults."
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
|
static func get_audio_channel_defaults() -> Dictionary:
|
||||||
|
return ProjectSettings.get_setting('dialogic/audio/channel_defaults', {
|
||||||
|
"": {
|
||||||
|
'volume': 0.0,
|
||||||
|
'audio_bus': '',
|
||||||
|
'fade_length': 0.0,
|
||||||
|
'loop': false,
|
||||||
|
},
|
||||||
|
"music": {
|
||||||
|
'volume': 0.0,
|
||||||
|
'audio_bus': '',
|
||||||
|
'fade_length': 0.0,
|
||||||
|
'loop': true,
|
||||||
|
}})
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_audio_channel_name(text: String) -> Dictionary:
|
||||||
|
var result := {}
|
||||||
|
var channel_name_regex := RegEx.create_from_string(r'(?<dash_only>^-$)|(?<invalid>[^\w-]{1})')
|
||||||
|
var matches := channel_name_regex.search_all(text)
|
||||||
|
var invalid_chars := []
|
||||||
|
|
||||||
|
for regex_match in matches:
|
||||||
|
if regex_match.get_string('dash_only'):
|
||||||
|
result['error_tooltip'] = "Channel name cannot be '-'."
|
||||||
|
result['valid_text'] = ''
|
||||||
|
else:
|
||||||
|
var invalid_char = regex_match.get_string('invalid')
|
||||||
|
if not invalid_char in invalid_chars:
|
||||||
|
invalid_chars.append(invalid_char)
|
||||||
|
|
||||||
|
if invalid_chars:
|
||||||
|
result['valid_text'] = channel_name_regex.sub(text, '', true)
|
||||||
|
result['error_tooltip'] = "Channel names cannot contain the following characters: " + "".join(invalid_chars)
|
||||||
|
|
||||||
|
return result
|
||||||
1
addons/dialogic/Core/DialogicUtil.gd.uid
Normal file
1
addons/dialogic/Core/DialogicUtil.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c848iwoo6mnms
|
||||||
41
addons/dialogic/Core/Dialogic_Subsystem.gd
Normal file
41
addons/dialogic/Core/Dialogic_Subsystem.gd
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
class_name DialogicSubsystem
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var dialogic: DialogicGameHandler = null
|
||||||
|
|
||||||
|
enum LoadFlags {FULL_LOAD, ONLY_DNODES}
|
||||||
|
|
||||||
|
# To be overriden by sub-classes
|
||||||
|
# Called once after every subsystem has been added to the tree
|
||||||
|
func post_install() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# To be overriden by sub-classes
|
||||||
|
# Fill in everything that should be cleared (for example before loading a different state)
|
||||||
|
func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# To be overriden by sub-classes
|
||||||
|
# Fill in everything that should be loaded using the dialogic_game_handler.current_state_info
|
||||||
|
# This is called when a save is loaded
|
||||||
|
func load_game_state(_load_flag:=LoadFlags.FULL_LOAD) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# To be overriden by sub-classes
|
||||||
|
# Fill in everything that should be saved into the dialogic_game_handler.current_state_info
|
||||||
|
# This is called when a save is saved
|
||||||
|
func save_game_state() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# To be overriden by sub-classes
|
||||||
|
func pause() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# To be overriden by sub-classes
|
||||||
|
func resume() -> void:
|
||||||
|
pass
|
||||||
1
addons/dialogic/Core/Dialogic_Subsystem.gd.uid
Normal file
1
addons/dialogic/Core/Dialogic_Subsystem.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d4iojsqnbdphm
|
||||||
152
addons/dialogic/Core/index_class.gd
Normal file
152
addons/dialogic/Core/index_class.gd
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
@tool
|
||||||
|
class_name DialogicIndexer
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Script that indexes events, subsystems, settings pages and more. [br]
|
||||||
|
## Place a script of this type in every folder in "addons/Events". [br]
|
||||||
|
## Overwrite the methods to return the contents of that folder.
|
||||||
|
|
||||||
|
|
||||||
|
var this_folder: String = get_script().resource_path.get_base_dir()
|
||||||
|
|
||||||
|
## Overwrite if this module contains any events. [br]
|
||||||
|
## Return an array with all the paths to the event scripts.[br]
|
||||||
|
## You can use the [property this_folder].path_join('my_event.gd')
|
||||||
|
func _get_events() -> Array:
|
||||||
|
if ResourceLoader.exists(this_folder.path_join('event.gd')):
|
||||||
|
return [this_folder.path_join('event.gd')]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
## Overwrite if this module contains any subsystems.
|
||||||
|
## Should return an array of dictionaries each with the following keys: [br]
|
||||||
|
## "name" -> name for this subsystem[br]
|
||||||
|
## "script" -> array of preview images[br]
|
||||||
|
func _get_subsystems() -> Array[Dictionary]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
func _get_editors() -> Array[String]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
func _get_settings_pages() -> Array:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
func _get_character_editor_sections() -> Array:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
#region TEXT EFFECTS & MODIFIERS
|
||||||
|
|
||||||
|
## Should return array of dictionaries with the following keys:[br]
|
||||||
|
## "command" -> the text e.g. "speed"[br]
|
||||||
|
## "node_path" or "subsystem" -> whichever contains your effect method[br]
|
||||||
|
## "method" -> name of the effect method[br]
|
||||||
|
func _get_text_effects() -> Array[Dictionary]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
## Should return array of dictionaries with the same arguments as _get_text_effects()
|
||||||
|
func _get_text_modifiers() -> Array[Dictionary]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
## Return a list of resources, scripts, etc.
|
||||||
|
## These can later be retrieved with DialogicResourceUtil.
|
||||||
|
## Each dictionary should contain (at least "type" and "path").
|
||||||
|
## E.g. {"type":"Animation", "path": "res://..."}
|
||||||
|
func _get_special_resources() -> Dictionary:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
## Return a list of dictionaries, each
|
||||||
|
func _get_portrait_scene_presets() -> Array[Dictionary]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
#region HELPERS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func list_dir(subdir:='') -> Array:
|
||||||
|
return Array(DirAccess.get_files_at(this_folder.path_join(subdir))).map(func(file):return this_folder.path_join(subdir).path_join(file))
|
||||||
|
|
||||||
|
|
||||||
|
func list_special_resources(subdir:='', extension:="") -> Dictionary:
|
||||||
|
var dict := {}
|
||||||
|
for i in list_dir(subdir):
|
||||||
|
if extension.is_empty() or i.ends_with(extension) or (extension == ".gd" and i.ends_with(".gdc")):
|
||||||
|
dict[DialogicUtil.pretty_name(i).to_lower()] = {"path":i}
|
||||||
|
return dict
|
||||||
|
|
||||||
|
|
||||||
|
func list_animations(subdir := "") -> Dictionary:
|
||||||
|
var full_animation_list := {}
|
||||||
|
for path in list_dir(subdir):
|
||||||
|
if not path.ends_with(".gd") and not path.ends_with(".gdc"):
|
||||||
|
continue
|
||||||
|
var anim_object: DialogicAnimation = load(path).new()
|
||||||
|
var versions := anim_object._get_named_variations()
|
||||||
|
for version_name in versions:
|
||||||
|
full_animation_list[version_name] = versions[version_name]
|
||||||
|
full_animation_list[version_name]["path"] = path
|
||||||
|
anim_object.queue_free()
|
||||||
|
return full_animation_list
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region STYLES & LAYOUTS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func _get_style_presets() -> Array[Dictionary]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
## Should return an array of dictionaries with the following keys:[br]
|
||||||
|
## "path" -> the path to the scene[br]
|
||||||
|
## "name" -> name for this layout[br]
|
||||||
|
## "description"-> description of this layout. list what features/events are supported[br]
|
||||||
|
## "preview_image"-> array of preview images[br]
|
||||||
|
func _get_layout_parts() -> Array[Dictionary]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
## Helper that allows scanning sub directories that might be layout parts or styles
|
||||||
|
func scan_for_layout_parts() -> Array[Dictionary]:
|
||||||
|
var dir := DirAccess.open(this_folder)
|
||||||
|
var style_list: Array[Dictionary] = []
|
||||||
|
if !dir:
|
||||||
|
return style_list
|
||||||
|
dir.list_dir_begin()
|
||||||
|
var dir_name := dir.get_next()
|
||||||
|
while dir_name != "":
|
||||||
|
if !dir.current_is_dir() or !dir.file_exists(dir_name.path_join('part_config.cfg')):
|
||||||
|
dir_name = dir.get_next()
|
||||||
|
continue
|
||||||
|
var config := ConfigFile.new()
|
||||||
|
config.load(this_folder.path_join(dir_name).path_join('part_config.cfg'))
|
||||||
|
var default_image_path: String = this_folder.path_join(dir_name).path_join('preview.png')
|
||||||
|
style_list.append(
|
||||||
|
{
|
||||||
|
'type': config.get_value('style', 'type', 'Unknown type'),
|
||||||
|
'name': config.get_value('style', 'name', 'Unnamed Layout'),
|
||||||
|
'path': this_folder.path_join(dir_name).path_join(config.get_value('style', 'scene', '')),
|
||||||
|
'author': config.get_value('style', 'author', 'Anonymous'),
|
||||||
|
'description': config.get_value('style', 'description', 'No description'),
|
||||||
|
'preview_image': [config.get_value('style', 'image', default_image_path)],
|
||||||
|
'style_path':config.get_value('style', 'style_path', ''),
|
||||||
|
'icon':this_folder.path_join(dir_name).path_join(config.get_value('style', 'icon', '')),
|
||||||
|
})
|
||||||
|
|
||||||
|
if not style_list[-1].style_path.begins_with('res://'):
|
||||||
|
style_list[-1].style_path = this_folder.path_join(dir_name).path_join(style_list[-1].style_path)
|
||||||
|
|
||||||
|
dir_name = dir.get_next()
|
||||||
|
|
||||||
|
return style_list
|
||||||
|
|
||||||
|
#endregion
|
||||||
1
addons/dialogic/Core/index_class.gd.uid
Normal file
1
addons/dialogic/Core/index_class.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ciwsx3rjhhmg7
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
@tool
|
||||||
|
extends DialogicCharacterEditorPortraitSection
|
||||||
|
|
||||||
|
## Section that allows setting values of exported scene variables
|
||||||
|
## for custom portrait scenes
|
||||||
|
|
||||||
|
var current_portrait_data := {}
|
||||||
|
var last_scene := ""
|
||||||
|
|
||||||
|
func _get_title() -> String:
|
||||||
|
return "Settings"
|
||||||
|
|
||||||
|
|
||||||
|
func _load_portrait_data(data:Dictionary) -> void:
|
||||||
|
_recheck(data, true)
|
||||||
|
|
||||||
|
|
||||||
|
## Recheck section visibility and reload export fields.
|
||||||
|
## This allows reacting to changes of the portrait_scene setting.
|
||||||
|
func _recheck(data: Dictionary, force:=false):
|
||||||
|
if last_scene == data.get("scene", "") and not force:
|
||||||
|
current_portrait_data = data
|
||||||
|
last_scene = data.get("scene", "")
|
||||||
|
return
|
||||||
|
|
||||||
|
last_scene = data.get("scene", "")
|
||||||
|
current_portrait_data = data
|
||||||
|
|
||||||
|
for child in $Grid.get_children():
|
||||||
|
child.get_parent().remove_child(child)
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
var scene: Variant = null
|
||||||
|
|
||||||
|
if current_portrait_data.get('scene', '').is_empty():
|
||||||
|
if ProjectSettings.get_setting('dialogic/portraits/default_portrait', '').is_empty():
|
||||||
|
scene = load(character_editor.def_portrait_path)
|
||||||
|
else:
|
||||||
|
scene = load(ProjectSettings.get_setting('dialogic/portraits/default_portrait', ''))
|
||||||
|
else:
|
||||||
|
scene = load(current_portrait_data.get('scene'))
|
||||||
|
|
||||||
|
if not scene:
|
||||||
|
return
|
||||||
|
|
||||||
|
scene = scene.instantiate()
|
||||||
|
|
||||||
|
var skip := false
|
||||||
|
for i in scene.script.get_script_property_list():
|
||||||
|
if i['usage'] & PROPERTY_USAGE_EDITOR == PROPERTY_USAGE_EDITOR and !skip:
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = i['name'].capitalize()
|
||||||
|
$Grid.add_child(label)
|
||||||
|
|
||||||
|
var current_value: Variant = scene.get(i['name'])
|
||||||
|
if current_portrait_data.has('export_overrides') and current_portrait_data['export_overrides'].has(i['name']):
|
||||||
|
current_value = str_to_var(current_portrait_data.export_overrides[i['name']])
|
||||||
|
if current_value == null and typeof(scene.get(i['name'])) == TYPE_STRING:
|
||||||
|
current_value = current_portrait_data['export_overrides'][i['name']]
|
||||||
|
|
||||||
|
var input: Node = DialogicUtil.setup_script_property_edit_node(i, current_value, set_export_override)
|
||||||
|
input.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||||
|
$Grid.add_child(input)
|
||||||
|
|
||||||
|
if i['usage'] & PROPERTY_USAGE_GROUP:
|
||||||
|
if i['name'] == 'Main' or i["name"] == "Private":
|
||||||
|
skip = true
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
skip = false
|
||||||
|
|
||||||
|
if $Grid.get_child_count():
|
||||||
|
get_parent().get_child(get_index()-1).show()
|
||||||
|
show()
|
||||||
|
else:
|
||||||
|
hide()
|
||||||
|
get_parent().get_child(get_index()-1).hide()
|
||||||
|
get_parent().get_child(get_index()+1).hide()
|
||||||
|
|
||||||
|
|
||||||
|
## On any change, save the export override to the portrait items metadata.
|
||||||
|
func set_export_override(property_name:String, value:String = "") -> void:
|
||||||
|
var data: Dictionary = selected_item.get_metadata(0)
|
||||||
|
if !data.has('export_overrides'):
|
||||||
|
data['export_overrides'] = {}
|
||||||
|
if !value.is_empty():
|
||||||
|
data.export_overrides[property_name] = value
|
||||||
|
else:
|
||||||
|
data.export_overrides.erase(property_name)
|
||||||
|
changed.emit()
|
||||||
|
update_preview.emit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bcsda7vbawlgv
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://cfcs7lb6gqnmd"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://bcsda7vbawlgv" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_exports.gd" id="1_isys8"]
|
||||||
|
|
||||||
|
[node name="Settings" type="VBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(0, 35)
|
||||||
|
offset_right = 367.0
|
||||||
|
offset_bottom = 82.0
|
||||||
|
script = ExtResource("1_isys8")
|
||||||
|
|
||||||
|
[node name="Grid" type="GridContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/h_separation = 10
|
||||||
|
columns = 2
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
@tool
|
||||||
|
extends DialogicCharacterEditorPortraitSection
|
||||||
|
|
||||||
|
## Tab that allows setting size, offset and mirror of a portrait.
|
||||||
|
|
||||||
|
|
||||||
|
func _get_title() -> String:
|
||||||
|
return "Scale, Offset & Mirror"
|
||||||
|
|
||||||
|
|
||||||
|
func _load_portrait_data(data:Dictionary) -> void:
|
||||||
|
%IgnoreScale.set_pressed_no_signal(data.get('ignore_char_scale', false))
|
||||||
|
%PortraitScale.set_value(data.get('scale', 1.0)*100)
|
||||||
|
%PortraitOffset.set_value(data.get('offset', Vector2()))
|
||||||
|
%PortraitOffset._load_display_info({'step':1})
|
||||||
|
%PortraitMirror.set_pressed_no_signal(data.get('mirror', false))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_portrait_scale_value_changed(_property:String, value:float) -> void:
|
||||||
|
var data: Dictionary = selected_item.get_metadata(0)
|
||||||
|
data['scale'] = value/100.0
|
||||||
|
update_preview.emit()
|
||||||
|
changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_portrait_mirror_toggled(button_pressed:bool)-> void:
|
||||||
|
var data: Dictionary = selected_item.get_metadata(0)
|
||||||
|
data['mirror'] = button_pressed
|
||||||
|
update_preview.emit()
|
||||||
|
changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_ignore_scale_toggled(button_pressed:bool) -> void:
|
||||||
|
var data: Dictionary = selected_item.get_metadata(0)
|
||||||
|
data['ignore_char_scale'] = button_pressed
|
||||||
|
update_preview.emit()
|
||||||
|
changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_portrait_offset_value_changed(_property:String, value:Vector2) -> void:
|
||||||
|
var data: Dictionary = selected_item.get_metadata(0)
|
||||||
|
data['offset'] = value
|
||||||
|
update_preview.emit()
|
||||||
|
changed.emit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://uv6dx3sofwae
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
[gd_scene load_steps=4 format=3 uid="uid://crke8suvv52c6"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://uv6dx3sofwae" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_layout.gd" id="1_76vf2"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dtimnsj014cu" path="res://addons/dialogic/Editor/Events/Fields/field_vector2.tscn" id="2_c8kyi"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://kdpp3mibml33" path="res://addons/dialogic/Editor/Events/Fields/field_number.tscn" id="2_daw3l"]
|
||||||
|
|
||||||
|
[node name="Layout" type="HFlowContainer"]
|
||||||
|
offset_right = 428.0
|
||||||
|
offset_bottom = 128.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
script = ExtResource("1_76vf2")
|
||||||
|
|
||||||
|
[node name="Label3" type="Label" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Ignore Main Scale: "
|
||||||
|
|
||||||
|
[node name="IgnoreScale" type="CheckBox" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "This portrait will ignore the main scale."
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Scale:"
|
||||||
|
|
||||||
|
[node name="PortraitScale" parent="HBoxContainer" instance=ExtResource("2_daw3l")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
mode = 1
|
||||||
|
step = 1.0
|
||||||
|
min_value = 0.0
|
||||||
|
max_value = 999.0
|
||||||
|
suffix = "%"
|
||||||
|
|
||||||
|
[node name="HBoxContainer2" type="HBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label2" type="Label" parent="HBoxContainer2"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Offset:"
|
||||||
|
|
||||||
|
[node name="PortraitOffset" parent="HBoxContainer2" instance=ExtResource("2_c8kyi")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Offset that is applied on top of the main portrait offset."
|
||||||
|
|
||||||
|
[node name="MirrorOption" type="HBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="MirrorOption"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Mirror:"
|
||||||
|
|
||||||
|
[node name="PortraitMirror" type="CheckBox" parent="MirrorOption"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Mirroring that is applied on top of the main portrait mirror."
|
||||||
|
|
||||||
|
[connection signal="toggled" from="IgnoreScale" to="." method="_on_ignore_scale_toggled"]
|
||||||
|
[connection signal="value_changed" from="HBoxContainer/PortraitScale" to="." method="_on_portrait_scale_value_changed"]
|
||||||
|
[connection signal="value_changed" from="HBoxContainer2/PortraitOffset" to="." method="_on_portrait_offset_value_changed"]
|
||||||
|
[connection signal="toggled" from="MirrorOption/PortraitMirror" to="." method="_on_portrait_mirror_toggled"]
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
@tool
|
||||||
|
extends DialogicCharacterEditorPortraitSection
|
||||||
|
|
||||||
|
## Tab that allows setting a custom scene for a portrait.
|
||||||
|
|
||||||
|
func _get_title() -> String:
|
||||||
|
return "Scene"
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
hint_text = "You can use a custom scene for this portrait."
|
||||||
|
|
||||||
|
func _start_opened() -> bool:
|
||||||
|
return true
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
%ChangeSceneButton.icon = get_theme_icon("Loop", "EditorIcons")
|
||||||
|
%ScenePicker.file_filter = "*.tscn, *.scn; Scenes"
|
||||||
|
%ScenePicker.resource_icon = get_theme_icon('PackedScene', 'EditorIcons')
|
||||||
|
%ScenePicker.placeholder = 'Default scene'
|
||||||
|
|
||||||
|
%OpenSceneButton.icon = get_theme_icon("ExternalLink", "EditorIcons")
|
||||||
|
|
||||||
|
|
||||||
|
func _load_portrait_data(data:Dictionary) -> void:
|
||||||
|
reload_ui(data)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_open_scene_button_pressed() -> void:
|
||||||
|
var data: Dictionary = selected_item.get_metadata(0)
|
||||||
|
if ResourceLoader.exists(data.get("scene", "")):
|
||||||
|
EditorInterface.open_scene_from_path(data.get("scene", ""))
|
||||||
|
await get_tree().process_frame
|
||||||
|
EditorInterface.set_main_screen_editor("2D")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_change_scene_button_pressed() -> void:
|
||||||
|
%PortraitSceneBrowserWindow.popup_centered_ratio(0.6)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_portrait_scene_browser_activate_part(part_info: Dictionary) -> void:
|
||||||
|
%PortraitSceneBrowserWindow.hide()
|
||||||
|
match part_info.type:
|
||||||
|
"General":
|
||||||
|
set_scene_path(part_info.path)
|
||||||
|
"Preset":
|
||||||
|
find_parent("EditorView").godot_file_dialog(
|
||||||
|
create_new_portrait_scene.bind(part_info),
|
||||||
|
'*.tscn,*.scn',
|
||||||
|
EditorFileDialog.FILE_MODE_SAVE_FILE,
|
||||||
|
"Select where to save the new scene",
|
||||||
|
part_info.path.get_file().trim_suffix("."+part_info.path.get_extension())+"_"+character_editor.current_resource.get_character_name().to_lower())
|
||||||
|
"Custom":
|
||||||
|
find_parent("EditorView").godot_file_dialog(
|
||||||
|
set_scene_path,
|
||||||
|
'*.tscn, *.scn',
|
||||||
|
EditorFileDialog.FILE_MODE_OPEN_FILE,
|
||||||
|
"Select custom portrait scene",)
|
||||||
|
"Default":
|
||||||
|
set_scene_path("")
|
||||||
|
|
||||||
|
|
||||||
|
func create_new_portrait_scene(target_file: String, info: Dictionary) -> void:
|
||||||
|
var path := make_portrait_preset_custom(target_file, info)
|
||||||
|
set_scene_path(path)
|
||||||
|
|
||||||
|
|
||||||
|
func make_portrait_preset_custom(target_file:String, info: Dictionary) -> String:
|
||||||
|
var previous_file: String = info.path
|
||||||
|
|
||||||
|
var result_path := DialogicUtil.make_file_custom(previous_file, target_file.get_base_dir(), target_file.get_file())
|
||||||
|
|
||||||
|
return result_path
|
||||||
|
|
||||||
|
|
||||||
|
func set_scene_path(path:String) -> void:
|
||||||
|
var data: Dictionary = selected_item.get_metadata(0)
|
||||||
|
data['scene'] = path
|
||||||
|
update_preview.emit()
|
||||||
|
changed.emit()
|
||||||
|
reload_ui(data)
|
||||||
|
|
||||||
|
|
||||||
|
func reload_ui(data: Dictionary) -> void:
|
||||||
|
var path: String = data.get('scene', '')
|
||||||
|
%OpenSceneButton.hide()
|
||||||
|
|
||||||
|
if path.is_empty():
|
||||||
|
%SceneLabel.text = "Default Portrait Scene"
|
||||||
|
%SceneLabel.tooltip_text = "Can be changed in the settings."
|
||||||
|
%SceneLabel.add_theme_color_override("font_color", get_theme_color("readonly_color", "Editor"))
|
||||||
|
|
||||||
|
elif %PortraitSceneBrowser.is_premade_portrait_scene(path):
|
||||||
|
%SceneLabel.text = %PortraitSceneBrowser.portrait_scenes_info[path].name
|
||||||
|
%SceneLabel.tooltip_text = path
|
||||||
|
%SceneLabel.add_theme_color_override("font_color", get_theme_color("accent_color", "Editor"))
|
||||||
|
|
||||||
|
else:
|
||||||
|
%SceneLabel.text = path.get_file()
|
||||||
|
%SceneLabel.tooltip_text = path
|
||||||
|
%SceneLabel.add_theme_color_override("font_color", get_theme_color("property_color_x", "Editor"))
|
||||||
|
%OpenSceneButton.show()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://busjn8oo7kl1s
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
[gd_scene load_steps=6 format=3 uid="uid://djq4aasoihexj"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://busjn8oo7kl1s" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_main.gd" id="1_ht8lu"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://7mvxuaulctcq" path="res://addons/dialogic/Editor/Events/Fields/field_file.tscn" id="2_k8xs0"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b1wn8r84uh11b" path="res://addons/dialogic/Editor/CharacterEditor/portrait_scene_browser.tscn" id="3_ngvgq"]
|
||||||
|
|
||||||
|
[sub_resource type="Image" id="Image_m6kd3"]
|
||||||
|
data = {
|
||||||
|
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||||
|
"format": "RGBA8",
|
||||||
|
"height": 16,
|
||||||
|
"mipmaps": false,
|
||||||
|
"width": 16
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="ImageTexture" id="ImageTexture_f5xt2"]
|
||||||
|
image = SubResource("Image_m6kd3")
|
||||||
|
|
||||||
|
[node name="Scene" type="GridContainer"]
|
||||||
|
offset_right = 298.0
|
||||||
|
offset_bottom = 86.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
script = ExtResource("1_ht8lu")
|
||||||
|
|
||||||
|
[node name="HBox" type="HBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="ChangeSceneButton" type="Button" parent="HBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Change Scene"
|
||||||
|
icon = SubResource("ImageTexture_f5xt2")
|
||||||
|
|
||||||
|
[node name="SceneLabel" type="Label" parent="HBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
mouse_filter = 0
|
||||||
|
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||||
|
text = "asdsdasdasd"
|
||||||
|
clip_text = true
|
||||||
|
|
||||||
|
[node name="ScenePicker" parent="HBox" instance=ExtResource("2_k8xs0")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
file_filter = "*.tscn, *.scn; Scenes"
|
||||||
|
placeholder = "Default scene"
|
||||||
|
|
||||||
|
[node name="OpenSceneButton" type="Button" parent="HBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Open/Edit Scene"
|
||||||
|
icon = SubResource("ImageTexture_f5xt2")
|
||||||
|
|
||||||
|
[node name="PortraitSceneBrowserWindow" type="Window" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
title = "Portrait Scene Browser"
|
||||||
|
position = Vector2i(0, 36)
|
||||||
|
visible = false
|
||||||
|
wrap_controls = true
|
||||||
|
transient = true
|
||||||
|
popup_window = true
|
||||||
|
|
||||||
|
[node name="PortraitSceneBrowser" parent="PortraitSceneBrowserWindow" instance=ExtResource("3_ngvgq")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
|
||||||
|
[connection signal="pressed" from="HBox/ChangeSceneButton" to="." method="_on_change_scene_button_pressed"]
|
||||||
|
[connection signal="pressed" from="HBox/OpenSceneButton" to="." method="_on_open_scene_button_pressed"]
|
||||||
|
[connection signal="activate_part" from="PortraitSceneBrowserWindow/PortraitSceneBrowser" to="." method="_on_portrait_scene_browser_activate_part"]
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
@tool
|
||||||
|
extends DialogicCharacterEditorPortraitSection
|
||||||
|
|
||||||
|
## Portrait Settings Section that only shows the MAIN settings of a portrait scene.
|
||||||
|
|
||||||
|
var current_portrait_data := {}
|
||||||
|
var last_scene := ""
|
||||||
|
|
||||||
|
func _show_title() -> bool:
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _load_portrait_data(data:Dictionary) -> void:
|
||||||
|
_recheck(data, true)
|
||||||
|
|
||||||
|
|
||||||
|
func _recheck(data:Dictionary, force := false) -> void:
|
||||||
|
get_parent().get_child(get_index()+1).hide()
|
||||||
|
if last_scene == data.get("scene", "") and not force:
|
||||||
|
current_portrait_data = data
|
||||||
|
last_scene = data.get("scene", "")
|
||||||
|
return
|
||||||
|
|
||||||
|
last_scene = data.get("scene", "")
|
||||||
|
current_portrait_data = data
|
||||||
|
|
||||||
|
load_portrait_scene_export_variables()
|
||||||
|
|
||||||
|
|
||||||
|
func load_portrait_scene_export_variables() -> void:
|
||||||
|
for child in $Grid.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
var scene: Variant = null
|
||||||
|
if current_portrait_data.get('scene', '').is_empty():
|
||||||
|
if ProjectSettings.get_setting('dialogic/portraits/default_portrait', '').is_empty():
|
||||||
|
scene = load(character_editor.def_portrait_path)
|
||||||
|
else:
|
||||||
|
scene = load(ProjectSettings.get_setting('dialogic/portraits/default_portrait', ''))
|
||||||
|
else:
|
||||||
|
scene = load(current_portrait_data.get('scene'))
|
||||||
|
|
||||||
|
if not scene:
|
||||||
|
return
|
||||||
|
|
||||||
|
scene = scene.instantiate()
|
||||||
|
var skip := true
|
||||||
|
for i in scene.script.get_script_property_list():
|
||||||
|
if i['usage'] & PROPERTY_USAGE_EDITOR == PROPERTY_USAGE_EDITOR and !skip:
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = i['name'].capitalize()
|
||||||
|
$Grid.add_child(label)
|
||||||
|
|
||||||
|
var current_value: Variant = scene.get(i['name'])
|
||||||
|
if current_portrait_data.has('export_overrides') and current_portrait_data['export_overrides'].has(i['name']):
|
||||||
|
current_value = str_to_var(current_portrait_data['export_overrides'][i['name']])
|
||||||
|
if current_value == null and typeof(scene.get(i['name'])) == TYPE_STRING:
|
||||||
|
current_value = current_portrait_data['export_overrides'][i['name']]
|
||||||
|
|
||||||
|
var input: Node = DialogicUtil.setup_script_property_edit_node(i, current_value, set_export_override)
|
||||||
|
input.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||||
|
$Grid.add_child(input)
|
||||||
|
|
||||||
|
if i['usage'] & PROPERTY_USAGE_GROUP:
|
||||||
|
if i['name'] == 'Main':
|
||||||
|
skip = false
|
||||||
|
else:
|
||||||
|
skip = true
|
||||||
|
continue
|
||||||
|
|
||||||
|
func set_export_override(property_name:String, value:String = "") -> void:
|
||||||
|
var data: Dictionary = selected_item.get_metadata(0)
|
||||||
|
if !data.has('export_overrides'):
|
||||||
|
data['export_overrides'] = {}
|
||||||
|
if !value.is_empty():
|
||||||
|
data['export_overrides'][property_name] = value
|
||||||
|
else:
|
||||||
|
data['export_overrides'].erase(property_name)
|
||||||
|
changed.emit()
|
||||||
|
update_preview.emit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cp0o6sycac85b
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://ba5w02lm3ewkj"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cp0o6sycac85b" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_main_exports.gd" id="1_mttrr"]
|
||||||
|
|
||||||
|
[node name="MainExports" type="VBoxContainer"]
|
||||||
|
offset_right = 374.0
|
||||||
|
offset_bottom = 82.0
|
||||||
|
script = ExtResource("1_mttrr")
|
||||||
|
|
||||||
|
[node name="Grid" type="GridContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/h_separation = 10
|
||||||
|
columns = 2
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
@tool
|
||||||
|
extends DialogicCharacterEditorMainSection
|
||||||
|
|
||||||
|
var min_width := 200
|
||||||
|
|
||||||
|
## The general character settings tab
|
||||||
|
func _get_title() -> String:
|
||||||
|
return "General"
|
||||||
|
|
||||||
|
|
||||||
|
func _start_opened() -> bool:
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Connecting all necessary signals
|
||||||
|
%ColorPickerButton.custom_minimum_size.x = DialogicUtil.get_editor_scale() * 30
|
||||||
|
%ColorPickerButton.color_changed.connect(character_editor.something_changed)
|
||||||
|
%DisplayNameLineEdit.text_changed.connect(character_editor.something_changed)
|
||||||
|
%NicknameLineEdit.text_changed.connect(character_editor.something_changed)
|
||||||
|
%DescriptionTextEdit.text_changed.connect(character_editor.something_changed)
|
||||||
|
min_width = get_minimum_size().x
|
||||||
|
resized.connect(_on_resized)
|
||||||
|
|
||||||
|
func _load_character(resource:DialogicCharacter) -> void:
|
||||||
|
%DisplayNameLineEdit.text = resource.display_name
|
||||||
|
%ColorPickerButton.color = resource.color
|
||||||
|
|
||||||
|
%NicknameLineEdit.text = ""
|
||||||
|
for nickname in resource.nicknames:
|
||||||
|
%NicknameLineEdit.text += nickname +", "
|
||||||
|
%NicknameLineEdit.text = %NicknameLineEdit.text.trim_suffix(', ')
|
||||||
|
|
||||||
|
%DescriptionTextEdit.text = resource.description
|
||||||
|
|
||||||
|
|
||||||
|
func _save_changes(resource:DialogicCharacter) -> DialogicCharacter:
|
||||||
|
resource.display_name = %DisplayNameLineEdit.text
|
||||||
|
resource.color = %ColorPickerButton.color
|
||||||
|
var nicknames := []
|
||||||
|
for n_name in %NicknameLineEdit.text.split(','):
|
||||||
|
nicknames.append(n_name.strip_edges())
|
||||||
|
resource.nicknames = nicknames
|
||||||
|
resource.description = %DescriptionTextEdit.text
|
||||||
|
|
||||||
|
return resource
|
||||||
|
|
||||||
|
|
||||||
|
func _on_resized() -> void:
|
||||||
|
if size.x > min_width+20:
|
||||||
|
self.columns = 2
|
||||||
|
else:
|
||||||
|
self.columns = 1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c0nilv2pybryh
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
[gd_scene format=3 uid="uid://bnkck3hocbkk5"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://c0nilv2pybryh" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_section_general.gd" id="1_3e1i1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_cxfqm"]
|
||||||
|
|
||||||
|
[node name="General" type="GridContainer" unique_id=1851637759]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 7.5
|
||||||
|
offset_top = 38.5
|
||||||
|
offset_right = -7.5
|
||||||
|
offset_bottom = -7.5
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/h_separation = 6
|
||||||
|
theme_override_constants/v_separation = 6
|
||||||
|
columns = 2
|
||||||
|
script = ExtResource("1_3e1i1")
|
||||||
|
|
||||||
|
[node name="HBox" type="HBoxContainer" parent="." unique_id=1894751349]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
|
||||||
|
[node name="Label2" type="Label" parent="HBox" unique_id=390789841]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
text = "Display Name"
|
||||||
|
|
||||||
|
[node name="HintTooltip" parent="HBox" unique_id=2005979450 instance=ExtResource("2_cxfqm")]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "This name will be displayed on the name label. You can use a dialogic variable. E.g. :{Player.name}"
|
||||||
|
texture = null
|
||||||
|
hint_text = "This name will be displayed on the name label. If you want it to change, you can use a dialogic variable. E.g.: {Player.Name}"
|
||||||
|
|
||||||
|
[node name="DisplayName" type="HBoxContainer" parent="." unique_id=1051746999]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="DisplayNameLineEdit" type="LineEdit" parent="DisplayName" unique_id=1885833416]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
caret_blink = true
|
||||||
|
caret_blink_interval = 0.5
|
||||||
|
|
||||||
|
[node name="HintTooltip4" parent="DisplayName" unique_id=598606526 instance=ExtResource("2_cxfqm")]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "This color can be used on the name label and for occurences of the characters name in text (autocolor names)."
|
||||||
|
texture = null
|
||||||
|
hint_text = "This color can be used on the name label and for occurences of the characters name in text (autocolor names)."
|
||||||
|
|
||||||
|
[node name="ColorPickerButton" type="ColorPickerButton" parent="DisplayName" unique_id=1061485751]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(30, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"FlatButton"
|
||||||
|
color = Color(1, 1, 1, 1)
|
||||||
|
edit_alpha = false
|
||||||
|
|
||||||
|
[node name="HBox2" type="HBoxContainer" parent="." unique_id=2113659797]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
|
||||||
|
[node name="Label3" type="Label" parent="HBox2" unique_id=1585220068]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
text = "Nicknames"
|
||||||
|
|
||||||
|
[node name="HintTooltip2" parent="HBox2" unique_id=1893234037 instance=ExtResource("2_cxfqm")]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "If autocolor names is enabled, these will be colored in the characters color as well."
|
||||||
|
texture = null
|
||||||
|
hint_text = "If autocolor names is enabled, these will be colored in the characters color as well."
|
||||||
|
|
||||||
|
[node name="NicknameLineEdit" type="LineEdit" parent="." unique_id=888809208]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
caret_blink = true
|
||||||
|
caret_blink_interval = 0.5
|
||||||
|
|
||||||
|
[node name="HBox3" type="HBoxContainer" parent="." unique_id=734718932]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
|
||||||
|
[node name="Label4" type="Label" parent="HBox3" unique_id=921032409]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
text = "Description"
|
||||||
|
|
||||||
|
[node name="HintTooltip3" parent="HBox3" unique_id=629870151 instance=ExtResource("2_cxfqm")]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "No effect, just for you."
|
||||||
|
texture = null
|
||||||
|
hint_text = "No effect, just for you."
|
||||||
|
|
||||||
|
[node name="DescriptionTextEdit" type="TextEdit" parent="." unique_id=1368503580]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(0, 65)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
wrap_mode = 1
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
@tool
|
||||||
|
extends DialogicCharacterEditorMainSection
|
||||||
|
|
||||||
|
## The general portrait settings section
|
||||||
|
|
||||||
|
var loading := false
|
||||||
|
|
||||||
|
func _get_title() -> String:
|
||||||
|
return "Portraits"
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Connecting all necessary signals
|
||||||
|
%DefaultPortraitPicker.value_changed.connect(default_portrait_changed)
|
||||||
|
%MainScale.value_changed.connect(main_portrait_settings_update)
|
||||||
|
%MainOffset._load_display_info({'step':1})
|
||||||
|
%MainOffset.value_changed.connect(main_portrait_settings_update)
|
||||||
|
%MainMirror.toggled.connect(main_portrait_settings_update)
|
||||||
|
|
||||||
|
# Setting up Default Portrait Picker
|
||||||
|
%DefaultPortraitPicker.resource_icon = load("res://addons/dialogic/Editor/Images/Resources/portrait.svg")
|
||||||
|
%DefaultPortraitPicker.suggestions_func = suggest_portraits
|
||||||
|
|
||||||
|
|
||||||
|
## Make sure preview get's updated when portrait settings change
|
||||||
|
func main_portrait_settings_update(_something=null, _value=null) -> void:
|
||||||
|
if loading:
|
||||||
|
return
|
||||||
|
character_editor.current_resource.scale = %MainScale.value/100.0
|
||||||
|
character_editor.current_resource.offset = %MainOffset.current_value
|
||||||
|
character_editor.current_resource.mirror = %MainMirror.button_pressed
|
||||||
|
character_editor.update_preview()
|
||||||
|
character_editor.something_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func default_portrait_changed(_property:String, value:String) -> void:
|
||||||
|
character_editor.current_resource.default_portrait = value
|
||||||
|
character_editor.update_default_portrait_star(value)
|
||||||
|
|
||||||
|
|
||||||
|
func set_default_portrait(portrait_name:String) -> void:
|
||||||
|
%DefaultPortraitPicker.set_value(portrait_name)
|
||||||
|
default_portrait_changed("", portrait_name)
|
||||||
|
|
||||||
|
|
||||||
|
func _load_character(resource:DialogicCharacter) -> void:
|
||||||
|
loading = true
|
||||||
|
%DefaultPortraitPicker.set_value(resource.default_portrait)
|
||||||
|
|
||||||
|
%MainScale.set_value(100*resource.scale)
|
||||||
|
%MainOffset.set_value(resource.offset)
|
||||||
|
%MainMirror.button_pressed = resource.mirror
|
||||||
|
loading = false
|
||||||
|
|
||||||
|
|
||||||
|
func _save_changes(resource:DialogicCharacter) -> DialogicCharacter:
|
||||||
|
# Portrait settings
|
||||||
|
if %DefaultPortraitPicker.current_value in resource.portraits.keys():
|
||||||
|
resource.default_portrait = %DefaultPortraitPicker.current_value
|
||||||
|
elif !resource.portraits.is_empty():
|
||||||
|
resource.default_portrait = resource.portraits.keys()[0]
|
||||||
|
else:
|
||||||
|
resource.default_portrait = ""
|
||||||
|
|
||||||
|
resource.scale = %MainScale.value/100.0
|
||||||
|
resource.offset = %MainOffset.current_value
|
||||||
|
resource.mirror = %MainMirror.button_pressed
|
||||||
|
return resource
|
||||||
|
|
||||||
|
|
||||||
|
## Get suggestions for DefaultPortraitPicker
|
||||||
|
func suggest_portraits(_search:String) -> Dictionary:
|
||||||
|
var suggestions := {}
|
||||||
|
for portrait in character_editor.get_updated_portrait_dict().keys():
|
||||||
|
suggestions[portrait] = {'value':portrait}
|
||||||
|
return suggestions
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://yulfiomudcob
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
[gd_scene load_steps=5 format=3 uid="uid://cmrgbo8qi145o"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://yulfiomudcob" path="res://addons/dialogic/Editor/CharacterEditor/char_edit_section_portraits.gd" id="1_6sxsl"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="2_birla"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dtimnsj014cu" path="res://addons/dialogic/Editor/Events/Fields/field_vector2.tscn" id="3_vcvin"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://kdpp3mibml33" path="res://addons/dialogic/Editor/Events/Fields/field_number.tscn" id="4_w4pvv"]
|
||||||
|
|
||||||
|
[node name="Portraits" type="GridContainer"]
|
||||||
|
offset_right = 453.0
|
||||||
|
offset_bottom = 141.0
|
||||||
|
theme_override_constants/h_separation = 1
|
||||||
|
theme_override_constants/v_separation = 6
|
||||||
|
columns = 2
|
||||||
|
script = ExtResource("1_6sxsl")
|
||||||
|
|
||||||
|
[node name="Label5" type="Label" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
text = "Default"
|
||||||
|
|
||||||
|
[node name="DefaultPortraitPicker" parent="." instance=ExtResource("2_birla")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "Select Default Portrait"
|
||||||
|
fit_text_length = false
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
text = "Main Scale"
|
||||||
|
|
||||||
|
[node name="MainScaleOld" type="SpinBox" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 8
|
||||||
|
value = 100.0
|
||||||
|
allow_greater = true
|
||||||
|
alignment = 1
|
||||||
|
suffix = "%"
|
||||||
|
|
||||||
|
[node name="MainScale" parent="." instance=ExtResource("4_w4pvv")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 8
|
||||||
|
mode = 1
|
||||||
|
step = 1.0
|
||||||
|
min_value = 0.0
|
||||||
|
max_value = 999.0
|
||||||
|
suffix = "%"
|
||||||
|
|
||||||
|
[node name="Label2" type="Label" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
text = "Main Offset"
|
||||||
|
|
||||||
|
[node name="MainOffset" parent="." instance=ExtResource("3_vcvin")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 2
|
||||||
|
|
||||||
|
[node name="Label3" type="Label" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
text = "Main Mirror"
|
||||||
|
|
||||||
|
[node name="MainMirror" type="CheckBox" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 8
|
||||||
700
addons/dialogic/Editor/CharacterEditor/character_editor.gd
Normal file
700
addons/dialogic/Editor/CharacterEditor/character_editor.gd
Normal file
@@ -0,0 +1,700 @@
|
|||||||
|
@tool
|
||||||
|
extends DialogicEditor
|
||||||
|
|
||||||
|
## Editor for editing character resources.
|
||||||
|
|
||||||
|
signal character_loaded(resource_path:String)
|
||||||
|
signal portrait_selected()
|
||||||
|
|
||||||
|
|
||||||
|
# Current state
|
||||||
|
var loading := false
|
||||||
|
var current_previewed_scene: Variant = null
|
||||||
|
var current_scene_path: String = ""
|
||||||
|
|
||||||
|
# References
|
||||||
|
var selected_item: TreeItem
|
||||||
|
var def_portrait_path: String = DialogicUtil.get_module_path('Character').path_join('default_portrait.tscn')
|
||||||
|
|
||||||
|
|
||||||
|
#region EDITOR STUFF and LOADING/SAVING
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
## Method is called once editors manager is ready to accept registers.
|
||||||
|
func _register() -> void:
|
||||||
|
## Makes the editor open this when a .dch file is selected.
|
||||||
|
## Then _open_resource() is called.
|
||||||
|
editors_manager.register_resource_editor("dch", self)
|
||||||
|
|
||||||
|
## Add an "add character" button
|
||||||
|
var add_character_button: Button = editors_manager.add_button(
|
||||||
|
load("res://addons/dialogic/Editor/Images/Toolbar/add-character.svg"),
|
||||||
|
"",
|
||||||
|
"New Character",
|
||||||
|
self, editors_manager.ButtonPlacement.SIDEBAR_LEFT_OF_FILTER)
|
||||||
|
add_character_button.pressed.connect(_on_create_character_button_pressed)
|
||||||
|
add_character_button.shortcut = Shortcut.new()
|
||||||
|
add_character_button.shortcut.events.append(InputEventKey.new())
|
||||||
|
add_character_button.shortcut.events[0].keycode = KEY_2
|
||||||
|
add_character_button.shortcut.events[0].ctrl_pressed = true
|
||||||
|
add_character_button.shortcut.events[0].command_or_control_autoremap = true
|
||||||
|
|
||||||
|
## By default show the no character screen
|
||||||
|
$NoCharacterScreen.show()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_title() -> String:
|
||||||
|
return "Character"
|
||||||
|
|
||||||
|
|
||||||
|
func _get_icon() -> Texture:
|
||||||
|
return load("res://addons/dialogic/Editor/Images/Resources/character.svg")
|
||||||
|
|
||||||
|
|
||||||
|
## Called when a character is opened somehow
|
||||||
|
func _open_resource(resource:Resource) -> void:
|
||||||
|
if resource == null:
|
||||||
|
$NoCharacterScreen.show()
|
||||||
|
return
|
||||||
|
|
||||||
|
## Update resource
|
||||||
|
current_resource = (resource as DialogicCharacter)
|
||||||
|
|
||||||
|
## Make sure changes in the ui won't trigger saving
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
## Load other main tabs
|
||||||
|
for child in %MainSettingsSections.get_children():
|
||||||
|
if child is DialogicCharacterEditorMainSection:
|
||||||
|
child._load_character(current_resource)
|
||||||
|
|
||||||
|
## Clear and then load Portrait section
|
||||||
|
%PortraitSearch.text = ""
|
||||||
|
load_portrait_tree()
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
character_loaded.emit(current_resource.resource_path)
|
||||||
|
|
||||||
|
%CharacterName.text = current_resource.get_identifier()
|
||||||
|
|
||||||
|
$NoCharacterScreen.hide()
|
||||||
|
%PortraitChangeInfo.hide()
|
||||||
|
|
||||||
|
|
||||||
|
## Called when the character is opened.
|
||||||
|
func _open(_extra_info:Variant="") -> void:
|
||||||
|
if not ProjectSettings.get_setting("dialogic/portraits/default_portrait", "").is_empty():
|
||||||
|
def_portrait_path = ProjectSettings.get_setting("dialogic/portraits/default_portrait", "")
|
||||||
|
else:
|
||||||
|
def_portrait_path = DialogicUtil.get_module_path("Character").path_join("default_portrait.tscn")
|
||||||
|
|
||||||
|
if current_resource == null:
|
||||||
|
$NoCharacterScreen.show()
|
||||||
|
return
|
||||||
|
|
||||||
|
update_preview(true)
|
||||||
|
%PortraitChangeInfo.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _clear() -> void:
|
||||||
|
current_resource = null
|
||||||
|
current_resource_state = ResourceStates.SAVED
|
||||||
|
$NoCharacterScreen.show()
|
||||||
|
|
||||||
|
|
||||||
|
func _save() -> void:
|
||||||
|
if not visible or not current_resource:
|
||||||
|
return
|
||||||
|
|
||||||
|
## Portrait list
|
||||||
|
current_resource.portraits = get_updated_portrait_dict()
|
||||||
|
|
||||||
|
## Main tabs
|
||||||
|
for child in %MainSettingsSections.get_children():
|
||||||
|
if child is DialogicCharacterEditorMainSection:
|
||||||
|
current_resource = child._save_changes(current_resource)
|
||||||
|
|
||||||
|
ResourceSaver.save(current_resource, current_resource.resource_path)
|
||||||
|
current_resource_state = ResourceStates.SAVED
|
||||||
|
DialogicResourceUtil.update_directory("dch")
|
||||||
|
|
||||||
|
|
||||||
|
## Saves a new empty character to the given path
|
||||||
|
func new_character(path: String) -> void:
|
||||||
|
if not path.ends_with(".dch"):
|
||||||
|
path = path.trim_suffix(".")
|
||||||
|
path += ".dch"
|
||||||
|
var resource := DialogicCharacter.new()
|
||||||
|
resource.resource_path = path
|
||||||
|
resource.display_name = path.get_file().trim_suffix("."+path.get_extension()).capitalize()
|
||||||
|
resource.color = Color(1,1,1,1)
|
||||||
|
resource.default_portrait = ""
|
||||||
|
resource.custom_info = {}
|
||||||
|
ResourceSaver.save(resource, path)
|
||||||
|
EditorInterface.get_resource_filesystem().update_file(path)
|
||||||
|
DialogicResourceUtil.update_directory('dch')
|
||||||
|
editors_manager.edit_resource(resource)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region INTERFACE
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
|
||||||
|
DialogicUtil.get_dialogic_plugin().resource_saved.connect(_on_some_resource_saved)
|
||||||
|
# NOTE: This check is required because up to 4.2 this signal is not exposed.
|
||||||
|
if DialogicUtil.get_dialogic_plugin().has_signal("scene_saved"):
|
||||||
|
DialogicUtil.get_dialogic_plugin().scene_saved.connect(_on_some_resource_saved)
|
||||||
|
|
||||||
|
$NoCharacterScreen.add_theme_stylebox_override("panel", get_theme_stylebox("Background", "EditorStyles"))
|
||||||
|
$NoCharacterScreen.show()
|
||||||
|
setup_portrait_list_tab()
|
||||||
|
|
||||||
|
%CloseButton.icon = get_theme_icon("Back", "EditorIcons")
|
||||||
|
%OpenButton.icon = get_theme_icon("Forward", "EditorIcons")
|
||||||
|
|
||||||
|
_on_fit_preview_toggle_toggled(DialogicUtil.get_editor_setting('character_preview_fit', true))
|
||||||
|
%PreviewLabel.add_theme_color_override("font_color", get_theme_color("readonly_color", "Editor"))
|
||||||
|
|
||||||
|
%PortraitChangeWarning.add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"))
|
||||||
|
|
||||||
|
%RealPreviewPivot.texture = get_theme_icon("EditorPivot", "EditorIcons")
|
||||||
|
|
||||||
|
|
||||||
|
set_portrait_settings_position(DialogicUtil.get_editor_setting('portrait_settings_position', true))
|
||||||
|
|
||||||
|
await find_parent('EditorView').ready
|
||||||
|
|
||||||
|
## Add general tabs
|
||||||
|
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_section_general.tscn").instantiate(), %MainSettingsSections)
|
||||||
|
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_section_portraits.tscn").instantiate(), %MainSettingsSections)
|
||||||
|
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/character_prefix_suffix.tscn").instantiate(), %MainSettingsSections)
|
||||||
|
|
||||||
|
|
||||||
|
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_main_exports.tscn").instantiate(), %PortraitSettingsSection)
|
||||||
|
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_exports.tscn").instantiate(), %PortraitSettingsSection)
|
||||||
|
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_main.tscn").instantiate(), %PortraitSettingsSection)
|
||||||
|
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_layout.tscn").instantiate(), %PortraitSettingsSection)
|
||||||
|
|
||||||
|
## Load custom sections from modules
|
||||||
|
for indexer in DialogicUtil.get_indexers():
|
||||||
|
for path in indexer._get_character_editor_sections():
|
||||||
|
var scene: Control = load(path).instantiate()
|
||||||
|
if scene is DialogicCharacterEditorMainSection:
|
||||||
|
add_settings_section(scene, %MainSettingsSections)
|
||||||
|
elif scene is DialogicCharacterEditorPortraitSection:
|
||||||
|
add_settings_section(scene, %PortraitSettingsSection)
|
||||||
|
|
||||||
|
|
||||||
|
## Add a section (a control) either to the given settings section (Main or Portraits)
|
||||||
|
## - sets up the title of the section
|
||||||
|
## - connects to various signals
|
||||||
|
func add_settings_section(edit:Control, parent:Node) -> void:
|
||||||
|
edit.changed.connect(something_changed)
|
||||||
|
edit.character_editor = self
|
||||||
|
|
||||||
|
if edit.has_signal('update_preview'):
|
||||||
|
edit.update_preview.connect(update_preview)
|
||||||
|
|
||||||
|
var button: Button
|
||||||
|
|
||||||
|
if edit._show_title():
|
||||||
|
var hbox := HBoxContainer.new()
|
||||||
|
hbox.name = edit._get_title()+"BOX"
|
||||||
|
button = Button.new()
|
||||||
|
button.flat = true
|
||||||
|
button.theme_type_variation = "DialogicSection"
|
||||||
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
|
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
|
button.text = edit._get_title()
|
||||||
|
button.icon_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
button.pressed.connect(_on_section_button_pressed.bind(button))
|
||||||
|
button.focus_mode = Control.FOCUS_NONE
|
||||||
|
button.icon = get_theme_icon("CodeFoldDownArrow", "EditorIcons")
|
||||||
|
button.add_theme_color_override('icon_normal_color', get_theme_color("font_color", "DialogicSection"))
|
||||||
|
|
||||||
|
hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
hbox.add_child(button)
|
||||||
|
|
||||||
|
if !edit.hint_text.is_empty():
|
||||||
|
var hint: Node = load("res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn").instantiate()
|
||||||
|
hint.hint_text = edit.hint_text
|
||||||
|
hbox.add_child(hint)
|
||||||
|
|
||||||
|
parent.add_child(hbox)
|
||||||
|
parent.add_child(edit)
|
||||||
|
parent.add_child(HSeparator.new())
|
||||||
|
if button and !edit._start_opened():
|
||||||
|
_on_section_button_pressed(button)
|
||||||
|
|
||||||
|
|
||||||
|
func get_settings_section_by_name(section_name:String, main:=true) -> Node:
|
||||||
|
var parent := %MainSettingsSections
|
||||||
|
if not main:
|
||||||
|
parent = %PortraitSettingsSection
|
||||||
|
|
||||||
|
if parent.has_node(section_name):
|
||||||
|
return parent.get_node(section_name)
|
||||||
|
elif parent.has_node(section_name+"BOX/"+section_name):
|
||||||
|
return parent.get_node(section_name+"BOX/"+section_name)
|
||||||
|
else:
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _on_section_button_pressed(button:Button) -> void:
|
||||||
|
var section_header := button.get_parent()
|
||||||
|
var section := section_header.get_parent().get_child(section_header.get_index()+1)
|
||||||
|
if section.visible:
|
||||||
|
button.icon = get_theme_icon("CodeFoldedRightArrow", "EditorIcons")
|
||||||
|
section.visible = false
|
||||||
|
else:
|
||||||
|
button.icon = get_theme_icon("CodeFoldDownArrow", "EditorIcons")
|
||||||
|
section.visible = true
|
||||||
|
|
||||||
|
if section_header.get_parent().get_child_count() > section_header.get_index()+2 and section_header.get_parent().get_child(section_header.get_index()+2) is Separator:
|
||||||
|
section_header.get_parent().get_child(section_header.get_index()+2).visible = section_header.get_parent().get_child(section_header.get_index()+1).visible
|
||||||
|
|
||||||
|
|
||||||
|
func something_changed(_fake_argument = "", _fake_arg2 = null) -> void:
|
||||||
|
if not loading:
|
||||||
|
current_resource_state = ResourceStates.UNSAVED
|
||||||
|
|
||||||
|
|
||||||
|
func hide_main_settings() -> void:
|
||||||
|
%MainSettings.hide()
|
||||||
|
%MainSettingsHidden.show()
|
||||||
|
%MainSettingsPanel.size_flags_horizontal = SIZE_SHRINK_BEGIN
|
||||||
|
%MainHSplit.collapsed = true
|
||||||
|
|
||||||
|
|
||||||
|
func show_main_settings() -> void:
|
||||||
|
%MainSettings.show()
|
||||||
|
%MainSettingsHidden.hide()
|
||||||
|
%MainSettingsPanel.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||||
|
%MainHSplit.collapsed = false
|
||||||
|
|
||||||
|
|
||||||
|
func _on_switch_portrait_settings_position_pressed() -> void:
|
||||||
|
set_portrait_settings_position(!%RightSection.vertical)
|
||||||
|
|
||||||
|
|
||||||
|
func set_portrait_settings_position(is_below:bool) -> void:
|
||||||
|
%RightSection.vertical = is_below
|
||||||
|
DialogicUtil.set_editor_setting('portrait_settings_position', is_below)
|
||||||
|
if is_below:
|
||||||
|
%SwitchPortraitSettingsPosition.icon = get_theme_icon("ControlAlignRightWide", "EditorIcons")
|
||||||
|
else:
|
||||||
|
%SwitchPortraitSettingsPosition.icon = get_theme_icon("ControlAlignBottomWide", "EditorIcons")
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
########## PORTRAIT SECTION ####################################################
|
||||||
|
|
||||||
|
#region Portrait Section
|
||||||
|
func setup_portrait_list_tab() -> void:
|
||||||
|
%PortraitTree.editor = self
|
||||||
|
|
||||||
|
## Portrait section styling/connections
|
||||||
|
%AddPortraitButton.icon = get_theme_icon("Add", "EditorIcons")
|
||||||
|
%AddPortraitButton.pressed.connect(add_portrait)
|
||||||
|
%AddPortraitGroupButton.icon = load("res://addons/dialogic/Editor/Images/Pieces/add-folder.svg")
|
||||||
|
%AddPortraitGroupButton.pressed.connect(add_portrait_group)
|
||||||
|
%ImportPortraitsButton.icon = get_theme_icon("Load", "EditorIcons")
|
||||||
|
%ImportPortraitsButton.pressed.connect(open_portrait_folder_select)
|
||||||
|
%PortraitSearch.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||||
|
%PortraitSearch.text_changed.connect(filter_portrait_list)
|
||||||
|
|
||||||
|
%PortraitTree.item_selected.connect(load_selected_portrait)
|
||||||
|
%PortraitTree.item_edited.connect(_on_item_edited)
|
||||||
|
%PortraitTree.item_activated.connect(_on_item_activated)
|
||||||
|
|
||||||
|
|
||||||
|
func open_portrait_folder_select() -> void:
|
||||||
|
find_parent("EditorView").godot_file_dialog(
|
||||||
|
import_portraits_from_file_list, "*.svg, *.png",
|
||||||
|
EditorFileDialog.FILE_MODE_OPEN_FILES, "Import Images From Folder")
|
||||||
|
|
||||||
|
|
||||||
|
func import_portraits_from_file_list(files:Array) -> void:
|
||||||
|
var parent: TreeItem = %PortraitTree.get_root()
|
||||||
|
|
||||||
|
if %PortraitTree.get_selected() and %PortraitTree.get_selected() != parent and %PortraitTree.get_selected().get_metadata(0).has('group'):
|
||||||
|
parent = %PortraitTree.get_selected()
|
||||||
|
|
||||||
|
var prefix: String = files[0]
|
||||||
|
for file in files:
|
||||||
|
while true:
|
||||||
|
if file.begins_with(prefix):
|
||||||
|
break
|
||||||
|
if prefix.is_empty():
|
||||||
|
break
|
||||||
|
prefix = prefix.substr(0, len(prefix)-1)
|
||||||
|
|
||||||
|
if len(files) == 1:
|
||||||
|
prefix = prefix.get_base_dir()+"/"
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if not file.get_extension() in ["tscn", "scn", "png", "webp", "jpg", "jpeg", "svg"]:
|
||||||
|
continue
|
||||||
|
var portrait_name: String = file.trim_prefix(prefix).trim_suffix('.'+file.get_extension()).capitalize()
|
||||||
|
var scene: String = file if file.get_extension() in ["tscn", "scn"] else ""
|
||||||
|
var export_overrides: Dictionary = {"image":var_to_str(file)} if file.get_extension() in ["png", "webp", "jpg", "jpeg", "svg"] else {}
|
||||||
|
var item : TreeItem = %PortraitTree.add_portrait_item(portrait_name,
|
||||||
|
{"scene":scene,"export_overrides":export_overrides, "scale":1, "offset":Vector2(), "mirror":false}, parent)
|
||||||
|
item.set_meta("new", true)
|
||||||
|
|
||||||
|
## Handle selection
|
||||||
|
if parent.get_child_count():
|
||||||
|
parent.get_first_child().select(0)
|
||||||
|
else:
|
||||||
|
# Call anyways to clear preview and hide portrait settings section
|
||||||
|
load_selected_portrait()
|
||||||
|
|
||||||
|
something_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func add_portrait(portrait_name:String='New portrait', portrait_data:Dictionary={'scene':"", 'export_overrides':{'image':''}, 'scale':1, 'offset':Vector2(), 'mirror':false}) -> void:
|
||||||
|
var parent: TreeItem = %PortraitTree.get_root()
|
||||||
|
if %PortraitTree.get_selected():
|
||||||
|
if %PortraitTree.get_selected().get_metadata(0) and %PortraitTree.get_selected().get_metadata(0).has('group'):
|
||||||
|
parent = %PortraitTree.get_selected()
|
||||||
|
else:
|
||||||
|
parent = %PortraitTree.get_selected().get_parent()
|
||||||
|
var item: TreeItem = %PortraitTree.add_portrait_item(portrait_name, portrait_data, parent)
|
||||||
|
item.set_meta('new', true)
|
||||||
|
item.set_editable(0, true)
|
||||||
|
item.select(0)
|
||||||
|
%PortraitTree.call_deferred('edit_selected')
|
||||||
|
something_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func add_portrait_group() -> void:
|
||||||
|
var parent_item: TreeItem = %PortraitTree.get_root()
|
||||||
|
if %PortraitTree.get_selected() and %PortraitTree.get_selected().get_metadata(0) and %PortraitTree.get_selected().get_metadata(0).has('group'):
|
||||||
|
parent_item = %PortraitTree.get_selected()
|
||||||
|
var item: TreeItem = %PortraitTree.add_portrait_group("Group", parent_item)
|
||||||
|
item.set_meta('new', true)
|
||||||
|
item.set_editable(0, true)
|
||||||
|
item.select(0)
|
||||||
|
%PortraitTree.call_deferred('edit_selected')
|
||||||
|
|
||||||
|
|
||||||
|
func load_portrait_tree() -> void:
|
||||||
|
%PortraitTree.clear_tree()
|
||||||
|
var root: TreeItem = %PortraitTree.create_item()
|
||||||
|
|
||||||
|
for portrait in current_resource.portraits.keys():
|
||||||
|
var portrait_label: String = portrait
|
||||||
|
var parent: TreeItem = %PortraitTree.get_root()
|
||||||
|
if '/' in portrait:
|
||||||
|
parent = %PortraitTree.create_necessary_group_items(portrait)
|
||||||
|
portrait_label = portrait.split('/')[-1]
|
||||||
|
|
||||||
|
%PortraitTree.add_portrait_item(portrait_label, current_resource.portraits[portrait], parent)
|
||||||
|
|
||||||
|
update_default_portrait_star(current_resource.default_portrait)
|
||||||
|
|
||||||
|
if root.get_child_count():
|
||||||
|
root.get_first_child().select(0)
|
||||||
|
while %PortraitTree.get_selected().get_child_count():
|
||||||
|
%PortraitTree.get_selected().get_child(0).select(0)
|
||||||
|
else:
|
||||||
|
# Call anyways to clear preview and hide portrait settings section
|
||||||
|
load_selected_portrait()
|
||||||
|
|
||||||
|
|
||||||
|
func filter_portrait_list(filter_term := "") -> void:
|
||||||
|
filter_branch(%PortraitTree.get_root(), filter_term)
|
||||||
|
|
||||||
|
|
||||||
|
func filter_branch(parent: TreeItem, filter_term: String) -> bool:
|
||||||
|
var anything_visible := false
|
||||||
|
for item in parent.get_children():
|
||||||
|
if item.get_metadata(0).has('group'):
|
||||||
|
item.visible = filter_branch(item, filter_term)
|
||||||
|
anything_visible = item.visible
|
||||||
|
elif filter_term.is_empty() or filter_term.to_lower() in item.get_text(0).to_lower():
|
||||||
|
item.visible = true
|
||||||
|
anything_visible = true
|
||||||
|
else:
|
||||||
|
item.visible = false
|
||||||
|
return anything_visible
|
||||||
|
|
||||||
|
|
||||||
|
## This is used to save the portrait data
|
||||||
|
func get_updated_portrait_dict() -> Dictionary:
|
||||||
|
return list_portraits(%PortraitTree.get_root().get_children())
|
||||||
|
|
||||||
|
|
||||||
|
func list_portraits(tree_items: Array[TreeItem], dict := {}, path_prefix := "") -> Dictionary:
|
||||||
|
for item in tree_items:
|
||||||
|
if item.get_metadata(0).has('group'):
|
||||||
|
dict = list_portraits(item.get_children(), dict, path_prefix+item.get_text(0)+"/")
|
||||||
|
else:
|
||||||
|
dict[path_prefix +item.get_text(0)] = item.get_metadata(0)
|
||||||
|
return dict
|
||||||
|
|
||||||
|
|
||||||
|
func load_selected_portrait() -> void:
|
||||||
|
if selected_item and is_instance_valid(selected_item):
|
||||||
|
selected_item.set_editable(0, false)
|
||||||
|
|
||||||
|
selected_item = %PortraitTree.get_selected()
|
||||||
|
|
||||||
|
if selected_item and selected_item.get_metadata(0) != null and !selected_item.get_metadata(0).has('group'):
|
||||||
|
%PortraitSettingsSection.show()
|
||||||
|
var current_portrait_data: Dictionary = selected_item.get_metadata(0)
|
||||||
|
portrait_selected.emit(%PortraitTree.get_full_item_name(selected_item), current_portrait_data)
|
||||||
|
|
||||||
|
update_preview()
|
||||||
|
|
||||||
|
for child in %PortraitSettingsSection.get_children():
|
||||||
|
if child is DialogicCharacterEditorPortraitSection:
|
||||||
|
child.selected_item = selected_item
|
||||||
|
child._load_portrait_data(current_portrait_data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
%PortraitSettingsSection.hide()
|
||||||
|
update_preview()
|
||||||
|
|
||||||
|
|
||||||
|
func delete_portrait_item(item: TreeItem) -> void:
|
||||||
|
if item.get_next_visible(true) and item.get_next_visible(true) != item:
|
||||||
|
item.get_next_visible(true).select(0)
|
||||||
|
else:
|
||||||
|
selected_item = null
|
||||||
|
load_selected_portrait()
|
||||||
|
item.free()
|
||||||
|
something_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func duplicate_item(item: TreeItem) -> void:
|
||||||
|
var new_item: TreeItem = %PortraitTree.add_portrait_item(item.get_text(0)+'_duplicated', item.get_metadata(0).duplicate(true), item.get_parent())
|
||||||
|
new_item.set_meta('new', true)
|
||||||
|
new_item.select(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if !is_visible_in_tree() or (get_viewport().gui_get_focus_owner()!= null and !name+'/' in str(get_viewport().gui_get_focus_owner().get_path())):
|
||||||
|
return
|
||||||
|
if event is InputEventKey and event.pressed:
|
||||||
|
if event.keycode == KEY_F2 and %PortraitTree.get_selected():
|
||||||
|
%PortraitTree.get_selected().set_editable(0, true)
|
||||||
|
%PortraitTree.edit_selected()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event.keycode == KEY_DELETE and get_viewport().gui_get_focus_owner() is Tree and %PortraitTree.get_selected():
|
||||||
|
delete_portrait_item(%PortraitTree.get_selected())
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_portrait_right_click_menu_index_pressed(id: int) -> void:
|
||||||
|
# RENAME BUTTON
|
||||||
|
if id == 0:
|
||||||
|
_on_item_activated()
|
||||||
|
# DELETE BUTTON
|
||||||
|
if id == 2:
|
||||||
|
delete_portrait_item(%PortraitTree.get_selected())
|
||||||
|
# DUPLICATE ITEM
|
||||||
|
elif id == 1:
|
||||||
|
duplicate_item(%PortraitTree.get_selected())
|
||||||
|
elif id == 4:
|
||||||
|
get_settings_section_by_name("Portraits").set_default_portrait(%PortraitTree.get_full_item_name(%PortraitTree.get_selected()))
|
||||||
|
|
||||||
|
|
||||||
|
## This removes/and adds the DEFAULT star on the portrait list
|
||||||
|
func update_default_portrait_star(default_portrait_name: String) -> void:
|
||||||
|
var item_list: Array = %PortraitTree.get_root().get_children()
|
||||||
|
if item_list.is_empty() == false:
|
||||||
|
while true:
|
||||||
|
var item: TreeItem = item_list.pop_back()
|
||||||
|
if item.get_button_by_id(0, 2) != -1:
|
||||||
|
item.erase_button(0, item.get_button_by_id(0, 2))
|
||||||
|
if %PortraitTree.get_full_item_name(item) == default_portrait_name:
|
||||||
|
item.add_button(0, get_theme_icon("Favorites", "EditorIcons"), 2, true, "Default")
|
||||||
|
item_list.append_array(item.get_children())
|
||||||
|
if item_list.is_empty():
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
func _on_item_edited() -> void:
|
||||||
|
selected_item = %PortraitTree.get_selected()
|
||||||
|
something_changed()
|
||||||
|
if selected_item:
|
||||||
|
if %PreviewLabel.text.trim_prefix('Preview of "').trim_suffix('"') == current_resource.default_portrait:
|
||||||
|
current_resource.default_portrait = %PortraitTree.get_full_item_name(selected_item)
|
||||||
|
selected_item.set_editable(0, false)
|
||||||
|
|
||||||
|
if !selected_item.has_meta('new') and %PortraitTree.get_full_item_name(selected_item) != selected_item.get_meta('previous_name'):
|
||||||
|
report_name_change(selected_item)
|
||||||
|
%PortraitChangeInfo.show()
|
||||||
|
update_preview()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_item_activated() -> void:
|
||||||
|
if %PortraitTree.get_selected() == null:
|
||||||
|
return
|
||||||
|
%PortraitTree.get_selected().set_editable(0, true)
|
||||||
|
%PortraitTree.edit_selected()
|
||||||
|
|
||||||
|
|
||||||
|
func report_name_change(item: TreeItem) -> void:
|
||||||
|
if item.get_metadata(0).has('group'):
|
||||||
|
for s_item in item.get_children():
|
||||||
|
if s_item.get_metadata(0).has('group') or !s_item.has_meta('new'):
|
||||||
|
report_name_change(s_item)
|
||||||
|
else:
|
||||||
|
if item.get_meta('previous_name') == %PortraitTree.get_full_item_name(item):
|
||||||
|
return
|
||||||
|
editors_manager.reference_manager.add_portrait_ref_change(
|
||||||
|
item.get_meta('previous_name'),
|
||||||
|
%PortraitTree.get_full_item_name(item),
|
||||||
|
[current_resource.get_identifier()])
|
||||||
|
item.set_meta('previous_name', %PortraitTree.get_full_item_name(item))
|
||||||
|
%PortraitChangeInfo.show()
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region PREVIEW
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func update_preview(force := false, ignore_settings_reload := false) -> void:
|
||||||
|
%ScenePreviewWarning.hide()
|
||||||
|
|
||||||
|
if selected_item and is_instance_valid(selected_item) and selected_item.get_metadata(0) != null and !selected_item.get_metadata(0).has('group'):
|
||||||
|
%PreviewLabel.text = 'Preview of "'+%PortraitTree.get_full_item_name(selected_item)+'"'
|
||||||
|
|
||||||
|
var current_portrait_data: Dictionary = selected_item.get_metadata(0)
|
||||||
|
|
||||||
|
if not force and current_previewed_scene != null \
|
||||||
|
and scene_file_path == current_portrait_data.get('scene') \
|
||||||
|
and current_previewed_scene.has_method('_should_do_portrait_update') \
|
||||||
|
and is_instance_valid(current_previewed_scene.get_script()) \
|
||||||
|
and current_previewed_scene._should_do_portrait_update(current_resource, selected_item.get_text(0)):
|
||||||
|
# We keep the same scene.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
|
||||||
|
for node in %RealPreviewPivot.get_children():
|
||||||
|
node.queue_free()
|
||||||
|
|
||||||
|
current_previewed_scene = null
|
||||||
|
current_scene_path = ""
|
||||||
|
|
||||||
|
var scene_path := def_portrait_path
|
||||||
|
if not current_portrait_data.get('scene', '').is_empty():
|
||||||
|
scene_path = current_portrait_data.get('scene')
|
||||||
|
|
||||||
|
if ResourceLoader.exists(scene_path):
|
||||||
|
current_previewed_scene = load(scene_path).instantiate()
|
||||||
|
current_scene_path = scene_path
|
||||||
|
|
||||||
|
if not current_previewed_scene == null:
|
||||||
|
%RealPreviewPivot.add_child(current_previewed_scene)
|
||||||
|
|
||||||
|
if not current_previewed_scene == null:
|
||||||
|
var scene: Node = current_previewed_scene
|
||||||
|
|
||||||
|
scene.show_behind_parent = true
|
||||||
|
DialogicUtil.apply_scene_export_overrides(scene, current_portrait_data.get('export_overrides', {}))
|
||||||
|
|
||||||
|
var mirror: bool = current_portrait_data.get('mirror', false) != current_resource.mirror
|
||||||
|
var scale: float = current_portrait_data.get('scale', 1) * current_resource.scale
|
||||||
|
|
||||||
|
if current_portrait_data.get('ignore_char_scale', false):
|
||||||
|
scale = current_portrait_data.get('scale', 1)
|
||||||
|
|
||||||
|
var offset: Vector2 = current_portrait_data.get('offset', Vector2()) + current_resource.offset
|
||||||
|
|
||||||
|
if is_instance_valid(scene.get_script()) and scene.script.is_tool():
|
||||||
|
|
||||||
|
if scene.has_method('_update_portrait'):
|
||||||
|
## Create a fake duplicate resource that has all the portrait changes applied already
|
||||||
|
var preview_character := current_resource.duplicate()
|
||||||
|
preview_character.portraits = get_updated_portrait_dict()
|
||||||
|
scene._update_portrait(preview_character, %PortraitTree.get_full_item_name(selected_item))
|
||||||
|
|
||||||
|
if scene.has_method('_set_mirror'):
|
||||||
|
scene._set_mirror(mirror)
|
||||||
|
|
||||||
|
if !%FitPreview_Toggle.button_pressed:
|
||||||
|
scene.position = Vector2() + offset
|
||||||
|
scene.scale = Vector2(1,1)*scale
|
||||||
|
else:
|
||||||
|
|
||||||
|
if not scene.get_script() == null and scene.script.is_tool() and scene.has_method('_get_covered_rect'):
|
||||||
|
var rect: Rect2 = scene._get_covered_rect()
|
||||||
|
var available_rect: Rect2 = %FullPreviewAvailableRect.get_rect()
|
||||||
|
scene.scale = Vector2(1,1) * min(available_rect.size.x/rect.size.x, available_rect.size.y/rect.size.y)
|
||||||
|
%RealPreviewPivot.position = (rect.position)*-1*scene.scale
|
||||||
|
%RealPreviewPivot.position.x = %FullPreviewAvailableRect.size.x/2
|
||||||
|
scene.position = Vector2()
|
||||||
|
|
||||||
|
else:
|
||||||
|
%ScenePreviewWarning.show()
|
||||||
|
else:
|
||||||
|
%PreviewLabel.text = 'Nothing to preview'
|
||||||
|
|
||||||
|
if not ignore_settings_reload:
|
||||||
|
for child in %PortraitSettingsSection.get_children():
|
||||||
|
if child is DialogicCharacterEditorPortraitSection:
|
||||||
|
child._recheck(current_portrait_data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
%PreviewLabel.text = 'No portrait to preview.'
|
||||||
|
|
||||||
|
for node in %RealPreviewPivot.get_children():
|
||||||
|
node.queue_free()
|
||||||
|
|
||||||
|
current_previewed_scene = null
|
||||||
|
current_scene_path = ""
|
||||||
|
|
||||||
|
|
||||||
|
func _on_some_resource_saved(file:Variant) -> void:
|
||||||
|
if current_previewed_scene == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
if file is Resource and file == current_previewed_scene.script:
|
||||||
|
update_preview(true)
|
||||||
|
|
||||||
|
if typeof(file) == TYPE_STRING and file == current_previewed_scene.get_meta("path", ""):
|
||||||
|
update_preview(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_full_preview_available_rect_resized() -> void:
|
||||||
|
if %FitPreview_Toggle.button_pressed:
|
||||||
|
update_preview(false, true)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_create_character_button_pressed() -> void:
|
||||||
|
editors_manager.show_add_resource_dialog(
|
||||||
|
new_character,
|
||||||
|
'*.dch; DialogicCharacter',
|
||||||
|
'Create new character',
|
||||||
|
'character',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_fit_preview_toggle_toggled(button_pressed):
|
||||||
|
%FitPreview_Toggle.set_pressed_no_signal(button_pressed)
|
||||||
|
if button_pressed:
|
||||||
|
%FitPreview_Toggle.icon = get_theme_icon("ScrollContainer", "EditorIcons")
|
||||||
|
%FitPreview_Toggle.tooltip_text = "Real scale"
|
||||||
|
else:
|
||||||
|
%FitPreview_Toggle.tooltip_text = "Fit into preview"
|
||||||
|
%FitPreview_Toggle.icon = get_theme_icon("CenterContainer", "EditorIcons")
|
||||||
|
DialogicUtil.set_editor_setting('character_preview_fit', button_pressed)
|
||||||
|
update_preview(false, true)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
## Open the reference manager
|
||||||
|
func _on_reference_manger_button_pressed() -> void:
|
||||||
|
editors_manager.reference_manager.open()
|
||||||
|
%PortraitChangeInfo.hide()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cwhe7tpe75oh7
|
||||||
444
addons/dialogic/Editor/CharacterEditor/character_editor.tscn
Normal file
444
addons/dialogic/Editor/CharacterEditor/character_editor.tscn
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
[gd_scene format=3 uid="uid://dlskc36c5hrwv"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cwhe7tpe75oh7" path="res://addons/dialogic/Editor/CharacterEditor/character_editor.gd" id="2"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_uhhqs"]
|
||||||
|
[ext_resource type="Script" uid="uid://deliic6d8vajo" path="res://addons/dialogic/Editor/CharacterEditor/character_editor_portrait_tree.gd" id="2_vad0i"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://my600mb32ydt" path="res://addons/dialogic/Editor/Images/Toolbar/add-character.svg" id="6_2ogmx"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_es2rd"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_4xgdx"]
|
||||||
|
|
||||||
|
[node name="CharacterEditor" type="Control" unique_id=1430618019]
|
||||||
|
self_modulate = Color(0, 0, 0, 1)
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("2")
|
||||||
|
|
||||||
|
[node name="Scroll" type="ScrollContainer" parent="." unique_id=2103751498]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="Scroll" unique_id=478102003]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
size_flags_stretch_ratio = 0.3
|
||||||
|
theme_override_constants/separation = 0
|
||||||
|
|
||||||
|
[node name="TopSection" type="HBoxContainer" parent="Scroll/VBox" unique_id=196003140]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="NameContainer" type="HBoxContainer" parent="Scroll/VBox/TopSection" unique_id=1249855270]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="CharacterName" type="Label" parent="Scroll/VBox/TopSection/NameContainer" unique_id=1631969065]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"DialogicTitle"
|
||||||
|
text = "My Character"
|
||||||
|
|
||||||
|
[node name="NameTooltip" parent="Scroll/VBox/TopSection/NameContainer" unique_id=1870345891 instance=ExtResource("2_uhhqs")]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "This unique identifier is based on the file name. You can change it in the Reference Manager.
|
||||||
|
Use this name in timelines to reference this character."
|
||||||
|
texture = null
|
||||||
|
hint_text = "This unique identifier is based on the file name. You can change it in the Reference Manager.
|
||||||
|
Use this name in timelines to reference this character."
|
||||||
|
|
||||||
|
[node name="MainHSplit" type="HSplitContainer" parent="Scroll/VBox" unique_id=1603115244]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="MainSettingsPanel" type="PanelContainer" parent="Scroll/VBox/MainHSplit" unique_id=465890127]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_stretch_ratio = 0.2
|
||||||
|
theme_type_variation = &"DialogicPanelA"
|
||||||
|
|
||||||
|
[node name="MainSettings" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/MainSettingsPanel" unique_id=843706490]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_stretch_ratio = 0.2
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/MainSettingsPanel/MainSettings" unique_id=989140237]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="MainSettingsTitle" type="Label" parent="Scroll/VBox/MainHSplit/MainSettingsPanel/MainSettings/HBoxContainer" unique_id=1680089144]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_type_variation = &"DialogicSubTitle"
|
||||||
|
text = "Main Settings"
|
||||||
|
|
||||||
|
[node name="CloseButton" type="Button" parent="Scroll/VBox/MainHSplit/MainSettingsPanel/MainSettings/HBoxContainer" unique_id=21396123]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Hide Main Settings"
|
||||||
|
theme_type_variation = &"FlatButton"
|
||||||
|
text = " "
|
||||||
|
|
||||||
|
[node name="MainSettingsScroll" type="ScrollContainer" parent="Scroll/VBox/MainHSplit/MainSettingsPanel/MainSettings" unique_id=802937484]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxEmpty_es2rd")
|
||||||
|
horizontal_scroll_mode = 0
|
||||||
|
|
||||||
|
[node name="MainSettingsSections" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/MainSettingsPanel/MainSettings/MainSettingsScroll" unique_id=667263369]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="MainSettingsHidden" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/MainSettingsPanel" unique_id=190117290]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="OpenButton" type="Button" parent="Scroll/VBox/MainHSplit/MainSettingsPanel/MainSettingsHidden" unique_id=349430792]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
size_flags_vertical = 3
|
||||||
|
tooltip_text = "Show Main Settings"
|
||||||
|
theme_type_variation = &"FlatButton"
|
||||||
|
theme_override_constants/icon_max_width = 20
|
||||||
|
|
||||||
|
[node name="Split" type="HSplitContainer" parent="Scroll/VBox/MainHSplit" unique_id=203063071]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/Split" unique_id=1087042926]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_stretch_ratio = 0.2
|
||||||
|
theme_override_constants/separation = 0
|
||||||
|
|
||||||
|
[node name="MarginContainer" type="MarginContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer" unique_id=1163073523]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_stretch_ratio = 0.2
|
||||||
|
|
||||||
|
[node name="PortraitListSection" type="PanelContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer" unique_id=95553638]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_type_variation = &"DialogicPanelA"
|
||||||
|
|
||||||
|
[node name="Portraits" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection" unique_id=331914429]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Title" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits" unique_id=1467490767]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="PortraitsTitle" type="Label" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/Title" unique_id=1012902349]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"DialogicSubTitle"
|
||||||
|
text = "Portraits"
|
||||||
|
|
||||||
|
[node name="AddPortraitButton" type="Button" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/Title" unique_id=1343539624]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Add Empty Portrait"
|
||||||
|
theme_type_variation = &"FlatButton"
|
||||||
|
|
||||||
|
[node name="AddPortraitGroupButton" type="Button" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/Title" unique_id=554125997]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Add Portrait Group"
|
||||||
|
theme_type_variation = &"FlatButton"
|
||||||
|
|
||||||
|
[node name="ImportPortraitsButton" type="Button" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/Title" unique_id=423626215]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Import Images as Portraits"
|
||||||
|
theme_type_variation = &"FlatButton"
|
||||||
|
|
||||||
|
[node name="PortraitListTools" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits" unique_id=1864279451]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="PortraitSearch" type="LineEdit" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitListTools" unique_id=1047008144]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 4
|
||||||
|
placeholder_text = "Filter Portraits"
|
||||||
|
expand_to_text_length = true
|
||||||
|
clear_button_enabled = true
|
||||||
|
caret_blink = true
|
||||||
|
caret_blink_interval = 0.5
|
||||||
|
|
||||||
|
[node name="PortraitTreePanel" type="PanelContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits" unique_id=1248108876]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxEmpty_4xgdx")
|
||||||
|
|
||||||
|
[node name="PortraitTree" type="Tree" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitTreePanel" unique_id=1126516054]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
allow_rmb_select = true
|
||||||
|
hide_root = true
|
||||||
|
script = ExtResource("2_vad0i")
|
||||||
|
|
||||||
|
[node name="PortraitRightClickMenu" type="PopupMenu" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitTreePanel/PortraitTree" unique_id=1852436528]
|
||||||
|
size = Vector2i(118, 100)
|
||||||
|
item_count = 5
|
||||||
|
item_0/text = "Rename"
|
||||||
|
item_0/id = 2
|
||||||
|
item_1/text = "Duplicate"
|
||||||
|
item_1/id = 0
|
||||||
|
item_2/text = "Delete"
|
||||||
|
item_2/id = 1
|
||||||
|
item_3/id = 3
|
||||||
|
item_3/separator = true
|
||||||
|
item_4/text = "Make Default"
|
||||||
|
item_4/id = 4
|
||||||
|
|
||||||
|
[node name="PortraitChangeInfo" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits" unique_id=867743359]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="PortraitChangeWarning" type="Label" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitChangeInfo" unique_id=505408175]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||||
|
text = "Some portraits were renamed. Make sure no references broke!"
|
||||||
|
autowrap_mode = 3
|
||||||
|
|
||||||
|
[node name="ReferenceMangerButton" type="Button" parent="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitChangeInfo" unique_id=1685213609]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 4
|
||||||
|
text = "Reference
|
||||||
|
Manager"
|
||||||
|
|
||||||
|
[node name="RightSection2" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/Split" unique_id=223577878]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
size_flags_stretch_ratio = 0.5
|
||||||
|
|
||||||
|
[node name="Spacer" type="Control" parent="Scroll/VBox/MainHSplit/Split/RightSection2" unique_id=1547642731]
|
||||||
|
custom_minimum_size = Vector2(0, 10)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="RightSection" type="SplitContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2" unique_id=1177475928]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
size_flags_stretch_ratio = 0.5
|
||||||
|
vertical = true
|
||||||
|
|
||||||
|
[node name="PortraitPreviewSection" type="Panel" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection" unique_id=1063379299]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
show_behind_parent = true
|
||||||
|
custom_minimum_size = Vector2(100, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_type_variation = &"DialogicPanelB"
|
||||||
|
|
||||||
|
[node name="ClipRect" type="Control" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection" unique_id=167304182]
|
||||||
|
clip_contents = true
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="Node2D" type="Node2D" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/ClipRect" unique_id=2047494054]
|
||||||
|
position = Vector2(13, 17)
|
||||||
|
|
||||||
|
[node name="RealPreviewPivot" type="Sprite2D" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/ClipRect/Node2D" unique_id=1422961354]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
position = Vector2(329, 287)
|
||||||
|
|
||||||
|
[node name="ScenePreviewWarning" type="Label" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection" unique_id=1589232067]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -143.0
|
||||||
|
offset_top = -44.5
|
||||||
|
offset_right = 143.0
|
||||||
|
offset_bottom = 85.5
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
text = "Custom scenes can only be viewed in \"Full mode\" if they are in @tool mode and override _get_covered_rect"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
autowrap_mode = 3
|
||||||
|
metadata/_edit_layout_mode = 1
|
||||||
|
|
||||||
|
[node name="PreviewReal" type="CenterContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection" unique_id=817899797]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 7
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -302.0
|
||||||
|
offset_top = -80.0
|
||||||
|
offset_right = 302.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 0
|
||||||
|
mouse_filter = 2
|
||||||
|
metadata/_edit_layout_mode = 1
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/PreviewReal" unique_id=1058864451]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="RealSizeRemotePivotTransform" type="RemoteTransform2D" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/PreviewReal/Control" unique_id=995132075]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
remote_path = NodePath("../../../ClipRect/Node2D/RealPreviewPivot")
|
||||||
|
update_rotation = false
|
||||||
|
update_scale = false
|
||||||
|
|
||||||
|
[node name="FullPreviewAvailableRect" type="Control" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection" unique_id=115907348]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 10.0
|
||||||
|
offset_top = 28.0
|
||||||
|
offset_right = -10.0
|
||||||
|
offset_bottom = -16.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
metadata/_edit_layout_mode = 1
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection" unique_id=1496501948]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 10
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = 6.0
|
||||||
|
offset_top = 7.0
|
||||||
|
offset_right = -6.0
|
||||||
|
offset_bottom = 43.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="PreviewLabel" type="Label" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/HBoxContainer" unique_id=564645258]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
show_behind_parent = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 0
|
||||||
|
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||||
|
text = "No portrait to preview."
|
||||||
|
text_overrun_behavior = 1
|
||||||
|
|
||||||
|
[node name="FitPreview_Toggle" type="Button" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/HBoxContainer" unique_id=388224784]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
tooltip_text = "Real scale"
|
||||||
|
focus_mode = 0
|
||||||
|
toggle_mode = true
|
||||||
|
button_pressed = true
|
||||||
|
flat = true
|
||||||
|
metadata/_edit_layout_mode = 1
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection" unique_id=1189439711]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
size_flags_stretch_ratio = 0.75
|
||||||
|
|
||||||
|
[node name="Hbox" type="HBoxContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox" unique_id=130485759]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="PortraitSettingsTitle" type="Label" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox/Hbox" unique_id=1706205105]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"DialogicSubTitle"
|
||||||
|
text = "Portrait Settings"
|
||||||
|
|
||||||
|
[node name="SwitchPortraitSettingsPosition" type="Button" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox/Hbox" unique_id=1532886824]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
modulate = Color(1, 1, 1, 0.647059)
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Switch position"
|
||||||
|
focus_mode = 0
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="Scroll" type="ScrollContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox" unique_id=1486898798]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
size_flags_stretch_ratio = 0.4
|
||||||
|
|
||||||
|
[node name="PortraitSettingsSection" type="VBoxContainer" parent="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox/Scroll" unique_id=1171723602]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
size_flags_stretch_ratio = 0.3
|
||||||
|
|
||||||
|
[node name="Spacer2" type="Control" parent="Scroll/VBox/MainHSplit/Split/RightSection2" unique_id=1167242753]
|
||||||
|
visible = false
|
||||||
|
custom_minimum_size = Vector2(0, 20)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="NoCharacterScreen" type="PanelContainer" parent="." unique_id=190821474]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="CenterContainer" type="CenterContainer" parent="NoCharacterScreen" unique_id=1885703718]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="NoCharacterScreen/CenterContainer" unique_id=135127017]
|
||||||
|
custom_minimum_size = Vector2(250, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="NoCharacterScreen/CenterContainer/VBoxContainer" unique_id=429783209]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "No character opened.
|
||||||
|
Create a character or double-click one in the file system dock."
|
||||||
|
horizontal_alignment = 1
|
||||||
|
autowrap_mode = 3
|
||||||
|
|
||||||
|
[node name="CreateCharacterButton" type="Button" parent="NoCharacterScreen/CenterContainer/VBoxContainer" unique_id=1496778365]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
text = "Create New Character"
|
||||||
|
icon = ExtResource("6_2ogmx")
|
||||||
|
|
||||||
|
[connection signal="pressed" from="Scroll/VBox/MainHSplit/MainSettingsPanel/MainSettings/HBoxContainer/CloseButton" to="." method="hide_main_settings"]
|
||||||
|
[connection signal="pressed" from="Scroll/VBox/MainHSplit/MainSettingsPanel/MainSettingsHidden/OpenButton" to="." method="show_main_settings"]
|
||||||
|
[connection signal="item_mouse_selected" from="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitTreePanel/PortraitTree" to="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitTreePanel/PortraitTree" method="_on_item_mouse_selected"]
|
||||||
|
[connection signal="index_pressed" from="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitTreePanel/PortraitTree/PortraitRightClickMenu" to="." method="_on_portrait_right_click_menu_index_pressed"]
|
||||||
|
[connection signal="pressed" from="Scroll/VBox/MainHSplit/Split/HBoxContainer/MarginContainer/PortraitListSection/Portraits/PortraitChangeInfo/ReferenceMangerButton" to="." method="_on_reference_manger_button_pressed"]
|
||||||
|
[connection signal="resized" from="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/FullPreviewAvailableRect" to="." method="_on_full_preview_available_rect_resized"]
|
||||||
|
[connection signal="toggled" from="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/PortraitPreviewSection/HBoxContainer/FitPreview_Toggle" to="." method="_on_fit_preview_toggle_toggled"]
|
||||||
|
[connection signal="pressed" from="Scroll/VBox/MainHSplit/Split/RightSection2/RightSection/VBox/Hbox/SwitchPortraitSettingsPosition" to="." method="_on_switch_portrait_settings_position_pressed"]
|
||||||
|
[connection signal="pressed" from="NoCharacterScreen/CenterContainer/VBoxContainer/CreateCharacterButton" to="." method="_on_create_character_button_pressed"]
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@tool
|
||||||
|
class_name DialogicCharacterEditorMainSection
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
## Base class for all character editor main sections. Methods should be overriden.
|
||||||
|
|
||||||
|
## Emit this, if something changed
|
||||||
|
@warning_ignore("unused_signal") # this is used by extending scripts
|
||||||
|
signal changed
|
||||||
|
|
||||||
|
## Reference to the character editor, set when instantiated
|
||||||
|
var character_editor: Control
|
||||||
|
|
||||||
|
## If not empty, a hint icon is added to the section title.
|
||||||
|
var hint_text := ""
|
||||||
|
|
||||||
|
|
||||||
|
## Overwrite to set the title of this section
|
||||||
|
func _get_title() -> String:
|
||||||
|
return "MainSection"
|
||||||
|
|
||||||
|
|
||||||
|
## Overwrite to set the visibility of the section title
|
||||||
|
func _show_title() -> bool:
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Overwrite to set whether this should initially be opened.
|
||||||
|
func _start_opened() -> bool:
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
## Overwrite to load all the information from the character into this section.
|
||||||
|
func _load_character(_resource:DialogicCharacter) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
## Overwrite to save all changes made in this section to the resource.
|
||||||
|
## In custom sections you will mostly likely save to the [resource.custom_info]
|
||||||
|
## dictionary.
|
||||||
|
func _save_changes(resource:DialogicCharacter) -> DialogicCharacter:
|
||||||
|
return resource
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://wltbab3qq63b
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
@tool
|
||||||
|
class_name DialogicCharacterEditorPortraitSection
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
## Base class for all portrait settings sections. Methods should be overriden.
|
||||||
|
## Changes made through fields in such a section should instantly be "saved"
|
||||||
|
## to the portrait_items metadata from where they will be saved to the resource.
|
||||||
|
|
||||||
|
## Emit this, if something changed
|
||||||
|
signal changed
|
||||||
|
## Emit this if the preview should reload
|
||||||
|
signal update_preview
|
||||||
|
|
||||||
|
## Reference to the character editor, set when instantiated
|
||||||
|
var character_editor: Control
|
||||||
|
## Reference to the selected portrait item.
|
||||||
|
## `selected_item.get_metadata(0)` can access the portraits data
|
||||||
|
var selected_item: TreeItem = null
|
||||||
|
|
||||||
|
## If not empty a hint icon is added to the section title
|
||||||
|
var hint_text := ""
|
||||||
|
|
||||||
|
|
||||||
|
## Overwrite to set the title of this section
|
||||||
|
func _get_title() -> String:
|
||||||
|
return "CustomSection"
|
||||||
|
|
||||||
|
|
||||||
|
## Overwrite to set the visibility of the section title
|
||||||
|
func _show_title() -> bool:
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Overwrite to set whether this should initially be opened.
|
||||||
|
func _start_opened() -> bool:
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
## Overwrite to load all the information from the character into this section.
|
||||||
|
func _load_portrait_data(data:Dictionary) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
## Overwrite to recheck visibility of your section and the content of your fields.
|
||||||
|
## This is called whenever the preview is updated so it allows reacting to major
|
||||||
|
## changes in other portrait sections.
|
||||||
|
func _recheck(data:Dictionary) -> void:
|
||||||
|
pass
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ckblurnjla80i
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
@tool
|
||||||
|
extends Tree
|
||||||
|
|
||||||
|
## Tree that displays the portrait list as a hirarchy
|
||||||
|
|
||||||
|
var editor := find_parent('Character Editor')
|
||||||
|
var current_group_nodes := {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if owner.get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
$PortraitRightClickMenu.set_item_icon(0, get_theme_icon('Rename', 'EditorIcons'))
|
||||||
|
$PortraitRightClickMenu.set_item_icon(1, get_theme_icon('Duplicate', 'EditorIcons'))
|
||||||
|
$PortraitRightClickMenu.set_item_icon(2, get_theme_icon('Remove', 'EditorIcons'))
|
||||||
|
$PortraitRightClickMenu.set_item_icon(4, get_theme_icon("Favorites", "EditorIcons"))
|
||||||
|
|
||||||
|
|
||||||
|
func clear_tree() -> void:
|
||||||
|
clear()
|
||||||
|
update_left_item_margin(false)
|
||||||
|
current_group_nodes = {}
|
||||||
|
|
||||||
|
|
||||||
|
func add_portrait_item(portrait_name: String, portrait_data: Dictionary, parent_item: TreeItem, previous_name := "") -> TreeItem:
|
||||||
|
var item: TreeItem = %PortraitTree.create_item(parent_item)
|
||||||
|
item.set_text(0, portrait_name)
|
||||||
|
item.set_metadata(0, portrait_data)
|
||||||
|
if previous_name.is_empty():
|
||||||
|
item.set_meta('previous_name', get_full_item_name(item))
|
||||||
|
else:
|
||||||
|
item.set_meta('previous_name', previous_name)
|
||||||
|
if portrait_name == editor.current_resource.default_portrait:
|
||||||
|
item.add_button(0, get_theme_icon('Favorites', 'EditorIcons'), 2, true, 'Default')
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
func add_portrait_group(goup_name := "Group", parent_item: TreeItem = get_root(), previous_name := "") -> TreeItem:
|
||||||
|
var item: TreeItem = %PortraitTree.create_item(parent_item)
|
||||||
|
item.set_icon(0, get_theme_icon("Folder", "EditorIcons"))
|
||||||
|
item.set_text(0, goup_name)
|
||||||
|
item.set_metadata(0, {'group':true})
|
||||||
|
if previous_name.is_empty():
|
||||||
|
item.set_meta('previous_name', get_full_item_name(item))
|
||||||
|
else:
|
||||||
|
item.set_meta('previous_name', previous_name)
|
||||||
|
update_left_item_margin(true)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
func get_full_item_name(item: TreeItem) -> String:
|
||||||
|
var item_name := item.get_text(0)
|
||||||
|
while item.get_parent() != get_root() and item != get_root():
|
||||||
|
item_name = item.get_parent().get_text(0)+"/"+item_name
|
||||||
|
item = item.get_parent()
|
||||||
|
return item_name
|
||||||
|
|
||||||
|
|
||||||
|
## Will create all not yet existing folders in the given path.
|
||||||
|
## Returns the last folder (the parent of the portrait item of this path).
|
||||||
|
func create_necessary_group_items(path: String) -> TreeItem:
|
||||||
|
var last_item := get_root()
|
||||||
|
var item_path := ""
|
||||||
|
|
||||||
|
for i in Array(path.split('/')).slice(0, -1):
|
||||||
|
item_path += "/"+i
|
||||||
|
item_path = item_path.trim_prefix('/')
|
||||||
|
if current_group_nodes.has(item_path+"/"+i):
|
||||||
|
last_item = current_group_nodes[item_path+"/"+i]
|
||||||
|
else:
|
||||||
|
var new_item: TreeItem = add_portrait_group(i, last_item)
|
||||||
|
current_group_nodes[item_path+"/"+i] = new_item
|
||||||
|
last_item = new_item
|
||||||
|
return last_item
|
||||||
|
|
||||||
|
|
||||||
|
func _on_item_mouse_selected(pos: Vector2, mouse_button_index: int) -> void:
|
||||||
|
if mouse_button_index == MOUSE_BUTTON_RIGHT:
|
||||||
|
$PortraitRightClickMenu.set_item_disabled(1, get_selected().get_metadata(0).has('group'))
|
||||||
|
$PortraitRightClickMenu.popup_on_parent(Rect2(get_global_mouse_position(),Vector2()))
|
||||||
|
|
||||||
|
|
||||||
|
func update_left_item_margin(margin_on:bool) -> void:
|
||||||
|
if not margin_on:
|
||||||
|
add_theme_constant_override("item_margin", 0)
|
||||||
|
else:
|
||||||
|
remove_theme_constant_override("item_margin")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#region DRAG AND DROP
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func _get_drag_data(at_position: Vector2) -> Variant:
|
||||||
|
var drag_item := get_item_at_position(at_position)
|
||||||
|
if not drag_item:
|
||||||
|
return null
|
||||||
|
|
||||||
|
drop_mode_flags = DROP_MODE_INBETWEEN
|
||||||
|
var preview := Label.new()
|
||||||
|
preview.text = " "+drag_item.get_text(0)
|
||||||
|
preview.add_theme_stylebox_override('normal', get_theme_stylebox("Background", "EditorStyles"))
|
||||||
|
set_drag_preview(preview)
|
||||||
|
|
||||||
|
return drag_item
|
||||||
|
|
||||||
|
|
||||||
|
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
|
||||||
|
if typeof(data) == TYPE_DICTIONARY and 'files' in data.keys():
|
||||||
|
return true
|
||||||
|
return data is TreeItem
|
||||||
|
|
||||||
|
|
||||||
|
func _drop_data(at_position: Vector2, item: Variant) -> void:
|
||||||
|
if item is Dictionary:
|
||||||
|
owner.import_portraits_from_file_list(item.files)
|
||||||
|
return
|
||||||
|
|
||||||
|
var to_item := get_item_at_position(at_position)
|
||||||
|
if to_item:
|
||||||
|
var test_item := to_item
|
||||||
|
while true:
|
||||||
|
if test_item == item:
|
||||||
|
return
|
||||||
|
test_item = test_item.get_parent()
|
||||||
|
if test_item == get_root():
|
||||||
|
break
|
||||||
|
|
||||||
|
var drop_section := get_drop_section_at_position(at_position)
|
||||||
|
var parent := get_root()
|
||||||
|
if to_item:
|
||||||
|
parent = to_item.get_parent()
|
||||||
|
|
||||||
|
if to_item and to_item.get_metadata(0).has('group') and drop_section == 1:
|
||||||
|
parent = to_item
|
||||||
|
|
||||||
|
var new_item := copy_branch_or_item(item, parent)
|
||||||
|
|
||||||
|
if to_item and !to_item.get_metadata(0).has('group') and drop_section == 1:
|
||||||
|
new_item.move_after(to_item)
|
||||||
|
|
||||||
|
if drop_section == -1:
|
||||||
|
new_item.move_before(to_item)
|
||||||
|
|
||||||
|
editor.report_name_change(new_item)
|
||||||
|
|
||||||
|
item.free()
|
||||||
|
|
||||||
|
|
||||||
|
func copy_branch_or_item(item: TreeItem, new_parent: TreeItem) -> TreeItem:
|
||||||
|
var new_item: TreeItem = null
|
||||||
|
if item.get_metadata(0).has('group'):
|
||||||
|
new_item = add_portrait_group(item.get_text(0), new_parent, item.get_meta('previous_name'))
|
||||||
|
else:
|
||||||
|
new_item = add_portrait_item(item.get_text(0), item.get_metadata(0), new_parent, item.get_meta('previous_name'))
|
||||||
|
|
||||||
|
for child in item.get_children():
|
||||||
|
copy_branch_or_item(child, new_item)
|
||||||
|
return new_item
|
||||||
|
|
||||||
|
#endregion
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://deliic6d8vajo
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
@tool
|
||||||
|
class_name DialogicCharacterPrefixSuffixSection
|
||||||
|
extends DialogicCharacterEditorMainSection
|
||||||
|
## Character Editor Section for setting the prefix and suffix of a character.
|
||||||
|
##
|
||||||
|
## loads and sets the prefix and suffix of a character.
|
||||||
|
## Provides [const PREFIX_CUSTOM_KEY] and [const SUFFIX_CUSTOM_KEY] to
|
||||||
|
## access the `custom_info` dictionary of the [class DialogicCharacter].
|
||||||
|
|
||||||
|
@export var prefix_input: LineEdit
|
||||||
|
@export var suffix_input: LineEdit
|
||||||
|
|
||||||
|
## We won't force any prefixes or suffixes onto the player,
|
||||||
|
## to ensure their games are working as previously when updating.
|
||||||
|
const DEFAULT_PREFIX = ""
|
||||||
|
const DEFAULT_SUFFIX = ""
|
||||||
|
|
||||||
|
## `custom_info` dictionary keys for the prefix.
|
||||||
|
const PREFIX_CUSTOM_KEY = "prefix"
|
||||||
|
|
||||||
|
## `custom_info` dictionary keys for the prefix.
|
||||||
|
const SUFFIX_CUSTOM_KEY = "suffix"
|
||||||
|
|
||||||
|
var suffix := ""
|
||||||
|
var prefix := ""
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
suffix_input.text_changed.connect(_suffix_changed)
|
||||||
|
prefix_input.text_changed.connect(_prefix_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func _suffix_changed(text: String) -> void:
|
||||||
|
suffix = text
|
||||||
|
|
||||||
|
|
||||||
|
func _prefix_changed(text: String) -> void:
|
||||||
|
prefix = text
|
||||||
|
|
||||||
|
|
||||||
|
func _get_title() -> String:
|
||||||
|
return "Character Prefix & Suffix"
|
||||||
|
|
||||||
|
|
||||||
|
func _show_title() -> bool:
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _start_opened() -> bool:
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _load_portrait_data(portrait_data: Dictionary) -> void:
|
||||||
|
_load_prefix_data(portrait_data)
|
||||||
|
|
||||||
|
|
||||||
|
## We load the prefix and suffix from the character's `custom_info` dictionary.
|
||||||
|
func _load_character(resource: DialogicCharacter) -> void:
|
||||||
|
_load_prefix_data(resource.custom_info)
|
||||||
|
|
||||||
|
|
||||||
|
func _load_prefix_data(data: Dictionary) -> void:
|
||||||
|
suffix = data.get(SUFFIX_CUSTOM_KEY, DEFAULT_SUFFIX)
|
||||||
|
prefix = data.get(PREFIX_CUSTOM_KEY, DEFAULT_PREFIX)
|
||||||
|
|
||||||
|
suffix_input.text = suffix
|
||||||
|
prefix_input.text = prefix
|
||||||
|
|
||||||
|
|
||||||
|
## Whenever the user makes a save to the character, we save the prefix and suffix.
|
||||||
|
func _save_changes(character: DialogicCharacter) -> DialogicCharacter:
|
||||||
|
if not character:
|
||||||
|
printerr("[Dialogic] Unable to save Prefix and Suffix, the character is missing.")
|
||||||
|
return character
|
||||||
|
|
||||||
|
character.custom_info[PREFIX_CUSTOM_KEY] = prefix
|
||||||
|
character.custom_info[SUFFIX_CUSTOM_KEY] = suffix
|
||||||
|
|
||||||
|
return character
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://i1ujoar8jf80
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://1ctcs6ywjjtd"]
|
||||||
|
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="1_o3alv"]
|
||||||
|
[ext_resource type="Script" uid="uid://i1ujoar8jf80" path="res://addons/dialogic/Editor/CharacterEditor/character_prefix_suffix.gd" id="1_tkxff"]
|
||||||
|
|
||||||
|
[node name="CharacterPrefixSuffix" type="GridContainer" node_paths=PackedStringArray("prefix_input", "suffix_input")]
|
||||||
|
offset_right = 121.0
|
||||||
|
offset_bottom = 66.0
|
||||||
|
columns = 2
|
||||||
|
script = ExtResource("1_tkxff")
|
||||||
|
prefix_input = NodePath("PrefixInput")
|
||||||
|
suffix_input = NodePath("SuffixInput")
|
||||||
|
|
||||||
|
[node name="Prefix" type="HBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Prefix"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Prefix"
|
||||||
|
|
||||||
|
[node name="HintTooltip" parent="Prefix" instance=ExtResource("1_o3alv")]
|
||||||
|
layout_mode = 2
|
||||||
|
texture = null
|
||||||
|
hint_text = "If a character speaks, this appears before their text.
|
||||||
|
Example: Color Tags or Quotation Marks."
|
||||||
|
|
||||||
|
[node name="PrefixInput" type="LineEdit" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
caret_blink = true
|
||||||
|
|
||||||
|
[node name="Suffix" type="HBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Suffix"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Suffix"
|
||||||
|
|
||||||
|
[node name="HintTooltip" parent="Suffix" instance=ExtResource("1_o3alv")]
|
||||||
|
layout_mode = 2
|
||||||
|
texture = null
|
||||||
|
hint_text = "If a character speaks, this appears after their text.
|
||||||
|
Example: Color Tags or Quotation Marks."
|
||||||
|
|
||||||
|
[node name="SuffixInput" type="LineEdit" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
caret_blink = true
|
||||||
126
addons/dialogic/Editor/CharacterEditor/portrait_scene_browser.gd
Normal file
126
addons/dialogic/Editor/CharacterEditor/portrait_scene_browser.gd
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
@tool
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
var ListItem := load("res://addons/dialogic/Editor/Common/BrowserItem.tscn")
|
||||||
|
|
||||||
|
enum Types {ALL, GENERAL, PRESET}
|
||||||
|
var current_type := Types.ALL
|
||||||
|
var current_info := {}
|
||||||
|
|
||||||
|
var portrait_scenes_info := {}
|
||||||
|
|
||||||
|
signal activate_part(part_info:Dictionary)
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
collect_portrait_scenes()
|
||||||
|
|
||||||
|
%Search.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||||
|
%CloseButton.icon = get_theme_icon("Close", "EditorIcons")
|
||||||
|
|
||||||
|
get_parent().close_requested.connect(_on_close_button_pressed)
|
||||||
|
get_parent().visibility_changed.connect(func():if get_parent().visible: open())
|
||||||
|
|
||||||
|
|
||||||
|
func collect_portrait_scenes() -> void:
|
||||||
|
for indexer in DialogicUtil.get_indexers():
|
||||||
|
for element in indexer._get_portrait_scene_presets():
|
||||||
|
portrait_scenes_info[element.get('path', '')] = element
|
||||||
|
|
||||||
|
|
||||||
|
func open() -> void:
|
||||||
|
collect_portrait_scenes()
|
||||||
|
load_parts()
|
||||||
|
|
||||||
|
|
||||||
|
func is_premade_portrait_scene(scene_path:String) -> bool:
|
||||||
|
return scene_path in portrait_scenes_info
|
||||||
|
|
||||||
|
|
||||||
|
func load_parts() -> void:
|
||||||
|
for i in %PartGrid.get_children():
|
||||||
|
i.queue_free()
|
||||||
|
|
||||||
|
%Search.placeholder_text = "Search for "
|
||||||
|
%Search.text = ""
|
||||||
|
match current_type:
|
||||||
|
Types.GENERAL: %Search.placeholder_text += "general portrait scenes"
|
||||||
|
Types.PRESET: %Search.placeholder_text += "portrait scene presets"
|
||||||
|
Types.ALL: %Search.placeholder_text += "general portrait scenes and presets"
|
||||||
|
|
||||||
|
for info in portrait_scenes_info.values():
|
||||||
|
var type: String = info.get('type', '_')
|
||||||
|
if (current_type == Types.GENERAL and type != "General") or (current_type == Types.PRESET and type != "Preset"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
var item: Node = ListItem.instantiate()
|
||||||
|
item.load_info(info)
|
||||||
|
%PartGrid.add_child(item)
|
||||||
|
item.set_meta('info', info)
|
||||||
|
item.clicked.connect(_on_item_clicked.bind(item, info))
|
||||||
|
item.focused.connect(_on_item_clicked.bind(item, info))
|
||||||
|
item.double_clicked.connect(emit_signal.bind('activate_part', info))
|
||||||
|
|
||||||
|
await get_tree().process_frame
|
||||||
|
|
||||||
|
if %PartGrid.get_child_count() > 0:
|
||||||
|
%PartGrid.get_child(0).clicked.emit()
|
||||||
|
%PartGrid.get_child(0).grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_item_clicked(item: Node, info:Dictionary) -> void:
|
||||||
|
load_part_info(info)
|
||||||
|
|
||||||
|
|
||||||
|
func load_part_info(info:Dictionary) -> void:
|
||||||
|
current_info = info
|
||||||
|
%PartTitle.text = info.get('name', 'Unknown Part')
|
||||||
|
%PartAuthor.text = "by "+info.get('author', 'Anonymus')
|
||||||
|
%PartDescription.text = info.get('description', '')
|
||||||
|
|
||||||
|
if info.get('preview_image', null) and ResourceLoader.exists(info.preview_image[0]):
|
||||||
|
%PreviewImage.texture = load(info.preview_image[0])
|
||||||
|
%PreviewImage.show()
|
||||||
|
else:
|
||||||
|
%PreviewImage.hide()
|
||||||
|
|
||||||
|
match info.type:
|
||||||
|
"General":
|
||||||
|
%ActivateButton.text = "Use this scene"
|
||||||
|
%TypeDescription.text = "This is a general use scene, it can be used directly."
|
||||||
|
"Preset":
|
||||||
|
%ActivateButton.text = "Customize this scene"
|
||||||
|
%TypeDescription.text = "This is a preset you can use for a custom portrait scene. Dialogic will promt you to save a copy of this scene that you can then use and customize."
|
||||||
|
"Default":
|
||||||
|
%ActivateButton.text = "Use default scene"
|
||||||
|
%TypeDescription.text = ""
|
||||||
|
"Custom":
|
||||||
|
%ActivateButton.text = "Select a custom scene"
|
||||||
|
%TypeDescription.text = ""
|
||||||
|
|
||||||
|
if info.get("documentation", ""):
|
||||||
|
%DocumentationButton.show()
|
||||||
|
%DocumentationButton.uri = info.documentation
|
||||||
|
else:
|
||||||
|
%DocumentationButton.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_activate_button_pressed() -> void:
|
||||||
|
activate_part.emit(current_info)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_close_button_pressed() -> void:
|
||||||
|
get_parent().hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_search_text_changed(new_text: String) -> void:
|
||||||
|
for item in %PartGrid.get_children():
|
||||||
|
if new_text.is_empty():
|
||||||
|
item.show()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if new_text.to_lower() in item.get_meta('info').name.to_lower():
|
||||||
|
item.show()
|
||||||
|
continue
|
||||||
|
|
||||||
|
item.hide()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://iwv7qff6g0f0
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
[gd_scene load_steps=11 format=3 uid="uid://b1wn8r84uh11b"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://iwv7qff6g0f0" path="res://addons/dialogic/Editor/CharacterEditor/portrait_scene_browser.gd" id="1_an6nc"]
|
||||||
|
|
||||||
|
[sub_resource type="Gradient" id="Gradient_0o1u0"]
|
||||||
|
colors = PackedColorArray(0.100572, 0.303996, 0.476999, 1, 0.296448, 0.231485, 0.52887, 1)
|
||||||
|
|
||||||
|
[sub_resource type="GradientTexture2D" id="GradientTexture2D_gxpvv"]
|
||||||
|
gradient = SubResource("Gradient_0o1u0")
|
||||||
|
fill = 2
|
||||||
|
fill_from = Vector2(0.478632, 1)
|
||||||
|
fill_to = Vector2(0, 0)
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_we8bq"]
|
||||||
|
content_margin_left = 6.0
|
||||||
|
content_margin_top = 3.0
|
||||||
|
content_margin_right = 6.0
|
||||||
|
content_margin_bottom = 3.0
|
||||||
|
draw_center = false
|
||||||
|
border_width_left = 2
|
||||||
|
border_width_top = 2
|
||||||
|
border_width_right = 2
|
||||||
|
border_width_bottom = 2
|
||||||
|
border_color = Color(1, 1, 1, 0.615686)
|
||||||
|
corner_radius_top_left = 5
|
||||||
|
corner_radius_top_right = 5
|
||||||
|
corner_radius_bottom_right = 5
|
||||||
|
corner_radius_bottom_left = 5
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3x0xw"]
|
||||||
|
content_margin_left = 6.0
|
||||||
|
content_margin_top = 3.0
|
||||||
|
content_margin_right = 6.0
|
||||||
|
content_margin_bottom = 3.0
|
||||||
|
draw_center = false
|
||||||
|
border_width_left = 3
|
||||||
|
border_width_top = 3
|
||||||
|
border_width_right = 3
|
||||||
|
border_width_bottom = 3
|
||||||
|
border_color = Color(1, 1, 1, 1)
|
||||||
|
corner_radius_top_left = 5
|
||||||
|
corner_radius_top_right = 5
|
||||||
|
corner_radius_bottom_right = 5
|
||||||
|
corner_radius_bottom_left = 5
|
||||||
|
expand_margin_left = 2.0
|
||||||
|
expand_margin_top = 2.0
|
||||||
|
expand_margin_right = 2.0
|
||||||
|
expand_margin_bottom = 2.0
|
||||||
|
|
||||||
|
[sub_resource type="Image" id="Image_h0nfr"]
|
||||||
|
data = {
|
||||||
|
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||||
|
"format": "RGBA8",
|
||||||
|
"height": 16,
|
||||||
|
"mipmaps": false,
|
||||||
|
"width": 16
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="ImageTexture" id="ImageTexture_d2gam"]
|
||||||
|
image = SubResource("Image_h0nfr")
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lf1ht"]
|
||||||
|
bg_color = Color(0.0588235, 0.0313726, 0.0980392, 1)
|
||||||
|
border_width_left = 5
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_a5iyu"]
|
||||||
|
bg_color = Color(1, 1, 1, 1)
|
||||||
|
draw_center = false
|
||||||
|
corner_radius_top_left = 10
|
||||||
|
corner_radius_top_right = 10
|
||||||
|
corner_radius_bottom_right = 10
|
||||||
|
corner_radius_bottom_left = 10
|
||||||
|
shadow_color = Color(0.992157, 0.992157, 0.992157, 0.101961)
|
||||||
|
shadow_size = 10
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_htwsp"]
|
||||||
|
bg_color = Color(1, 1, 1, 1)
|
||||||
|
corner_radius_top_left = 10
|
||||||
|
corner_radius_top_right = 10
|
||||||
|
corner_radius_bottom_right = 10
|
||||||
|
corner_radius_bottom_left = 10
|
||||||
|
|
||||||
|
[node name="PortraitSceneBrowser" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_an6nc")
|
||||||
|
|
||||||
|
[node name="BGColor" type="TextureRect" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
texture = SubResource("GradientTexture2D_gxpvv")
|
||||||
|
|
||||||
|
[node name="HSplitContainer" type="HSplitContainer" parent="."]
|
||||||
|
clip_contents = true
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Margin" type="MarginContainer" parent="HSplitContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_stretch_ratio = 1.5
|
||||||
|
theme_override_constants/margin_left = 10
|
||||||
|
theme_override_constants/margin_top = 10
|
||||||
|
theme_override_constants/margin_right = 10
|
||||||
|
theme_override_constants/margin_bottom = 10
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="HSplitContainer/Margin"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="BrowserTitle" type="Label" parent="HSplitContainer/Margin/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"DialogicSubTitle"
|
||||||
|
theme_override_font_sizes/font_size = 25
|
||||||
|
text = "Dialogic Portrait Scene Browser"
|
||||||
|
|
||||||
|
[node name="HBox" type="HBoxContainer" parent="HSplitContainer/Margin/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Search" type="LineEdit" parent="HSplitContainer/Margin/VBox/HBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_we8bq")
|
||||||
|
theme_override_styles/focus = SubResource("StyleBoxFlat_3x0xw")
|
||||||
|
placeholder_text = "Search"
|
||||||
|
right_icon = SubResource("ImageTexture_d2gam")
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="HSplitContainer/Margin/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="PartGrid" type="HFlowContainer" parent="HSplitContainer/Margin/VBox/ScrollContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Buttons" type="HBoxContainer" parent="HSplitContainer/Margin/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="CloseButton" type="Button" parent="HSplitContainer/Margin/VBox/Buttons"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Close"
|
||||||
|
icon = SubResource("ImageTexture_d2gam")
|
||||||
|
|
||||||
|
[node name="PanelContainer" type="PanelContainer" parent="HSplitContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_lf1ht")
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="HSplitContainer/PanelContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Panel" type="Panel" parent="HSplitContainer/PanelContainer/Control"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 9
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -4.0
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 71.0
|
||||||
|
grow_vertical = 2
|
||||||
|
rotation = 0.0349066
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_lf1ht")
|
||||||
|
|
||||||
|
[node name="MarginContainer" type="MarginContainer" parent="HSplitContainer/PanelContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/margin_left = 5
|
||||||
|
theme_override_constants/margin_top = 10
|
||||||
|
theme_override_constants/margin_right = 10
|
||||||
|
theme_override_constants/margin_bottom = 10
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="HSplitContainer/PanelContainer/MarginContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="Panel" type="PanelContainer" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_a5iyu")
|
||||||
|
|
||||||
|
[node name="Panel" type="PanelContainer" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/Panel"]
|
||||||
|
clip_children = 1
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_htwsp")
|
||||||
|
|
||||||
|
[node name="PreviewImage" type="TextureRect" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/Panel/Panel"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
expand_mode = 5
|
||||||
|
stretch_mode = 6
|
||||||
|
|
||||||
|
[node name="HFlowContainer" type="HFlowContainer" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="PartTitle" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/HFlowContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 8
|
||||||
|
theme_type_variation = &"DialogicTitle"
|
||||||
|
text = "Cool Style Part"
|
||||||
|
|
||||||
|
[node name="PartAuthor" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/HFlowContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 8
|
||||||
|
theme_type_variation = &"DialogicHintText"
|
||||||
|
text = "by Jowan"
|
||||||
|
|
||||||
|
[node name="PartType" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox/HFlowContainer"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 8
|
||||||
|
theme_type_variation = &"DialogicHintText"
|
||||||
|
text = "a style"
|
||||||
|
|
||||||
|
[node name="PartDescription" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"DialogicHintText2"
|
||||||
|
text = "A cool textbox layer"
|
||||||
|
autowrap_mode = 3
|
||||||
|
|
||||||
|
[node name="DocumentationButton" type="LinkButton" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Learn more"
|
||||||
|
|
||||||
|
[node name="HSeparator" type="HSeparator" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ActivateButton" type="Button" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Use"
|
||||||
|
|
||||||
|
[node name="TypeDescription" type="Label" parent="HSplitContainer/PanelContainer/MarginContainer/VBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"DialogicHintText"
|
||||||
|
text = "A cool textbox layer"
|
||||||
|
autowrap_mode = 3
|
||||||
|
|
||||||
|
[connection signal="text_changed" from="HSplitContainer/Margin/VBox/HBox/Search" to="." method="_on_search_text_changed"]
|
||||||
|
[connection signal="pressed" from="HSplitContainer/Margin/VBox/Buttons/CloseButton" to="." method="_on_close_button_pressed"]
|
||||||
|
[connection signal="pressed" from="HSplitContainer/PanelContainer/MarginContainer/VBox/ActivateButton" to="." method="_on_activate_button_pressed"]
|
||||||
86
addons/dialogic/Editor/Common/BrowserItem.gd
Normal file
86
addons/dialogic/Editor/Common/BrowserItem.gd
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
@tool
|
||||||
|
extends Container
|
||||||
|
|
||||||
|
signal clicked
|
||||||
|
signal middle_clicked
|
||||||
|
signal double_clicked
|
||||||
|
signal focused
|
||||||
|
|
||||||
|
var base_size := 1
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
|
||||||
|
%Name.add_theme_font_override("font", get_theme_font("bold", "EditorFonts"))
|
||||||
|
custom_minimum_size = base_size * Vector2(200, 150) * DialogicUtil.get_editor_scale()
|
||||||
|
%CurrentIcon.texture = get_theme_icon("Favorites", "EditorIcons")
|
||||||
|
if %Image.texture == null:
|
||||||
|
%Image.texture = get_theme_icon("ImportFail", "EditorIcons")
|
||||||
|
%Image.stretch_mode = TextureRect.STRETCH_KEEP_CENTERED
|
||||||
|
|
||||||
|
|
||||||
|
func load_info(info:Dictionary) -> void:
|
||||||
|
%Name.text = info.name
|
||||||
|
if not info.has("preview_image"):
|
||||||
|
pass
|
||||||
|
elif info.preview_image[0] == 'custom':
|
||||||
|
await ready
|
||||||
|
%Image.texture = get_theme_icon("CreateNewSceneFrom", "EditorIcons")
|
||||||
|
%Image.stretch_mode = TextureRect.STRETCH_KEEP_CENTERED
|
||||||
|
%Panel.self_modulate = get_theme_color("property_color_z", "Editor")
|
||||||
|
elif info.preview_image[0].ends_with('scn'):
|
||||||
|
EditorInterface.get_resource_previewer().queue_resource_preview(info.preview_image[0], self, 'set_scene_preview', null)
|
||||||
|
elif ResourceLoader.exists(info.preview_image[0]):
|
||||||
|
%Image.texture = load(info.preview_image[0])
|
||||||
|
elif info.preview_image[0].is_valid_html_color():
|
||||||
|
%Image.texture = null
|
||||||
|
%Panel.self_modulate = Color(info.preview_image[0])
|
||||||
|
|
||||||
|
if ResourceLoader.exists(info.get('icon', '')):
|
||||||
|
%Icon.get_parent().show()
|
||||||
|
%Icon.texture = load(info.get('icon'))
|
||||||
|
else:
|
||||||
|
%Icon.get_parent().hide()
|
||||||
|
|
||||||
|
tooltip_text = info.description
|
||||||
|
|
||||||
|
|
||||||
|
func set_scene_preview(path:String, preview:Texture2D, thumbnail:Texture2D, userdata:Variant) -> void:
|
||||||
|
if preview:
|
||||||
|
%Image.texture = preview
|
||||||
|
else:
|
||||||
|
%Image.texture = get_theme_icon("PackedScene", "EditorIcons")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func set_current(current:bool):
|
||||||
|
%CurrentIcon.visible = current
|
||||||
|
|
||||||
|
|
||||||
|
func _on_mouse_entered() -> void:
|
||||||
|
%HoverBG.show()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_mouse_exited() -> void:
|
||||||
|
%HoverBG.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_gui_input(event):
|
||||||
|
if event.is_action_pressed('ui_accept') or event.is_action_pressed("ui_select") or (
|
||||||
|
event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT):
|
||||||
|
clicked.emit()
|
||||||
|
if not event is InputEventMouseButton or event.double_click:
|
||||||
|
double_clicked.emit()
|
||||||
|
elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_MIDDLE:
|
||||||
|
middle_clicked.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_focus_entered() -> void:
|
||||||
|
$FocusFG.show()
|
||||||
|
focused.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_focus_exited() -> void:
|
||||||
|
$FocusFG.hide()
|
||||||
1
addons/dialogic/Editor/Common/BrowserItem.gd.uid
Normal file
1
addons/dialogic/Editor/Common/BrowserItem.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ckthmmkodqqwt
|
||||||
154
addons/dialogic/Editor/Common/BrowserItem.tscn
Normal file
154
addons/dialogic/Editor/Common/BrowserItem.tscn
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
[gd_scene load_steps=6 format=3 uid="uid://ddlxjde1cx035"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://ckthmmkodqqwt" path="res://addons/dialogic/Editor/Common/BrowserItem.gd" id="1_s3kf0"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pfw08"]
|
||||||
|
bg_color = Color(1, 1, 1, 0.32549)
|
||||||
|
corner_radius_top_left = 10
|
||||||
|
corner_radius_top_right = 10
|
||||||
|
corner_radius_bottom_right = 10
|
||||||
|
corner_radius_bottom_left = 10
|
||||||
|
expand_margin_left = 4.0
|
||||||
|
expand_margin_top = 4.0
|
||||||
|
expand_margin_right = 4.0
|
||||||
|
expand_margin_bottom = 4.0
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ab24c"]
|
||||||
|
bg_color = Color(1, 1, 1, 1)
|
||||||
|
corner_radius_top_left = 10
|
||||||
|
corner_radius_top_right = 10
|
||||||
|
corner_radius_bottom_right = 10
|
||||||
|
corner_radius_bottom_left = 10
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qnehp"]
|
||||||
|
bg_color = Color(0, 0, 0, 1)
|
||||||
|
corner_radius_top_left = 10
|
||||||
|
corner_radius_top_right = 10
|
||||||
|
corner_radius_bottom_right = 10
|
||||||
|
corner_radius_bottom_left = 10
|
||||||
|
shadow_color = Color(0.847059, 0.847059, 0.847059, 0.384314)
|
||||||
|
shadow_size = 5
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nxx8t"]
|
||||||
|
bg_color = Color(0.435294, 0.435294, 0.435294, 0.211765)
|
||||||
|
draw_center = false
|
||||||
|
border_width_left = 2
|
||||||
|
border_width_top = 2
|
||||||
|
border_width_right = 2
|
||||||
|
border_width_bottom = 2
|
||||||
|
corner_radius_top_left = 10
|
||||||
|
corner_radius_top_right = 10
|
||||||
|
corner_radius_bottom_right = 10
|
||||||
|
corner_radius_bottom_left = 10
|
||||||
|
expand_margin_left = 4.0
|
||||||
|
expand_margin_top = 4.0
|
||||||
|
expand_margin_right = 4.0
|
||||||
|
expand_margin_bottom = 4.0
|
||||||
|
|
||||||
|
[node name="BrowserItem" type="MarginContainer"]
|
||||||
|
custom_minimum_size = Vector2(200, 150)
|
||||||
|
offset_left = 1.0
|
||||||
|
offset_top = 1.0
|
||||||
|
offset_right = 128.0
|
||||||
|
offset_bottom = 102.0
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
focus_mode = 2
|
||||||
|
theme_override_constants/margin_left = 4
|
||||||
|
theme_override_constants/margin_top = 4
|
||||||
|
theme_override_constants/margin_right = 4
|
||||||
|
theme_override_constants/margin_bottom = 4
|
||||||
|
script = ExtResource("1_s3kf0")
|
||||||
|
|
||||||
|
[node name="HoverBG" type="Panel" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_pfw08")
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
theme_override_constants/separation = 0
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="Panel" type="PanelContainer" parent="VBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
self_modulate = Color(0.0705882, 0.0705882, 0.0705882, 1)
|
||||||
|
clip_children = 2
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
mouse_filter = 2
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_ab24c")
|
||||||
|
|
||||||
|
[node name="Image" type="TextureRect" parent="VBox/Panel"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
mouse_filter = 2
|
||||||
|
expand_mode = 1
|
||||||
|
stretch_mode = 6
|
||||||
|
|
||||||
|
[node name="CurrentIcon" type="TextureRect" parent="VBox/Panel/Image"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 1
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = -22.0
|
||||||
|
offset_top = 5.0
|
||||||
|
offset_right = -6.0
|
||||||
|
offset_bottom = 21.0
|
||||||
|
grow_horizontal = 0
|
||||||
|
tooltip_text = "Currently in use"
|
||||||
|
stretch_mode = 2
|
||||||
|
|
||||||
|
[node name="Panel" type="Panel" parent="VBox/Panel/Image"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 3
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -37.0
|
||||||
|
offset_top = -36.0
|
||||||
|
offset_right = -7.0
|
||||||
|
offset_bottom = -6.0
|
||||||
|
grow_horizontal = 0
|
||||||
|
grow_vertical = 0
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_qnehp")
|
||||||
|
|
||||||
|
[node name="Icon" type="TextureRect" parent="VBox/Panel/Image/Panel"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 4.0
|
||||||
|
offset_top = 4.0
|
||||||
|
offset_right = -4.0
|
||||||
|
offset_bottom = -4.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
expand_mode = 1
|
||||||
|
stretch_mode = 5
|
||||||
|
|
||||||
|
[node name="Name" type="Label" parent="VBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Dialogic Theme"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="FocusFG" type="Panel" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_nxx8t")
|
||||||
|
|
||||||
|
[connection signal="focus_entered" from="." to="." method="_on_focus_entered"]
|
||||||
|
[connection signal="focus_exited" from="." to="." method="_on_focus_exited"]
|
||||||
|
[connection signal="gui_input" from="." to="." method="_on_gui_input"]
|
||||||
|
[connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"]
|
||||||
|
[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"]
|
||||||
47
addons/dialogic/Editor/Common/DCSS.gd
Normal file
47
addons/dialogic/Editor/Common/DCSS.gd
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
@tool
|
||||||
|
class_name DCSS
|
||||||
|
|
||||||
|
static func inline(style: Dictionary) -> StyleBoxFlat:
|
||||||
|
var scale: float = DialogicUtil.get_editor_scale()
|
||||||
|
var s := StyleBoxFlat.new()
|
||||||
|
for property in style.keys():
|
||||||
|
match property:
|
||||||
|
'border-left':
|
||||||
|
s.set('border_width_left', style[property] * scale)
|
||||||
|
'border-radius':
|
||||||
|
var radius: float = style[property] * scale
|
||||||
|
s.set('corner_radius_top_left', radius)
|
||||||
|
s.set('corner_radius_top_right', radius)
|
||||||
|
s.set('corner_radius_bottom_left', radius)
|
||||||
|
s.set('corner_radius_bottom_right', radius)
|
||||||
|
'background':
|
||||||
|
if typeof(style[property]) == TYPE_STRING and style[property] == "none":
|
||||||
|
s.set('draw_center', false)
|
||||||
|
else:
|
||||||
|
s.set('bg_color', style[property])
|
||||||
|
'border':
|
||||||
|
var width: float = style[property] * scale
|
||||||
|
s.set('border_width_left', width)
|
||||||
|
s.set('border_width_right', width)
|
||||||
|
s.set('border_width_top', width)
|
||||||
|
s.set('border_width_bottom', width)
|
||||||
|
'border-color':
|
||||||
|
s.set('border_color', style[property])
|
||||||
|
'padding':
|
||||||
|
var value_v: float = 0.0
|
||||||
|
var value_h: float = 0.0
|
||||||
|
if style[property] is int:
|
||||||
|
value_v = style[property] * scale
|
||||||
|
value_h = value_v
|
||||||
|
else:
|
||||||
|
value_v = style[property][0] * scale
|
||||||
|
value_h = style[property][1] * scale
|
||||||
|
s.set('content_margin_top', value_v)
|
||||||
|
s.set('content_margin_bottom', value_v)
|
||||||
|
s.set('content_margin_left', value_h)
|
||||||
|
s.set('content_margin_right', value_h)
|
||||||
|
'padding-right':
|
||||||
|
s.set('content_margin_right', style[property] * scale)
|
||||||
|
'padding-left':
|
||||||
|
s.set('content_margin_left', style[property] * scale)
|
||||||
|
return s
|
||||||
1
addons/dialogic/Editor/Common/DCSS.gd.uid
Normal file
1
addons/dialogic/Editor/Common/DCSS.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://m3ufop8ao16l
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
@tool
|
||||||
|
extends PanelContainer
|
||||||
|
|
||||||
|
|
||||||
|
enum Modes {EDIT, ADD}
|
||||||
|
|
||||||
|
var mode := Modes.EDIT
|
||||||
|
var item: TreeItem = null
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
hide()
|
||||||
|
%Character.resource_icon = load("res://addons/dialogic/Editor/Images/Resources/character.svg")
|
||||||
|
%Character.suggestions_func = get_character_suggestions
|
||||||
|
|
||||||
|
%WholeWords.icon = get_theme_icon("FontItem", "EditorIcons")
|
||||||
|
%MatchCase.icon = get_theme_icon("MatchCase", "EditorIcons")
|
||||||
|
|
||||||
|
func _on_add_pressed() -> void:
|
||||||
|
if visible:
|
||||||
|
if mode == Modes.ADD:
|
||||||
|
hide()
|
||||||
|
return
|
||||||
|
elif mode == Modes.EDIT:
|
||||||
|
save()
|
||||||
|
|
||||||
|
%AddButton.text = "Add"
|
||||||
|
mode = Modes.ADD
|
||||||
|
show()
|
||||||
|
%Type.selected = 0
|
||||||
|
_on_type_item_selected(0)
|
||||||
|
%Where.selected = 2
|
||||||
|
_on_where_item_selected(2)
|
||||||
|
%Old.text = ""
|
||||||
|
%New.text = ""
|
||||||
|
|
||||||
|
|
||||||
|
func open_existing(_item:TreeItem, info:Dictionary):
|
||||||
|
mode = Modes.EDIT
|
||||||
|
item = _item
|
||||||
|
show()
|
||||||
|
%AddButton.text = "Update"
|
||||||
|
%Type.selected = info.type
|
||||||
|
_on_type_item_selected(info.type)
|
||||||
|
if !info.character_names.is_empty():
|
||||||
|
%Where.selected = 1
|
||||||
|
%Character.set_value(info.character_names[0])
|
||||||
|
else:
|
||||||
|
%Where.selected = 0
|
||||||
|
_on_where_item_selected(%Where.selected)
|
||||||
|
|
||||||
|
%Old.text = info.what
|
||||||
|
%New.text = info.forwhat
|
||||||
|
|
||||||
|
%MatchCase.button_pressed = info.case_sensitive
|
||||||
|
%WholeWords.button_pressed = info.whole_words
|
||||||
|
|
||||||
|
func _on_type_item_selected(index:int) -> void:
|
||||||
|
match index:
|
||||||
|
0:
|
||||||
|
%Where.select(0)
|
||||||
|
%Where.set_item_disabled(0, false)
|
||||||
|
%Where.set_item_disabled(1, false)
|
||||||
|
%Where.set_item_disabled(2, true)
|
||||||
|
1:
|
||||||
|
%Where.select(0)
|
||||||
|
%Where.set_item_disabled(0, false)
|
||||||
|
%Where.set_item_disabled(1, false)
|
||||||
|
%Where.set_item_disabled(2, true)
|
||||||
|
2:
|
||||||
|
%Where.select(1)
|
||||||
|
%Where.set_item_disabled(0, true)
|
||||||
|
%Where.set_item_disabled(1, false)
|
||||||
|
%Where.set_item_disabled(2, true)
|
||||||
|
3,4:
|
||||||
|
%Where.select(0)
|
||||||
|
%Where.set_item_disabled(0, false)
|
||||||
|
%Where.set_item_disabled(1, true)
|
||||||
|
%Where.set_item_disabled(2, true)
|
||||||
|
%PureTextFlags.visible = index == 0
|
||||||
|
_on_where_item_selected(%Where.selected)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_where_item_selected(index:int) -> void:
|
||||||
|
%Character.visible = index == 1
|
||||||
|
|
||||||
|
|
||||||
|
func get_character_suggestions(search_text:String) -> Dictionary:
|
||||||
|
var suggestions := {}
|
||||||
|
|
||||||
|
#override the previous _character_directory with the meta, specifically for searching otherwise new nodes wont work
|
||||||
|
var _character_directory := DialogicResourceUtil.get_character_directory()
|
||||||
|
|
||||||
|
var icon := load("res://addons/dialogic/Editor/Images/Resources/character.svg")
|
||||||
|
suggestions['(No one)'] = {'value':null, 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
|
||||||
|
|
||||||
|
for resource in _character_directory.keys():
|
||||||
|
suggestions[resource] = {
|
||||||
|
'value' : resource,
|
||||||
|
'tooltip' : _character_directory[resource],
|
||||||
|
'icon' : icon.duplicate()}
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
|
func save() -> void:
|
||||||
|
if %Old.text.is_empty() or %New.text.is_empty():
|
||||||
|
return
|
||||||
|
if %Where.selected == 1 and %Character.current_value == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var previous := {}
|
||||||
|
if mode == Modes.EDIT:
|
||||||
|
previous = item.get_metadata(0)
|
||||||
|
item.get_parent()
|
||||||
|
item.free()
|
||||||
|
|
||||||
|
var ref_manager := find_parent('ReferenceManager')
|
||||||
|
var character_names := []
|
||||||
|
if %Character.current_value != null:
|
||||||
|
character_names = [%Character.current_value]
|
||||||
|
ref_manager.add_ref_change(%Old.text, %New.text, %Type.selected, %Where.selected, character_names, %WholeWords.button_pressed, %MatchCase.button_pressed, previous)
|
||||||
|
hide()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dca6a1a74jfur
|
||||||
8
addons/dialogic/Editor/Common/TitleBgStylebox.tres
Normal file
8
addons/dialogic/Editor/Common/TitleBgStylebox.tres
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[gd_resource type="StyleBoxFlat" format=3 uid="uid://dmsjhgv22dns8"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
content_margin_left = 5.0
|
||||||
|
content_margin_top = 5.0
|
||||||
|
content_margin_right = 5.0
|
||||||
|
content_margin_bottom = 5.0
|
||||||
|
bg_color = Color(0.545098, 0.545098, 0.545098, 0.211765)
|
||||||
391
addons/dialogic/Editor/Common/broken_reference_manager.gd
Normal file
391
addons/dialogic/Editor/Common/broken_reference_manager.gd
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
@tool
|
||||||
|
extends VSplitContainer
|
||||||
|
|
||||||
|
## This manager shows a list of changed references and allows searching for them and replacing them.
|
||||||
|
|
||||||
|
var reference_changes: Array[Dictionary] = []:
|
||||||
|
set(changes):
|
||||||
|
reference_changes = changes
|
||||||
|
update_indicator()
|
||||||
|
|
||||||
|
var search_regexes: Array[Array]
|
||||||
|
var finder_thread: Thread
|
||||||
|
var progress_mutex: Mutex
|
||||||
|
var progress_percent: float = 0.0
|
||||||
|
var progress_message: String = ""
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if owner.get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
|
||||||
|
%TabA.text = "Broken References"
|
||||||
|
%TabA.icon = get_theme_icon("Unlinked", "EditorIcons")
|
||||||
|
|
||||||
|
owner.get_parent().visibility_changed.connect(func(): if is_visible_in_tree(): open())
|
||||||
|
|
||||||
|
%ReplacementSection.hide()
|
||||||
|
|
||||||
|
%CheckButton.icon = get_theme_icon("Search", "EditorIcons")
|
||||||
|
%Replace.icon = get_theme_icon("ArrowRight", "EditorIcons")
|
||||||
|
|
||||||
|
%State.add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"))
|
||||||
|
visibility_changed.connect(func(): if !visible: close())
|
||||||
|
await get_parent().ready
|
||||||
|
|
||||||
|
var tab_button: Control = %TabA
|
||||||
|
var dot := Sprite2D.new()
|
||||||
|
dot.texture = get_theme_icon("GuiGraphNodePort", "EditorIcons")
|
||||||
|
dot.scale = Vector2(0.8, 0.8)
|
||||||
|
dot.z_index = 10
|
||||||
|
dot.position = Vector2(tab_button.size.x, tab_button.size.y*0.25)
|
||||||
|
dot.modulate = get_theme_color("warning_color", "Editor").lightened(0.5)
|
||||||
|
|
||||||
|
tab_button.add_child(dot)
|
||||||
|
update_indicator()
|
||||||
|
|
||||||
|
|
||||||
|
func open() -> void:
|
||||||
|
%ReplacementEditPanel.hide()
|
||||||
|
%ReplacementSection.hide()
|
||||||
|
%ChangeTree.clear()
|
||||||
|
%ChangeTree.create_item()
|
||||||
|
%ChangeTree.set_column_expand(0, false)
|
||||||
|
%ChangeTree.set_column_expand(2, false)
|
||||||
|
%ChangeTree.set_column_custom_minimum_width(2, 50)
|
||||||
|
var categories := {null:%ChangeTree.get_root()}
|
||||||
|
for i in reference_changes:
|
||||||
|
var parent: TreeItem = null
|
||||||
|
if !i.get('category', null) in categories:
|
||||||
|
parent = %ChangeTree.create_item()
|
||||||
|
parent.set_text(1, i.category)
|
||||||
|
parent.set_custom_color(1, get_theme_color("disabled_font_color", "Editor"))
|
||||||
|
categories[i.category] = parent
|
||||||
|
else:
|
||||||
|
parent = categories[i.get('category')]
|
||||||
|
|
||||||
|
var item: TreeItem = %ChangeTree.create_item(parent)
|
||||||
|
item.set_text(1, i.what+" -> "+i.forwhat)
|
||||||
|
item.add_button(1, get_theme_icon("Edit", "EditorIcons"), 1, false, 'Edit')
|
||||||
|
item.add_button(1, get_theme_icon("Remove", "EditorIcons"), 0, false, 'Remove Change from List')
|
||||||
|
item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
|
||||||
|
item.set_checked(0, true)
|
||||||
|
item.set_editable(0, true)
|
||||||
|
item.set_metadata(0, i)
|
||||||
|
%CheckButton.disabled = reference_changes.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_change_tree_button_clicked(item:TreeItem, column:int, id:int, mouse_button_index:int) -> void:
|
||||||
|
if id == 0:
|
||||||
|
reference_changes.erase(item.get_metadata(0))
|
||||||
|
if item.get_parent().get_child_count() == 1:
|
||||||
|
item.get_parent().free()
|
||||||
|
else:
|
||||||
|
item.free()
|
||||||
|
update_indicator()
|
||||||
|
%CheckButton.disabled = reference_changes.is_empty()
|
||||||
|
|
||||||
|
if id == 1:
|
||||||
|
%ReplacementEditPanel.open_existing(item, item.get_metadata(0))
|
||||||
|
|
||||||
|
%ReplacementSection.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_change_tree_item_edited() -> void:
|
||||||
|
if !%ChangeTree.get_selected():
|
||||||
|
return
|
||||||
|
%CheckButton.disabled = false
|
||||||
|
|
||||||
|
|
||||||
|
func _on_check_button_pressed() -> void:
|
||||||
|
var to_be_checked: Array[Dictionary]= []
|
||||||
|
var item: TreeItem = %ChangeTree.get_root()
|
||||||
|
while item.get_next_visible():
|
||||||
|
item = item.get_next_visible()
|
||||||
|
|
||||||
|
if item.get_child_count():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.is_checked(0):
|
||||||
|
to_be_checked.append(item.get_metadata(0))
|
||||||
|
to_be_checked[-1]['item'] = item
|
||||||
|
to_be_checked[-1]['count'] = 0
|
||||||
|
|
||||||
|
open_finder(to_be_checked)
|
||||||
|
%CheckButton.disabled = true
|
||||||
|
|
||||||
|
|
||||||
|
func open_finder(replacements:Array[Dictionary]) -> void:
|
||||||
|
%ReplacementSection.show()
|
||||||
|
%Progress.show()
|
||||||
|
%ReferenceTree.hide()
|
||||||
|
|
||||||
|
search_regexes = []
|
||||||
|
for i in replacements:
|
||||||
|
if i.has('character_names') and !i.character_names.is_empty():
|
||||||
|
i['character_regex'] = RegEx.create_from_string("(?m)^(join|update|leave)?\\s*("+str(i.character_names).replace('"', '').replace(', ', '|').trim_suffix(']').trim_prefix('[').replace('/', '\\/')+")(?(1).*|.*:)")
|
||||||
|
|
||||||
|
for regex_string in i.regex:
|
||||||
|
var regex := RegEx.create_from_string(regex_string)
|
||||||
|
search_regexes.append([regex, i])
|
||||||
|
|
||||||
|
finder_thread = Thread.new()
|
||||||
|
progress_mutex = Mutex.new()
|
||||||
|
finder_thread.start(search_timelines.bind(search_regexes))
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if finder_thread and finder_thread.is_started():
|
||||||
|
if finder_thread.is_alive():
|
||||||
|
progress_mutex.lock()
|
||||||
|
%State.text = progress_message
|
||||||
|
%Progress.value = progress_percent
|
||||||
|
progress_mutex.unlock()
|
||||||
|
else:
|
||||||
|
var finds: Variant = finder_thread.wait_to_finish()
|
||||||
|
display_search_results(finds)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func display_search_results(finds:Array[Dictionary]) -> void:
|
||||||
|
%Progress.hide()
|
||||||
|
%ReferenceTree.show()
|
||||||
|
for regex_info in search_regexes:
|
||||||
|
regex_info[1]['item'].set_text(2, str(regex_info[1]['count']))
|
||||||
|
|
||||||
|
update_count_coloring()
|
||||||
|
%State.text = str(len(finds))+ " occurrences found"
|
||||||
|
|
||||||
|
%ReferenceTree.clear()
|
||||||
|
%ReferenceTree.set_column_expand(0, false)
|
||||||
|
%ReferenceTree.set_column_expand(1, false)
|
||||||
|
%ReferenceTree.set_column_custom_minimum_width(1, 50)
|
||||||
|
%ReferenceTree.create_item()
|
||||||
|
|
||||||
|
var timelines := {}
|
||||||
|
var height := 0
|
||||||
|
for i in finds:
|
||||||
|
var parent: TreeItem = null
|
||||||
|
if !i.timeline in timelines:
|
||||||
|
parent = %ReferenceTree.create_item()
|
||||||
|
parent.set_text(0, i.timeline)
|
||||||
|
parent.set_custom_color(0, get_theme_color("disabled_font_color", "Editor"))
|
||||||
|
parent.set_expand_right(0, true)
|
||||||
|
timelines[i.timeline] = parent
|
||||||
|
height += %ReferenceTree.get_item_area_rect(parent).size.y+10
|
||||||
|
else:
|
||||||
|
parent = timelines[i.timeline]
|
||||||
|
|
||||||
|
var item: TreeItem = %ReferenceTree.create_item(parent)
|
||||||
|
item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
|
||||||
|
item.set_checked(0, true)
|
||||||
|
item.set_editable(0, true)
|
||||||
|
item.set_metadata(0, i)
|
||||||
|
item.set_text(1, str(i.line_number)+':')
|
||||||
|
item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
|
||||||
|
item.set_cell_mode(2, TreeItem.CELL_MODE_CUSTOM)
|
||||||
|
item.set_text(2, i.line)
|
||||||
|
item.set_tooltip_text(2, i.info.what+' -> '+i.info.forwhat)
|
||||||
|
item.set_custom_draw_callback(2, _custom_draw)
|
||||||
|
height += %ReferenceTree.get_item_area_rect(item).size.y+10
|
||||||
|
var change_item: TreeItem = i.info.item
|
||||||
|
change_item.set_meta('found_items', change_item.get_meta('found_items', [])+[item])
|
||||||
|
|
||||||
|
%ReferenceTree.custom_minimum_size.y = min(height, 200)
|
||||||
|
|
||||||
|
%ReferenceTree.visible = !finds.is_empty()
|
||||||
|
%Replace.disabled = finds.is_empty()
|
||||||
|
if finds.is_empty():
|
||||||
|
%State.text = "Nothing found"
|
||||||
|
else:
|
||||||
|
%Replace.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
## Highlights the found text in the result tree
|
||||||
|
## Inspired by how godot highlights stuff in its search results
|
||||||
|
func _custom_draw(item:TreeItem, rect:Rect2) -> void:
|
||||||
|
var text := item.get_text(2)
|
||||||
|
var find: Dictionary = item.get_metadata(0)
|
||||||
|
|
||||||
|
var font: Font = %ReferenceTree.get_theme_font("font")
|
||||||
|
var font_size: int = %ReferenceTree.get_theme_font_size("font_size")
|
||||||
|
|
||||||
|
var match_rect := rect
|
||||||
|
var beginning_index: int = find.match.get_start("replace")-find.line_start-1
|
||||||
|
match_rect.position.x += font.get_string_size(text.left(beginning_index), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x -1
|
||||||
|
match_rect.size.x = font.get_string_size(find.info.what, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x + 1
|
||||||
|
match_rect.position.y += 1 * DialogicUtil.get_editor_scale()
|
||||||
|
match_rect.size.y -= 2 * DialogicUtil.get_editor_scale()
|
||||||
|
match_rect.position.x += 4
|
||||||
|
|
||||||
|
%ReferenceTree.draw_rect(match_rect, get_theme_color("highlight_color", "Editor"), true)
|
||||||
|
%ReferenceTree.draw_rect(match_rect, get_theme_color("box_selection_stroke_color", "Editor"), false)
|
||||||
|
|
||||||
|
|
||||||
|
func search_timelines(regexes:Array[Array]) -> Array[Dictionary]:
|
||||||
|
var finds: Array[Dictionary] = []
|
||||||
|
|
||||||
|
var timeline_paths := DialogicResourceUtil.list_resources_of_type('.dtl')
|
||||||
|
|
||||||
|
var progress := 0
|
||||||
|
var progress_max: float = len(timeline_paths)*len(regexes)
|
||||||
|
|
||||||
|
for timeline_path:String in timeline_paths:
|
||||||
|
|
||||||
|
var timeline_file := FileAccess.open(timeline_path, FileAccess.READ)
|
||||||
|
var timeline_text: String = timeline_file.get_as_text()
|
||||||
|
var timeline_event: PackedStringArray = timeline_text.split('\n')
|
||||||
|
timeline_file.close()
|
||||||
|
|
||||||
|
for regex_info in regexes:
|
||||||
|
progress += 1
|
||||||
|
progress_mutex.lock()
|
||||||
|
progress_percent = 1/progress_max*progress
|
||||||
|
progress_message = "Searching '"+timeline_path+"' for "+regex_info[1].what+' -> '+regex_info[1].forwhat
|
||||||
|
progress_mutex.unlock()
|
||||||
|
for i in regex_info[0].search_all(timeline_text):
|
||||||
|
if regex_info[1].has('character_regex'):
|
||||||
|
if regex_info[1].character_regex.search(get_line(timeline_text, i.get_start()+1)) == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var line_number := timeline_text.count('\n', 0, i.get_start()+1)+1
|
||||||
|
var line := timeline_text.get_slice('\n', line_number-1)
|
||||||
|
finds.append({
|
||||||
|
'match':i,
|
||||||
|
'timeline':timeline_path,
|
||||||
|
'info': regex_info[1],
|
||||||
|
'line_number': line_number,
|
||||||
|
'line': line,
|
||||||
|
'line_start': timeline_text.rfind('\n', i.get_start())
|
||||||
|
})
|
||||||
|
regex_info[1]['count'] += 1
|
||||||
|
return finds
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
# Shutting of
|
||||||
|
if finder_thread and finder_thread.is_alive():
|
||||||
|
finder_thread.wait_to_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func get_line(string:String, at_index:int) -> String:
|
||||||
|
return string.substr(max(string.rfind('\n', at_index), 0), string.find('\n', at_index)-string.rfind('\n', at_index))
|
||||||
|
|
||||||
|
|
||||||
|
func update_count_coloring() -> void:
|
||||||
|
var item: TreeItem = %ChangeTree.get_root()
|
||||||
|
while item.get_next_visible():
|
||||||
|
item = item.get_next_visible()
|
||||||
|
|
||||||
|
if item.get_child_count():
|
||||||
|
continue
|
||||||
|
if int(item.get_text(2)) > 0:
|
||||||
|
item.set_custom_bg_color(1, get_theme_color("warning_color", "Editor").darkened(0.8))
|
||||||
|
item.set_custom_color(1, get_theme_color("warning_color", "Editor"))
|
||||||
|
item.set_custom_color(2, get_theme_color("warning_color", "Editor"))
|
||||||
|
else:
|
||||||
|
item.set_custom_color(2, get_theme_color("success_color", "Editor"))
|
||||||
|
item.set_custom_color(1, get_theme_color("readonly_font_color", "Editor"))
|
||||||
|
if item.get_button_count(1):
|
||||||
|
item.erase_button(1, 1)
|
||||||
|
item.add_button(1, get_theme_icon("Eraser", "EditorIcons"), -1, true, "This reference was not found anywhere and will be removed from this list.")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_replace_pressed() -> void:
|
||||||
|
var to_be_replaced: Array[Dictionary]= []
|
||||||
|
var item: TreeItem = %ReferenceTree.get_root()
|
||||||
|
var affected_timelines: Array[String]= []
|
||||||
|
|
||||||
|
while item.get_next_visible():
|
||||||
|
item = item.get_next_visible()
|
||||||
|
|
||||||
|
if item.get_child_count():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.is_checked(0):
|
||||||
|
to_be_replaced.append(item.get_metadata(0))
|
||||||
|
to_be_replaced[-1]['f_item'] = item
|
||||||
|
if !item.get_metadata(0).timeline in affected_timelines:
|
||||||
|
affected_timelines.append(item.get_metadata(0).timeline)
|
||||||
|
replace(affected_timelines, to_be_replaced)
|
||||||
|
|
||||||
|
|
||||||
|
func replace(timelines:Array[String], replacement_info:Array[Dictionary]) -> void:
|
||||||
|
var reopen_timeline := ""
|
||||||
|
var timeline_editor: DialogicEditor = find_parent('EditorView').editors_manager.editors['Timeline'].node
|
||||||
|
if timeline_editor.current_resource != null and timeline_editor.current_resource.resource_path in timelines:
|
||||||
|
reopen_timeline = timeline_editor.current_resource.resource_path
|
||||||
|
find_parent('EditorView').editors_manager.clear_editor(timeline_editor)
|
||||||
|
|
||||||
|
replacement_info.sort_custom(func(a,b): return a.match.get_start() < b.match.get_start())
|
||||||
|
|
||||||
|
for timeline_path in timelines:
|
||||||
|
%State.text = "Loading '"+timeline_path+"'"
|
||||||
|
|
||||||
|
var timeline_file := FileAccess.open(timeline_path, FileAccess.READ_WRITE)
|
||||||
|
var timeline_text: String = timeline_file.get_as_text()
|
||||||
|
var timeline_events := timeline_text.split('\n')
|
||||||
|
timeline_file.close()
|
||||||
|
|
||||||
|
var idx := 1
|
||||||
|
var offset_correction := 0
|
||||||
|
for replacement in replacement_info:
|
||||||
|
if replacement.timeline != timeline_path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
%State.text = "Replacing in '"+timeline_path + "' ("+str(idx)+"/"+str(len(replacement_info))+")"
|
||||||
|
var group := 'replace'
|
||||||
|
if not 'replace' in replacement.match.names:
|
||||||
|
group = ''
|
||||||
|
|
||||||
|
|
||||||
|
timeline_text = timeline_text.substr(0, replacement.match.get_start(group) + offset_correction) + \
|
||||||
|
replacement.info.regex_replacement + \
|
||||||
|
timeline_text.substr(replacement.match.get_end(group) + offset_correction)
|
||||||
|
offset_correction += len(replacement.info.regex_replacement)-len(replacement.match.get_string(group))
|
||||||
|
|
||||||
|
replacement.info.count -= 1
|
||||||
|
replacement.info.item.set_text(2, str(replacement.info.count))
|
||||||
|
replacement.f_item.set_custom_bg_color(1, get_theme_color("success_color", "Editor").darkened(0.8))
|
||||||
|
|
||||||
|
timeline_file = FileAccess.open(timeline_path, FileAccess.WRITE)
|
||||||
|
timeline_file.store_string(timeline_text.strip_edges(false, true))
|
||||||
|
timeline_file.close()
|
||||||
|
|
||||||
|
if ResourceLoader.has_cached(timeline_path):
|
||||||
|
var tml := load(timeline_path)
|
||||||
|
tml.from_text(timeline_text)
|
||||||
|
|
||||||
|
if !reopen_timeline.is_empty():
|
||||||
|
find_parent('EditorView').editors_manager.edit_resource(load(reopen_timeline), false, true)
|
||||||
|
|
||||||
|
update_count_coloring()
|
||||||
|
|
||||||
|
%Replace.disabled = true
|
||||||
|
%CheckButton.disabled = false
|
||||||
|
%State.text = "Done Replacing"
|
||||||
|
|
||||||
|
|
||||||
|
func update_indicator() -> void:
|
||||||
|
%TabA.get_child(0).visible = !reference_changes.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func close() -> void:
|
||||||
|
var item: TreeItem = %ChangeTree.get_root()
|
||||||
|
if item:
|
||||||
|
while item.get_next_visible():
|
||||||
|
item = item.get_next_visible()
|
||||||
|
|
||||||
|
if item.get_child_count():
|
||||||
|
continue
|
||||||
|
if item.get_text(2) != "" and int(item.get_text(2)) == 0:
|
||||||
|
reference_changes.erase(item.get_metadata(0))
|
||||||
|
for i in reference_changes:
|
||||||
|
i.item = null
|
||||||
|
DialogicUtil.set_editor_setting('reference_changes', reference_changes)
|
||||||
|
update_indicator()
|
||||||
|
find_parent("ReferenceManager").update_indicator()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_add_button_pressed() -> void:
|
||||||
|
%ReplacementEditPanel._on_add_pressed()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://nrhtjk2rgmgk
|
||||||
12
addons/dialogic/Editor/Common/hint_tooltip_icon.gd
Normal file
12
addons/dialogic/Editor/Common/hint_tooltip_icon.gd
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@tool
|
||||||
|
extends TextureRect
|
||||||
|
|
||||||
|
@export_multiline var hint_text := ""
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if owner and owner.get_parent() is SubViewport:
|
||||||
|
texture = null
|
||||||
|
return
|
||||||
|
texture = get_theme_icon("NodeInfo", "EditorIcons")
|
||||||
|
modulate = get_theme_color("contrast_color_1", "Editor")
|
||||||
|
tooltip_text = hint_text
|
||||||
1
addons/dialogic/Editor/Common/hint_tooltip_icon.gd.uid
Normal file
1
addons/dialogic/Editor/Common/hint_tooltip_icon.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b0vm440bs3ckd
|
||||||
21
addons/dialogic/Editor/Common/hint_tooltip_icon.tscn
Normal file
21
addons/dialogic/Editor/Common/hint_tooltip_icon.tscn
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[gd_scene load_steps=4 format=3 uid="uid://dbpkta2tjsqim"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b0vm440bs3ckd" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.gd" id="1_x8t45"]
|
||||||
|
|
||||||
|
[sub_resource type="Image" id="Image_c5s34"]
|
||||||
|
data = {
|
||||||
|
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||||
|
"format": "RGBA8",
|
||||||
|
"height": 16,
|
||||||
|
"mipmaps": false,
|
||||||
|
"width": 16
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="ImageTexture" id="ImageTexture_ydy7j"]
|
||||||
|
image = SubResource("Image_c5s34")
|
||||||
|
|
||||||
|
[node name="HintTooltip" type="TextureRect"]
|
||||||
|
modulate = Color(0, 0, 0, 1)
|
||||||
|
texture = SubResource("ImageTexture_ydy7j")
|
||||||
|
stretch_mode = 3
|
||||||
|
script = ExtResource("1_x8t45")
|
||||||
38
addons/dialogic/Editor/Common/reference_manager.gd
Normal file
38
addons/dialogic/Editor/Common/reference_manager.gd
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
@tool
|
||||||
|
extends PanelContainer
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
|
||||||
|
add_theme_stylebox_override("panel", get_theme_stylebox("Background", "EditorStyles"))
|
||||||
|
$Tabs/Close.icon = get_theme_icon("Close", "EditorIcons")
|
||||||
|
|
||||||
|
for tab in $Tabs/Tabs.get_children():
|
||||||
|
tab.add_theme_color_override("font_selected_color", get_theme_color("accent_color", "Editor"))
|
||||||
|
tab.add_theme_font_override("font", get_theme_font("main", "EditorFonts"))
|
||||||
|
tab.toggled.connect(tab_changed.bind(tab.get_index()+1))
|
||||||
|
|
||||||
|
|
||||||
|
func tab_changed(enabled:bool, index:int) -> void:
|
||||||
|
for child in $Tabs.get_children():
|
||||||
|
if child.get_index() == 0 or child.get_index() == index or child is Button:
|
||||||
|
child.show()
|
||||||
|
if child.get_index() == index:
|
||||||
|
child.open()
|
||||||
|
else:
|
||||||
|
if child.visible:
|
||||||
|
child.close()
|
||||||
|
child.hide()
|
||||||
|
for child in $Tabs/Tabs.get_children():
|
||||||
|
child.set_pressed_no_signal(index-1 == child.get_index())
|
||||||
|
|
||||||
|
|
||||||
|
func open() -> void:
|
||||||
|
show()
|
||||||
|
$Tabs/BrokenReferences.update_indicator()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_close_pressed() -> void:
|
||||||
|
get_parent()._on_close_requested()
|
||||||
1
addons/dialogic/Editor/Common/reference_manager.gd.uid
Normal file
1
addons/dialogic/Editor/Common/reference_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dugy11ebty3yq
|
||||||
331
addons/dialogic/Editor/Common/reference_manager.tscn
Normal file
331
addons/dialogic/Editor/Common/reference_manager.tscn
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
[gd_scene load_steps=12 format=3 uid="uid://c7lmt5cp7bxcm"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dugy11ebty3yq" path="res://addons/dialogic/Editor/Common/reference_manager.gd" id="1_3t531"]
|
||||||
|
[ext_resource type="Script" uid="uid://nrhtjk2rgmgk" path="res://addons/dialogic/Editor/Common/broken_reference_manager.gd" id="1_agmg4"]
|
||||||
|
[ext_resource type="Script" uid="uid://dca6a1a74jfur" path="res://addons/dialogic/Editor/Common/ReferenceManager_AddReplacementPanel.gd" id="2_tt4jd"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="3_yomsc"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="5_sdymt"]
|
||||||
|
[ext_resource type="Script" uid="uid://bvbsqai5sh0na" path="res://addons/dialogic/Editor/Common/unique_identifiers_manager.gd" id="5_wnvbq"]
|
||||||
|
|
||||||
|
[sub_resource type="ButtonGroup" id="ButtonGroup_l6uiy"]
|
||||||
|
|
||||||
|
[sub_resource type="Image" id="Image_pnutm"]
|
||||||
|
data = {
|
||||||
|
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||||
|
"format": "RGBA8",
|
||||||
|
"height": 16,
|
||||||
|
"mipmaps": false,
|
||||||
|
"width": 16
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="ImageTexture" id="ImageTexture_a0gfq"]
|
||||||
|
image = SubResource("Image_pnutm")
|
||||||
|
|
||||||
|
[sub_resource type="DPITexture" id="DPITexture_asrh0"]
|
||||||
|
_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M4 1a4 4 0 0 0-4 4v10h2v-4h4v4h2V5a4 4 0 0 0-4-4zm5 11a3 3 0 0 0 4 2.824V15h2V9a3 3 0 0 0-3-3h-1v2h1a1 1 0 0 1 1 1v.174A3 3 0 0 0 9 12zM4 3a2 2 0 0 1 2 2v4H2V5a2 2 0 0 1 2-2zm8 8a1 1 0 1 1 0 2 1 1 0 0 1 0-2z\"/></svg>
|
||||||
|
"
|
||||||
|
saturation = 2.0
|
||||||
|
color_map = {
|
||||||
|
Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
|
||||||
|
Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
|
||||||
|
Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="DPITexture" id="DPITexture_xvpjt"]
|
||||||
|
_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M1 1h14v4h-1a2 2 0 0 0-2-2H9v9a2 2 0 0 0 2 2v1H5v-1a2 2 0 0 0 2-2V3H4a2 2 0 0 0-2 2H1z\"/></svg>
|
||||||
|
"
|
||||||
|
saturation = 2.0
|
||||||
|
color_map = {
|
||||||
|
Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
|
||||||
|
Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
|
||||||
|
Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="Manager" type="PanelContainer" unique_id=120086211]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_3t531")
|
||||||
|
|
||||||
|
[node name="Tabs" type="VBoxContainer" parent="." unique_id=451817993]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Tabs" type="HBoxContainer" parent="Tabs" unique_id=391970232]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="TabA" type="Button" parent="Tabs/Tabs" unique_id=1545839736]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
toggle_mode = true
|
||||||
|
button_pressed = true
|
||||||
|
text = "Broken References"
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="TabB" type="Button" parent="Tabs/Tabs" unique_id=179829302]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
toggle_mode = true
|
||||||
|
button_group = SubResource("ButtonGroup_l6uiy")
|
||||||
|
text = "Unique Identifiers"
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="BrokenReferences" type="VSplitContainer" parent="Tabs" unique_id=2135102832]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("1_agmg4")
|
||||||
|
|
||||||
|
[node name="ChangesList" type="PanelContainer" parent="Tabs/BrokenReferences" unique_id=550482921]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_type_variation = &"DialogicPanelA"
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="Tabs/BrokenReferences/ChangesList" unique_id=1452099231]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="Tabs/BrokenReferences/ChangesList/VBox" unique_id=1052605248]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="SectionTitle" type="Label" parent="Tabs/BrokenReferences/ChangesList/VBox/HBoxContainer" unique_id=1577022836]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"HeaderLarge"
|
||||||
|
text = "Recent renames"
|
||||||
|
|
||||||
|
[node name="AddButton" type="Button" parent="Tabs/BrokenReferences/ChangesList/VBox/HBoxContainer" unique_id=1973618045]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 2
|
||||||
|
tooltip_text = "Add custom rename"
|
||||||
|
theme_type_variation = &"FlatButton"
|
||||||
|
icon = SubResource("ImageTexture_a0gfq")
|
||||||
|
|
||||||
|
[node name="ReplacementEditPanel" type="PanelContainer" parent="Tabs/BrokenReferences/ChangesList/VBox" unique_id=437555027]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
script = ExtResource("2_tt4jd")
|
||||||
|
|
||||||
|
[node name="VBox" type="HFlowContainer" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel" unique_id=397998494]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HBoxContainer3" type="HBoxContainer" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox" unique_id=1821654362]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Type" type="OptionButton" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer3" unique_id=1357396186]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "This decides the regexes for searching. Pure text allows you to enter your own regex into \"Old\". "
|
||||||
|
selected = 0
|
||||||
|
item_count = 5
|
||||||
|
popup/item_0/text = "Pure Text"
|
||||||
|
popup/item_0/id = 0
|
||||||
|
popup/item_1/text = "Variable"
|
||||||
|
popup/item_1/id = 1
|
||||||
|
popup/item_2/text = "Portrait"
|
||||||
|
popup/item_2/id = 2
|
||||||
|
popup/item_3/text = "Character (Ref)"
|
||||||
|
popup/item_3/id = 3
|
||||||
|
popup/item_4/text = "Timeline (Ref)"
|
||||||
|
popup/item_4/id = 4
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox" unique_id=1885755143]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Old" type="LineEdit" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer" unique_id=1795932103]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "Old"
|
||||||
|
|
||||||
|
[node name="Label2" type="Label" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer" unique_id=1934012091]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "->"
|
||||||
|
|
||||||
|
[node name="New" type="LineEdit" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer" unique_id=42477240]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "New"
|
||||||
|
|
||||||
|
[node name="PureTextFlags" type="HBoxContainer" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox" unique_id=1142691441]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 2
|
||||||
|
|
||||||
|
[node name="MatchCase" type="Button" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/PureTextFlags" unique_id=1479221705]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Match Case"
|
||||||
|
toggle_mode = true
|
||||||
|
icon = SubResource("DPITexture_asrh0")
|
||||||
|
|
||||||
|
[node name="WholeWords" type="Button" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/PureTextFlags" unique_id=597458715]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Whole World"
|
||||||
|
toggle_mode = true
|
||||||
|
icon = SubResource("DPITexture_xvpjt")
|
||||||
|
|
||||||
|
[node name="HBoxContainer4" type="HBoxContainer" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox" unique_id=1792095734]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Where" type="OptionButton" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer4" unique_id=661908057]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
selected = 0
|
||||||
|
fit_to_longest_item = false
|
||||||
|
item_count = 3
|
||||||
|
popup/item_0/text = "Everywhere"
|
||||||
|
popup/item_0/id = 0
|
||||||
|
popup/item_1/text = "Only for Character"
|
||||||
|
popup/item_1/id = 1
|
||||||
|
popup/item_2/text = "Texts only"
|
||||||
|
popup/item_2/id = 2
|
||||||
|
|
||||||
|
[node name="Character" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer4" unique_id=1881601849 instance=ExtResource("3_yomsc")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="AddButton" type="Button" parent="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox" unique_id=245495061]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Add/Save"
|
||||||
|
|
||||||
|
[node name="ChangeTree" type="Tree" parent="Tabs/BrokenReferences/ChangesList/VBox" unique_id=1796477890]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(0, 50)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/draw_relationship_lines = 1
|
||||||
|
columns = 3
|
||||||
|
hide_root = true
|
||||||
|
|
||||||
|
[node name="CheckButton" type="Button" parent="Tabs/BrokenReferences/ChangesList/VBox" unique_id=2068821656]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
tooltip_text = "Search timelines for occurences of these renames"
|
||||||
|
text = "Check Selected"
|
||||||
|
|
||||||
|
[node name="ReplacementSection" type="PanelContainer" parent="Tabs/BrokenReferences" unique_id=1219457619]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"DialogicPanelA"
|
||||||
|
|
||||||
|
[node name="FindList" type="VBoxContainer" parent="Tabs/BrokenReferences/ReplacementSection" unique_id=244850763]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="HBox" type="HBoxContainer" parent="Tabs/BrokenReferences/ReplacementSection/FindList" unique_id=1677612000]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="SectionTitle2" type="Label" parent="Tabs/BrokenReferences/ReplacementSection/FindList/HBox" unique_id=1144093962]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 16
|
||||||
|
text = "Found references"
|
||||||
|
|
||||||
|
[node name="State" type="Label" parent="Tabs/BrokenReferences/ReplacementSection/FindList/HBox" unique_id=2122509693]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 8
|
||||||
|
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||||
|
text = "State"
|
||||||
|
|
||||||
|
[node name="ReferenceTree" type="Tree" parent="Tabs/BrokenReferences/ReplacementSection/FindList" unique_id=580230309]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/draw_relationship_lines = 1
|
||||||
|
columns = 3
|
||||||
|
hide_root = true
|
||||||
|
|
||||||
|
[node name="Progress" type="ProgressBar" parent="Tabs/BrokenReferences/ReplacementSection/FindList" unique_id=1541410648]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
max_value = 1.0
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="Tabs/BrokenReferences/ReplacementSection/FindList" unique_id=1684136966]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="Replace" type="Button" parent="Tabs/BrokenReferences/ReplacementSection/FindList/HBoxContainer" unique_id=1800357011]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
tooltip_text = "Replace all selected findings (Careful, no undo!)"
|
||||||
|
text = "Replace Selected"
|
||||||
|
|
||||||
|
[node name="HintTooltip" parent="Tabs/BrokenReferences/ReplacementSection/FindList/HBoxContainer" unique_id=1728688034 instance=ExtResource("5_sdymt")]
|
||||||
|
layout_mode = 2
|
||||||
|
texture = null
|
||||||
|
hint_text = "Note that searching and replacing is only implemented for timelines.
|
||||||
|
E.g. variables used in character display names or glossary entries will have to be replaced manually."
|
||||||
|
|
||||||
|
[node name="UniqueIdentifiers" type="PanelContainer" parent="Tabs" unique_id=219749373]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_type_variation = &"DialogicPanelA"
|
||||||
|
script = ExtResource("5_wnvbq")
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="Tabs/UniqueIdentifiers" unique_id=738820172]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Tools" type="HBoxContainer" parent="Tabs/UniqueIdentifiers/VBox" unique_id=346633309]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="Search" type="LineEdit" parent="Tabs/UniqueIdentifiers/VBox/Tools" unique_id=498038148]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(400, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Filter Identifiers/Paths"
|
||||||
|
|
||||||
|
[node name="IdentifierTable" type="Tree" parent="Tabs/UniqueIdentifiers/VBox" unique_id=1440037436]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
columns = 2
|
||||||
|
column_titles_visible = true
|
||||||
|
hide_root = true
|
||||||
|
|
||||||
|
[node name="RenameNotification" type="Label" parent="Tabs/UniqueIdentifiers/VBox" unique_id=618862784]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(100, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "You've renamed some identifier(s)! Use the \"Broken References\" tab to check if you have used this identifier (and fix it if so)."
|
||||||
|
autowrap_mode = 3
|
||||||
|
|
||||||
|
[node name="Close" type="Button" parent="Tabs" unique_id=753377605]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
text = "Close"
|
||||||
|
|
||||||
|
[node name="HelpButton" type="LinkButton" parent="." unique_id=242293099]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 8
|
||||||
|
size_flags_vertical = 0
|
||||||
|
text = "Documentation"
|
||||||
|
uri = "https://docs.dialogic.pro/reference-manager.html"
|
||||||
|
|
||||||
|
[connection signal="pressed" from="Tabs/BrokenReferences/ChangesList/VBox/HBoxContainer/AddButton" to="Tabs/BrokenReferences" method="_on_add_button_pressed"]
|
||||||
|
[connection signal="item_selected" from="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer3/Type" to="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel" method="_on_type_item_selected"]
|
||||||
|
[connection signal="item_selected" from="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/HBoxContainer4/Where" to="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel" method="_on_where_item_selected"]
|
||||||
|
[connection signal="pressed" from="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel/VBox/AddButton" to="Tabs/BrokenReferences/ChangesList/VBox/ReplacementEditPanel" method="save"]
|
||||||
|
[connection signal="button_clicked" from="Tabs/BrokenReferences/ChangesList/VBox/ChangeTree" to="Tabs/BrokenReferences" method="_on_change_tree_button_clicked"]
|
||||||
|
[connection signal="item_edited" from="Tabs/BrokenReferences/ChangesList/VBox/ChangeTree" to="Tabs/BrokenReferences" method="_on_change_tree_item_edited"]
|
||||||
|
[connection signal="pressed" from="Tabs/BrokenReferences/ChangesList/VBox/CheckButton" to="Tabs/BrokenReferences" method="_on_check_button_pressed"]
|
||||||
|
[connection signal="pressed" from="Tabs/BrokenReferences/ReplacementSection/FindList/HBoxContainer/Replace" to="Tabs/BrokenReferences" method="_on_replace_pressed"]
|
||||||
|
[connection signal="text_changed" from="Tabs/UniqueIdentifiers/VBox/Tools/Search" to="Tabs/UniqueIdentifiers" method="_on_search_text_changed"]
|
||||||
|
[connection signal="button_clicked" from="Tabs/UniqueIdentifiers/VBox/IdentifierTable" to="Tabs/UniqueIdentifiers" method="_on_identifier_table_button_clicked"]
|
||||||
|
[connection signal="item_edited" from="Tabs/UniqueIdentifiers/VBox/IdentifierTable" to="Tabs/UniqueIdentifiers" method="_on_identifier_table_item_edited"]
|
||||||
|
[connection signal="pressed" from="Tabs/Close" to="." method="_on_close_pressed"]
|
||||||
201
addons/dialogic/Editor/Common/reference_manager_window.gd
Normal file
201
addons/dialogic/Editor/Common/reference_manager_window.gd
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
@tool
|
||||||
|
extends Window
|
||||||
|
|
||||||
|
## This window manages communication with the replacement manager it contains.
|
||||||
|
## Other scripts can call the add_ref_change() method to register changes directly
|
||||||
|
## or use the helpers add_variable_ref_change() and add_portrait_ref_change()
|
||||||
|
|
||||||
|
@onready var editors_manager := get_node("../EditorsManager")
|
||||||
|
@onready var broken_manager := get_node("Manager/Tabs/BrokenReferences")
|
||||||
|
enum Where {EVERYWHERE, BY_CHARACTER, TEXTS_ONLY}
|
||||||
|
enum Types {TEXT, VARIABLE, PORTRAIT, CHARACTER_NAME, TIMELINE_NAME}
|
||||||
|
|
||||||
|
var icon_button: Button = null
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if owner.get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
|
||||||
|
$Manager.theme = owner.get_theme()
|
||||||
|
|
||||||
|
icon_button = editors_manager.add_button(
|
||||||
|
get_theme_icon("Unlinked", "EditorIcons"),
|
||||||
|
"",
|
||||||
|
"Reference Manager",
|
||||||
|
null,
|
||||||
|
editors_manager.ButtonPlacement.SIDEBAR_LEFT_OF_FILTER)
|
||||||
|
icon_button.pressed.connect(open)
|
||||||
|
|
||||||
|
var dot := Sprite2D.new()
|
||||||
|
dot.texture = get_theme_icon("GuiGraphNodePort", "EditorIcons")
|
||||||
|
dot.scale = Vector2(0.8, 0.8)
|
||||||
|
dot.z_index = 10
|
||||||
|
dot.position = Vector2(icon_button.size.x*0.8, icon_button.size.x*0.2)
|
||||||
|
dot.modulate = get_theme_color("warning_color", "Editor").lightened(0.5)
|
||||||
|
|
||||||
|
icon_button.add_child(dot)
|
||||||
|
|
||||||
|
var old_changes: Array = DialogicUtil.get_editor_setting('reference_changes', [])
|
||||||
|
if !old_changes.is_empty():
|
||||||
|
broken_manager.reference_changes = old_changes
|
||||||
|
|
||||||
|
update_indicator()
|
||||||
|
|
||||||
|
hide()
|
||||||
|
|
||||||
|
EditorInterface.get_file_system_dock().files_moved.connect(_on_file_moved)
|
||||||
|
EditorInterface.get_file_system_dock().file_removed.connect(_on_file_removed)
|
||||||
|
get_parent().get_node('ResourceRenameWarning').confirmed.connect(open)
|
||||||
|
|
||||||
|
|
||||||
|
func add_ref_change(old_name:String, new_name:String, type:Types, where:=Where.TEXTS_ONLY, character_names:=[],
|
||||||
|
whole_words:=false, case_sensitive:=false, previous:Dictionary = {}) -> void:
|
||||||
|
var regexes := []
|
||||||
|
var category_name := ""
|
||||||
|
match type:
|
||||||
|
Types.TEXT:
|
||||||
|
category_name = "Texts"
|
||||||
|
if '<replace>' in old_name:
|
||||||
|
regexes = [old_name]
|
||||||
|
else:
|
||||||
|
regexes = [
|
||||||
|
r'(?<replace>%s)' % old_name.replace('/', '\\/')
|
||||||
|
]
|
||||||
|
if !case_sensitive:
|
||||||
|
regexes[0] = '(?i)'+regexes[0]
|
||||||
|
if whole_words:
|
||||||
|
regexes = ['\\b'+regexes[0]+'\\b']
|
||||||
|
|
||||||
|
Types.VARIABLE:
|
||||||
|
regexes = [
|
||||||
|
r'{(?<replace>\s*%s\s*)}' % old_name.replace("/", "\\/"),
|
||||||
|
r'var\s*=\s*"(?<replace>\s*%s\s*)"' % old_name.replace("/", "\\/")
|
||||||
|
]
|
||||||
|
category_name = "Variables"
|
||||||
|
|
||||||
|
Types.PORTRAIT:
|
||||||
|
regexes = [
|
||||||
|
r'(?m)^[^:(\n]*\((?<replace>%s)\)' % old_name.replace('/', '\\/'),
|
||||||
|
r'\[\s*portrait\s*=(?<replace>\s*%s\s*)\]' % old_name.replace('/', '\\/')
|
||||||
|
]
|
||||||
|
category_name = "Portraits by "+character_names[0]
|
||||||
|
|
||||||
|
Types.CHARACTER_NAME:
|
||||||
|
# for reference: ((join|leave|update) )?(?<replace>NAME)(?!\B)(?(1)|(?!([^:\n]|\\:)*(\n|$)))
|
||||||
|
regexes = [
|
||||||
|
r'((join|leave|update) )?(?<replace>%s)(?!\B)(?(1)|(?!([^:\n]|\\:)*(\n|$)))' % old_name
|
||||||
|
]
|
||||||
|
category_name = "Renamed Character Files"
|
||||||
|
|
||||||
|
Types.TIMELINE_NAME:
|
||||||
|
regexes = [
|
||||||
|
r'timeline ?= ?" ?(?<replace>%s) ?"' % old_name
|
||||||
|
]
|
||||||
|
category_name = "Renamed Timeline Files"
|
||||||
|
|
||||||
|
if where != Where.BY_CHARACTER:
|
||||||
|
character_names = []
|
||||||
|
|
||||||
|
# previous is only given when an existing item is edited
|
||||||
|
# in that case the old one is removed first
|
||||||
|
var idx := len(broken_manager.reference_changes)
|
||||||
|
if previous in broken_manager.reference_changes:
|
||||||
|
idx = broken_manager.reference_changes.find(previous)
|
||||||
|
broken_manager.reference_changes.erase(previous)
|
||||||
|
|
||||||
|
if _check_for_ref_change_cycle(old_name, new_name, category_name):
|
||||||
|
update_indicator()
|
||||||
|
return
|
||||||
|
|
||||||
|
broken_manager.reference_changes.insert(idx,
|
||||||
|
{'what':old_name,
|
||||||
|
'forwhat':new_name,
|
||||||
|
'regex': regexes,
|
||||||
|
'regex_replacement':new_name,
|
||||||
|
'category':category_name,
|
||||||
|
'character_names':character_names,
|
||||||
|
'texts_only':where == Where.TEXTS_ONLY,
|
||||||
|
'type':type,
|
||||||
|
'case_sensitive':case_sensitive,
|
||||||
|
'whole_words':whole_words,
|
||||||
|
})
|
||||||
|
|
||||||
|
update_indicator()
|
||||||
|
|
||||||
|
if visible:
|
||||||
|
$Manager.open()
|
||||||
|
broken_manager.open()
|
||||||
|
|
||||||
|
|
||||||
|
## Checks for reference cycles or chains.
|
||||||
|
## E.g. if you first rename a portrait from "happy" to "happy1" and then to "Happy/happy1"
|
||||||
|
## This will make sure only a change "happy" -> "Happy/happy1" is remembered
|
||||||
|
## This is very important for correct replacement
|
||||||
|
func _check_for_ref_change_cycle(old_name:String, new_name:String, category:String) -> bool:
|
||||||
|
for ref in broken_manager.reference_changes:
|
||||||
|
if ref['forwhat'] == old_name and ref['category'] == category:
|
||||||
|
if new_name == ref['what']:
|
||||||
|
broken_manager.reference_changes.erase(ref)
|
||||||
|
else:
|
||||||
|
broken_manager.reference_changes[broken_manager.reference_changes.find(ref)]['forwhat'] = new_name
|
||||||
|
broken_manager.reference_changes[broken_manager.reference_changes.find(ref)]['regex_replacement'] = new_name
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
## Helper for adding variable ref changes
|
||||||
|
func add_variable_ref_change(old_name:String, new_name:String) -> void:
|
||||||
|
add_ref_change(old_name, new_name, Types.VARIABLE, Where.EVERYWHERE)
|
||||||
|
|
||||||
|
|
||||||
|
## Helper for adding portrait ref changes
|
||||||
|
func add_portrait_ref_change(old_name:String, new_name:String, character_names:PackedStringArray) -> void:
|
||||||
|
add_ref_change(old_name, new_name, Types.PORTRAIT, Where.BY_CHARACTER, character_names)
|
||||||
|
|
||||||
|
|
||||||
|
## Helper for adding character name ref changes
|
||||||
|
func add_character_name_ref_change(old_name:String, new_name:String) -> void:
|
||||||
|
add_ref_change(old_name, new_name, Types.CHARACTER_NAME, Where.EVERYWHERE)
|
||||||
|
|
||||||
|
|
||||||
|
## Helper for adding timeline name ref changes
|
||||||
|
func add_timeline_name_ref_change(old_name:String, new_name:String) -> void:
|
||||||
|
add_ref_change(old_name, new_name, Types.TIMELINE_NAME, Where.EVERYWHERE)
|
||||||
|
|
||||||
|
|
||||||
|
func open() -> void:
|
||||||
|
DialogicResourceUtil.update_directory('dch')
|
||||||
|
DialogicResourceUtil.update_directory('dtl')
|
||||||
|
popup_centered_ratio(0.5)
|
||||||
|
grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_close_requested() -> void:
|
||||||
|
hide()
|
||||||
|
broken_manager.close()
|
||||||
|
|
||||||
|
|
||||||
|
func get_change_count() -> int:
|
||||||
|
return len(broken_manager.reference_changes)
|
||||||
|
|
||||||
|
|
||||||
|
func update_indicator() -> void:
|
||||||
|
icon_button.get_child(0).visible = !broken_manager.reference_changes.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
## FILE MANAGEMENT:
|
||||||
|
func _on_file_moved(old_file:String, new_file:String) -> void:
|
||||||
|
if old_file.ends_with('.dch') and new_file.ends_with('.dch'):
|
||||||
|
DialogicResourceUtil.change_resource_path(old_file, new_file)
|
||||||
|
if old_file.get_file() != new_file.get_file():
|
||||||
|
get_parent().get_node('ResourceRenameWarning').popup_centered()
|
||||||
|
elif old_file.ends_with('.dtl') and new_file.ends_with('.dtl'):
|
||||||
|
DialogicResourceUtil.change_resource_path(old_file, new_file)
|
||||||
|
if old_file.get_file() != new_file.get_file():
|
||||||
|
get_parent().get_node('ResourceRenameWarning').popup_centered()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_file_removed(file:String) -> void:
|
||||||
|
if file.get_extension() in ['dch', 'dtl']:
|
||||||
|
DialogicResourceUtil.remove_resource(file)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bxr2qomm7wm85
|
||||||
190
addons/dialogic/Editor/Common/side_bar.tscn
Normal file
190
addons/dialogic/Editor/Common/side_bar.tscn
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
[gd_scene format=3 uid="uid://cwe3r2tbh2og1"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://myogqmakusx3" path="res://addons/dialogic/Editor/Common/sidebar.gd" id="1_jnq65"]
|
||||||
|
[ext_resource type="Script" uid="uid://4injjcial4s4" path="res://addons/dialogic/Editor/Common/sidebar_resource_tree.gd" id="4_4dik3"]
|
||||||
|
|
||||||
|
[sub_resource type="Theme" id="Theme_pn0f4"]
|
||||||
|
VBoxContainer/constants/separation = 4
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_gxwm6"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_n8rql"]
|
||||||
|
|
||||||
|
[node name="SideBar" type="VSplitContainer" unique_id=497851771]
|
||||||
|
custom_minimum_size = Vector2(100, 130)
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme = SubResource("Theme_pn0f4")
|
||||||
|
split_offsets = PackedInt32Array(100)
|
||||||
|
split_offset = 100
|
||||||
|
script = ExtResource("1_jnq65")
|
||||||
|
|
||||||
|
[node name="VBoxHidden" type="VBoxContainer" parent="." unique_id=134910476]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="OpenButton" type="Button" parent="VBoxHidden" unique_id=1597877653]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
size_flags_vertical = 3
|
||||||
|
tooltip_text = "Show Sidebar"
|
||||||
|
theme_type_variation = &"FlatButton"
|
||||||
|
theme_override_constants/icon_max_width = 20
|
||||||
|
|
||||||
|
[node name="VBoxPrimary" type="VBoxContainer" parent="." unique_id=380969381]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Margin" type="MarginContainer" parent="VBoxPrimary" unique_id=1385891509]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="MainVSplit" type="VSplitContainer" parent="VBoxPrimary/Margin" unique_id=1550402925]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="VBoxPrimary/Margin/MainVSplit" unique_id=476357621]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Logo" type="TextureRect" parent="VBoxPrimary/Margin/MainVSplit/VBox" unique_id=845890360]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
modulate = Color(1, 1, 1, 0.623529)
|
||||||
|
texture_filter = 6
|
||||||
|
custom_minimum_size = Vector2(0, 25)
|
||||||
|
layout_mode = 2
|
||||||
|
expand_mode = 3
|
||||||
|
stretch_mode = 4
|
||||||
|
|
||||||
|
[node name="ToolButtons" type="HFlowContainer" parent="VBoxPrimary/Margin/MainVSplit/VBox" unique_id=107308664]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="CurrentResource" type="LineEdit" parent="VBoxPrimary/Margin/MainVSplit/VBox/ToolButtons" unique_id=314619873]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
text = "No resource"
|
||||||
|
alignment = 1
|
||||||
|
editable = false
|
||||||
|
|
||||||
|
[node name="Tools" type="HFlowContainer" parent="VBoxPrimary/Margin/MainVSplit/VBox" unique_id=1734187668]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/h_separation = 0
|
||||||
|
|
||||||
|
[node name="LeftTools" type="HBoxContainer" parent="VBoxPrimary/Margin/MainVSplit/VBox/Tools" unique_id=243893106]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HBox" type="HBoxContainer" parent="VBoxPrimary/Margin/MainVSplit/VBox/Tools" unique_id=1508785548]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Search" type="LineEdit" parent="VBoxPrimary/Margin/MainVSplit/VBox/Tools/HBox" unique_id=383396149]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
tooltip_text = "Filter Resources"
|
||||||
|
placeholder_text = "Filter Resources"
|
||||||
|
caret_blink = true
|
||||||
|
caret_blink_interval = 0.5
|
||||||
|
|
||||||
|
|
||||||
|
[node name="RightTools" type="HBoxContainer" parent="VBoxPrimary/Margin/MainVSplit/VBox/Tools/HBox" unique_id=803895286]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Options" type="MenuButton" parent="VBoxPrimary/Margin/MainVSplit/VBox/Tools/HBox" unique_id=622489996]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Resource List Options"
|
||||||
|
flat = false
|
||||||
|
|
||||||
|
[node name="ResourceTree" type="Tree" parent="VBoxPrimary/Margin/MainVSplit/VBox" unique_id=47398120]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_type_variation = &"TreeSecondary"
|
||||||
|
allow_rmb_select = true
|
||||||
|
hide_root = true
|
||||||
|
scroll_horizontal_enabled = false
|
||||||
|
script = ExtResource("4_4dik3")
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxPrimary/Margin/MainVSplit/VBox" unique_id=509945119]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxContainer" unique_id=1565963743]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 1
|
||||||
|
text = "Sort Order"
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="SortOption" type="OptionButton" parent="VBoxPrimary/Margin/MainVSplit/VBox/HBoxContainer" unique_id=503120854]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
item_count = 1
|
||||||
|
popup/item_0/text = "Alphabetical (All)"
|
||||||
|
popup/item_0/id = 0
|
||||||
|
|
||||||
|
[node name="ContentListSection" type="VBoxContainer" parent="VBoxPrimary/Margin/MainVSplit" unique_id=1652647940]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
custom_minimum_size = Vector2(0, 15)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
size_flags_stretch_ratio = 0.5
|
||||||
|
|
||||||
|
[node name="ContentList" type="ItemList" parent="VBoxPrimary/Margin/MainVSplit/ContentListSection" unique_id=841648603]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
tooltip_text = "Label events in your timeline will appear here, allowing you to jump to them."
|
||||||
|
theme_type_variation = &"ItemListSecondary"
|
||||||
|
theme_override_styles/selected = SubResource("StyleBoxEmpty_gxwm6")
|
||||||
|
theme_override_styles/selected_focus = SubResource("StyleBoxEmpty_n8rql")
|
||||||
|
allow_reselect = true
|
||||||
|
same_column_width = true
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxPrimary" unique_id=615882368]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="CloseButton" type="Button" parent="VBoxPrimary/HBoxContainer" unique_id=637854226]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Hide Sidebar"
|
||||||
|
theme_type_variation = &"FlatButton"
|
||||||
|
|
||||||
|
[node name="CurrentVersion" type="Button" parent="VBoxPrimary/HBoxContainer" unique_id=2110656799]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
mouse_default_cursor_shape = 2
|
||||||
|
text = "Some Version"
|
||||||
|
flat = true
|
||||||
|
clip_text = true
|
||||||
|
|
||||||
|
[node name="RightClickItemMenu" type="PopupMenu" parent="." unique_id=1188370902]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
size = Vector2i(164, 100)
|
||||||
|
|
||||||
|
[node name="RightClickNoItemMenu" type="PopupMenu" parent="." unique_id=2120335967]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
size = Vector2i(164, 100)
|
||||||
|
|
||||||
|
[connection signal="dragged" from="VBoxPrimary/Margin/MainVSplit" to="." method="_on_main_v_split_dragged"]
|
||||||
|
[connection signal="gui_input" from="VBoxPrimary/Margin/MainVSplit/VBox/Logo" to="." method="_on_logo_gui_input"]
|
||||||
|
[connection signal="text_changed" from="VBoxPrimary/Margin/MainVSplit/VBox/Tools/HBox/Search" to="." method="_on_search_text_changed"]
|
||||||
|
[connection signal="text_submitted" from="VBoxPrimary/Margin/MainVSplit/VBox/Tools/HBox/Search" to="." method="_on_search_text_submitted"]
|
||||||
|
[connection signal="empty_clicked" from="VBoxPrimary/Margin/MainVSplit/VBox/ResourceTree" to="." method="_on_resource_tree_empty_clicked"]
|
||||||
|
[connection signal="id_pressed" from="RightClickItemMenu" to="." method="_on_right_click_menu_id_pressed"]
|
||||||
|
[connection signal="id_pressed" from="RightClickNoItemMenu" to="." method="_on_right_click_no_item_menu_id_pressed"]
|
||||||
630
addons/dialogic/Editor/Common/sidebar.gd
Normal file
630
addons/dialogic/Editor/Common/sidebar.gd
Normal file
@@ -0,0 +1,630 @@
|
|||||||
|
@tool
|
||||||
|
class_name DialogicSidebar extends Control
|
||||||
|
|
||||||
|
## Script that handles the editor sidebar.
|
||||||
|
|
||||||
|
signal content_item_activated(item_name)
|
||||||
|
signal show_sidebar(show: bool)
|
||||||
|
|
||||||
|
# References
|
||||||
|
@onready var editors_manager = get_parent().get_parent()
|
||||||
|
@onready var resource_tree: Tree = %ResourceTree
|
||||||
|
|
||||||
|
var current_resource_list: Array = []
|
||||||
|
|
||||||
|
enum GroupMode {
|
||||||
|
NONE,
|
||||||
|
TYPE,
|
||||||
|
FOLDER,
|
||||||
|
PATH,
|
||||||
|
}
|
||||||
|
var group_mode: GroupMode = GroupMode.TYPE
|
||||||
|
|
||||||
|
var custom_right_click_options := {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if owner != null and owner.get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
if editors_manager is SubViewportContainer:
|
||||||
|
return
|
||||||
|
|
||||||
|
## CONNECTIONS
|
||||||
|
editors_manager.resource_opened.connect(_on_editors_resource_opened)
|
||||||
|
editors_manager.editor_changed.connect(_on_editors_editor_changed)
|
||||||
|
|
||||||
|
resource_tree.item_activated.connect(_on_resources_tree_item_activated)
|
||||||
|
resource_tree.item_mouse_selected.connect(_on_resources_tree_item_clicked)
|
||||||
|
resource_tree.item_collapsed.connect(_on_resources_tree_item_collapsed)
|
||||||
|
|
||||||
|
%ContentList.item_selected.connect(
|
||||||
|
func(idx: int): content_item_activated.emit(%ContentList.get_item_text(idx))
|
||||||
|
)
|
||||||
|
|
||||||
|
%OpenButton.pressed.connect(_show_sidebar)
|
||||||
|
%OpenButton.icon = get_theme_icon("Forward", "EditorIcons")
|
||||||
|
%CloseButton.pressed.connect(_hide_sidebar)
|
||||||
|
%CloseButton.icon = get_theme_icon("Back", "EditorIcons")
|
||||||
|
|
||||||
|
var editor_scale := DialogicUtil.get_editor_scale()
|
||||||
|
|
||||||
|
## ICONS
|
||||||
|
%Logo.texture = load("res://addons/dialogic/Editor/Images/dialogic-logo.svg")
|
||||||
|
%Logo.custom_minimum_size.y = 30 * editor_scale
|
||||||
|
%Search.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||||
|
|
||||||
|
## RESOURCE LIST OPTIONS MENU
|
||||||
|
%Options.icon = get_theme_icon("GuiTabMenuHl", "EditorIcons")
|
||||||
|
|
||||||
|
var grouping_menu := PopupMenu.new()
|
||||||
|
#grouping_menu.hide_on_item_selection = false
|
||||||
|
grouping_menu.hide_on_checkable_item_selection = false
|
||||||
|
grouping_menu.add_icon_radio_check_item(get_theme_icon("AnimationTrackList", "EditorIcons"), "None", 0)
|
||||||
|
grouping_menu.add_icon_radio_check_item(get_theme_icon("Folder", "EditorIcons"), "By Type", 1)
|
||||||
|
grouping_menu.add_icon_radio_check_item(get_theme_icon("FolderBrowse", "EditorIcons"), "By Folder", 2)
|
||||||
|
grouping_menu.add_icon_radio_check_item(get_theme_icon("AnimationTrackGroup", "EditorIcons"), "By Path", 3)
|
||||||
|
|
||||||
|
var options_popup: PopupMenu = %Options.get_popup()
|
||||||
|
options_popup.hide_on_checkable_item_selection = false
|
||||||
|
options_popup.add_submenu_node_item("Grouping", grouping_menu)
|
||||||
|
options_popup.add_check_item("Use Folder Colors", 11)
|
||||||
|
options_popup.add_check_item("Trim Folder Paths", 12)
|
||||||
|
options_popup.add_item("List All", 21)
|
||||||
|
options_popup.add_item("Clear List", 22)
|
||||||
|
|
||||||
|
grouping_menu.id_pressed.connect(_on_grouping_changed)
|
||||||
|
options_popup.id_pressed.connect(_on_resource_list_options_id_pressed)
|
||||||
|
|
||||||
|
## CONTENT LIST
|
||||||
|
%ContentList.add_theme_color_override(
|
||||||
|
"font_hovered_color", get_theme_color("warning_color", "Editor")
|
||||||
|
)
|
||||||
|
%ContentList.add_theme_color_override(
|
||||||
|
"font_selected_color", get_theme_color("property_color_z", "Editor")
|
||||||
|
)
|
||||||
|
|
||||||
|
## RIGHT CLICK MENU
|
||||||
|
%RightClickItemMenu.clear()
|
||||||
|
%RightClickItemMenu.add_icon_item(get_theme_icon("Remove", "EditorIcons"), "Remove From List", 1)
|
||||||
|
%RightClickItemMenu.add_separator()
|
||||||
|
%RightClickItemMenu.add_icon_item(get_theme_icon("ActionCopy", "EditorIcons"), "Copy Identifier", 4)
|
||||||
|
%RightClickItemMenu.add_separator()
|
||||||
|
%RightClickItemMenu.add_icon_item(
|
||||||
|
get_theme_icon("Filesystem", "EditorIcons"), "Show in FileSystem", 2
|
||||||
|
)
|
||||||
|
%RightClickItemMenu.add_icon_item(
|
||||||
|
get_theme_icon("ExternalLink", "EditorIcons"), "Open in External Program", 3
|
||||||
|
)
|
||||||
|
|
||||||
|
await get_tree().process_frame
|
||||||
|
if DialogicUtil.get_editor_setting("sidebar_collapsed", false):
|
||||||
|
_hide_sidebar()
|
||||||
|
|
||||||
|
%RightClickNoItemMenu.clear()
|
||||||
|
custom_right_click_options.clear()
|
||||||
|
var idx := 0
|
||||||
|
for i in get_node("%LeftTools").get_children():
|
||||||
|
if not i is Button: continue
|
||||||
|
if not "new" in i.tooltip_text.to_lower(): continue
|
||||||
|
%RightClickNoItemMenu.add_icon_item(i.icon, i.tooltip_text, idx)
|
||||||
|
custom_right_click_options[idx] = {"button":i}
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
%MainVSplit.split_offset = DialogicUtil.get_editor_setting("sidebar_v_split", 0)
|
||||||
|
|
||||||
|
group_mode = DialogicUtil.get_editor_setting("sidebar_group_mode", GroupMode.TYPE)
|
||||||
|
grouping_menu.set_item_checked(grouping_menu.get_item_index(group_mode), true)
|
||||||
|
options_popup.set_item_checked(options_popup.get_item_index(11), DialogicUtil.get_editor_setting("sidebar_use_folder_colors", true))
|
||||||
|
options_popup.set_item_checked(options_popup.get_item_index(12), DialogicUtil.get_editor_setting("sidebar_trim_folder_paths", true))
|
||||||
|
|
||||||
|
reload_resource_list_from_grouping()
|
||||||
|
|
||||||
|
|
||||||
|
func set_unsaved_indicator(saved: bool = true) -> void:
|
||||||
|
if saved and %CurrentResource.text.ends_with("(*)"):
|
||||||
|
%CurrentResource.text = %CurrentResource.text.trim_suffix("(*)")
|
||||||
|
if not saved and not %CurrentResource.text.ends_with("(*)"):
|
||||||
|
%CurrentResource.text = %CurrentResource.text + "(*)"
|
||||||
|
|
||||||
|
|
||||||
|
func _on_logo_gui_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||||
|
editors_manager.open_editor(editors_manager.editors["HomePage"].node)
|
||||||
|
|
||||||
|
|
||||||
|
#region SHOW/HIDE SIDEBAR
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func _show_sidebar() -> void:
|
||||||
|
%VBoxPrimary.show()
|
||||||
|
%VBoxHidden.hide()
|
||||||
|
DialogicUtil.set_editor_setting("sidebar_collapsed", false)
|
||||||
|
show_sidebar.emit(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _hide_sidebar() -> void:
|
||||||
|
%VBoxPrimary.hide()
|
||||||
|
%VBoxHidden.show()
|
||||||
|
DialogicUtil.set_editor_setting("sidebar_collapsed", true)
|
||||||
|
show_sidebar.emit(false)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## RESOURCE LIST
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
func _on_editors_resource_opened(_resource: Resource) -> void:
|
||||||
|
%ContentListSection.hide()
|
||||||
|
update_resource_list()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_editors_editor_changed(_previous: DialogicEditor, current: DialogicEditor) -> void:
|
||||||
|
update_resource_list()
|
||||||
|
%ContentListSection.hide()
|
||||||
|
update_content_list()
|
||||||
|
|
||||||
|
|
||||||
|
## Cleans resources that have been deleted from the resource list
|
||||||
|
func clean_resource_list(resources_list: Array = []) -> PackedStringArray:
|
||||||
|
return PackedStringArray(resources_list.filter(func(x): return ResourceLoader.exists(x)))
|
||||||
|
|
||||||
|
|
||||||
|
#region BULDING/FILTERING THE RESOURCE LIST
|
||||||
|
|
||||||
|
func update_resource_list(resources_list: PackedStringArray = []) -> void:
|
||||||
|
var filter: String = %Search.text
|
||||||
|
var current_file := ""
|
||||||
|
if editors_manager.current_editor and editors_manager.current_editor.current_resource:
|
||||||
|
current_file = editors_manager.current_editor.current_resource.resource_path
|
||||||
|
|
||||||
|
var character_directory: Dictionary = DialogicResourceUtil.get_character_directory()
|
||||||
|
var timeline_directory: Dictionary = DialogicResourceUtil.get_timeline_directory()
|
||||||
|
if resources_list.is_empty():
|
||||||
|
resources_list = DialogicUtil.get_editor_setting("last_resources", [])
|
||||||
|
if not current_file in resources_list:
|
||||||
|
resources_list.append(current_file)
|
||||||
|
|
||||||
|
resources_list = clean_resource_list(resources_list)
|
||||||
|
|
||||||
|
%CurrentResource.text = "No Resource"
|
||||||
|
%CurrentResource.add_theme_color_override(
|
||||||
|
"font_uneditable_color", get_theme_color("disabled_font_color", "Editor")
|
||||||
|
)
|
||||||
|
|
||||||
|
resource_tree.clear()
|
||||||
|
|
||||||
|
var character_items: Array = get_directory_items.call(character_directory, filter, load("res://addons/dialogic/Editor/Images/Resources/character.svg"), resources_list)
|
||||||
|
var timeline_items: Array = get_directory_items.call(timeline_directory, filter, load("res://addons/dialogic/Editor/Images/Resources/timeline.svg"), resources_list)
|
||||||
|
var all_items := character_items + timeline_items
|
||||||
|
|
||||||
|
# BUILD TREE
|
||||||
|
var root: TreeItem = resource_tree.create_item()
|
||||||
|
|
||||||
|
match group_mode:
|
||||||
|
GroupMode.NONE:
|
||||||
|
all_items.sort_custom(_sort_by_item_text)
|
||||||
|
for item in all_items:
|
||||||
|
add_item(item, root, current_file)
|
||||||
|
|
||||||
|
|
||||||
|
GroupMode.TYPE:
|
||||||
|
character_items.sort_custom(_sort_by_item_text)
|
||||||
|
timeline_items.sort_custom(_sort_by_item_text)
|
||||||
|
if character_items.size() > 0:
|
||||||
|
var character_tree := add_folder_item("Characters", root)
|
||||||
|
for item in character_items:
|
||||||
|
add_item(item, character_tree, current_file)
|
||||||
|
|
||||||
|
if timeline_items.size() > 0:
|
||||||
|
var timeline_tree := add_folder_item("Timelines", root)
|
||||||
|
for item in timeline_items:
|
||||||
|
add_item(item, timeline_tree, current_file)
|
||||||
|
|
||||||
|
|
||||||
|
GroupMode.FOLDER:
|
||||||
|
var dirs := {}
|
||||||
|
for item in all_items:
|
||||||
|
var dir := item.get_parent_directory() as String
|
||||||
|
if not dirs.has(dir):
|
||||||
|
dirs[dir] = []
|
||||||
|
dirs[dir].append(item)
|
||||||
|
|
||||||
|
for dir in dirs:
|
||||||
|
var dir_item := add_folder_item(dir, root)
|
||||||
|
|
||||||
|
for item in dirs[dir]:
|
||||||
|
add_item(item, dir_item, current_file)
|
||||||
|
|
||||||
|
|
||||||
|
GroupMode.PATH:
|
||||||
|
# Collect all different directories that contain resources
|
||||||
|
var dirs := {}
|
||||||
|
for item in all_items:
|
||||||
|
var path := (item.metadata.get_base_dir() as String).trim_prefix("res://")
|
||||||
|
if not dirs.has(path):
|
||||||
|
dirs[path] = []
|
||||||
|
dirs[path].append(item)
|
||||||
|
|
||||||
|
# Sort them into ones with the same folder name
|
||||||
|
var dir_names := {}
|
||||||
|
for dir in dirs:
|
||||||
|
var sliced: String = dir.get_slice("/", dir.get_slice_count("/")-1)
|
||||||
|
if not sliced in dir_names:
|
||||||
|
dir_names[sliced] = {"folders":[dir]}
|
||||||
|
else:
|
||||||
|
dir_names[sliced].folders.append(dir)
|
||||||
|
|
||||||
|
# Create a dictionary mapping a unique name to each directory
|
||||||
|
# If two have been found to have the same folder name, the parent directory is added
|
||||||
|
var unique_folder_names := {}
|
||||||
|
for dir_name in dir_names:
|
||||||
|
if dir_names[dir_name].folders.size() > 1:
|
||||||
|
for i in dir_names[dir_name].folders:
|
||||||
|
if "/" in i:
|
||||||
|
unique_folder_names[i.get_slice("/", i.get_slice_count("/")-2)+"/"+i.get_slice("/", i.get_slice_count("/")-1)] = i
|
||||||
|
else:
|
||||||
|
unique_folder_names[i] = i
|
||||||
|
else:
|
||||||
|
unique_folder_names[dir_name] = dir_names[dir_name].folders[0]
|
||||||
|
|
||||||
|
# Sort the folder names by their folder name (not by the full path)
|
||||||
|
var sorted_dir_keys := unique_folder_names.keys()
|
||||||
|
sorted_dir_keys.sort_custom(
|
||||||
|
func(x, y):
|
||||||
|
return x.get_slice("/", x.get_slice_count("/")-1) < y.get_slice("/", y.get_slice_count("/")-1)
|
||||||
|
)
|
||||||
|
var use_folder_colors : bool = DialogicUtil.get_editor_setting("sidebar_use_folder_colors")
|
||||||
|
var trim_folder_paths : bool = DialogicUtil.get_editor_setting("sidebar_trim_folder_paths")
|
||||||
|
var folder_colors: Dictionary = ProjectSettings.get_setting("file_customization/folder_colors", {})
|
||||||
|
|
||||||
|
for dir in sorted_dir_keys:
|
||||||
|
var display_name: String = dir
|
||||||
|
if not trim_folder_paths:
|
||||||
|
display_name = unique_folder_names[dir]
|
||||||
|
var dir_path: String = unique_folder_names[dir]
|
||||||
|
var dir_color_path := ""
|
||||||
|
var dir_color := Color.BLACK
|
||||||
|
if use_folder_colors:
|
||||||
|
for path in folder_colors:
|
||||||
|
if String("res://"+dir_path+"/").begins_with(path) and len(path) > len(dir_color_path):
|
||||||
|
dir_color_path = path
|
||||||
|
dir_color = folder_colors[path]
|
||||||
|
|
||||||
|
var dir_item := add_folder_item(display_name, root, dir_color, dir_path)
|
||||||
|
|
||||||
|
for item in dirs[dir_path]:
|
||||||
|
add_item(item, dir_item, current_file)
|
||||||
|
|
||||||
|
|
||||||
|
if %CurrentResource.text != "No Resource":
|
||||||
|
%CurrentResource.add_theme_color_override(
|
||||||
|
"font_uneditable_color", get_theme_color("font_color", "Editor")
|
||||||
|
)
|
||||||
|
|
||||||
|
DialogicUtil.set_editor_setting("last_resources", resources_list)
|
||||||
|
|
||||||
|
|
||||||
|
func add_item(item:ResourceListItem, parent:TreeItem, current_file := "") -> TreeItem:
|
||||||
|
var tree_item := resource_tree.create_item(parent)
|
||||||
|
tree_item.set_text(0, item.text)
|
||||||
|
tree_item.set_icon(0, item.icon)
|
||||||
|
tree_item.set_metadata(0, item.metadata)
|
||||||
|
tree_item.set_tooltip_text(0, item.tooltip)
|
||||||
|
|
||||||
|
if item.metadata == current_file:
|
||||||
|
%CurrentResource.text = item.metadata.get_file()
|
||||||
|
resource_tree.set_selected(tree_item, 0)
|
||||||
|
|
||||||
|
var bg_color := parent.get_custom_bg_color(0)
|
||||||
|
if bg_color != get_theme_color("base_color", "Editor"):
|
||||||
|
bg_color.a = 0.1
|
||||||
|
tree_item.set_custom_bg_color(0, bg_color)
|
||||||
|
|
||||||
|
return tree_item
|
||||||
|
|
||||||
|
|
||||||
|
func add_folder_item(label: String, parent:TreeItem, color:= Color.BLACK, tooltip:="") -> TreeItem:
|
||||||
|
var folder_item := resource_tree.create_item(parent)
|
||||||
|
folder_item.set_text(0, label)
|
||||||
|
folder_item.set_icon(0, get_theme_icon("Folder", "EditorIcons"))
|
||||||
|
folder_item.set_tooltip_text(0, tooltip)
|
||||||
|
if color == Color.BLACK:
|
||||||
|
folder_item.set_custom_bg_color(0, get_theme_color("base_color", "Editor"))
|
||||||
|
else:
|
||||||
|
color.a = 0.2
|
||||||
|
folder_item.set_custom_bg_color(0, color)
|
||||||
|
|
||||||
|
if label in DialogicUtil.get_editor_setting("resource_list_collapsed_info", []):
|
||||||
|
folder_item.collapsed = true
|
||||||
|
|
||||||
|
return folder_item
|
||||||
|
|
||||||
|
|
||||||
|
func get_directory_items(directory:Dictionary, filter:String, icon:Texture2D, resources_list:Array) -> Array:
|
||||||
|
var items := []
|
||||||
|
for item_name in directory:
|
||||||
|
if (directory[item_name] in resources_list) and (filter.is_empty() or filter.to_lower() in item_name.to_lower()):
|
||||||
|
var item := ResourceListItem.new()
|
||||||
|
item.text = item_name
|
||||||
|
item.icon = icon
|
||||||
|
item.metadata = directory[item_name]
|
||||||
|
item.tooltip = directory[item_name]
|
||||||
|
items.append(item)
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceListItem:
|
||||||
|
extends Object
|
||||||
|
|
||||||
|
var text: String
|
||||||
|
var index: int = -1
|
||||||
|
var icon: Texture
|
||||||
|
var metadata: String
|
||||||
|
var tooltip: String
|
||||||
|
|
||||||
|
func _to_string() -> String:
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
"text": text,
|
||||||
|
"index": index,
|
||||||
|
"icon": icon.resource_path,
|
||||||
|
"metadata": metadata,
|
||||||
|
"tooltip": tooltip,
|
||||||
|
"parent_dir": get_parent_directory()
|
||||||
|
},
|
||||||
|
"\t",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
func get_parent_directory() -> String:
|
||||||
|
return (metadata.get_base_dir() as String).split("/")[-1]
|
||||||
|
|
||||||
|
|
||||||
|
func _sort_by_item_text(a: ResourceListItem, b: ResourceListItem) -> bool:
|
||||||
|
return a.text < b.text
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region INTERACTING WITH RESOURCES
|
||||||
|
|
||||||
|
|
||||||
|
func _on_resources_tree_item_activated() -> void:
|
||||||
|
if resource_tree.get_selected() == null:
|
||||||
|
return
|
||||||
|
var item := resource_tree.get_selected()
|
||||||
|
if item.get_metadata(0) == null:
|
||||||
|
return
|
||||||
|
edit_resource(item.get_metadata(0))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_resources_tree_item_clicked(_pos: Vector2, mouse_button_index: int) -> void:
|
||||||
|
match mouse_button_index:
|
||||||
|
MOUSE_BUTTON_LEFT:
|
||||||
|
while Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
|
||||||
|
if get_viewport().gui_is_dragging():
|
||||||
|
return
|
||||||
|
await get_tree().physics_frame
|
||||||
|
var selected_item := resource_tree.get_selected()
|
||||||
|
if selected_item == null:
|
||||||
|
return
|
||||||
|
if selected_item.get_metadata(0) == null:
|
||||||
|
return
|
||||||
|
var resource_item := load(selected_item.get_metadata(0))
|
||||||
|
call_deferred("edit_resource", resource_item)
|
||||||
|
|
||||||
|
MOUSE_BUTTON_MIDDLE:
|
||||||
|
remove_item_from_list(resource_tree.get_selected())
|
||||||
|
|
||||||
|
MOUSE_BUTTON_RIGHT:
|
||||||
|
if resource_tree.get_selected().get_metadata(0):
|
||||||
|
%RightClickItemMenu.popup_on_parent(Rect2(get_global_mouse_position(), Vector2()))
|
||||||
|
%RightClickItemMenu.set_meta("item_clicked", resource_tree.get_selected())
|
||||||
|
|
||||||
|
|
||||||
|
func _on_resources_tree_item_collapsed(item:TreeItem) -> void:
|
||||||
|
var collapsed_info: Array = DialogicUtil.get_editor_setting("resource_list_collapsed_info", [])
|
||||||
|
if item.get_text(0) in collapsed_info:
|
||||||
|
if not item.collapsed:
|
||||||
|
collapsed_info.erase(item.get_text(0))
|
||||||
|
else:
|
||||||
|
if item.collapsed:
|
||||||
|
collapsed_info.append(item.get_text(0))
|
||||||
|
DialogicUtil.set_editor_setting("resource_list_collapsed_info", collapsed_info)
|
||||||
|
|
||||||
|
|
||||||
|
func edit_resource(resource_item: Variant) -> void:
|
||||||
|
if resource_item is Resource:
|
||||||
|
editors_manager.edit_resource(resource_item)
|
||||||
|
else:
|
||||||
|
editors_manager.edit_resource(load(resource_item))
|
||||||
|
|
||||||
|
|
||||||
|
func remove_item_from_list(item: TreeItem) -> void:
|
||||||
|
var new_list := []
|
||||||
|
for entry in DialogicUtil.get_editor_setting("last_resources", []):
|
||||||
|
if entry != item.get_metadata(0):
|
||||||
|
new_list.append(entry)
|
||||||
|
for editor in editors_manager.editors.values():
|
||||||
|
if editor.node.current_resource and editor.node.current_resource.resource_path == item.get_metadata(0):
|
||||||
|
editors_manager.clear_editor(editor.node, true)
|
||||||
|
DialogicUtil.set_editor_setting("last_resources", new_list)
|
||||||
|
update_resource_list(new_list)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_right_click_menu_id_pressed(id: int) -> void:
|
||||||
|
match id:
|
||||||
|
1: # REMOVE ITEM FROM LIST
|
||||||
|
remove_item_from_list(%RightClickItemMenu.get_meta("item_clicked"))
|
||||||
|
2: # OPEN IN FILESYSTEM
|
||||||
|
EditorInterface.get_file_system_dock().navigate_to_path(
|
||||||
|
%RightClickItemMenu.get_meta("item_clicked").get_metadata(0)
|
||||||
|
)
|
||||||
|
3: # OPEN IN EXTERNAL EDITOR
|
||||||
|
OS.shell_open(
|
||||||
|
ProjectSettings.globalize_path(
|
||||||
|
%RightClickItemMenu.get_meta("item_clicked").get_metadata(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
4: # COPY IDENTIFIER
|
||||||
|
DisplayServer.clipboard_set(
|
||||||
|
DialogicResourceUtil.get_unique_identifier_by_path(
|
||||||
|
%RightClickItemMenu.get_meta("item_clicked").get_metadata(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region FILTERING
|
||||||
|
|
||||||
|
func _on_search_text_changed(_new_text: String) -> void:
|
||||||
|
update_resource_list()
|
||||||
|
for item in resource_tree.get_root().get_children():
|
||||||
|
if item.get_children().size() > 0:
|
||||||
|
resource_tree.set_selected(item.get_child(0), 0)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
func _on_search_text_submitted(_new_text: String) -> void:
|
||||||
|
if resource_tree.get_selected() == null:
|
||||||
|
return
|
||||||
|
var item := resource_tree.get_selected()
|
||||||
|
if item.get_metadata(0) == null:
|
||||||
|
return
|
||||||
|
edit_resource(item.get_metadata(0))
|
||||||
|
%Search.clear()
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region CONTENT LIST
|
||||||
|
|
||||||
|
func update_content_list() -> void:
|
||||||
|
var current_resource: Resource = editors_manager.get_current_editor().current_resource
|
||||||
|
if not current_resource is DialogicTimeline:
|
||||||
|
%ContentListSection.hide()
|
||||||
|
return
|
||||||
|
|
||||||
|
var current_labels := DialogicResourceUtil.get_label_cache().get(current_resource.get_identifier(), [])
|
||||||
|
if current_labels.is_empty():
|
||||||
|
%ContentListSection.hide()
|
||||||
|
return
|
||||||
|
|
||||||
|
var prev_selected := ""
|
||||||
|
if %ContentList.is_anything_selected():
|
||||||
|
prev_selected = %ContentList.get_item_text(%ContentList.get_selected_items()[0])
|
||||||
|
%ContentList.clear()
|
||||||
|
%ContentList.add_item("~ Top")
|
||||||
|
for i in current_labels:
|
||||||
|
if i.is_empty():
|
||||||
|
continue
|
||||||
|
%ContentList.add_item(i)
|
||||||
|
if i == prev_selected:
|
||||||
|
%ContentList.select(%ContentList.item_count - 1)
|
||||||
|
|
||||||
|
%ContentListSection.show()
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region RESOURCE LIST OPTIONS
|
||||||
|
func _on_resource_list_options_id_pressed(id:int) -> void:
|
||||||
|
var popup: PopupMenu = %Options.get_popup()
|
||||||
|
var index := popup.get_item_index(id)
|
||||||
|
|
||||||
|
match id:
|
||||||
|
11: # Folder Colors
|
||||||
|
popup.toggle_item_checked(index)
|
||||||
|
DialogicUtil.set_editor_setting("sidebar_use_folder_colors", popup.is_item_checked(index))
|
||||||
|
12: # Trim Paths
|
||||||
|
popup.toggle_item_checked(index)
|
||||||
|
DialogicUtil.set_editor_setting("sidebar_trim_folder_paths", popup.is_item_checked(index))
|
||||||
|
21: # List All
|
||||||
|
list_all()
|
||||||
|
popup.hide()
|
||||||
|
22: # Clear List
|
||||||
|
DialogicUtil.set_editor_setting("last_resources", [])
|
||||||
|
for editor in editors_manager.editors.values():
|
||||||
|
if editor.node.current_resource:
|
||||||
|
editors_manager.clear_editor(editor.node, true)
|
||||||
|
popup.hide()
|
||||||
|
|
||||||
|
update_resource_list()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_grouping_changed(id: int) -> void:
|
||||||
|
if not (GroupMode as Dictionary).values().has(id):
|
||||||
|
return
|
||||||
|
|
||||||
|
group_mode = (id as GroupMode)
|
||||||
|
DialogicUtil.set_editor_setting("sidebar_group_mode", id)
|
||||||
|
reload_resource_list_from_grouping()
|
||||||
|
|
||||||
|
|
||||||
|
func reload_resource_list_from_grouping() -> void:
|
||||||
|
update_resource_list()
|
||||||
|
|
||||||
|
if group_mode == GroupMode.NONE:
|
||||||
|
%ResourceTree.add_theme_constant_override("item_margin", 0)
|
||||||
|
else:
|
||||||
|
%ResourceTree.remove_theme_constant_override("item_margin")
|
||||||
|
|
||||||
|
var popup: PopupMenu = %Options.get_popup()
|
||||||
|
var grouping_menu := popup.get_item_submenu_node(0)
|
||||||
|
for index in range(grouping_menu.item_count):
|
||||||
|
grouping_menu.set_item_checked(index, grouping_menu.get_item_id(index) == group_mode)
|
||||||
|
popup.set_item_disabled(popup.get_item_index(11), group_mode != GroupMode.PATH)
|
||||||
|
popup.set_item_disabled(popup.get_item_index(12), group_mode != GroupMode.PATH)
|
||||||
|
|
||||||
|
var index := grouping_menu.get_item_index(group_mode)
|
||||||
|
popup.set_item_icon(0, grouping_menu.get_item_icon(index))
|
||||||
|
|
||||||
|
|
||||||
|
func list_all() -> void:
|
||||||
|
var character_directory: Dictionary = DialogicResourceUtil.get_character_directory()
|
||||||
|
var timeline_directory: Dictionary = DialogicResourceUtil.get_timeline_directory()
|
||||||
|
|
||||||
|
var resource_list := []
|
||||||
|
for i in character_directory:
|
||||||
|
resource_list.append(character_directory[i])
|
||||||
|
for i in timeline_directory:
|
||||||
|
resource_list.append(timeline_directory[i])
|
||||||
|
|
||||||
|
DialogicUtil.set_editor_setting("last_resources", resource_list)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
func _on_main_v_split_dragged(offset: int) -> void:
|
||||||
|
DialogicUtil.set_editor_setting("sidebar_v_split", offset)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_resource_tree_empty_clicked(click_position: Vector2, mouse_button_index: int) -> void:
|
||||||
|
if mouse_button_index == MOUSE_BUTTON_RIGHT:
|
||||||
|
%RightClickNoItemMenu.popup_on_parent(Rect2(get_global_mouse_position(), Vector2()))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _on_right_click_no_item_menu_id_pressed(id: int) -> void:
|
||||||
|
custom_right_click_options[id].button.pressed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func add_button(icon: Texture, label:String, tooltip: String, placement:int) -> Button:
|
||||||
|
var button := Button.new()
|
||||||
|
button.icon = icon
|
||||||
|
button.text = label
|
||||||
|
button.tooltip_text = tooltip
|
||||||
|
button.theme_type_variation = "FlatButton"
|
||||||
|
button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
|
||||||
|
match placement:
|
||||||
|
editors_manager.ButtonPlacement.SIDEBAR_LEFT_OF_FILTER:
|
||||||
|
%LeftTools.add_child(button)
|
||||||
|
editors_manager.ButtonPlacement.SIDEBAR_RIGHT_OF_FILTER:
|
||||||
|
%RightTools.add_child(button)
|
||||||
|
return button
|
||||||
1
addons/dialogic/Editor/Common/sidebar.gd.uid
Normal file
1
addons/dialogic/Editor/Common/sidebar.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://myogqmakusx3
|
||||||
10
addons/dialogic/Editor/Common/sidebar_resource_tree.gd
Normal file
10
addons/dialogic/Editor/Common/sidebar_resource_tree.gd
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@tool
|
||||||
|
extends Tree
|
||||||
|
|
||||||
|
var previous_selected : TreeItem = null
|
||||||
|
|
||||||
|
func _get_drag_data(at_position: Vector2) -> Variant:
|
||||||
|
var item := get_item_at_position(at_position)
|
||||||
|
if item.get_metadata(0) and typeof(item.get_metadata(0)) == TYPE_STRING and item.get_metadata(0).begins_with("res://"):
|
||||||
|
return {"files":[item.get_metadata(0)]}
|
||||||
|
return null
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://4injjcial4s4
|
||||||
27
addons/dialogic/Editor/Common/toolbar.gd
Normal file
27
addons/dialogic/Editor/Common/toolbar.gd
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
@tool
|
||||||
|
extends HBoxContainer
|
||||||
|
|
||||||
|
# Dialogic Editor toolbar. Works together with editors_mangager.
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## EDITOR BUTTONS/LABELS
|
||||||
|
################################################################################
|
||||||
|
func _ready() -> void:
|
||||||
|
if owner.get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
%CustomButtons.custom_minimum_size.y = 33 * DialogicUtil.get_editor_scale()
|
||||||
|
|
||||||
|
for child in get_children():
|
||||||
|
if child is Button:
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func add_button(icon: Texture, label:String, tooltip: String, placement:int) -> Button:
|
||||||
|
var button := Button.new()
|
||||||
|
button.icon = icon
|
||||||
|
button.text = label
|
||||||
|
button.tooltip_text = tooltip
|
||||||
|
button.theme_type_variation = "FlatButton"
|
||||||
|
button.size_flags_vertical = Control.SIZE_FILL
|
||||||
|
%CustomButtons.add_child(button)
|
||||||
|
return button
|
||||||
1
addons/dialogic/Editor/Common/toolbar.gd.uid
Normal file
1
addons/dialogic/Editor/Common/toolbar.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://1m3sqaws1hin
|
||||||
97
addons/dialogic/Editor/Common/unique_identifiers_manager.gd
Normal file
97
addons/dialogic/Editor/Common/unique_identifiers_manager.gd
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
@tool
|
||||||
|
extends PanelContainer
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if owner.get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
|
||||||
|
%TabB.text = "Unique Identifiers"
|
||||||
|
%TabB.icon = get_theme_icon("CryptoKey", "EditorIcons")
|
||||||
|
|
||||||
|
%Search.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||||
|
|
||||||
|
owner.get_parent().visibility_changed.connect(func(): if is_visible_in_tree(): open())
|
||||||
|
|
||||||
|
%RenameNotification.add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"))
|
||||||
|
|
||||||
|
|
||||||
|
func open() -> void:
|
||||||
|
fill_table()
|
||||||
|
%RenameNotification.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func close() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func fill_table() -> void:
|
||||||
|
var t: Tree = %IdentifierTable
|
||||||
|
t.set_column_expand(1, true)
|
||||||
|
t.clear()
|
||||||
|
t.set_column_title(1, "Identifier")
|
||||||
|
t.set_column_title(0, "Resource Path")
|
||||||
|
t.set_column_title_alignment(0, 0)
|
||||||
|
t.set_column_title_alignment(1, 0)
|
||||||
|
t.create_item()
|
||||||
|
|
||||||
|
for d in [["Characters", 'dch'], ["Timelines", "dtl"]]:
|
||||||
|
var directory := DialogicResourceUtil.get_directory(d[1])
|
||||||
|
var directory_item := t.create_item()
|
||||||
|
directory_item.set_text(0, d[0])
|
||||||
|
directory_item.set_metadata(0, d[1])
|
||||||
|
for key in directory:
|
||||||
|
var item: TreeItem = t.create_item(directory_item)
|
||||||
|
item.set_text(0, directory[key])
|
||||||
|
item.set_text(1, key)
|
||||||
|
item.set_editable(1, true)
|
||||||
|
item.set_metadata(1, key)
|
||||||
|
item.add_button(1, get_theme_icon("Edit", "EditorIcons"), 0, false, "Edit")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_identifier_table_item_edited() -> void:
|
||||||
|
var item: TreeItem = %IdentifierTable.get_edited()
|
||||||
|
var new_identifier: String = item.get_text(1)
|
||||||
|
|
||||||
|
|
||||||
|
if new_identifier == item.get_metadata(1):
|
||||||
|
return
|
||||||
|
|
||||||
|
if new_identifier.is_empty() or not DialogicResourceUtil.is_identifier_unused(item.get_parent().get_metadata(0), new_identifier):
|
||||||
|
item.set_text(1, item.get_metadata(1))
|
||||||
|
return
|
||||||
|
|
||||||
|
DialogicResourceUtil.change_unique_identifier(item.get_text(0), new_identifier)
|
||||||
|
|
||||||
|
match item.get_parent().get_metadata(0):
|
||||||
|
'dch':
|
||||||
|
owner.get_parent().add_character_name_ref_change(item.get_metadata(1), new_identifier)
|
||||||
|
'dtl':
|
||||||
|
owner.get_parent().add_timeline_name_ref_change(item.get_metadata(1), new_identifier)
|
||||||
|
|
||||||
|
%RenameNotification.show()
|
||||||
|
item.set_metadata(1, new_identifier)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_identifier_table_button_clicked(item: TreeItem, column: int, id: int, mouse_button_index: int) -> void:
|
||||||
|
item.select(column)
|
||||||
|
%IdentifierTable.edit_selected(true)
|
||||||
|
|
||||||
|
|
||||||
|
func filter_tree(filter:String= "", item:TreeItem = null) -> bool:
|
||||||
|
if item == null:
|
||||||
|
item = %IdentifierTable.get_root()
|
||||||
|
|
||||||
|
var any := false
|
||||||
|
for child in item.get_children():
|
||||||
|
if child.get_child_count() > 0:
|
||||||
|
child.visible = filter_tree(filter, child)
|
||||||
|
if child.visible: any = true
|
||||||
|
else:
|
||||||
|
child.visible = filter.is_empty() or filter.to_lower() in child.get_text(0).to_lower() or filter.to_lower() in child.get_text(1).to_lower()
|
||||||
|
if child.visible: any = true
|
||||||
|
|
||||||
|
return any
|
||||||
|
|
||||||
|
|
||||||
|
func _on_search_text_changed(new_text: String) -> void:
|
||||||
|
filter_tree(new_text)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bvbsqai5sh0na
|
||||||
179
addons/dialogic/Editor/Common/update_install_window.gd
Normal file
179
addons/dialogic/Editor/Common/update_install_window.gd
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
@tool
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
var current_info := {}
|
||||||
|
@onready var editor_view := find_parent('EditorView')
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
await editor_view.ready
|
||||||
|
theme = editor_view.theme
|
||||||
|
|
||||||
|
%Install.icon = editor_view.get_theme_icon("AssetLib", "EditorIcons")
|
||||||
|
%LoadingIcon.texture = editor_view.get_theme_icon("KeyTrackScale", "EditorIcons")
|
||||||
|
%InstallWarning.modulate = editor_view.get_theme_color("warning_color", "Editor")
|
||||||
|
%CloseButton.icon = editor_view.get_theme_icon("Close", "EditorIcons")
|
||||||
|
EditorInterface.get_resource_filesystem().resources_reimported.connect(_on_resources_reimported)
|
||||||
|
|
||||||
|
|
||||||
|
func open() -> void:
|
||||||
|
get_parent().popup_centered_ratio(0.5)
|
||||||
|
get_parent().mode = Window.MODE_WINDOWED
|
||||||
|
get_parent().grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func load_info(info:Dictionary, update_type:int) -> void:
|
||||||
|
current_info = info
|
||||||
|
if update_type == 2:
|
||||||
|
%State.text = "No Information Available"
|
||||||
|
%UpdateName.text = "Unable to access versions."
|
||||||
|
%UpdateName.add_theme_color_override("font_color", editor_view.get_theme_color("readonly_color", "Editor"))
|
||||||
|
%Content.text = "You are probably not connected to the internet. Fair enough."
|
||||||
|
%ShortInfo.text = "Huh, what happened here?"
|
||||||
|
%ReadFull.hide()
|
||||||
|
%Install.disabled = true
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we are up to date (or beyond):
|
||||||
|
if info.is_empty():
|
||||||
|
info['name'] = "You are in the future, Marty!"
|
||||||
|
info["body"] = "# 😎 You are using the WIP branch!\nSeems like you are using a version that isn't even released yet. Be careful and give us your feedback ;)"
|
||||||
|
info["published_at"] = "????T"
|
||||||
|
info["author"] = {'login':"???"}
|
||||||
|
%State.text = "Where are we Doc?"
|
||||||
|
%UpdateName.add_theme_color_override("font_color", editor_view.get_theme_color("property_color_z", "Editor"))
|
||||||
|
%Install.disabled = true
|
||||||
|
|
||||||
|
elif update_type == 0:
|
||||||
|
%State.text = "Update Available!"
|
||||||
|
%UpdateName.add_theme_color_override("font_color", editor_view.get_theme_color("warning_color", "Editor"))
|
||||||
|
%Install.disabled = false
|
||||||
|
else:
|
||||||
|
%State.text = "You are up to date:"
|
||||||
|
%UpdateName.add_theme_color_override("font_color", editor_view.get_theme_color("success_color", "Editor"))
|
||||||
|
%Install.disabled = true
|
||||||
|
|
||||||
|
%UpdateName.text = info.name
|
||||||
|
%Content.text = markdown_to_bbcode(info.body).get_slice("\n[font_size", 0).strip_edges()
|
||||||
|
%ShortInfo.text = "Published on "+info.published_at.substr(0, info.published_at.find('T'))+" by "+info.author.login
|
||||||
|
if info.has("html_url"):
|
||||||
|
%ReadFull.uri = info.html_url
|
||||||
|
%ReadFull.show()
|
||||||
|
else:
|
||||||
|
%ReadFull.hide()
|
||||||
|
if info.has('reactions'):
|
||||||
|
%Reactions.show()
|
||||||
|
var reactions := {"laugh":"😂", "hooray":"🎉", "confused":"😕", "heart":"❤️", "rocket":"🚀", "eyes":"👀"}
|
||||||
|
for i in reactions:
|
||||||
|
%Reactions.get_node(i.capitalize()).visible = info.reactions[i] > 0
|
||||||
|
%Reactions.get_node(i.capitalize()).text = reactions[i]+" "+str(info.reactions[i]) if info.reactions[i] > 0 else reactions[i]
|
||||||
|
if info.reactions['+1']+info.reactions['-1'] > 0:
|
||||||
|
%Reactions.get_node("Likes").visible = true
|
||||||
|
%Reactions.get_node("Likes").text = "👍 "+str(info.reactions['+1']+info.reactions['-1'])
|
||||||
|
else:
|
||||||
|
%Reactions.get_node("Likes").visible = false
|
||||||
|
else:
|
||||||
|
%Reactions.hide()
|
||||||
|
|
||||||
|
func _on_window_close_requested() -> void:
|
||||||
|
get_parent().visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func _on_install_pressed() -> void:
|
||||||
|
find_parent('UpdateManager').request_update_download()
|
||||||
|
|
||||||
|
%InfoLabel.text = "Downloading. This can take a moment."
|
||||||
|
%Loading.show()
|
||||||
|
%LoadingIcon.create_tween().set_loops().tween_property(%LoadingIcon, 'rotation', 2*PI, 1).from(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_refresh_pressed() -> void:
|
||||||
|
find_parent('UpdateManager').request_update_check()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_update_manager_downdload_completed(result:int):
|
||||||
|
%Loading.hide()
|
||||||
|
match result:
|
||||||
|
0: # success
|
||||||
|
%InfoLabel.text = "Installed successfully. Restart needed!"
|
||||||
|
%InfoLabel.modulate = editor_view.get_theme_color("success_color", "Editor")
|
||||||
|
%Restart.show()
|
||||||
|
%Restart.grab_focus()
|
||||||
|
1: # failure
|
||||||
|
%InfoLabel.text = "Download failed."
|
||||||
|
%InfoLabel.modulate = editor_view.get_theme_color("readonly_color", "Editor")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_resources_reimported(resources:Array) -> void:
|
||||||
|
if is_inside_tree():
|
||||||
|
await get_tree().process_frame
|
||||||
|
get_parent().grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func markdown_to_bbcode(text:String) -> String:
|
||||||
|
var font_sizes := {1:20, 2:16, 3:16,4:14, 5:14}
|
||||||
|
var title_regex := RegEx.create_from_string('(^|\n)((?<level>#+)(?<title>.*))\\n')
|
||||||
|
var res := title_regex.search(text)
|
||||||
|
while res:
|
||||||
|
text = text.replace(res.get_string(2), '[font_size='+str(font_sizes[len(res.get_string('level'))])+']'+res.get_string('title').strip_edges()+'[/font_size]')
|
||||||
|
res = title_regex.search(text)
|
||||||
|
|
||||||
|
var link_regex := RegEx.create_from_string('(?<!\\!)\\[(?<text>[^\\]]*)]\\((?<link>[^)]*)\\)')
|
||||||
|
res = link_regex.search(text)
|
||||||
|
while res:
|
||||||
|
text = text.replace(res.get_string(), '[url='+res.get_string('link')+']'+res.get_string('text').strip_edges()+'[/url]')
|
||||||
|
res = link_regex.search(text)
|
||||||
|
|
||||||
|
var image_regex := RegEx.create_from_string('\\!\\[(?<text>[^\\]]*)]\\((?<link>[^)]*)\\)\n*')
|
||||||
|
res = image_regex.search(text)
|
||||||
|
while res:
|
||||||
|
text = text.replace(res.get_string(), '[url='+res.get_string('link')+']'+res.get_string('text').strip_edges()+'[/url]')
|
||||||
|
res = image_regex.search(text)
|
||||||
|
|
||||||
|
var italics_regex := RegEx.create_from_string('\\*(?<text>[^\\*\\n]*)\\*')
|
||||||
|
res = italics_regex.search(text)
|
||||||
|
while res:
|
||||||
|
text = text.replace(res.get_string(), '[i]'+res.get_string('text').strip_edges()+'[/i]')
|
||||||
|
res = italics_regex.search(text)
|
||||||
|
|
||||||
|
var bullets_regex := RegEx.create_from_string('(?<=\\n)(\\*|-)(?<text>[^\\*\\n]*)\\n')
|
||||||
|
res = bullets_regex.search(text)
|
||||||
|
while res:
|
||||||
|
text = text.replace(res.get_string(), '[ul]'+res.get_string('text').strip_edges()+'[/ul]\n')
|
||||||
|
res = bullets_regex.search(text)
|
||||||
|
|
||||||
|
var small_code_regex := RegEx.create_from_string('(?<!`)`(?<text>[^`]+)`')
|
||||||
|
res = small_code_regex.search(text)
|
||||||
|
while res:
|
||||||
|
text = text.replace(res.get_string(), '[code][color='+get_theme_color("accent_color", "Editor").to_html()+']'+res.get_string('text').strip_edges()+'[/color][/code]')
|
||||||
|
res = small_code_regex.search(text)
|
||||||
|
|
||||||
|
var big_code_regex := RegEx.create_from_string('(?<!`)```(?<text>[^`]+)```')
|
||||||
|
res = big_code_regex.search(text)
|
||||||
|
while res:
|
||||||
|
text = text.replace(res.get_string(), '[code][bgcolor='+get_theme_color("box_selection_fill_color", "Editor").to_html()+']'+res.get_string('text').strip_edges()+'[/bgcolor][/code]')
|
||||||
|
res = big_code_regex.search(text)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _on_content_meta_clicked(meta:Variant) -> void:
|
||||||
|
OS.shell_open(str(meta))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_install_mouse_entered() -> void:
|
||||||
|
if not %Install.disabled:
|
||||||
|
%InstallWarning.show()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_install_mouse_exited() -> void:
|
||||||
|
%InstallWarning.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_restart_pressed() -> void:
|
||||||
|
EditorInterface.restart_editor(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_close_button_pressed() -> void:
|
||||||
|
get_parent().hide()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cskkip1wso0pu
|
||||||
308
addons/dialogic/Editor/Common/update_install_window.tscn
Normal file
308
addons/dialogic/Editor/Common/update_install_window.tscn
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
[gd_scene load_steps=9 format=3 uid="uid://vv3m5m68fwg7"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cskkip1wso0pu" path="res://addons/dialogic/Editor/Common/update_install_window.gd" id="1_p1pbx"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://dybg3l5pwetne" path="res://addons/dialogic/Editor/Images/plugin-icon.svg" id="2_20ke0"]
|
||||||
|
|
||||||
|
[sub_resource type="Gradient" id="Gradient_lt7uf"]
|
||||||
|
colors = PackedColorArray(0.296484, 0.648457, 1, 1, 0.732014, 0.389374, 1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="GradientTexture2D" id="GradientTexture2D_nl8ke"]
|
||||||
|
gradient = SubResource("Gradient_lt7uf")
|
||||||
|
fill_from = Vector2(0.151515, 0.272727)
|
||||||
|
fill_to = Vector2(1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1g1am"]
|
||||||
|
content_margin_left = 0.0
|
||||||
|
content_margin_top = 15.0
|
||||||
|
content_margin_right = 15.0
|
||||||
|
content_margin_bottom = 15.0
|
||||||
|
bg_color = Color(0.0627451, 0.0627451, 0.0627451, 0.407843)
|
||||||
|
corner_radius_top_left = 20
|
||||||
|
corner_radius_top_right = 20
|
||||||
|
corner_radius_bottom_right = 20
|
||||||
|
corner_radius_bottom_left = 20
|
||||||
|
expand_margin_left = 20.0
|
||||||
|
expand_margin_right = 20.0
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_j1mw2"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h4v2s"]
|
||||||
|
content_margin_left = 5.0
|
||||||
|
content_margin_top = 3.0
|
||||||
|
content_margin_right = 5.0
|
||||||
|
content_margin_bottom = 3.0
|
||||||
|
bg_color = Color(0, 0, 0, 0.631373)
|
||||||
|
corner_radius_top_left = 10
|
||||||
|
corner_radius_top_right = 10
|
||||||
|
corner_radius_bottom_right = 10
|
||||||
|
corner_radius_bottom_left = 10
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_utju1"]
|
||||||
|
content_margin_left = 5.0
|
||||||
|
content_margin_top = 3.0
|
||||||
|
content_margin_right = 5.0
|
||||||
|
content_margin_bottom = 3.0
|
||||||
|
bg_color = Color(0.0470588, 0.0470588, 0.0470588, 1)
|
||||||
|
corner_radius_top_left = 10
|
||||||
|
corner_radius_top_right = 10
|
||||||
|
corner_radius_bottom_right = 10
|
||||||
|
corner_radius_bottom_left = 10
|
||||||
|
|
||||||
|
[node name="UpdateInstallWindow" type="ColorRect"]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
color = Color(0.207843, 0.129412, 0.372549, 1)
|
||||||
|
script = ExtResource("1_p1pbx")
|
||||||
|
|
||||||
|
[node name="TextureRect" type="TextureRect" parent="."]
|
||||||
|
modulate = Color(0.447059, 0.447059, 0.447059, 1)
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
texture = SubResource("GradientTexture2D_nl8ke")
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 14.0
|
||||||
|
offset_top = 13.0
|
||||||
|
offset_right = -14.0
|
||||||
|
offset_bottom = -13.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="VBoxContainer/HBoxContainer2"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 7
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="VBoxContainer/HBoxContainer2"]
|
||||||
|
custom_minimum_size = Vector2(450, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_stretch_ratio = 3.74
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/HBoxContainer2/VBox"]
|
||||||
|
clip_contents = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="Panel" type="PanelContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_1g1am")
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = -8
|
||||||
|
|
||||||
|
[node name="State" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel/VBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"DialogicSubTitle"
|
||||||
|
text = "Update Available!"
|
||||||
|
|
||||||
|
[node name="UpdateName" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel/VBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"DialogicTitle"
|
||||||
|
theme_override_font_sizes/font_size = 25
|
||||||
|
text = "Dialogic 2.0 - alpha 9"
|
||||||
|
uppercase = true
|
||||||
|
|
||||||
|
[node name="ShortInfo" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel/VBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_type_variation = &"DialogicHintText2"
|
||||||
|
theme_override_font_sizes/font_size = 10
|
||||||
|
text = "12/31/23"
|
||||||
|
|
||||||
|
[node name="Refresh" type="Button" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 8
|
||||||
|
size_flags_vertical = 0
|
||||||
|
text = "Refresh
|
||||||
|
"
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="Content" type="RichTextLabel" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_font_sizes/normal_font_size = 14
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxEmpty_j1mw2")
|
||||||
|
bbcode_enabled = true
|
||||||
|
text = "[font_size=25]🎉 New alpha, new stuff![/font_size]
|
||||||
|
If you are using dialogic 2 alphas then we've got an exciting update. It's not the beta yet, but we are getting closer! As always if you have questions or feedback it's best to reach out on [url=https://discord.gg/2hHQzkf2pX]emilios discord[/url].
|
||||||
|
|
||||||
|
This alpha brings a couple of very useful new features to dialogic as well as some syntax changes and a design overhaul (and many, many bug fixes).
|
||||||
|
"
|
||||||
|
fit_content = true
|
||||||
|
|
||||||
|
[node name="Reactions" type="HBoxContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Likes" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
|
||||||
|
text = "👍12"
|
||||||
|
|
||||||
|
[node name="Hooray" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
|
||||||
|
text = "🎉12"
|
||||||
|
|
||||||
|
[node name="Laugh" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
|
||||||
|
text = "👀12"
|
||||||
|
|
||||||
|
[node name="Heart" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
|
||||||
|
text = "❤️12"
|
||||||
|
|
||||||
|
[node name="Rocket" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
|
||||||
|
text = "😕12"
|
||||||
|
|
||||||
|
[node name="Eyes" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
|
||||||
|
text = "🚀12"
|
||||||
|
|
||||||
|
[node name="Confused" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_h4v2s")
|
||||||
|
text = "😂12"
|
||||||
|
|
||||||
|
[node name="ReadFull" type="LinkButton" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Reactions"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 10
|
||||||
|
text = "Read Full Announcement"
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(0, 20)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 2
|
||||||
|
|
||||||
|
[node name="InfoLabel" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
horizontal_alignment = 2
|
||||||
|
autowrap_mode = 3
|
||||||
|
|
||||||
|
[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer"]
|
||||||
|
self_modulate = Color(0, 0, 0, 1)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_h4v2s")
|
||||||
|
|
||||||
|
[node name="HBox" type="HBoxContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 2
|
||||||
|
|
||||||
|
[node name="Loading" type="CenterContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
custom_minimum_size = Vector2(30, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Loading"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="LoadingIcon" type="Sprite2D" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Loading/Control"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
texture = ExtResource("2_20ke0")
|
||||||
|
|
||||||
|
[node name="Restart" type="Button" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 4
|
||||||
|
text = "Restart Now"
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="Install" type="Button" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 4
|
||||||
|
text = "Install"
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="InstallWarning" type="PanelContainer" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Install"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
self_modulate = Color(0, 0, 0, 1)
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 1
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = -493.0
|
||||||
|
offset_top = -92.0
|
||||||
|
offset_right = 5.0
|
||||||
|
offset_bottom = -8.0
|
||||||
|
grow_horizontal = 0
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_utju1")
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Install/InstallWarning"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
text = "Be careful. This will delete the addons/dialogic folder and install the new version. Any custom changes in that folder will be lost.
|
||||||
|
To be on the safe side, use version control!"
|
||||||
|
autowrap_mode = 3
|
||||||
|
|
||||||
|
[node name="Control2" type="Control" parent="VBoxContainer/HBoxContainer2"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 7
|
||||||
|
|
||||||
|
[node name="Close" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="CloseButton" type="Button" parent="VBoxContainer/Close"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Close"
|
||||||
|
|
||||||
|
[connection signal="pressed" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Panel/Refresh" to="." method="_on_refresh_pressed"]
|
||||||
|
[connection signal="meta_clicked" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/Content" to="." method="_on_content_meta_clicked"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Restart" to="." method="_on_restart_pressed"]
|
||||||
|
[connection signal="mouse_entered" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Install" to="." method="_on_install_mouse_entered"]
|
||||||
|
[connection signal="mouse_exited" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Install" to="." method="_on_install_mouse_exited"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/HBoxContainer2/VBox/ScrollContainer/VBoxContainer/HBoxContainer/PanelContainer/HBox/Install" to="." method="_on_install_pressed"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/Close/CloseButton" to="." method="_on_close_button_pressed"]
|
||||||
190
addons/dialogic/Editor/Common/update_manager.gd
Normal file
190
addons/dialogic/Editor/Common/update_manager.gd
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
@tool
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
## Script that checks for new versions and can install them.
|
||||||
|
|
||||||
|
signal update_check_completed(result:UpdateCheckResult)
|
||||||
|
signal downdload_completed(result:DownloadResult)
|
||||||
|
|
||||||
|
enum UpdateCheckResult {UPDATE_AVAILABLE, UP_TO_DATE, NO_ACCESS}
|
||||||
|
enum DownloadResult {SUCCESS, FAILURE}
|
||||||
|
enum ReleaseState {ALPHA, BETA, STABLE}
|
||||||
|
|
||||||
|
const REMOTE_RELEASES_URL := "https://api.github.com/repos/dialogic-godot/dialogic/releases"
|
||||||
|
const TEMP_FILE_NAME := "user://temp.zip"
|
||||||
|
|
||||||
|
var current_version := ""
|
||||||
|
var update_info: Dictionary
|
||||||
|
var current_info: Dictionary
|
||||||
|
|
||||||
|
var version_indicator: Button
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
request_update_check()
|
||||||
|
|
||||||
|
setup_version_indicator()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func get_current_version() -> String:
|
||||||
|
var plugin_cfg := ConfigFile.new()
|
||||||
|
plugin_cfg.load("res://addons/dialogic/plugin.cfg")
|
||||||
|
return plugin_cfg.get_value('plugin', 'version', 'unknown version')
|
||||||
|
|
||||||
|
|
||||||
|
func request_update_check() -> void:
|
||||||
|
if $UpdateCheckRequest.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED:
|
||||||
|
$UpdateCheckRequest.request(REMOTE_RELEASES_URL)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_UpdateCheck_request_completed(result:int, response_code:int, headers:PackedStringArray, body:PackedByteArray) -> void:
|
||||||
|
if result != HTTPRequest.RESULT_SUCCESS:
|
||||||
|
update_check_completed.emit(UpdateCheckResult.NO_ACCESS)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Work out the next version from the releases information on GitHub
|
||||||
|
var response: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||||
|
if typeof(response) != TYPE_ARRAY: return
|
||||||
|
|
||||||
|
|
||||||
|
var current_release_info := get_release_tag_info(get_current_version())
|
||||||
|
|
||||||
|
# GitHub releases are in order of creation, not order of version
|
||||||
|
var versions: Array = (response as Array).filter(compare_versions.bind(current_release_info))
|
||||||
|
if versions.size() > 0:
|
||||||
|
update_info = versions[0]
|
||||||
|
update_check_completed.emit(UpdateCheckResult.UPDATE_AVAILABLE)
|
||||||
|
else:
|
||||||
|
update_info = current_info
|
||||||
|
update_check_completed.emit(UpdateCheckResult.UP_TO_DATE)
|
||||||
|
|
||||||
|
|
||||||
|
func compare_versions(release, current_release_info:Dictionary) -> bool:
|
||||||
|
var checked_release_info := get_release_tag_info(release.tag_name)
|
||||||
|
|
||||||
|
if checked_release_info.major < current_release_info.major:
|
||||||
|
return false
|
||||||
|
|
||||||
|
if checked_release_info.minor < current_release_info.minor:
|
||||||
|
return false
|
||||||
|
|
||||||
|
if checked_release_info.state < current_release_info.state:
|
||||||
|
return false
|
||||||
|
|
||||||
|
elif checked_release_info.state == current_release_info.state:
|
||||||
|
if checked_release_info.state_version < current_release_info.state_version:
|
||||||
|
return false
|
||||||
|
|
||||||
|
if checked_release_info.state_version == current_release_info.state_version:
|
||||||
|
current_info = release
|
||||||
|
return false
|
||||||
|
|
||||||
|
if checked_release_info.state == ReleaseState.STABLE:
|
||||||
|
if checked_release_info.minor == current_release_info.minor:
|
||||||
|
current_info = release
|
||||||
|
return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_release_tag_info(release_tag:String) -> Dictionary:
|
||||||
|
release_tag = release_tag.strip_edges().trim_prefix('v')
|
||||||
|
release_tag = release_tag.substr(0, release_tag.find('('))
|
||||||
|
release_tag = release_tag.to_lower()
|
||||||
|
|
||||||
|
var regex := RegEx.create_from_string(r"(?<major>\d+\.\d+)(-(?<state>alpha|beta)-)?(?(2)(?<stateversion>\d*)|\.(?<minor>\d*))?")
|
||||||
|
|
||||||
|
var result: RegExMatch = regex.search(release_tag)
|
||||||
|
if !result:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
var info: Dictionary = {'tag':release_tag}
|
||||||
|
info['major'] = float(result.get_string('major'))
|
||||||
|
info['minor'] = int(result.get_string('minor'))
|
||||||
|
|
||||||
|
match result.get_string('state'):
|
||||||
|
'alpha':
|
||||||
|
info['state'] = ReleaseState.ALPHA
|
||||||
|
'beta':
|
||||||
|
info['state'] = ReleaseState.BETA
|
||||||
|
_:
|
||||||
|
info['state'] = ReleaseState.STABLE
|
||||||
|
|
||||||
|
info['state_version'] = int(result.get_string('stateversion'))
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
func request_update_download() -> void:
|
||||||
|
# Safeguard the actual dialogue manager repo from accidentally updating itself
|
||||||
|
if DirAccess.dir_exists_absolute("res://test-project/"):
|
||||||
|
prints("[Dialogic] Looks like you are working on the addon. You can't update the addon from within itself.")
|
||||||
|
downdload_completed.emit(DownloadResult.FAILURE)
|
||||||
|
return
|
||||||
|
|
||||||
|
$DownloadRequest.request(update_info.zipball_url)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_DownloadRequest_completed(result:int, response_code:int, headers:PackedStringArray, body:PackedByteArray):
|
||||||
|
if result != HTTPRequest.RESULT_SUCCESS:
|
||||||
|
downdload_completed.emit(DownloadResult.FAILURE)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Save the downloaded zip
|
||||||
|
var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE)
|
||||||
|
zip_file.store_buffer(body)
|
||||||
|
zip_file.close()
|
||||||
|
|
||||||
|
OS.move_to_trash(ProjectSettings.globalize_path("res://addons/dialogic"))
|
||||||
|
|
||||||
|
var zip_reader: ZIPReader = ZIPReader.new()
|
||||||
|
zip_reader.open(TEMP_FILE_NAME)
|
||||||
|
var files: PackedStringArray = zip_reader.get_files()
|
||||||
|
|
||||||
|
var base_path: String = files[0].path_join('addons/')
|
||||||
|
for path in files:
|
||||||
|
if not "dialogic/" in path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var new_file_path: String = path.replace(base_path, "")
|
||||||
|
if path.ends_with("/"):
|
||||||
|
DirAccess.make_dir_recursive_absolute("res://addons/".path_join(new_file_path))
|
||||||
|
else:
|
||||||
|
var file: FileAccess = FileAccess.open("res://addons/".path_join(new_file_path), FileAccess.WRITE)
|
||||||
|
file.store_buffer(zip_reader.read_file(path))
|
||||||
|
|
||||||
|
zip_reader.close()
|
||||||
|
DirAccess.remove_absolute(TEMP_FILE_NAME)
|
||||||
|
|
||||||
|
downdload_completed.emit(DownloadResult.SUCCESS)
|
||||||
|
|
||||||
|
|
||||||
|
###################### SOME UI MANAGEMENT #####################################
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func setup_version_indicator() -> void:
|
||||||
|
version_indicator = %Sidebar.get_node('%CurrentVersion')
|
||||||
|
version_indicator.pressed.connect($Window/UpdateInstallWindow.open)
|
||||||
|
version_indicator.text = get_current_version()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_update_check_completed(result:int):
|
||||||
|
var result_color: Color
|
||||||
|
match result:
|
||||||
|
UpdateCheckResult.UPDATE_AVAILABLE:
|
||||||
|
result_color = version_indicator.get_theme_color("warning_color", "Editor")
|
||||||
|
version_indicator.icon = version_indicator.get_theme_icon("StatusWarning", "EditorIcons")
|
||||||
|
$Window/UpdateInstallWindow.load_info(update_info, result)
|
||||||
|
UpdateCheckResult.UP_TO_DATE:
|
||||||
|
result_color = version_indicator.get_theme_color("success_color", "Editor")
|
||||||
|
version_indicator.icon = version_indicator.get_theme_icon("StatusSuccess", "EditorIcons")
|
||||||
|
$Window/UpdateInstallWindow.load_info(current_info, result)
|
||||||
|
UpdateCheckResult.NO_ACCESS:
|
||||||
|
result_color = version_indicator.get_theme_color("success_color", "Editor")
|
||||||
|
version_indicator.icon = version_indicator.get_theme_icon("GuiRadioCheckedDisabled", "EditorIcons")
|
||||||
|
$Window/UpdateInstallWindow.load_info(update_info, result)
|
||||||
|
|
||||||
|
version_indicator.add_theme_color_override('font_color', result_color)
|
||||||
|
version_indicator.add_theme_color_override('font_hover_color', result_color.lightened(0.5))
|
||||||
|
version_indicator.add_theme_color_override('font_pressed_color', result_color)
|
||||||
|
version_indicator.add_theme_color_override('font_focus_color', result_color)
|
||||||
1
addons/dialogic/Editor/Common/update_manager.gd.uid
Normal file
1
addons/dialogic/Editor/Common/update_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://1tph6ios6ry2
|
||||||
78
addons/dialogic/Editor/Events/BranchEnd.gd
Normal file
78
addons/dialogic/Editor/Events/BranchEnd.gd
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
@tool
|
||||||
|
extends Control
|
||||||
|
## A scene shown at the end of events that contain other events
|
||||||
|
|
||||||
|
var resource: DialogicEndBranchEvent
|
||||||
|
|
||||||
|
# References
|
||||||
|
var parent_node: Control = null
|
||||||
|
var end_control: Control = null
|
||||||
|
|
||||||
|
# Indent
|
||||||
|
var indent_size := 22
|
||||||
|
var current_indent_level := 1
|
||||||
|
|
||||||
|
var selected := false
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
$Icon.icon = get_theme_icon("GuiSpinboxUpdown", "EditorIcons")
|
||||||
|
$Spacer.custom_minimum_size.x = 90 * DialogicUtil.get_editor_scale()
|
||||||
|
visual_deselect()
|
||||||
|
parent_node_changed()
|
||||||
|
|
||||||
|
|
||||||
|
## Called by the visual timeline editor
|
||||||
|
func visual_select() -> void:
|
||||||
|
modulate = get_theme_color("highlighted_font_color", "Editor")
|
||||||
|
selected = true
|
||||||
|
|
||||||
|
|
||||||
|
## Called by the visual timeline editor
|
||||||
|
func visual_deselect() -> void:
|
||||||
|
if !parent_node:return
|
||||||
|
selected = false
|
||||||
|
modulate = parent_node.resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.3)
|
||||||
|
|
||||||
|
|
||||||
|
func is_selected() -> bool:
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
## Called by the visual timeline editor
|
||||||
|
func highlight() -> void:
|
||||||
|
if !parent_node:return
|
||||||
|
modulate = parent_node.resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.6)
|
||||||
|
|
||||||
|
|
||||||
|
## Called by the visual timeline editor
|
||||||
|
func unhighlight() -> void:
|
||||||
|
modulate = parent_node.resource.event_color
|
||||||
|
|
||||||
|
|
||||||
|
## Called by the visual timeline editor
|
||||||
|
func set_indent(indent: int) -> void:
|
||||||
|
$Indent.custom_minimum_size = Vector2(indent_size * indent * DialogicUtil.get_editor_scale(), 0)
|
||||||
|
$Indent.visible = indent != 0
|
||||||
|
current_indent_level = indent
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
## Called by the visual timeline editor if something was edited on the parent event block
|
||||||
|
func parent_node_changed() -> void:
|
||||||
|
if parent_node and end_control and end_control.has_method('refresh'):
|
||||||
|
end_control.refresh()
|
||||||
|
|
||||||
|
|
||||||
|
## Called on creation if the parent event provides an end control
|
||||||
|
func add_end_control(control:Control) -> void:
|
||||||
|
if !control:
|
||||||
|
return
|
||||||
|
add_child(control)
|
||||||
|
control.size_flags_vertical = SIZE_SHRINK_CENTER
|
||||||
|
if "parent_resource" in control:
|
||||||
|
control.parent_resource = parent_node.resource
|
||||||
|
if control.has_method('refresh'):
|
||||||
|
control.refresh()
|
||||||
|
end_control = control
|
||||||
1
addons/dialogic/Editor/Events/BranchEnd.gd.uid
Normal file
1
addons/dialogic/Editor/Events/BranchEnd.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cyjmcay08lmr8
|
||||||
30
addons/dialogic/Editor/Events/BranchEnd.tscn
Normal file
30
addons/dialogic/Editor/Events/BranchEnd.tscn
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[gd_scene format=3 uid="uid://de13fdeebrkcb"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cyjmcay08lmr8" path="res://addons/dialogic/Editor/Events/BranchEnd.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="EndBranch" type="HBoxContainer" unique_id=1281051416]
|
||||||
|
anchors_preset = 10
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_bottom = 24.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
mouse_filter = 0
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="Indent" type="Control" parent="." unique_id=1267249634]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
|
||||||
|
[node name="Spacer" type="Control" parent="." unique_id=818747539]
|
||||||
|
custom_minimum_size = Vector2(90, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 0
|
||||||
|
|
||||||
|
[node name="Icon" type="Button" parent="." unique_id=489933577]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(20, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 4
|
||||||
|
tooltip_text = "Click and drag"
|
||||||
|
focus_mode = 0
|
||||||
|
mouse_filter = 1
|
||||||
|
flat = true
|
||||||
484
addons/dialogic/Editor/Events/EventBlock/event_block.gd
Normal file
484
addons/dialogic/Editor/Events/EventBlock/event_block.gd
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
@tool
|
||||||
|
extends MarginContainer
|
||||||
|
|
||||||
|
## Scene that represents an event in the visual timeline editor.
|
||||||
|
|
||||||
|
signal content_changed()
|
||||||
|
|
||||||
|
## REFERENCES
|
||||||
|
var resource: DialogicEvent
|
||||||
|
var editor_reference
|
||||||
|
# for choice and condition
|
||||||
|
var end_node: Node = null:
|
||||||
|
get:
|
||||||
|
return end_node
|
||||||
|
set(node):
|
||||||
|
end_node = node
|
||||||
|
%ToggleChildrenVisibilityButton.visible = true if end_node else false
|
||||||
|
|
||||||
|
|
||||||
|
## FLAGS
|
||||||
|
var selected := false
|
||||||
|
# Whether the body is visible
|
||||||
|
var expanded := true
|
||||||
|
var body_was_build := false
|
||||||
|
var has_any_enabled_body_content := false
|
||||||
|
# Whether contained events (e.g. in choices) are visible
|
||||||
|
var collapsed := false
|
||||||
|
|
||||||
|
## CONSTANTS
|
||||||
|
const icon_size := 28
|
||||||
|
const indent_size := 22
|
||||||
|
|
||||||
|
## STATE
|
||||||
|
# List that stores visibility conditions
|
||||||
|
var field_list := []
|
||||||
|
var current_indent_level := 1
|
||||||
|
var contained_events := []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#region UI AND LOGIC INITIALIZATION
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if get_parent() is SubViewport:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not resource:
|
||||||
|
printerr("[Dialogic] Event block was added without a resource specified.")
|
||||||
|
return
|
||||||
|
|
||||||
|
initialize_ui()
|
||||||
|
initialize_logic()
|
||||||
|
|
||||||
|
|
||||||
|
func initialize_ui() -> void:
|
||||||
|
var _scale := DialogicUtil.get_editor_scale()
|
||||||
|
|
||||||
|
add_theme_constant_override("margin_bottom", DialogicUtil.get_editor_setting("event_block_margin", 0) * _scale)
|
||||||
|
|
||||||
|
%PanelContainer.self_modulate = get_theme_color("accent_color", "Editor")
|
||||||
|
|
||||||
|
# Warning Icon
|
||||||
|
%Warning.texture = get_theme_icon("NodeWarning", "EditorIcons")
|
||||||
|
%Warning.size = Vector2(16 * _scale, 16 * _scale)
|
||||||
|
%Warning.position = Vector2(-5 * _scale, -10 * _scale)
|
||||||
|
|
||||||
|
# Expand Button
|
||||||
|
%ToggleBodyVisibilityButton.icon = get_theme_icon("CodeFoldedRightArrow", "EditorIcons")
|
||||||
|
%ToggleBodyVisibilityButton.set("theme_override_colors/icon_normal_color", get_theme_color("contrast_color_2", "Editor"))
|
||||||
|
%ToggleBodyVisibilityButton.set("theme_override_colors/icon_hover_color", get_theme_color("accent_color", "Editor"))
|
||||||
|
%ToggleBodyVisibilityButton.set("theme_override_colors/icon_pressed_color", get_theme_color("contrast_color_2", "Editor"))
|
||||||
|
%ToggleBodyVisibilityButton.set("theme_override_colors/icon_hover_pressed_color", get_theme_color("accent_color", "Editor"))
|
||||||
|
%ToggleBodyVisibilityButton.add_theme_stylebox_override('hover_pressed', StyleBoxEmpty.new())
|
||||||
|
|
||||||
|
# Icon Panel
|
||||||
|
%IconPanel.tooltip_text = resource.event_name
|
||||||
|
%IconPanel.self_modulate = resource.event_color
|
||||||
|
|
||||||
|
# Event Icon
|
||||||
|
%IconTexture.texture = resource._get_icon()
|
||||||
|
|
||||||
|
%IconPanel.custom_minimum_size = Vector2(icon_size, icon_size) * _scale
|
||||||
|
%IconTexture.custom_minimum_size = %IconPanel.custom_minimum_size
|
||||||
|
|
||||||
|
var custom_style: StyleBoxFlat = %IconPanel.get_theme_stylebox("panel")
|
||||||
|
custom_style.set_corner_radius_all(5 * _scale)
|
||||||
|
|
||||||
|
# Focus Mode
|
||||||
|
set_focus_mode(1) # Allowing this node to grab focus
|
||||||
|
|
||||||
|
# Separation on the header
|
||||||
|
%Header.add_theme_constant_override("custom_constants/separation", 5 * _scale)
|
||||||
|
|
||||||
|
# Collapse Button
|
||||||
|
%ToggleChildrenVisibilityButton.toggled.connect(_on_collapse_toggled)
|
||||||
|
%ToggleChildrenVisibilityButton.icon = get_theme_icon("ExpandTree", "EditorIcons")
|
||||||
|
%ToggleChildrenVisibilityButton.hide()
|
||||||
|
|
||||||
|
%Body.add_theme_constant_override("margin_left", icon_size * _scale)
|
||||||
|
|
||||||
|
visual_deselect()
|
||||||
|
update_contain_ui()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func initialize_logic() -> void:
|
||||||
|
resized.connect(func(): await get_tree().process_frame;get_parent().get_parent().queue_redraw())
|
||||||
|
|
||||||
|
resource.ui_update_needed.connect(_on_resource_ui_update_needed)
|
||||||
|
resource.ui_update_warning.connect(set_warning)
|
||||||
|
|
||||||
|
content_changed.connect(recalculate_field_visibility)
|
||||||
|
|
||||||
|
_on_ToggleBodyVisibility_toggled(resource.expand_by_default or resource.created_by_button and not resource.collapse_on_create)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region VISUAL METHODS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func visual_select() -> void:
|
||||||
|
%PanelContainer.add_theme_stylebox_override("panel", load("res://addons/dialogic/Editor/Events/styles/selected_styleboxflat.tres"))
|
||||||
|
selected = true
|
||||||
|
%IconPanel.self_modulate = resource.event_color
|
||||||
|
%IconTexture.modulate = get_theme_color("icon_saturation", "Editor")
|
||||||
|
|
||||||
|
|
||||||
|
func visual_deselect() -> void:
|
||||||
|
%PanelContainer.add_theme_stylebox_override("panel", load("res://addons/dialogic/Editor/Events/styles/unselected_stylebox.tres"))
|
||||||
|
selected = false
|
||||||
|
%IconPanel.self_modulate = resource.event_color.lerp(Color.DARK_SLATE_GRAY, 0.1)
|
||||||
|
%IconTexture.modulate = get_theme_color("font_color", "Label")
|
||||||
|
|
||||||
|
|
||||||
|
func is_selected() -> bool:
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
func set_warning(text:String= "") -> void:
|
||||||
|
if not text.is_empty():
|
||||||
|
%Warning.show()
|
||||||
|
%Warning.tooltip_text = text
|
||||||
|
else:
|
||||||
|
%Warning.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func set_indent(indent: int) -> void:
|
||||||
|
add_theme_constant_override("margin_left", indent_size * indent * DialogicUtil.get_editor_scale())
|
||||||
|
current_indent_level = indent
|
||||||
|
post_indent_update()
|
||||||
|
|
||||||
|
|
||||||
|
func post_indent_update() -> void:
|
||||||
|
%ToggleChildrenVisibilityButton.visible = end_node and contained_events
|
||||||
|
update_contain_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func update_contain_ui():
|
||||||
|
if end_node and ((contained_events and collapsed) or (not contained_events and not collapsed)):
|
||||||
|
add_theme_constant_override("margin_bottom", 30)
|
||||||
|
else:
|
||||||
|
add_theme_constant_override("margin_bottom", 0)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
if selected and end_node and not contained_events and not collapsed:
|
||||||
|
draw_string(
|
||||||
|
get_theme_default_font(),
|
||||||
|
Vector2(
|
||||||
|
%HeaderContent.global_position.x-global_position.x,
|
||||||
|
%VBox.global_position.y-global_position.y+%VBox.size.y + get_theme_default_font_size()+10),
|
||||||
|
"Add Events Here",
|
||||||
|
HORIZONTAL_ALIGNMENT_LEFT,
|
||||||
|
-1,
|
||||||
|
get_theme_default_font_size(), get_theme_color("font_placeholder_color", "Editor"))
|
||||||
|
|
||||||
|
if end_node and contained_events and collapsed:
|
||||||
|
var start_pos := Vector2(
|
||||||
|
%HeaderContent.global_position.x-global_position.x,
|
||||||
|
%VBox.global_position.y-global_position.y+%VBox.size.y+10)
|
||||||
|
var offset := Vector2()
|
||||||
|
var stylebox := StyleBoxFlat.new()
|
||||||
|
stylebox.set_corner_radius_all(4)
|
||||||
|
stylebox.bg_color = get_theme_color("disabled_bg_color", "Editor")
|
||||||
|
for i:DialogicEvent in contained_events:
|
||||||
|
if i is DialogicEndBranchEvent: continue
|
||||||
|
draw_style_box(stylebox, Rect2(start_pos+offset, Vector2(25,25)))
|
||||||
|
draw_texture_rect(i._get_icon(), Rect2(start_pos+offset, Vector2(25,25)), false, get_theme_color("font_placeholder_color", "Editor").lerp(i.event_color, 0.5))
|
||||||
|
offset += Vector2(28, 0)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region EVENT FIELDS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
var FIELD_SCENES := {
|
||||||
|
DialogicEvent.ValueType.MULTILINE_TEXT: "res://addons/dialogic/Editor/Events/Fields/field_text_multiline.tscn",
|
||||||
|
DialogicEvent.ValueType.SINGLELINE_TEXT: "res://addons/dialogic/Editor/Events/Fields/field_text_singleline.tscn",
|
||||||
|
DialogicEvent.ValueType.FILE: "res://addons/dialogic/Editor/Events/Fields/field_file.tscn",
|
||||||
|
DialogicEvent.ValueType.BOOL: "res://addons/dialogic/Editor/Events/Fields/field_bool_check.tscn",
|
||||||
|
DialogicEvent.ValueType.BOOL_BUTTON: "res://addons/dialogic/Editor/Events/Fields/field_bool_button.tscn",
|
||||||
|
DialogicEvent.ValueType.CONDITION: "res://addons/dialogic/Editor/Events/Fields/field_condition.tscn",
|
||||||
|
DialogicEvent.ValueType.ARRAY: "res://addons/dialogic/Editor/Events/Fields/field_array.tscn",
|
||||||
|
DialogicEvent.ValueType.DICTIONARY: "res://addons/dialogic/Editor/Events/Fields/field_dictionary.tscn",
|
||||||
|
DialogicEvent.ValueType.DYNAMIC_OPTIONS: "res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn",
|
||||||
|
DialogicEvent.ValueType.FIXED_OPTIONS : "res://addons/dialogic/Editor/Events/Fields/field_options_fixed.tscn",
|
||||||
|
DialogicEvent.ValueType.NUMBER: "res://addons/dialogic/Editor/Events/Fields/field_number.tscn",
|
||||||
|
DialogicEvent.ValueType.VECTOR2: "res://addons/dialogic/Editor/Events/Fields/field_vector2.tscn",
|
||||||
|
DialogicEvent.ValueType.VECTOR3: "res://addons/dialogic/Editor/Events/Fields/field_vector3.tscn",
|
||||||
|
DialogicEvent.ValueType.VECTOR4: "res://addons/dialogic/Editor/Events/Fields/field_vector4.tscn",
|
||||||
|
DialogicEvent.ValueType.COLOR: "res://addons/dialogic/Editor/Events/Fields/field_color.tscn",
|
||||||
|
DialogicEvent.ValueType.AUDIO_PREVIEW: "res://addons/dialogic/Editor/Events/Fields/field_audio_preview.tscn",
|
||||||
|
DialogicEvent.ValueType.IMAGE_PREVIEW: "res://addons/dialogic/Editor/Events/Fields/field_image_preview.tscn",
|
||||||
|
}
|
||||||
|
|
||||||
|
func build_editor(build_header:bool = true, build_body:bool = false) -> void:
|
||||||
|
var current_body_container: HFlowContainer = null
|
||||||
|
|
||||||
|
if build_body and body_was_build:
|
||||||
|
build_body = false
|
||||||
|
|
||||||
|
if build_body:
|
||||||
|
if body_was_build:
|
||||||
|
return
|
||||||
|
current_body_container = HFlowContainer.new()
|
||||||
|
%BodyContent.add_child(current_body_container)
|
||||||
|
body_was_build = true
|
||||||
|
|
||||||
|
for p in resource.get_event_editor_info():
|
||||||
|
field_list.append({'node':null, 'location':p.location})
|
||||||
|
if p.has('condition'):
|
||||||
|
field_list[-1]['condition'] = p.condition
|
||||||
|
|
||||||
|
if !build_body and p.location == 1:
|
||||||
|
continue
|
||||||
|
elif !build_header and p.location == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
### --------------------------------------------------------------------
|
||||||
|
### 1. CREATE A NODE OF THE CORRECT TYPE FOR THE PROPERTY
|
||||||
|
var editor_node: Control
|
||||||
|
|
||||||
|
### LINEBREAK
|
||||||
|
if p.name == "linebreak":
|
||||||
|
field_list.remove_at(field_list.size()-1)
|
||||||
|
if !current_body_container.get_child_count():
|
||||||
|
current_body_container.queue_free()
|
||||||
|
current_body_container = HFlowContainer.new()
|
||||||
|
%BodyContent.add_child(current_body_container)
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif p.field_type in FIELD_SCENES:
|
||||||
|
editor_node = load(FIELD_SCENES[p.field_type]).instantiate()
|
||||||
|
|
||||||
|
elif p.field_type == resource.ValueType.LABEL:
|
||||||
|
editor_node = Label.new()
|
||||||
|
editor_node.text = p.display_info.text
|
||||||
|
editor_node.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
editor_node.set('custom_colors/font_color', Color("#7b7b7b"))
|
||||||
|
editor_node.add_theme_color_override('font_color', resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.8))
|
||||||
|
|
||||||
|
elif p.field_type == resource.ValueType.BUTTON:
|
||||||
|
editor_node = Button.new()
|
||||||
|
editor_node.text = p.display_info.text
|
||||||
|
editor_node.tooltip_text = p.display_info.get('tooltip', '')
|
||||||
|
if typeof(p.display_info.icon) == TYPE_ARRAY:
|
||||||
|
editor_node.icon = callv('get_theme_icon', p.display_info.icon)
|
||||||
|
else:
|
||||||
|
editor_node.icon = p.display_info.icon
|
||||||
|
editor_node.flat = true
|
||||||
|
editor_node.custom_minimum_size.x = 30 * DialogicUtil.get_editor_scale()
|
||||||
|
editor_node.pressed.connect(p.display_info.callable)
|
||||||
|
|
||||||
|
## CUSTOM
|
||||||
|
elif p.field_type == resource.ValueType.CUSTOM:
|
||||||
|
if p.display_info.has('path'):
|
||||||
|
editor_node = load(p.display_info.path).instantiate()
|
||||||
|
|
||||||
|
## ELSE
|
||||||
|
else:
|
||||||
|
editor_node = Label.new()
|
||||||
|
editor_node.text = p.name
|
||||||
|
editor_node.add_theme_color_override('font_color', resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.8))
|
||||||
|
|
||||||
|
|
||||||
|
field_list[-1]['node'] = editor_node
|
||||||
|
### --------------------------------------------------------------------
|
||||||
|
# Some things need to be called BEFORE the field is added to the tree
|
||||||
|
if editor_node is DialogicVisualEditorField:
|
||||||
|
editor_node.event_resource = resource
|
||||||
|
|
||||||
|
editor_node.property_name = p.name
|
||||||
|
field_list[-1]['property'] = p.name
|
||||||
|
|
||||||
|
editor_node._load_display_info(p.display_info)
|
||||||
|
|
||||||
|
var location: Control = %HeaderContent
|
||||||
|
if p.location == 1:
|
||||||
|
location = current_body_container
|
||||||
|
location.add_child(editor_node)
|
||||||
|
|
||||||
|
# Some things need to be called AFTER the field is added to the tree
|
||||||
|
if editor_node is DialogicVisualEditorField:
|
||||||
|
# Only set the value if the field is visible
|
||||||
|
#
|
||||||
|
# This prevents events with varied value types (event_setting, event_variable)
|
||||||
|
# from injecting incorrect types into hidden fields, which then throw errors
|
||||||
|
# in the console.
|
||||||
|
if p.has('condition') and not p.condition.is_empty():
|
||||||
|
if _evaluate_visibility_condition(p):
|
||||||
|
editor_node._set_value(resource.get(p.name))
|
||||||
|
else:
|
||||||
|
editor_node._set_value(resource.get(p.name))
|
||||||
|
|
||||||
|
editor_node.value_changed.connect(set_property)
|
||||||
|
|
||||||
|
editor_node.tooltip_text = p.display_info.get('tooltip', '')
|
||||||
|
|
||||||
|
# Apply autofocus
|
||||||
|
if resource.created_by_button and p.display_info.get('autofocus', false):
|
||||||
|
editor_node.call_deferred('take_autofocus')
|
||||||
|
|
||||||
|
### --------------------------------------------------------------------
|
||||||
|
### 4. ADD LEFT AND RIGHT TEXT
|
||||||
|
var left_label: Label = null
|
||||||
|
var right_label: Label = null
|
||||||
|
if !p.get('left_text', '').is_empty():
|
||||||
|
left_label = Label.new()
|
||||||
|
left_label.text = p.get('left_text')
|
||||||
|
left_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
left_label.add_theme_color_override('font_color', resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.8))
|
||||||
|
location.add_child(left_label)
|
||||||
|
location.move_child(left_label, editor_node.get_index())
|
||||||
|
|
||||||
|
if !p.get('right_text', '').is_empty():
|
||||||
|
right_label = Label.new()
|
||||||
|
right_label.text = p.get('right_text')
|
||||||
|
right_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
right_label.add_theme_color_override('font_color', resource.event_color.lerp(get_theme_color("font_color", "Editor"), 0.8))
|
||||||
|
location.add_child(right_label)
|
||||||
|
location.move_child(right_label, editor_node.get_index()+1)
|
||||||
|
|
||||||
|
### --------------------------------------------------------------------
|
||||||
|
### 5. REGISTER CONDITION
|
||||||
|
if p.has('condition'):
|
||||||
|
field_list[-1]['condition'] = p.condition
|
||||||
|
if left_label:
|
||||||
|
field_list.append({'node': left_label, 'condition':p.condition, 'location':p.location})
|
||||||
|
if right_label:
|
||||||
|
field_list.append({'node': right_label, 'condition':p.condition, 'location':p.location})
|
||||||
|
|
||||||
|
|
||||||
|
if build_body:
|
||||||
|
if current_body_container.get_child_count() == 0:
|
||||||
|
expanded = false
|
||||||
|
%Body.visible = false
|
||||||
|
|
||||||
|
update_contain_ui()
|
||||||
|
|
||||||
|
recalculate_field_visibility()
|
||||||
|
|
||||||
|
|
||||||
|
func recalculate_field_visibility() -> void:
|
||||||
|
has_any_enabled_body_content = false
|
||||||
|
for p in field_list:
|
||||||
|
if not p.has("condition") or p.condition.is_empty():
|
||||||
|
if p.node != null:
|
||||||
|
p.node.show()
|
||||||
|
if p.location == 1:
|
||||||
|
has_any_enabled_body_content = true
|
||||||
|
else:
|
||||||
|
if _evaluate_visibility_condition(p):
|
||||||
|
if p.node != null:
|
||||||
|
if p.node.visible == false and p.has("property"):
|
||||||
|
p.node._set_value(resource.get(p.property))
|
||||||
|
p.node.show()
|
||||||
|
if p.location == 1:
|
||||||
|
has_any_enabled_body_content = true
|
||||||
|
else:
|
||||||
|
if p.node != null:
|
||||||
|
p.node.hide()
|
||||||
|
%ToggleBodyVisibilityButton.visible = has_any_enabled_body_content
|
||||||
|
|
||||||
|
|
||||||
|
func set_property(property_name:String, value:Variant) -> void:
|
||||||
|
resource.set(property_name, value)
|
||||||
|
content_changed.emit()
|
||||||
|
if end_node:
|
||||||
|
end_node.parent_node_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func _evaluate_visibility_condition(p: Dictionary) -> bool:
|
||||||
|
var expr := Expression.new()
|
||||||
|
expr.parse(p.condition)
|
||||||
|
var result: bool
|
||||||
|
if expr.execute([], resource):
|
||||||
|
result = true
|
||||||
|
else:
|
||||||
|
result = false
|
||||||
|
if expr.has_execute_failed():
|
||||||
|
printerr("[Dialogic] Failed executing visibility condition for '",p.get("property", "unnamed"),"': " + expr.get_error_text())
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func get_field_node(property_name:String) -> Node:
|
||||||
|
for i in field_list:
|
||||||
|
if i.get("property", "") == property_name:
|
||||||
|
return i.node
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _on_resource_ui_update_needed() -> void:
|
||||||
|
for node_info in field_list:
|
||||||
|
if node_info.node and node_info.node.has_method("set_value"):
|
||||||
|
# Only set the value if the field is visible
|
||||||
|
#
|
||||||
|
# This prevents events with varied value types (event_setting, event_variable)
|
||||||
|
# from injecting incorrect types into hidden fields, which then throw errors
|
||||||
|
# in the console.
|
||||||
|
if node_info.has("condition") and not node_info.condition.is_empty():
|
||||||
|
if _evaluate_visibility_condition(node_info):
|
||||||
|
node_info.node.set_value(resource.get(node_info.property))
|
||||||
|
else:
|
||||||
|
node_info.node.set_value(resource.get(node_info.property))
|
||||||
|
recalculate_field_visibility()
|
||||||
|
|
||||||
|
|
||||||
|
#region SIGNALS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func _on_collapse_toggled(toggled:bool) -> void:
|
||||||
|
collapsed = toggled
|
||||||
|
var timeline_editor: Node = find_parent("VisualEditor")
|
||||||
|
if (timeline_editor != null):
|
||||||
|
# @todo select item and clear selection is marked as "private" in TimelineEditor.gd
|
||||||
|
# consider to make it "public" or add a public helper function
|
||||||
|
timeline_editor.indent_events()
|
||||||
|
%ToggleChildrenVisibilityButton.icon = get_theme_icon("ExpandTree", "EditorIcons") if toggled else get_theme_icon("CollapseTree", "EditorIcons")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _on_ToggleBodyVisibility_toggled(button_pressed:bool) -> void:
|
||||||
|
if button_pressed and not body_was_build:
|
||||||
|
build_editor(false, true)
|
||||||
|
%ToggleBodyVisibilityButton.set_pressed_no_signal(button_pressed)
|
||||||
|
|
||||||
|
if button_pressed:
|
||||||
|
%ToggleBodyVisibilityButton.icon = get_theme_icon("CodeFoldDownArrow", "EditorIcons")
|
||||||
|
else:
|
||||||
|
%ToggleBodyVisibilityButton.icon = get_theme_icon("CodeFoldedRightArrow", "EditorIcons")
|
||||||
|
|
||||||
|
expanded = button_pressed
|
||||||
|
%Body.visible = button_pressed
|
||||||
|
|
||||||
|
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
if find_parent("TimelineArea") != null:
|
||||||
|
find_parent("TimelineArea").queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _on_EventNode_gui_input(event:InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.is_pressed() and event.button_index == 1:
|
||||||
|
grab_focus() # Grab focus to avoid copy pasting text or events
|
||||||
|
if event.double_click:
|
||||||
|
if has_any_enabled_body_content:
|
||||||
|
_on_ToggleBodyVisibility_toggled(!expanded)
|
||||||
|
# For opening the context menu
|
||||||
|
if event is InputEventMouseButton:
|
||||||
|
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
|
||||||
|
var popup: PopupMenu = get_parent().get_parent().get_node('EventPopupMenu')
|
||||||
|
popup.current_event = self
|
||||||
|
popup.popup_on_parent(Rect2(get_global_mouse_position(),Vector2()))
|
||||||
|
if resource.help_page_path == "":
|
||||||
|
popup.set_item_disabled(4, true)
|
||||||
|
else:
|
||||||
|
popup.set_item_disabled(4, false)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dbncx2w0btjyx
|
||||||
121
addons/dialogic/Editor/Events/EventBlock/event_block.tscn
Normal file
121
addons/dialogic/Editor/Events/EventBlock/event_block.tscn
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
[gd_scene format=3 uid="uid://bwaxj1n401fp4"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dbncx2w0btjyx" path="res://addons/dialogic/Editor/Events/EventBlock/event_block.gd" id="1"]
|
||||||
|
[ext_resource type="StyleBox" uid="uid://cl75ikyq2is7c" path="res://addons/dialogic/Editor/Events/styles/unselected_stylebox.tres" id="2_axj84"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://dybg3l5pwetne" path="res://addons/dialogic/Editor/Images/plugin-icon.svg" id="6"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_otutu"]
|
||||||
|
bg_color = Color(1, 1, 1, 1)
|
||||||
|
corner_radius_top_left = 5
|
||||||
|
corner_radius_top_right = 5
|
||||||
|
corner_radius_bottom_right = 5
|
||||||
|
corner_radius_bottom_left = 5
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ee4ub"]
|
||||||
|
|
||||||
|
[node name="EventNode" type="MarginContainer" unique_id=76538949]
|
||||||
|
anchors_preset = 10
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_bottom = 34.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 9
|
||||||
|
focus_mode = 1
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="PanelContainer" type="PanelContainer" parent="." unique_id=1573085599]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
self_modulate = Color(0, 0, 0, 1)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
mouse_filter = 2
|
||||||
|
theme_override_styles/panel = ExtResource("2_axj84")
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="PanelContainer" unique_id=1513215857]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Header" type="HBoxContainer" parent="PanelContainer/VBox" unique_id=430202985]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="IconPanel" type="Panel" parent="PanelContainer/VBox/Header" unique_id=1917445070]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
size_flags_vertical = 4
|
||||||
|
mouse_filter = 1
|
||||||
|
mouse_default_cursor_shape = 6
|
||||||
|
theme_override_styles/panel = SubResource("StyleBoxFlat_otutu")
|
||||||
|
|
||||||
|
[node name="IconTexture" type="TextureRect" parent="PanelContainer/VBox/Header/IconPanel" unique_id=329739635]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 0
|
||||||
|
grow_vertical = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
texture = ExtResource("6")
|
||||||
|
expand_mode = 1
|
||||||
|
stretch_mode = 5
|
||||||
|
|
||||||
|
[node name="Warning" type="TextureRect" parent="PanelContainer/VBox/Header/IconPanel" unique_id=610199099]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = -5.5
|
||||||
|
offset_top = -11.0
|
||||||
|
offset_right = 12.1
|
||||||
|
offset_bottom = 6.6
|
||||||
|
stretch_mode = 5
|
||||||
|
|
||||||
|
[node name="HeaderContent" type="HBoxContainer" parent="PanelContainer/VBox/Header" unique_id=450894660]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ToggleBodyVisibilityButton" type="Button" parent="PanelContainer/VBox/Header" unique_id=1546027131]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(20, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
tooltip_text = "Fold/Unfold Settings"
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxEmpty_ee4ub")
|
||||||
|
theme_override_styles/pressed = SubResource("StyleBoxEmpty_ee4ub")
|
||||||
|
theme_override_styles/hover = SubResource("StyleBoxEmpty_ee4ub")
|
||||||
|
theme_override_styles/disabled = SubResource("StyleBoxEmpty_ee4ub")
|
||||||
|
theme_override_styles/focus = SubResource("StyleBoxEmpty_ee4ub")
|
||||||
|
toggle_mode = true
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="ToggleChildrenVisibilityButton" type="Button" parent="PanelContainer/VBox/Header" unique_id=2142489134]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 10
|
||||||
|
tooltip_text = "Collapse Contained Events"
|
||||||
|
toggle_mode = true
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="Body" type="MarginContainer" parent="PanelContainer/VBox" unique_id=447556959]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_constants/margin_left = 4
|
||||||
|
|
||||||
|
[node name="BodyContent" type="VBoxContainer" parent="PanelContainer/VBox/Body" unique_id=1278785126]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[connection signal="gui_input" from="." to="." method="_on_EventNode_gui_input"]
|
||||||
|
[connection signal="toggled" from="PanelContainer/VBox/Header/ToggleBodyVisibilityButton" to="." method="_on_ToggleBodyVisibility_toggled"]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
@tool
|
||||||
|
extends PopupMenu
|
||||||
|
|
||||||
|
var current_event: Node = null
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
clear()
|
||||||
|
add_icon_item(get_theme_icon("Duplicate", "EditorIcons"), "Duplicate", 0)
|
||||||
|
add_separator()
|
||||||
|
add_icon_item(get_theme_icon("PlayStart", "EditorIcons"), "Play from here", 1)
|
||||||
|
add_separator()
|
||||||
|
add_icon_item(get_theme_icon("Help", "EditorIcons"), "Documentation", 2)
|
||||||
|
add_icon_item(get_theme_icon("CodeHighlighter", "EditorIcons"), "Open Code", 3)
|
||||||
|
add_separator()
|
||||||
|
add_icon_item(get_theme_icon("ArrowUp", "EditorIcons"), "Move up", 4)
|
||||||
|
add_icon_item(get_theme_icon("ArrowDown", "EditorIcons"), "Move down", 5)
|
||||||
|
add_separator()
|
||||||
|
add_icon_item(get_theme_icon("Remove", "EditorIcons"), "Delete", 6)
|
||||||
|
|
||||||
|
var menu_background := StyleBoxFlat.new()
|
||||||
|
menu_background.bg_color = get_parent().get_theme_color("base_color", "Editor")
|
||||||
|
add_theme_stylebox_override('panel', menu_background)
|
||||||
|
add_theme_stylebox_override('hover', get_theme_stylebox("FocusViewport", "EditorStyles"))
|
||||||
|
add_theme_color_override('font_color_hover', get_parent().get_theme_color("accent_color", "Editor"))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://n1knm2ohcehu
|
||||||
28
addons/dialogic/Editor/Events/Fields/array_part.gd
Normal file
28
addons/dialogic/Editor/Events/Fields/array_part.gd
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
@tool
|
||||||
|
extends PanelContainer
|
||||||
|
|
||||||
|
## Event block field part for the Array field.
|
||||||
|
|
||||||
|
signal value_changed()
|
||||||
|
|
||||||
|
var value_field: Node
|
||||||
|
var value_type: int = -1
|
||||||
|
|
||||||
|
var current_value: Variant
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
%FlexValue.value_changed.connect(emit_signal.bind("value_changed"))
|
||||||
|
%Delete.icon = get_theme_icon("Remove", "EditorIcons")
|
||||||
|
|
||||||
|
|
||||||
|
func set_value(value:Variant):
|
||||||
|
%FlexValue.set_value(value)
|
||||||
|
|
||||||
|
|
||||||
|
func get_value() -> Variant:
|
||||||
|
return %FlexValue.current_value
|
||||||
|
|
||||||
|
|
||||||
|
func _on_delete_pressed() -> void:
|
||||||
|
queue_free()
|
||||||
|
value_changed.emit()
|
||||||
1
addons/dialogic/Editor/Events/Fields/array_part.gd.uid
Normal file
1
addons/dialogic/Editor/Events/Fields/array_part.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cm8w2iamuulp7
|
||||||
39
addons/dialogic/Editor/Events/Fields/array_part.tscn
Normal file
39
addons/dialogic/Editor/Events/Fields/array_part.tscn
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
[gd_scene load_steps=5 format=3 uid="uid://ch4j2lesn1sis"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cm8w2iamuulp7" path="res://addons/dialogic/Editor/Events/Fields/array_part.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dl08ubinx6ugu" path="res://addons/dialogic/Editor/Events/Fields/field_flex_value.tscn" id="3_s4j7i"]
|
||||||
|
|
||||||
|
[sub_resource type="Image" id="Image_28ws6"]
|
||||||
|
data = {
|
||||||
|
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||||
|
"format": "RGBA8",
|
||||||
|
"height": 16,
|
||||||
|
"mipmaps": false,
|
||||||
|
"width": 16
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="ImageTexture" id="ImageTexture_cpbga"]
|
||||||
|
image = SubResource("Image_28ws6")
|
||||||
|
|
||||||
|
[node name="ArrayValue" type="PanelContainer"]
|
||||||
|
offset_left = 2.0
|
||||||
|
offset_right = 76.0
|
||||||
|
offset_bottom = 24.0
|
||||||
|
theme_type_variation = &"DialogicEventEditGroup"
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="Value" type="HBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="FlexValue" parent="Value" instance=ExtResource("3_s4j7i")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Delete" type="Button" parent="Value"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Remove"
|
||||||
|
icon = SubResource("ImageTexture_cpbga")
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[connection signal="pressed" from="Value/Delete" to="." method="_on_delete_pressed"]
|
||||||
43
addons/dialogic/Editor/Events/Fields/dictionary_part.gd
Normal file
43
addons/dialogic/Editor/Events/Fields/dictionary_part.gd
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
@tool
|
||||||
|
extends PanelContainer
|
||||||
|
|
||||||
|
## Event block field part for the Dictionary field.
|
||||||
|
|
||||||
|
signal value_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func set_key(value:String) -> void:
|
||||||
|
%Key.text = str(value)
|
||||||
|
|
||||||
|
|
||||||
|
func get_key() -> String:
|
||||||
|
return %Key.text
|
||||||
|
|
||||||
|
|
||||||
|
func set_value(value:Variant) -> void:
|
||||||
|
%FlexValue.set_value(value)
|
||||||
|
|
||||||
|
|
||||||
|
func get_value() -> Variant:
|
||||||
|
return %FlexValue.current_value
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
%Delete.icon = get_theme_icon("Remove", "EditorIcons")
|
||||||
|
|
||||||
|
|
||||||
|
func focus_key() -> void:
|
||||||
|
%Key.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_key_text_changed(new_text: String) -> void:
|
||||||
|
value_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_flex_value_value_changed() -> void:
|
||||||
|
value_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_delete_pressed() -> void:
|
||||||
|
queue_free()
|
||||||
|
value_changed.emit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b41laec1d54io
|
||||||
54
addons/dialogic/Editor/Events/Fields/dictionary_part.tscn
Normal file
54
addons/dialogic/Editor/Events/Fields/dictionary_part.tscn
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
[gd_scene load_steps=5 format=3 uid="uid://b27yweami3mxi"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b41laec1d54io" path="res://addons/dialogic/Editor/Events/Fields/dictionary_part.gd" id="2_q88pg"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dl08ubinx6ugu" path="res://addons/dialogic/Editor/Events/Fields/field_flex_value.tscn" id="3_p082d"]
|
||||||
|
|
||||||
|
[sub_resource type="Image" id="Image_teqf1"]
|
||||||
|
data = {
|
||||||
|
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||||
|
"format": "RGBA8",
|
||||||
|
"height": 16,
|
||||||
|
"mipmaps": false,
|
||||||
|
"width": 16
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="ImageTexture" id="ImageTexture_cpbga"]
|
||||||
|
image = SubResource("Image_teqf1")
|
||||||
|
|
||||||
|
[node name="DictionaryPart" type="PanelContainer"]
|
||||||
|
offset_left = 1.0
|
||||||
|
offset_top = -1.0
|
||||||
|
offset_right = 131.0
|
||||||
|
offset_bottom = 32.0
|
||||||
|
theme_type_variation = &"DialogicEventEditGroup"
|
||||||
|
script = ExtResource("2_q88pg")
|
||||||
|
|
||||||
|
[node name="HBox" type="HBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Key" type="LineEdit" parent="HBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_type_variation = &"DialogicEventEdit"
|
||||||
|
expand_to_text_length = true
|
||||||
|
select_all_on_focus = true
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = ":"
|
||||||
|
|
||||||
|
[node name="FlexValue" parent="HBox" instance=ExtResource("3_p082d")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Delete" type="Button" parent="HBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Remove"
|
||||||
|
icon = SubResource("ImageTexture_cpbga")
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[connection signal="text_changed" from="HBox/Key" to="." method="_on_key_text_changed"]
|
||||||
|
[connection signal="value_changed" from="HBox/FlexValue" to="." method="_on_flex_value_value_changed"]
|
||||||
|
[connection signal="pressed" from="HBox/Delete" to="." method="_on_delete_pressed"]
|
||||||
48
addons/dialogic/Editor/Events/Fields/field_array.gd
Normal file
48
addons/dialogic/Editor/Events/Fields/field_array.gd
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
@tool
|
||||||
|
extends DialogicVisualEditorField
|
||||||
|
|
||||||
|
## Event block field for editing arrays.
|
||||||
|
|
||||||
|
|
||||||
|
const ArrayValue := "res://addons/dialogic/Editor/Events/Fields/array_part.tscn"
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
%Add.icon = get_theme_icon("Add", "EditorIcons")
|
||||||
|
%Add.pressed.connect(_on_AddButton_pressed)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_value(value:Variant) -> void:
|
||||||
|
value = value as Array
|
||||||
|
for child in get_children():
|
||||||
|
if child != %Add:
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
for item in value:
|
||||||
|
var x: Node = load(ArrayValue).instantiate()
|
||||||
|
add_child(x)
|
||||||
|
x.set_value(item)
|
||||||
|
x.value_changed.connect(recalculate_values)
|
||||||
|
move_child(%Add, -1)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_value_changed(value:Variant) -> void:
|
||||||
|
value_changed.emit(property_name, value)
|
||||||
|
|
||||||
|
|
||||||
|
func recalculate_values() -> void:
|
||||||
|
var arr := []
|
||||||
|
for child in get_children():
|
||||||
|
if child != %Add and !child.is_queued_for_deletion():
|
||||||
|
arr.append(child.get_value())
|
||||||
|
_on_value_changed(arr)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_AddButton_pressed() -> void:
|
||||||
|
var x: Control = load(ArrayValue).instantiate()
|
||||||
|
add_child(x)
|
||||||
|
x.set_value("")
|
||||||
|
x.value_changed.connect(recalculate_values)
|
||||||
|
recalculate_values()
|
||||||
|
move_child(%Add, -1)
|
||||||
|
|
||||||
1
addons/dialogic/Editor/Events/Fields/field_array.gd.uid
Normal file
1
addons/dialogic/Editor/Events/Fields/field_array.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://kmn7rns1g4fc
|
||||||
28
addons/dialogic/Editor/Events/Fields/field_array.tscn
Normal file
28
addons/dialogic/Editor/Events/Fields/field_array.tscn
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[gd_scene load_steps=4 format=3 uid="uid://btmy7ageqpyq1"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://kmn7rns1g4fc" path="res://addons/dialogic/Editor/Events/Fields/field_array.gd" id="2"]
|
||||||
|
|
||||||
|
[sub_resource type="Image" id="Image_v6fhx"]
|
||||||
|
data = {
|
||||||
|
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||||
|
"format": "RGBA8",
|
||||||
|
"height": 16,
|
||||||
|
"mipmaps": false,
|
||||||
|
"width": 16
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="ImageTexture" id="ImageTexture_cpbga"]
|
||||||
|
image = SubResource("Image_v6fhx")
|
||||||
|
|
||||||
|
[node name="Field_Array" type="HFlowContainer"]
|
||||||
|
offset_right = 329.0
|
||||||
|
offset_bottom = 256.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
script = ExtResource("2")
|
||||||
|
|
||||||
|
[node name="Add" type="Button" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Add value"
|
||||||
|
icon = SubResource("ImageTexture_cpbga")
|
||||||
|
flat = true
|
||||||
52
addons/dialogic/Editor/Events/Fields/field_audio_preview.gd
Normal file
52
addons/dialogic/Editor/Events/Fields/field_audio_preview.gd
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
@tool
|
||||||
|
extends DialogicVisualEditorField
|
||||||
|
|
||||||
|
|
||||||
|
var file_path: String
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
self.pressed.connect(_on_pressed)
|
||||||
|
%AudioStreamPlayer.finished.connect(_on_finished)
|
||||||
|
|
||||||
|
|
||||||
|
#region OVERWRITES
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
## To be overwritten
|
||||||
|
func _set_value(value:Variant) -> void:
|
||||||
|
file_path = value
|
||||||
|
self.disabled = file_path.is_empty()
|
||||||
|
_stop()
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region SIGNAL METHODS
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
func _on_pressed() -> void:
|
||||||
|
if %AudioStreamPlayer.playing:
|
||||||
|
_stop()
|
||||||
|
elif not file_path.is_empty():
|
||||||
|
_play()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_finished() -> void:
|
||||||
|
_stop()
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
func _stop() -> void:
|
||||||
|
%AudioStreamPlayer.stop()
|
||||||
|
%AudioStreamPlayer.stream = null
|
||||||
|
self.icon = get_theme_icon("Play", "EditorIcons")
|
||||||
|
|
||||||
|
|
||||||
|
func _play() -> void:
|
||||||
|
if ResourceLoader.exists(file_path):
|
||||||
|
%AudioStreamPlayer.stream = load(file_path)
|
||||||
|
%AudioStreamPlayer.play()
|
||||||
|
self.icon = get_theme_icon("Stop", "EditorIcons")
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user