Merge remote-tracking branch 'origin/main' into mimosa
This commit is contained in:
commit
94dc4e774e
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/addons/Wol/editor/WolEditor.tscn filter=lfs diff=lfs merge=lfs -text
|
10
Dialogue.gd
10
Dialogue.gd
|
@ -4,6 +4,10 @@ func _ready():
|
||||||
$RichTextLabel/Logo.hide()
|
$RichTextLabel/Logo.hide()
|
||||||
$VBoxContainer/ButtonTemplate.hide()
|
$VBoxContainer/ButtonTemplate.hide()
|
||||||
|
|
||||||
|
$Wol.connect('line', self, '_on_line')
|
||||||
|
$Wol.connect('options', self, '_on_options')
|
||||||
|
$Wol.connect('finished', self, '_on_finished')
|
||||||
|
|
||||||
func continue_dialogue():
|
func continue_dialogue():
|
||||||
if $Tween.is_active():
|
if $Tween.is_active():
|
||||||
$Tween.remove_all()
|
$Tween.remove_all()
|
||||||
|
@ -12,7 +16,7 @@ func continue_dialogue():
|
||||||
|
|
||||||
$Wol.resume()
|
$Wol.resume()
|
||||||
|
|
||||||
func _on_Wol_line(line):
|
func _on_line(line):
|
||||||
$RichTextLabel.bbcode_text = line.text
|
$RichTextLabel.bbcode_text = line.text
|
||||||
|
|
||||||
$Tween.remove_all()
|
$Tween.remove_all()
|
||||||
|
@ -26,7 +30,7 @@ func _on_Wol_line(line):
|
||||||
|
|
||||||
$Tween.start()
|
$Tween.start()
|
||||||
|
|
||||||
func _on_Wol_options(options):
|
func _on_options(options):
|
||||||
var button_template = $VBoxContainer/ButtonTemplate
|
var button_template = $VBoxContainer/ButtonTemplate
|
||||||
|
|
||||||
for option in options:
|
for option in options:
|
||||||
|
@ -45,7 +49,7 @@ func _on_option_selected(option):
|
||||||
if not 'Template' in child.name:
|
if not 'Template' in child.name:
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
|
|
||||||
func _on_Wol_finished():
|
func _on_finished():
|
||||||
$RichTextLabel.text = ''
|
$RichTextLabel.text = ''
|
||||||
|
|
||||||
func _input(event):
|
func _input(event):
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
[gd_scene load_steps=12 format=2]
|
[gd_scene load_steps=11 format=2]
|
||||||
|
|
||||||
[ext_resource path="res://addons/Wol/Wol.gd" type="Script" id=1]
|
|
||||||
[ext_resource path="res://addons/Wol/logo.svg" type="Texture" id=3]
|
[ext_resource path="res://addons/Wol/logo.svg" type="Texture" id=3]
|
||||||
[ext_resource path="res://addons/Wol/font/Italic.tres" type="DynamicFont" id=4]
|
[ext_resource path="res://addons/Wol/font/Italic.tres" type="DynamicFont" id=4]
|
||||||
[ext_resource path="res://addons/Wol/font/Regular.tres" type="DynamicFont" id=5]
|
[ext_resource path="res://addons/Wol/font/Regular.tres" type="DynamicFont" id=5]
|
||||||
|
@ -58,13 +57,6 @@ __meta__ = {
|
||||||
"_edit_use_anchors_": false
|
"_edit_use_anchors_": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[node name="Wol" type="Node" parent="."]
|
|
||||||
script = ExtResource( 1 )
|
|
||||||
path = "res://dialogue.yarn"
|
|
||||||
auto_start = true
|
|
||||||
variable_storage = {
|
|
||||||
}
|
|
||||||
|
|
||||||
[node name="Logo" type="TextureRect" parent="."]
|
[node name="Logo" type="TextureRect" parent="."]
|
||||||
anchor_left = 1.0
|
anchor_left = 1.0
|
||||||
anchor_top = 1.0
|
anchor_top = 1.0
|
||||||
|
@ -137,7 +129,3 @@ custom_styles/normal = SubResource( 1 )
|
||||||
text = "This is a dialogue option"
|
text = "This is a dialogue option"
|
||||||
|
|
||||||
[node name="Tween" type="Tween" parent="."]
|
[node name="Tween" type="Tween" parent="."]
|
||||||
|
|
||||||
[connection signal="finished" from="Wol" to="." method="_on_Wol_finished"]
|
|
||||||
[connection signal="line" from="Wol" to="." method="_on_Wol_line"]
|
|
||||||
[connection signal="options" from="Wol" to="." method="_on_Wol_options"]
|
|
||||||
|
|
27
ExampleDialogue/ExampleScene.gd
Normal file
27
ExampleDialogue/ExampleScene.gd
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
var current_dialogue
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
$Sally/DialogueStarter.connect('body_entered', self, '_on_player_near_dialogue', [$Sally, true])
|
||||||
|
$Sally/DialogueStarter.connect('body_exited', self, '_on_player_near_dialogue', [$Sally, false])
|
||||||
|
$Ship/DialogueStarter.connect('body_entered', self, '_on_player_near_dialogue', [$Ship, true])
|
||||||
|
$Ship/DialogueStarter.connect('body_exited', self, '_on_player_near_dialogue', [$Ship, false])
|
||||||
|
|
||||||
|
$Dialogue/Wol.connect('finished', self, '_on_finished')
|
||||||
|
|
||||||
|
func _on_player_near_dialogue(_player, node, entered):
|
||||||
|
if entered:
|
||||||
|
current_dialogue = node.name
|
||||||
|
else:
|
||||||
|
current_dialogue = null
|
||||||
|
|
||||||
|
func _on_finished():
|
||||||
|
$DialogueCooldown.start()
|
||||||
|
|
||||||
|
func _process(_delta):
|
||||||
|
if Input.is_action_just_released('ui_accept') \
|
||||||
|
and current_dialogue and not $Dialogue/Wol.running and $DialogueCooldown.time_left == 0:
|
||||||
|
$Dialogue/Wol.starting_node = current_dialogue
|
||||||
|
$Dialogue/Wol.path = 'res://ExampleDialogue/%s.yarn' % current_dialogue
|
||||||
|
$Dialogue/Wol.start()
|
236
ExampleDialogue/ExampleScene.tscn
Normal file
236
ExampleDialogue/ExampleScene.tscn
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
[gd_scene load_steps=7 format=2]
|
||||||
|
|
||||||
|
[ext_resource path="res://Dialogue.tscn" type="PackedScene" id=1]
|
||||||
|
[ext_resource path="res://ExampleDialogue/Player.gd" type="Script" id=2]
|
||||||
|
[ext_resource path="res://addons/Wol/Wol.gd" type="Script" id=3]
|
||||||
|
[ext_resource path="res://ExampleDialogue/ExampleScene.gd" type="Script" id=4]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id=1]
|
||||||
|
|
||||||
|
[sub_resource type="CircleShape2D" id=2]
|
||||||
|
radius = 62.2013
|
||||||
|
|
||||||
|
[node name="ExampleScene" type="Node2D"]
|
||||||
|
script = ExtResource( 4 )
|
||||||
|
|
||||||
|
[node name="Dialogue" parent="." instance=ExtResource( 1 )]
|
||||||
|
anchor_right = 0.0
|
||||||
|
anchor_bottom = 0.0
|
||||||
|
margin_right = 1024.0
|
||||||
|
margin_bottom = 600.0
|
||||||
|
|
||||||
|
[node name="Wol" type="Node" parent="Dialogue"]
|
||||||
|
script = ExtResource( 3 )
|
||||||
|
variable_storage = {
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="Player" type="KinematicBody2D" parent="."]
|
||||||
|
position = Vector2( 360, 488 )
|
||||||
|
script = ExtResource( 2 )
|
||||||
|
|
||||||
|
[node name="Visuals" type="Node2D" parent="Player"]
|
||||||
|
|
||||||
|
[node name="ColorRect" type="ColorRect" parent="Player/Visuals"]
|
||||||
|
margin_left = -21.0
|
||||||
|
margin_top = -52.0
|
||||||
|
margin_right = 19.0
|
||||||
|
margin_bottom = -12.0
|
||||||
|
color = Color( 0, 0, 0, 1 )
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="ColorRect2" type="ColorRect" parent="Player/Visuals"]
|
||||||
|
margin_left = -8.0
|
||||||
|
margin_top = -47.0
|
||||||
|
margin_bottom = -28.0
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="ColorRect4" type="ColorRect" parent="Player/Visuals"]
|
||||||
|
margin_top = -23.0
|
||||||
|
margin_right = 8.0
|
||||||
|
margin_bottom = -19.0
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="ColorRect3" type="ColorRect" parent="Player/Visuals"]
|
||||||
|
margin_left = 7.0
|
||||||
|
margin_top = -47.0
|
||||||
|
margin_right = 15.0
|
||||||
|
margin_bottom = -28.0
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="LeftFoot" type="ColorRect" parent="Player"]
|
||||||
|
margin_left = -12.0
|
||||||
|
margin_top = -14.0
|
||||||
|
margin_right = -8.0
|
||||||
|
margin_bottom = 5.0
|
||||||
|
color = Color( 0, 0, 0, 1 )
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="RightFoot" type="ColorRect" parent="Player"]
|
||||||
|
margin_left = 5.0
|
||||||
|
margin_top = -14.0
|
||||||
|
margin_right = 9.0
|
||||||
|
margin_bottom = 5.0
|
||||||
|
color = Color( 0, 0, 0, 1 )
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"]
|
||||||
|
position = Vector2( -1, -9 )
|
||||||
|
shape = SubResource( 1 )
|
||||||
|
|
||||||
|
[node name="Sally" type="KinematicBody2D" parent="."]
|
||||||
|
position = Vector2( 755, 595 )
|
||||||
|
|
||||||
|
[node name="DialogueStarter" type="Area2D" parent="Sally"]
|
||||||
|
collision_layer = 0
|
||||||
|
monitorable = false
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="Sally/DialogueStarter"]
|
||||||
|
shape = SubResource( 2 )
|
||||||
|
|
||||||
|
[node name="Visuals" type="Node2D" parent="Sally"]
|
||||||
|
position = Vector2( 1, 3 )
|
||||||
|
scale = Vector2( -1, 1 )
|
||||||
|
|
||||||
|
[node name="ColorRect" type="ColorRect" parent="Sally/Visuals"]
|
||||||
|
margin_left = -21.0
|
||||||
|
margin_top = -52.0
|
||||||
|
margin_right = 19.0
|
||||||
|
margin_bottom = -12.0
|
||||||
|
color = Color( 0.729412, 0.160784, 0.160784, 1 )
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="ColorRect2" type="ColorRect" parent="Sally/Visuals"]
|
||||||
|
margin_left = -8.0
|
||||||
|
margin_top = -47.0
|
||||||
|
margin_bottom = -28.0
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="ColorRect4" type="ColorRect" parent="Sally/Visuals"]
|
||||||
|
margin_top = -23.0
|
||||||
|
margin_right = 8.0
|
||||||
|
margin_bottom = -19.0
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="ColorRect3" type="ColorRect" parent="Sally/Visuals"]
|
||||||
|
margin_left = 7.0
|
||||||
|
margin_top = -47.0
|
||||||
|
margin_right = 15.0
|
||||||
|
margin_bottom = -28.0
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="LeftFoot" type="ColorRect" parent="Sally"]
|
||||||
|
margin_left = -12.0
|
||||||
|
margin_top = -14.0
|
||||||
|
margin_right = -8.0
|
||||||
|
margin_bottom = 5.0
|
||||||
|
color = Color( 0.729412, 0.160784, 0.160784, 1 )
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="RightFoot" type="ColorRect" parent="Sally"]
|
||||||
|
margin_left = 5.0
|
||||||
|
margin_top = -14.0
|
||||||
|
margin_right = 9.0
|
||||||
|
margin_bottom = 5.0
|
||||||
|
color = Color( 0.729412, 0.160784, 0.160784, 1 )
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="Ship" type="KinematicBody2D" parent="."]
|
||||||
|
position = Vector2( 43, 595 )
|
||||||
|
|
||||||
|
[node name="DialogueStarter" type="Area2D" parent="Ship"]
|
||||||
|
collision_layer = 0
|
||||||
|
monitorable = false
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="Ship/DialogueStarter"]
|
||||||
|
shape = SubResource( 2 )
|
||||||
|
|
||||||
|
[node name="Visuals" type="Node2D" parent="Ship"]
|
||||||
|
position = Vector2( 1, 3 )
|
||||||
|
|
||||||
|
[node name="ColorRect" type="ColorRect" parent="Ship/Visuals"]
|
||||||
|
margin_left = -21.0
|
||||||
|
margin_top = -52.0
|
||||||
|
margin_right = 19.0
|
||||||
|
margin_bottom = -12.0
|
||||||
|
color = Color( 0.329412, 0.231373, 1, 1 )
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="ColorRect2" type="ColorRect" parent="Ship/Visuals"]
|
||||||
|
margin_left = -8.0
|
||||||
|
margin_top = -47.0
|
||||||
|
margin_bottom = -28.0
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="ColorRect4" type="ColorRect" parent="Ship/Visuals"]
|
||||||
|
margin_top = -23.0
|
||||||
|
margin_right = 8.0
|
||||||
|
margin_bottom = -19.0
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="ColorRect3" type="ColorRect" parent="Ship/Visuals"]
|
||||||
|
margin_left = 7.0
|
||||||
|
margin_top = -47.0
|
||||||
|
margin_right = 15.0
|
||||||
|
margin_bottom = -28.0
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="LeftFoot" type="ColorRect" parent="Ship"]
|
||||||
|
margin_left = -12.0
|
||||||
|
margin_top = -14.0
|
||||||
|
margin_right = -8.0
|
||||||
|
margin_bottom = 5.0
|
||||||
|
color = Color( 0.329412, 0.231373, 1, 1 )
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="RightFoot" type="ColorRect" parent="Ship"]
|
||||||
|
margin_left = 5.0
|
||||||
|
margin_top = -14.0
|
||||||
|
margin_right = 9.0
|
||||||
|
margin_bottom = 5.0
|
||||||
|
color = Color( 0.329412, 0.231373, 1, 1 )
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="StaticBody2D" type="StaticBody2D" parent="."]
|
||||||
|
|
||||||
|
[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="StaticBody2D"]
|
||||||
|
polygon = PoolVector2Array( -3, 427, -40, 426, -40, 648, 838, 647, 838, 419, 787, 420, 788, 600, 2, 600, 0, 420 )
|
||||||
|
|
||||||
|
[node name="DialogueCooldown" type="Timer" parent="."]
|
||||||
|
wait_time = 0.4
|
||||||
|
one_shot = true
|
32
ExampleDialogue/Player.gd
Normal file
32
ExampleDialogue/Player.gd
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
extends KinematicBody2D
|
||||||
|
|
||||||
|
var velocity = Vector2.ZERO
|
||||||
|
var time = .0
|
||||||
|
|
||||||
|
func _physics_process(_delta):
|
||||||
|
var gravity = Vector2.DOWN * 9.81
|
||||||
|
|
||||||
|
velocity += gravity
|
||||||
|
|
||||||
|
move_and_slide(velocity, Vector2.UP)
|
||||||
|
|
||||||
|
func _process(delta):
|
||||||
|
var direction = Input.get_vector('ui_left', 'ui_right', 'ui_select', 'ui_select')
|
||||||
|
|
||||||
|
# Jump
|
||||||
|
if Input.is_action_just_released('ui_select') and is_on_floor():
|
||||||
|
velocity += Vector2.UP * 9.81 * 50
|
||||||
|
|
||||||
|
velocity.x = direction.x * 200
|
||||||
|
|
||||||
|
if direction.x != 0:
|
||||||
|
time += delta
|
||||||
|
|
||||||
|
$LeftFoot.visible = fmod(time * 4, 2) > 1
|
||||||
|
$RightFoot.visible = not $LeftFoot.visible
|
||||||
|
else:
|
||||||
|
$LeftFoot.visible = true
|
||||||
|
$RightFoot.visible = true
|
||||||
|
|
||||||
|
$Visuals.scale.x = -1 if velocity.x < 0 else 1
|
||||||
|
|
52
ExampleDialogue/Sally.yarn
Normal file
52
ExampleDialogue/Sally.yarn
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
title: Sally.Watch
|
||||||
|
tags:
|
||||||
|
colorID:
|
||||||
|
position: 500, 400
|
||||||
|
---
|
||||||
|
Sally: Not really. #line:8c3f98
|
||||||
|
Sally: Same old nebula, doing the same old thing. #line:24c418
|
||||||
|
Sally: Oh, Ship wanted to see you. Go say hi to it. #line:df4eaf
|
||||||
|
<<set $should_see_ship to true>>
|
||||||
|
<<if visited("Ship") is true>>
|
||||||
|
Player: Already done! #line:1fea6c
|
||||||
|
Sally: Go say hi again. #line:5df323
|
||||||
|
<<endif>>
|
||||||
|
===
|
||||||
|
title: Sally.Exit
|
||||||
|
tags:
|
||||||
|
colorID:
|
||||||
|
position: 100, 400
|
||||||
|
---
|
||||||
|
Sally: Bye. #line:60c282
|
||||||
|
===
|
||||||
|
title: Sally.Sorry
|
||||||
|
tags:
|
||||||
|
colorID:
|
||||||
|
position: 900, 400
|
||||||
|
---
|
||||||
|
Sally: Yeah. Don't do it again. #line:d7df49
|
||||||
|
===
|
||||||
|
title: Sally
|
||||||
|
tags:
|
||||||
|
colorID:
|
||||||
|
position: 500, 100
|
||||||
|
---
|
||||||
|
<<if visited("Sally") is false>>
|
||||||
|
Player: Hey, Sally. #line:794945
|
||||||
|
Sally: Oh! Hi. #line:2dc39b
|
||||||
|
Sally: You snuck up on me. #line:34de2f
|
||||||
|
Sally: Don't do that. #line:dcc2bc
|
||||||
|
<<else>>
|
||||||
|
Player: Hey. #line:a8e70c
|
||||||
|
Sally: Hi. #line:305cde
|
||||||
|
<<endif>>
|
||||||
|
|
||||||
|
<<if not visited("Sally.Watch")>>
|
||||||
|
[[Anything exciting happen on your watch?|Sally.Watch]] #line:5d7a7c
|
||||||
|
<<endif>>
|
||||||
|
|
||||||
|
<<if $sally_warning and not visited("Sally.Sorry")>>
|
||||||
|
[[Sorry about the console.|Sally.Sorry]] #line:0a7e39
|
||||||
|
<<endif>>
|
||||||
|
[[See you later.|Sally.Exit]] #line:0facf7
|
||||||
|
===
|
34
ExampleDialogue/Ship.yarn
Normal file
34
ExampleDialogue/Ship.yarn
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
title: Ship
|
||||||
|
tags:
|
||||||
|
colorID: 0
|
||||||
|
position: 721,130
|
||||||
|
---
|
||||||
|
<<if visited("Ship") is false>>
|
||||||
|
Ship: Hey, friend. #line:5837f2
|
||||||
|
Player: Hi, Ship. #line:ship9
|
||||||
|
Player: How's space? #line:ship10
|
||||||
|
Ship: Oh, man. #line:ship11
|
||||||
|
<<setsprite ShipFace happy>>
|
||||||
|
Ship: It's HUGE! #line:ship12
|
||||||
|
<<setsprite ShipFace neutral>>
|
||||||
|
<<else>>
|
||||||
|
<<setsprite ShipFace happy>>
|
||||||
|
Ship: Hey!! #line:ship13
|
||||||
|
<<setsprite ShipFace neutral>>
|
||||||
|
<<endif>>
|
||||||
|
|
||||||
|
<<if $should_see_ship is true and $sally_warning is false>>
|
||||||
|
Player: Sally said you wanted to see me? #line:ship1
|
||||||
|
<<setsprite ShipFace happy>>
|
||||||
|
Ship: She totally did!! #line:ship3
|
||||||
|
<<setsprite ShipFace neutral>>
|
||||||
|
Ship: She wanted me to tell you... #line:ship4
|
||||||
|
Ship: If you ever go off-watch without resetting the console again... #line:ship5
|
||||||
|
<<setsprite ShipFace happy>>
|
||||||
|
Ship: She'll flay you alive! #line:ship6
|
||||||
|
<<set $sally_warning to true>>
|
||||||
|
<<wait 1>>
|
||||||
|
Player: Uh. #line:ship7
|
||||||
|
<<setsprite ShipFace neutral>>
|
||||||
|
<<endif>>
|
||||||
|
===
|
226
README.md
226
README.md
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Wol** is a tool for creating interactive dialogue for games. Its based on [YarnSpinner](https://yarnspinner.dev/) and it's [Yarn language](https://yarnspinner.dev/docs/syntax/).
|
**Wol** is a tool for creating interactive dialogue for games. Its based on [YarnSpinner](https://yarnspinner.dev/) and it's [Yarn language](https://yarnspinner.dev/docs/syntax/). (**_currently under heavy development, very unstable, do not use for production_**)
|
||||||
|
|
||||||
Write your conversations in *Yarn*, a simple programming language that's designed to be easy for writers to learn, while also powerful enough to handle whatever you need.
|
Write your conversations in *Yarn*, a simple programming language that's designed to be easy for writers to learn, while also powerful enough to handle whatever you need.
|
||||||
|
|
||||||
|
@ -16,7 +16,9 @@ Wol is actively maintained by [Bram Dingelstad](https://bram.dingelstad.works),
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
This repo contains the source code for the Wol compiler. If you want to use it in a game engine other than Godot, you should get the appropriate package for your game engine. Check out [YarnSpinner-Unity](https://github.com/YarnSpinnerTool/YarnSpinner-Unity) for a Unity version of this project.
|
This repo contains the code for the Wol nodes & Yarn compiler.
|
||||||
|
If you want to use it in a game engine other than Godot, you should get the appropriate package for your game engine.
|
||||||
|
Check out [YarnSpinner-Unity](https://github.com/YarnSpinnerTool/YarnSpinner-Unity) for a Unity version of this project.
|
||||||
|
|
||||||
### Download from AssetLib
|
### Download from AssetLib
|
||||||
Unfortunately, this option isn't available yet. Stay tuned!
|
Unfortunately, this option isn't available yet. Stay tuned!
|
||||||
|
@ -35,13 +37,15 @@ Unfortunately, this option isn't available yet. Stay tuned!
|
||||||
There are few things that need to be ironed out to be 100% feature compatible with the original YarnSpinner.
|
There are few things that need to be ironed out to be 100% feature compatible with the original YarnSpinner.
|
||||||
|
|
||||||
- [ ] Integration with Godot's translation/localization system.
|
- [ ] Integration with Godot's translation/localization system.
|
||||||
- [ ] Full support for [format functions](https://yarnspinner.dev/docs/syntax/#format-functions).
|
- [ ] Auto generation for `#line:` suffixes
|
||||||
|
- [ ] Support for [format functions](https://yarnspinner.dev/docs/syntax/#format-functions).
|
||||||
|
- [ ] ~Support~ Fix for conditional options.
|
||||||
- [ ] In-editor dialogue editor with preview.
|
- [ ] In-editor dialogue editor with preview.
|
||||||
- [ ] Fully extend the documentation of this project.
|
- [ ] Lines connecting different nodes if they refer to eachother.
|
||||||
- [x] Document the [Option](README.md#Option) object.
|
- [x] Error hints when doing something wrong.
|
||||||
- [x] Write the method descriptions for the `Wol` node.
|
- [x] Basic saving, opening and saving-as.
|
||||||
- [ ] Write a basic "Hello World"-esque tutorial.
|
- [ ] Remove all `printerr` in favor of (soft) `assert`s.
|
||||||
- [x] Provide helpful anchors in the documentation.
|
- [x] Fully extend the documentation of this project.
|
||||||
- [x] Porting to usable signals in Godot.
|
- [x] Porting to usable signals in Godot.
|
||||||
- [x] Providing helpful errors when failing to compile.
|
- [x] Providing helpful errors when failing to compile.
|
||||||
- [x] Having a working repository with example code.
|
- [x] Having a working repository with example code.
|
||||||
|
@ -81,6 +85,205 @@ Wol & Yarn Spinner needs your help to be as awesome as it can be! You don't have
|
||||||
* Join Secret Lab's discussion on Slack by joining the [narrative game development](http://lab.to/narrativegamedev) channel.
|
* Join Secret Lab's discussion on Slack by joining the [narrative game development](http://lab.to/narrativegamedev) channel.
|
||||||
* Follow [Bram Dingelstad](https://twitter.com/bram_dingelstad) & [Yarn Spinner](http://twitter.com/YarnSpinnerTool).
|
* Follow [Bram Dingelstad](https://twitter.com/bram_dingelstad) & [Yarn Spinner](http://twitter.com/YarnSpinnerTool).
|
||||||
|
|
||||||
|
# Tutorial
|
||||||
|
|
||||||
|
Welcome to Wol! In this tutorial, you’ll learn how to use Wol in a Godot project to create interactive dialogue.
|
||||||
|
|
||||||
|
We’ll start by downloading and installing Wol. We’ll then take a look at the core concepts that power Wol / Yarn, and write some dialogue.
|
||||||
|
After that, we’ll explore some of the more advanced features of Wol & Yarn.
|
||||||
|
|
||||||
|
## Introducing Wol
|
||||||
|
|
||||||
|
Wol & Yarn (Spinner) are tools for writing interactive dialogue in games - that is, conversations that the player can have with characters in the game.
|
||||||
|
Yarn Spinner does this by letting you write your dialogue in a programming language called Yarn.
|
||||||
|
|
||||||
|
Yarn is designed to be as minimal as possible.
|
||||||
|
For example, the following is valid Yarn code:
|
||||||
|
|
||||||
|
```yarn
|
||||||
|
Gregg: I think I might be sick.
|
||||||
|
Mae: True friendship: Letting your friend make you sick.
|
||||||
|
Gregg: True bros.
|
||||||
|
Mae: True bros.
|
||||||
|
```
|
||||||
|
|
||||||
|
Wol will take each line, one at a time, and deliver them to the game.
|
||||||
|
It’s entirely up to the game to decide what to do with the lines; for example, the game that these lines are from,
|
||||||
|
Night in the Woods, displays them in speech bubbles, one character at a time, and waits for the user to press a button before showing the next one.
|
||||||
|
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
We’ll begin by playing the example game that comes with Wol. It’s very short - about 2 minutes long. After that we'll make some small changes!
|
||||||
|
|
||||||
|
1. Create a new empty Godot project.
|
||||||
|
2. Download and install Wol. Go to the [Getting started section](#README.md#Getting-Started), and follow the directions there.
|
||||||
|
3. Open the example scene (`res://ExampleDialogue/ExampleScene.tscn`).
|
||||||
|
4. Play the game. Use the left and right arrow keys to move, and the enter key to talk to characters.
|
||||||
|
|
||||||
|
We’re now ready to start looking under the hood, to see how Wol & Yarn power this game.
|
||||||
|
|
||||||
|
### The Wol Editor
|
||||||
|
|
||||||
|
Wol & Yarn Spinner stores its dialogue in .yarn (or .wol) files. These are plain text files, which means you can edit them in any plain text editor (Visual Studio Code is a good option, and Secret Labs offers a syntax highlighting extension to make it nice to use!)
|
||||||
|
|
||||||
|
You can also use the Wol Editor, which is a tool in the Godot editor for working with Yarn code. This editor is useful because it lets you view the structure of your dialogue in a very visual way. (This is not completed yet however)
|
||||||
|
|
||||||
|
### Reading Yarn
|
||||||
|
|
||||||
|
In this section of the tutorial, we’re going to open the file Sally.yarn, and look at what it’s doing.
|
||||||
|
|
||||||
|
#### Open Sally.yarn in your editor of choice.
|
||||||
|
|
||||||
|
Wol & Yarn groups all of its dialogue into nodes. Nodes contain everything: your lines of dialogue, the choices you show to the player, and the commands that you send to the game. The Sally.yarn file contains four of them: Sally, Sally.Watch, Sally.Exit, and Sally.Sorry. The example game is set up so that when you walk up to Sally and press the spacebar, the game will start running the Sally node.
|
||||||
|
|
||||||
|
#### Go to the Sally node.
|
||||||
|
|
||||||
|
Let’s take a look at what that node contains. Here’s the entire text of it:
|
||||||
|
|
||||||
|
```yarn
|
||||||
|
<<if visited("Sally") is false>>
|
||||||
|
Player: Hey, Sally. #line:794945
|
||||||
|
Sally: Oh! Hi. #line:2dc39b
|
||||||
|
Sally: You snuck up on me. #line:34de2f
|
||||||
|
Sally: Don't do that. #line:dcc2bc
|
||||||
|
<<else>>
|
||||||
|
Player: Hey. #line:a8e70c
|
||||||
|
Sally: Hi. #line:305cde
|
||||||
|
<<endif>>
|
||||||
|
|
||||||
|
<<if not visited("Sally.Watch")>>
|
||||||
|
[[Anything exciting happen on your watch?|Sally.Watch]] #line:5d7a7c
|
||||||
|
<<endif>>
|
||||||
|
|
||||||
|
<<if $sally_warning and not visited("Sally.Sorry")>>
|
||||||
|
[[Sorry about the console.|Sally.Sorry]] #line:0a7e39
|
||||||
|
<<endif>>
|
||||||
|
[[See you later.|Sally.Exit]] #line:0facf7
|
||||||
|
```
|
||||||
|
|
||||||
|
Take a second now to look at this code, and get a feel for its structure.
|
||||||
|
|
||||||
|
### Lines and Logic
|
||||||
|
|
||||||
|
We’ll now take a closer look at each part of this code, and explain what’s going on.
|
||||||
|
|
||||||
|
```yarn
|
||||||
|
<<if visited("Sally") is false>>
|
||||||
|
Player: Hey, Sally. #line:794945
|
||||||
|
Sally: Oh! Hi. #line:2dc39b
|
||||||
|
Sally: You snuck up on me. #line:34de2f
|
||||||
|
Sally: Don't do that. #line:dcc2bc
|
||||||
|
<<else>>
|
||||||
|
Player: Hey. #line:a8e70c
|
||||||
|
Sally: Hi. #line:305cde
|
||||||
|
<<endif>>
|
||||||
|
```
|
||||||
|
|
||||||
|
The first line of code in this node checks to see if Wol has already run this node. `visited` is a function that
|
||||||
|
is built into Wol. It returns true if the node you specify has been run before.
|
||||||
|
You’ll notice that this line is wrapped in `<<` and `>>` symbols. This tells Wol that it’s control code, and not meant to be shown to the player.
|
||||||
|
|
||||||
|
If they haven’t run the `Sally` node yet, it means that this is the first time that we’ve spoken to `Sally` in this game. As a result, we run lines in which Sally and the player character meet. Otherwise, we instead run some shorter lines.
|
||||||
|
Each line in Wol is just a run of text, which is sent directly to the game. It’s up to the game to decide how it wants to display it; in the example game, it’s shown at the top of the screen.
|
||||||
|
|
||||||
|
At the end of each line, you’ll see a `#line:` tag. This tag lets Wol identify lines across multiple translations, and is optional if you aren’t translating your game into other languages. Wol can automatically generate them for you (not supported yet however).
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
Here’s the next part of the code.
|
||||||
|
|
||||||
|
```yarn
|
||||||
|
<<if not visited("Sally.Watch")>>
|
||||||
|
[[Anything exciting happen on your watch?|Sally.Watch]] #line:5d7a7c
|
||||||
|
<<endif>>
|
||||||
|
|
||||||
|
<<if $sally_warning and not visited("Sally.Sorry")>>
|
||||||
|
[[Sorry about the console.|Sally.Sorry]] #line:0a7e39
|
||||||
|
<<endif>>
|
||||||
|
```
|
||||||
|
|
||||||
|
In the next part of the code, we do a check, and if it passes, we add an option. Options are things that the player can select; in this game, they’re things the player can say, but like lines, it’s up to the game to decide what to do with them. Options are shown to the player when the end of a node is reached.
|
||||||
|
|
||||||
|
The first couple of lines here test to see whether the player has run the node `Sally.Watch`. If they haven’t, then the code adds a new option. Options are wrapped with `[[` and `]]`. The text before the `|` is shown to the player, and the text after is the name of the node that will be run if the player chooses the option. Like lines, options can have line tags for localisation.
|
||||||
|
|
||||||
|
If the player has run the `Sally.Watch` node before, this code won’t be run, which means that the option to run it again won’t appear.
|
||||||
|
|
||||||
|
The rest of this part does a similar thing as the first: it does a check, and adds another option if the check passes. In this case, it checks to see if the variable `$sally_warning` is `true`, and if the player has not yet run the `Sally.Sorry` node. `$sally_warning` is set in a different node - it’s in the node Ship, which is stored in the file `Ship.yarn`.
|
||||||
|
|
||||||
|
```yarn
|
||||||
|
[[See you later.|Sally.Exit]] #line:0facf7
|
||||||
|
```
|
||||||
|
|
||||||
|
The very last line of the node adds an option, which takes the player to the Sally.Exit line. Because this option isn’t inside an if statement, it’s always added.
|
||||||
|
|
||||||
|
When Wol hits the end of the node, all of the options that have been accumulated so far will be shown to the player. Wol will then wait for the player to make a selection, and then start running the node that they selected.
|
||||||
|
|
||||||
|
And that’s how the node works!
|
||||||
|
|
||||||
|
### Writing Some Dialogue
|
||||||
|
|
||||||
|
Let’s write some dialogue! We’ll add a couple of lines to the Ship.
|
||||||
|
|
||||||
|
> Open the file Ship.yarn. It contains a single node, called Ship - go to it.
|
||||||
|
|
||||||
|
This code uses couple of features that we didn’t see in Sally: commands, and variables.
|
||||||
|
|
||||||
|
#### Commands
|
||||||
|
|
||||||
|
Commands are messages that Wol sends to your game, but aren’t intended to be shown to the player. Commands let you control things in your scene, like moving the camera around, or instructing a character to move to another point.
|
||||||
|
|
||||||
|
Because every game is different, Wol leaves the task of defining most commands to you. Wol defines two built-in commands: wait, which pauses the dialogue for a certain number of seconds, and stop, which ends the dialogue immediately.
|
||||||
|
|
||||||
|
The example game defines its own command, `setsprite`, which is used to change the sprite that the Ship character’s face is displaying. You can see this in action in the file `Ship.yarn`:
|
||||||
|
|
||||||
|
```yarn
|
||||||
|
Player: How's space?
|
||||||
|
Ship: Oh, man.
|
||||||
|
<<setsprite ShipFace happy>>
|
||||||
|
Ship: It's HUGE!
|
||||||
|
<<setsprite ShipFace neutral>>
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- TODO: make tutorial about setting up commands
|
||||||
|
You can learn how to define your own custom commands in Working With Commands. -->
|
||||||
|
|
||||||
|
#### Variables
|
||||||
|
|
||||||
|
Variables are how you store information about what the player has done in the game. We saw variables in use in the `Sally` node, where the variable `$sally_warning` was used to control whether some content was shown or not. This variable is set in here, in the `Ship` node - it represents whether or not the player has heard Sally’s warning about the console from the Ship.
|
||||||
|
|
||||||
|
Variables in Wol start with a `$`, and can store text, numbers, booleans (`true` or `false` values), or `null`. If you try and access a variable that hasn’t been set, you’ll get the value `null`, which represents “no value”.
|
||||||
|
Adding Some Content
|
||||||
|
|
||||||
|
#### Add some new dialogue. Add the following text to the end of the node:
|
||||||
|
|
||||||
|
```
|
||||||
|
Ship: Anything else I can help with?
|
||||||
|
|
||||||
|
-> No, thanks.
|
||||||
|
Ship: Aw, ok!
|
||||||
|
-> I'm good.
|
||||||
|
Ship: Let me know!
|
||||||
|
|
||||||
|
Ship: Bye!
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Shortcut Options
|
||||||
|
|
||||||
|
The `->` items that we just added are called shortcut options. Shortcut options let you put choices in your node without having to create new nodes, which you link to through the `[[Option]]` syntax. They exist in-line with the rest of your node.
|
||||||
|
|
||||||
|
To use a shortcut option, you write a `->`, followed by the text that you want to display. Then, on the next lines, indent the code a few spaces (it doesn’t matter how many, as long as you’re consistent.) The indented lines will run if the option they’re attached to is selected. Shortcut options can be nested, which means you can put a group of shortcut options inside another. You can put any kind of code inside a shortcut option’s lines.
|
||||||
|
|
||||||
|
Because shortcut options don’t require you to create new nodes, they’re really good for situations where you want to offer the player some kind of choice that doesn’t significantly change the flow of the story.
|
||||||
|
|
||||||
|
Save the file, and go back to the game. Play the game again, and talk to the Ship. At the end of the conversation, you’ll see new dialogue.
|
||||||
|
|
||||||
|
### Where Next
|
||||||
|
|
||||||
|
The example game is set up so that when you talk to Sally, the node Sally is run, and when you talk to the Ship, the node Ship is run. With this in mind, change the story so that after you get told off by Sally, she asks you to go and fix a problem with the Ship.
|
||||||
|
|
||||||
|
You can also read the [Syntax Reference](https://yarnspinner.dev/docs/syntax/) for Yarn.
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
## `Wol`
|
## `Wol`
|
||||||
|
@ -207,7 +410,7 @@ It has several properties that you can change either in-editor or using GDScript
|
||||||
When getting an option from the `options` signal, use this function to let Wol node which option you want to select.
|
When getting an option from the `options` signal, use this function to let Wol node which option you want to select.
|
||||||
Use `Option.id` for the `id` parameter.
|
Use `Option.id` for the `id` parameter.
|
||||||
|
|
||||||
## [Line](README.md#Line)
|
## `Line`
|
||||||
_Inherits from [Object](https://docs.godotengine.org/en/stable/classes/class_object.html)_
|
_Inherits from [Object](https://docs.godotengine.org/en/stable/classes/class_object.html)_
|
||||||
|
|
||||||
An object holding all information related to a line in your dialogue.
|
An object holding all information related to a line in your dialogue.
|
||||||
|
@ -245,7 +448,7 @@ The [Line](README.md#Line) object is _the_ object that you're gonna be interacti
|
||||||
|
|
||||||
Currently unimplemented.
|
Currently unimplemented.
|
||||||
|
|
||||||
## [Option](README.md#Option)
|
## `Option`
|
||||||
_Inherits from [Object](https://docs.godotengine.org/en/stable/classes/class_object.html)_
|
_Inherits from [Object](https://docs.godotengine.org/en/stable/classes/class_object.html)_
|
||||||
|
|
||||||
An object holding information of an option in your dialogue.
|
An object holding information of an option in your dialogue.
|
||||||
|
@ -276,6 +479,3 @@ It has a reference to a [Line](README.md#Line) with it's `line` property so you
|
||||||
The node that you will jump to when this option is selected. (Only relevant for jump questions, not inline ones).
|
The node that you will jump to when this option is selected. (Only relevant for jump questions, not inline ones).
|
||||||
|
|
||||||
|
|
||||||
# Tutorial
|
|
||||||
|
|
||||||
_The tutorial is currently under construction, stay tuned!_
|
|
||||||
|
|
20
TestScene.tscn
Normal file
20
TestScene.tscn
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[gd_scene load_steps=3 format=2]
|
||||||
|
|
||||||
|
[ext_resource path="res://addons/Wol/Wol.gd" type="Script" id=1]
|
||||||
|
[ext_resource path="res://Dialogue.tscn" type="PackedScene" id=2]
|
||||||
|
|
||||||
|
[node name="TestScene" type="Control"]
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="Dialogue" parent="." instance=ExtResource( 2 )]
|
||||||
|
|
||||||
|
[node name="Wol" type="Node" parent="Dialogue"]
|
||||||
|
script = ExtResource( 1 )
|
||||||
|
path = "res://dialogue.wol"
|
||||||
|
auto_start = true
|
||||||
|
variable_storage = {
|
||||||
|
}
|
|
@ -24,6 +24,8 @@ export var auto_substitute = true
|
||||||
|
|
||||||
export(Dictionary) var variable_storage = {}
|
export(Dictionary) var variable_storage = {}
|
||||||
|
|
||||||
|
var running = false
|
||||||
|
|
||||||
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
||||||
const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd')
|
const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd')
|
||||||
const Library = preload('res://addons/Wol/core/Library.gd')
|
const Library = preload('res://addons/Wol/core/Library.gd')
|
||||||
|
@ -50,10 +52,13 @@ func _ready():
|
||||||
func set_path(_path):
|
func set_path(_path):
|
||||||
path = _path
|
path = _path
|
||||||
|
|
||||||
if not Engine.editor_hint and virtual_machine:
|
if not Engine.editor_hint and virtual_machine and not path.empty():
|
||||||
var compiler = Compiler.new(path)
|
var compiler = Compiler.new(path)
|
||||||
virtual_machine.program = compiler.compile()
|
virtual_machine.program = compiler.compile()
|
||||||
|
|
||||||
|
func set_program(program):
|
||||||
|
virtual_machine.program = program
|
||||||
|
|
||||||
func _on_line(line):
|
func _on_line(line):
|
||||||
if auto_substitute:
|
if auto_substitute:
|
||||||
var index = 0
|
var index = 0
|
||||||
|
@ -101,9 +106,15 @@ func pause():
|
||||||
func start(node = starting_node):
|
func start(node = starting_node):
|
||||||
running = true
|
running = true
|
||||||
emit_signal('started')
|
emit_signal('started')
|
||||||
|
running = true
|
||||||
|
|
||||||
virtual_machine.set_node(node)
|
virtual_machine.set_node(node)
|
||||||
virtual_machine.start()
|
virtual_machine.start()
|
||||||
|
|
||||||
|
func stop():
|
||||||
|
if running:
|
||||||
|
virtual_machine.call_deferred('stop')
|
||||||
|
running = false
|
||||||
|
|
||||||
func resume():
|
func resume():
|
||||||
virtual_machine.call_deferred('resume')
|
virtual_machine.call_deferred('resume')
|
||||||
|
|
|
@ -211,3 +211,4 @@ static func token_name(type):
|
||||||
if TokenType[key] == type:
|
if TokenType[key] == type:
|
||||||
return key
|
return key
|
||||||
return ''
|
return ''
|
||||||
|
|
|
@ -29,7 +29,6 @@ class Line:
|
||||||
func _to_string():
|
func _to_string():
|
||||||
return '%s:%d: "%s"' % [file_name.get_file(), line_number, text]
|
return '%s:%d: "%s"' % [file_name.get_file(), line_number, text]
|
||||||
|
|
||||||
|
|
||||||
class Option:
|
class Option:
|
||||||
var line
|
var line
|
||||||
var id = -1
|
var id = -1
|
|
@ -79,7 +79,11 @@ func node_visit_count(node = virtual_machine.current_node.name):
|
||||||
if node is Value:
|
if node is Value:
|
||||||
node = virtual_machine.program.strings[node.value()].text
|
node = virtual_machine.program.strings[node.value()].text
|
||||||
|
|
||||||
|
|
||||||
var variable_storage = virtual_machine.dialogue.variable_storage
|
var variable_storage = virtual_machine.dialogue.variable_storage
|
||||||
var visited_node_count = variable_storage[virtual_machine.program.filename]
|
var visited_node_count = variable_storage[virtual_machine.program.filename]
|
||||||
|
|
||||||
|
print('checking node "%s"' % node)
|
||||||
|
print(variable_storage)
|
||||||
|
|
||||||
return visited_node_count[node] if visited_node_count.has(node) else 0
|
return visited_node_count[node] if visited_node_count.has(node) else 0
|
||||||
|
|
|
@ -68,8 +68,8 @@ func set_node(name):
|
||||||
dialogue.variable_storage[program.filename] = {}
|
dialogue.variable_storage[program.filename] = {}
|
||||||
|
|
||||||
if not dialogue.variable_storage[program.filename].has(name):
|
if not dialogue.variable_storage[program.filename].has(name):
|
||||||
dialogue.variable_storage[program.filename][name] = 0
|
dialogue.variable_storage[program.filename][name] = 1
|
||||||
|
else:
|
||||||
dialogue.variable_storage[program.filename][name] += 1
|
dialogue.variable_storage[program.filename][name] += 1
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
@ -229,8 +229,11 @@ func run_instruction(instruction):
|
||||||
|
|
||||||
Constants.ByteCode.PushVariable:
|
Constants.ByteCode.PushVariable:
|
||||||
var name = instruction.operands[0].value
|
var name = instruction.operands[0].value
|
||||||
|
if dialogue.variable_storage.has(name.replace('$', '')):
|
||||||
var value = dialogue.variable_storage[name.replace('$', '')]
|
var value = dialogue.variable_storage[name.replace('$', '')]
|
||||||
state.push_value(value)
|
state.push_value(value)
|
||||||
|
else:
|
||||||
|
state.push_value(null)
|
||||||
|
|
||||||
Constants.ByteCode.StoreVariable:
|
Constants.ByteCode.StoreVariable:
|
||||||
var value = state.peek_value()
|
var value = state.peek_value()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
extends Object
|
extends Object
|
||||||
|
signal error(message, line_number, column)
|
||||||
|
|
||||||
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
||||||
const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd')
|
const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd')
|
||||||
|
@ -12,48 +13,52 @@ var filename = ''
|
||||||
|
|
||||||
var current_node
|
var current_node
|
||||||
var has_implicit_string_tags = false
|
var has_implicit_string_tags = false
|
||||||
|
var soft_assert = false
|
||||||
|
|
||||||
var string_count = 0
|
var string_count = 0
|
||||||
var string_table = {}
|
var string_table = {}
|
||||||
var label_count = 0
|
var label_count = 0
|
||||||
|
|
||||||
func _init(_filename, _source = null):
|
func _init(_filename, _source = null, _soft_assert = false):
|
||||||
filename = _filename
|
filename = _filename
|
||||||
|
soft_assert = _soft_assert
|
||||||
|
|
||||||
if not _filename and _source:
|
if not _filename and _source:
|
||||||
self.source = _source
|
filename = 'inline_source'
|
||||||
|
source = _source
|
||||||
else:
|
else:
|
||||||
var file = File.new()
|
var file = File.new()
|
||||||
file.open(_filename, File.READ)
|
file.open(_filename, File.READ)
|
||||||
self.source = file.get_as_text()
|
source = file.get_as_text()
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
func compile():
|
var source_lines = source.split('\n')
|
||||||
var header_sep = RegEx.new()
|
|
||||||
var header_property = RegEx.new()
|
|
||||||
header_sep.compile('---(\r\n|\r|\n)')
|
|
||||||
header_property.compile('(?<field>.*): *(?<value>.*)')
|
|
||||||
|
|
||||||
assert(header_sep.search(source), 'No headers found!')
|
|
||||||
|
|
||||||
var line_number = 0
|
|
||||||
|
|
||||||
var source_lines = source.split('\n', false)
|
|
||||||
for i in range(source_lines.size()):
|
for i in range(source_lines.size()):
|
||||||
source_lines[i] = source_lines[i].strip_edges(false, true)
|
source_lines[i] = source_lines[i].strip_edges(false, true)
|
||||||
|
|
||||||
var parsed_nodes = []
|
source = source_lines.join('\n')
|
||||||
while line_number < source_lines.size():
|
|
||||||
var title = ''
|
|
||||||
var body = ''
|
|
||||||
|
|
||||||
# Parse header
|
func get_headers(offset = 0):
|
||||||
while true:
|
var header_property = RegEx.new()
|
||||||
|
var header_sep = RegEx.new()
|
||||||
|
|
||||||
|
header_sep.compile('---(\r\n|\r|\n)')
|
||||||
|
header_property.compile('(?<field>.*): *(?<value>.*)')
|
||||||
|
|
||||||
|
self.assert(header_sep.search(source), 'No headers found!')
|
||||||
|
|
||||||
|
var title = ''
|
||||||
|
var position = Vector2.ZERO
|
||||||
|
|
||||||
|
var source_lines = source.split('\n')
|
||||||
|
var line_number = offset
|
||||||
|
while line_number < source_lines.size():
|
||||||
var line = source_lines[line_number]
|
var line = source_lines[line_number]
|
||||||
line_number += 1
|
line_number += 1
|
||||||
|
|
||||||
if not line.empty():
|
if not line.empty():
|
||||||
var result = header_property.search(line)
|
var result = header_property.search(line)
|
||||||
|
|
||||||
if result != null:
|
if result != null:
|
||||||
var field = result.get_string('field')
|
var field = result.get_string('field')
|
||||||
var value = result.get_string('value')
|
var value = result.get_string('value')
|
||||||
|
@ -61,37 +66,88 @@ func compile():
|
||||||
if field == 'title':
|
if field == 'title':
|
||||||
var regex = RegEx.new()
|
var regex = RegEx.new()
|
||||||
regex.compile(INVALID_TITLE)
|
regex.compile(INVALID_TITLE)
|
||||||
assert(not regex.search(value), 'Invalid characters in title "%s", correct to "%s"' % [value, regex.sub(value, '', true)])
|
self.assert(not regex.search(value), 'Invalid characters in title "%s", correct to "%s"' % [value, regex.sub(value, '', true)])
|
||||||
|
|
||||||
title = value
|
title = value
|
||||||
# TODO: Implement position, color and tags
|
|
||||||
|
|
||||||
if line_number >= source_lines.size() or line == '---':
|
if field == 'position':
|
||||||
|
var regex = RegEx.new()
|
||||||
|
regex.compile('^position:.*,.*\\d$')
|
||||||
|
self.assert(regex.search(line), 'Couldn\'t parse position property in the headers, got "%s" instead in node "%s"' % [value, title])
|
||||||
|
|
||||||
|
position = Vector2(int(value.split(',')[0].strip_edges()), int(value.split(',')[1].strip_edges()))
|
||||||
|
|
||||||
|
# TODO: Implement color and tags
|
||||||
|
|
||||||
|
if line == '---':
|
||||||
break
|
break
|
||||||
|
|
||||||
# past header
|
return {
|
||||||
|
'title': title,
|
||||||
|
'position': position
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_body(offset = 0):
|
||||||
var body_lines = []
|
var body_lines = []
|
||||||
|
|
||||||
|
var source_lines = source.split('\n')
|
||||||
|
var recording = false
|
||||||
|
var line_number = offset
|
||||||
|
|
||||||
while line_number < source_lines.size() and source_lines[line_number] != '===':
|
while line_number < source_lines.size() and source_lines[line_number] != '===':
|
||||||
|
if recording:
|
||||||
body_lines.append(source_lines[line_number])
|
body_lines.append(source_lines[line_number])
|
||||||
|
|
||||||
|
recording = recording or source_lines[line_number] == '---'
|
||||||
line_number += 1
|
line_number += 1
|
||||||
|
|
||||||
line_number += 1
|
line_number += 1
|
||||||
|
|
||||||
body = PoolStringArray(body_lines).join('\n')
|
return PoolStringArray(body_lines).join('\n')
|
||||||
var lexer = Lexer.new(filename, title, body)
|
|
||||||
var tokens = lexer.tokenize()
|
|
||||||
|
|
||||||
var parser = Parser.new(title, tokens)
|
func get_nodes():
|
||||||
var parser_node = parser.parse_node()
|
var nodes = []
|
||||||
|
var line_number = 0
|
||||||
|
var source_lines = source.split('\n')
|
||||||
|
while line_number < source_lines.size():
|
||||||
|
var headers = get_headers(line_number)
|
||||||
|
var body = get_body(line_number)
|
||||||
|
headers.body = body
|
||||||
|
|
||||||
parser_node.name = title
|
nodes.append(headers)
|
||||||
# parser_node.tags = title
|
|
||||||
parsed_nodes.append(parser_node)
|
# Add +2 to the final line to skip the === from that node
|
||||||
|
line_number = Array(source_lines).find_last(body.split('\n')[-1]) + 2
|
||||||
|
|
||||||
while line_number < source_lines.size() and source_lines[line_number].empty():
|
while line_number < source_lines.size() and source_lines[line_number].empty():
|
||||||
line_number += 1
|
line_number += 1
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
func assert(statement, message, line_number = -1, column = -1, _absolute_line_number = -1):
|
||||||
|
if not soft_assert:
|
||||||
|
assert(statement, '"%s" on line %d column %d' % [message, line_number, column])
|
||||||
|
elif not statement:
|
||||||
|
emit_signal('error', message, line_number, column)
|
||||||
|
|
||||||
|
return not statement
|
||||||
|
|
||||||
|
func compile():
|
||||||
|
var parsed_nodes = []
|
||||||
|
for node in get_nodes():
|
||||||
|
var lexer = Lexer.new(self, filename, node.title, node.body)
|
||||||
|
var tokens = lexer.tokenize()
|
||||||
|
|
||||||
|
# In case of lexer error
|
||||||
|
if not tokens:
|
||||||
|
return
|
||||||
|
|
||||||
|
var parser = Parser.new(self, node.title, tokens)
|
||||||
|
var parser_node = parser.parse_node()
|
||||||
|
|
||||||
|
parser_node.name = node.title
|
||||||
|
parsed_nodes.append(parser_node)
|
||||||
|
|
||||||
var program = Program.new()
|
var program = Program.new()
|
||||||
program.filename = filename
|
program.filename = filename
|
||||||
|
|
||||||
|
@ -104,7 +160,7 @@ func compile():
|
||||||
return program
|
return program
|
||||||
|
|
||||||
func compile_node(program, parsed_node):
|
func compile_node(program, parsed_node):
|
||||||
assert(not program.nodes.has(parsed_node.name), 'Duplicate node in program: %s' % parsed_node.name)
|
self.assert(not program.nodes.has(parsed_node.name), 'Duplicate node in program: %s' % parsed_node.name)
|
||||||
|
|
||||||
var node_compiled = Program.WolNode.new()
|
var node_compiled = Program.WolNode.new()
|
||||||
|
|
||||||
|
@ -205,7 +261,7 @@ func generate_statement(node, statement):
|
||||||
Constants.StatementTypes.Line:
|
Constants.StatementTypes.Line:
|
||||||
generate_line(node, statement)
|
generate_line(node, statement)
|
||||||
_:
|
_:
|
||||||
assert(false, 'Illegal statement type [%s]. Could not generate code.' % statement.type)
|
self.assert(false, statement.line_number, 'Illegal statement type [%s]. Could not generate code.' % statement.type)
|
||||||
|
|
||||||
func generate_custom_command(node, command):
|
func generate_custom_command(node, command):
|
||||||
# TODO: See if the first tree of this statement is being used
|
# TODO: See if the first tree of this statement is being used
|
||||||
|
@ -356,6 +412,9 @@ func generate_assignment(node, assignment):
|
||||||
emit(Constants.ByteCode.Pop, node)
|
emit(Constants.ByteCode.Pop, node)
|
||||||
|
|
||||||
func generate_expression(node, expression):
|
func generate_expression(node, expression):
|
||||||
|
if self.assert(expression != null, 'Wrong expression (perhaps unterminated command block ">>"?)'):
|
||||||
|
return false
|
||||||
|
|
||||||
match expression.type:
|
match expression.type:
|
||||||
Constants.ExpressionType.Value:
|
Constants.ExpressionType.Value:
|
||||||
generate_value(node, expression.value)
|
generate_value(node, expression.value)
|
|
@ -2,7 +2,7 @@ extends Object
|
||||||
|
|
||||||
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
||||||
|
|
||||||
const LINE_COMENT = '//'
|
const LINE_COMMENT = '//'
|
||||||
const FORWARD_SLASH = '/'
|
const FORWARD_SLASH = '/'
|
||||||
const LINE_SEPARATOR = '\n'
|
const LINE_SEPARATOR = '\n'
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ const FORMAT_FUNCTION = 'format'
|
||||||
|
|
||||||
var WHITESPACE = '\\s*'
|
var WHITESPACE = '\\s*'
|
||||||
|
|
||||||
|
var compiler
|
||||||
var filename = ''
|
var filename = ''
|
||||||
var title = ''
|
var title = ''
|
||||||
var text = ''
|
var text = ''
|
||||||
|
@ -33,19 +34,20 @@ var current_state
|
||||||
var indent_stack = []
|
var indent_stack = []
|
||||||
var should_track_indent = false
|
var should_track_indent = false
|
||||||
|
|
||||||
func _init(_filename, _title, _text):
|
func _init(_compiler, _filename, _title, _text):
|
||||||
createstates()
|
create_states()
|
||||||
|
|
||||||
|
compiler = _compiler
|
||||||
filename = _filename
|
filename = _filename
|
||||||
title = _title
|
title = _title
|
||||||
text = _text
|
text = _text
|
||||||
|
|
||||||
func createstates():
|
func create_states():
|
||||||
var patterns = {}
|
var patterns = {}
|
||||||
patterns[Constants.TokenType.Text] = ['.*', 'any text']
|
patterns[Constants.TokenType.Text] = ['.*', 'any text']
|
||||||
|
|
||||||
patterns[Constants.TokenType.Number] = ['\\-?[0-9]+(\\.[0-9+])?', 'any number']
|
patterns[Constants.TokenType.Number] = ['\\-?[0-9]+(\\.[0-9+])?', 'any number']
|
||||||
patterns[Constants.TokenType.Str] = ['\'([^\'\\\\]*(?:\\.[^\'\\\\]*)*)\'', 'any text']
|
patterns[Constants.TokenType.Str] = ['\"([^\"\\\\]*(?:\\.[^\"\\\\]*)*)\"', 'any text']
|
||||||
patterns[Constants.TokenType.TagMarker] = ['\\#', 'a tag #']
|
patterns[Constants.TokenType.TagMarker] = ['\\#', 'a tag #']
|
||||||
patterns[Constants.TokenType.LeftParen] = ['\\(', 'left parenthesis (']
|
patterns[Constants.TokenType.LeftParen] = ['\\(', 'left parenthesis (']
|
||||||
patterns[Constants.TokenType.RightParen] = ['\\)', 'right parenthesis )']
|
patterns[Constants.TokenType.RightParen] = ['\\)', 'right parenthesis )']
|
||||||
|
@ -111,7 +113,7 @@ func createstates():
|
||||||
states[BASE].add_transition(Constants.TokenType.TagMarker, TAG, true)
|
states[BASE].add_transition(Constants.TokenType.TagMarker, TAG, true)
|
||||||
states[BASE].add_text_rule(Constants.TokenType.Text)
|
states[BASE].add_text_rule(Constants.TokenType.Text)
|
||||||
|
|
||||||
#TODO: FIXME - Tags are not being proccessed properly this way. We must look for the format #{identifier}:{value}
|
#FIXME - Tags are not being proccessed properly this way. We must look for the format #{identifier}:{value}
|
||||||
# Possible solution is to add more transitions
|
# Possible solution is to add more transitions
|
||||||
states[TAG] = LexerState.new(patterns)
|
states[TAG] = LexerState.new(patterns)
|
||||||
states[TAG].add_transition(Constants.TokenType.Identifier, BASE)
|
states[TAG].add_transition(Constants.TokenType.Identifier, BASE)
|
||||||
|
@ -236,7 +238,11 @@ func tokenize():
|
||||||
lines.append('')
|
lines.append('')
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
tokens += tokenize_line(line, line_number)
|
var line_tokens = tokenize_line(line, line_number)
|
||||||
|
if line_tokens == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
tokens.append_array(line_tokens)
|
||||||
line_number += 1
|
line_number += 1
|
||||||
|
|
||||||
var end_of_input = Token.new(
|
var end_of_input = Token.new(
|
||||||
|
@ -252,12 +258,12 @@ func tokenize():
|
||||||
func tokenize_line(line, line_number):
|
func tokenize_line(line, line_number):
|
||||||
var token_stack = []
|
var token_stack = []
|
||||||
|
|
||||||
var fresh_line = line.replace('\t',' ').replace('\r','')
|
var fresh_line = line.replace('\t', ' ').replace('\r', '')
|
||||||
|
|
||||||
var indentation = line_indentation(line)
|
var indentation = line_indentation(line)
|
||||||
var previous_indentation = indent_stack.front()[0]
|
var previous_indentation = indent_stack.front()[0]
|
||||||
|
|
||||||
if should_track_indent && indentation > previous_indentation:
|
if should_track_indent and indentation > previous_indentation:
|
||||||
indent_stack.push_front([indentation, true])
|
indent_stack.push_front([indentation, true])
|
||||||
|
|
||||||
var indent = Token.new(
|
var indent = Token.new(
|
||||||
|
@ -284,7 +290,7 @@ func tokenize_line(line, line_number):
|
||||||
whitespace.compile(WHITESPACE)
|
whitespace.compile(WHITESPACE)
|
||||||
|
|
||||||
while column < fresh_line.length():
|
while column < fresh_line.length():
|
||||||
if fresh_line.substr(column).begins_with(LINE_COMENT):
|
if fresh_line.substr(column).begins_with(LINE_COMMENT):
|
||||||
break
|
break
|
||||||
|
|
||||||
var matched = false
|
var matched = false
|
||||||
|
@ -367,13 +373,13 @@ func tokenize_line(line, line_number):
|
||||||
line_number,
|
line_number,
|
||||||
column
|
column
|
||||||
]
|
]
|
||||||
assert(false, 'Expected %s in file %s in node "%s" on line #%d (column #%d)' % error_data)
|
compiler.assert(false, 'Expected %s in file %s in node "%s" on line #%d (column #%d)' % error_data, line_number, column)
|
||||||
|
return
|
||||||
|
|
||||||
var last_whitespace = whitespace.search(line, column)
|
var last_whitespace = whitespace.search(line, column)
|
||||||
if last_whitespace:
|
if last_whitespace:
|
||||||
column += last_whitespace.get_string().length()
|
column += last_whitespace.get_string().length()
|
||||||
|
|
||||||
|
|
||||||
token_stack.invert()
|
token_stack.invert()
|
||||||
|
|
||||||
return token_stack
|
return token_stack
|
|
@ -1,13 +1,17 @@
|
||||||
extends Object
|
extends Object
|
||||||
|
|
||||||
|
# warnings-disable
|
||||||
|
|
||||||
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
||||||
const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd')
|
const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd')
|
||||||
const Value = preload('res://addons/Wol/core/Value.gd')
|
const Value = preload('res://addons/Wol/core/Value.gd')
|
||||||
|
|
||||||
var tokens = []
|
var compiler
|
||||||
var title = ''
|
var title = ''
|
||||||
|
var tokens = []
|
||||||
|
|
||||||
func _init(_title, _tokens):
|
func _init(_compiler, _title, _tokens):
|
||||||
|
compiler = _compiler
|
||||||
title = _title
|
title = _title
|
||||||
tokens = _tokens
|
tokens = _tokens
|
||||||
|
|
||||||
|
@ -21,13 +25,19 @@ func parse_node():
|
||||||
return WolNode.new('Start', null, self)
|
return WolNode.new('Start', null, self)
|
||||||
|
|
||||||
func next_symbol_is(valid_types):
|
func next_symbol_is(valid_types):
|
||||||
var type = self.tokens.front().type
|
if tokens.size() == 0:
|
||||||
for valid_type in valid_types:
|
var error_tokens = []
|
||||||
if type == valid_type:
|
for token in valid_types:
|
||||||
return true
|
error_tokens.append(Constants.token_name(token))
|
||||||
return false
|
|
||||||
|
|
||||||
# NOTE:0 look ahead for `<<` and `else`
|
if error_tokens == ['TagMarker']:
|
||||||
|
error_tokens.append('OptionEnd')
|
||||||
|
|
||||||
|
compiler.assert(tokens.size() != 0, 'Ran out of tokens looking for next symbol "%s"!' % PoolStringArray(error_tokens).join(', '))
|
||||||
|
|
||||||
|
return tokens.front() and tokens.front().type in valid_types
|
||||||
|
|
||||||
|
# NOTE: 0 look ahead for `<<` and `else`
|
||||||
func next_symbols_are(valid_types):
|
func next_symbols_are(valid_types):
|
||||||
var temporary = [] + tokens
|
var temporary = [] + tokens
|
||||||
for type in valid_types:
|
for type in valid_types:
|
||||||
|
@ -36,12 +46,13 @@ func next_symbols_are(valid_types):
|
||||||
return true
|
return true
|
||||||
|
|
||||||
func expect_symbol(token_types = []):
|
func expect_symbol(token_types = []):
|
||||||
|
if compiler.assert(tokens.size() != 0, 'Ran out of tokens expecting next symbol!'):
|
||||||
|
return
|
||||||
|
|
||||||
var token = tokens.pop_front() as Lexer.Token
|
var token = tokens.pop_front() as Lexer.Token
|
||||||
|
|
||||||
if token_types.size() == 0:
|
if token_types.size() == 0:
|
||||||
if token.type == Constants.TokenType.EndOfInput:
|
compiler.assert(token.type != Constants.TokenType.EndOfInput, 'Unexpected end of input')
|
||||||
assert(false, 'Unexpected end of input')
|
|
||||||
return null
|
|
||||||
return token
|
return token
|
||||||
|
|
||||||
for type in token_types:
|
for type in token_types:
|
||||||
|
@ -61,15 +72,12 @@ func expect_symbol(token_types = []):
|
||||||
error_guess = ''
|
error_guess = ''
|
||||||
|
|
||||||
var error_data = [
|
var error_data = [
|
||||||
token.filename,
|
|
||||||
title,
|
|
||||||
token.line_number,
|
|
||||||
token.column,
|
|
||||||
PoolStringArray(token_names).join(', '),
|
PoolStringArray(token_names).join(', '),
|
||||||
Constants.token_type_name(token.type),
|
Constants.token_type_name(token.type),
|
||||||
error_guess
|
error_guess
|
||||||
]
|
]
|
||||||
assert(false, '[%s|%s:%d:%d]:\nExpected token "%s" but got "%s"%s' % error_data)
|
compiler.assert(false, 'Expected token "%s" but got "%s"%s' % error_data, token.line_number, token.column)
|
||||||
|
return
|
||||||
|
|
||||||
static func tab(indent_level, input, newline = true):
|
static func tab(indent_level, input, newline = true):
|
||||||
return '%*s| %s%s' % [indent_level * 2, '', input, '' if not newline else '\n']
|
return '%*s| %s%s' % [indent_level * 2, '', input, '' if not newline else '\n']
|
||||||
|
@ -112,12 +120,26 @@ class WolNode extends ParseNode:
|
||||||
|
|
||||||
var editor_node_tags = []
|
var editor_node_tags = []
|
||||||
var statements = []
|
var statements = []
|
||||||
|
var parser
|
||||||
|
|
||||||
func _init(name, parent, parser).(parent, parser):
|
func _init(_name, parent, _parser).(parent, _parser):
|
||||||
self.name = name
|
name = _name
|
||||||
|
parser = _parser
|
||||||
while parser.tokens.size() > 0 \
|
while parser.tokens.size() > 0 \
|
||||||
and not parser.next_symbol_is([Constants.TokenType.Dedent, Constants.TokenType.EndOfInput]):
|
and not parser.next_symbol_is([Constants.TokenType.Dedent, Constants.TokenType.EndOfInput]):
|
||||||
statements.append(Statement.new(self, parser))
|
|
||||||
|
parser.compiler.assert(
|
||||||
|
not parser.next_symbol_is([Constants.TokenType.Indent]),
|
||||||
|
'Found a stray indentation!',
|
||||||
|
parser.tokens.front().line_number,
|
||||||
|
parser.tokens.front().column
|
||||||
|
)
|
||||||
|
|
||||||
|
var statement = Statement.new(self, parser)
|
||||||
|
if statement.failed_to_parse:
|
||||||
|
break
|
||||||
|
|
||||||
|
statements.append(statement)
|
||||||
|
|
||||||
func tree_string(indent_level):
|
func tree_string(indent_level):
|
||||||
var info = []
|
var info = []
|
||||||
|
@ -126,9 +148,85 @@ class WolNode extends ParseNode:
|
||||||
|
|
||||||
return PoolStringArray(info).join('')
|
return PoolStringArray(info).join('')
|
||||||
|
|
||||||
# TODO: Evaluate use
|
class Statement extends ParseNode:
|
||||||
class Header extends ParseNode:
|
var Type = Constants.StatementTypes
|
||||||
pass
|
|
||||||
|
var type = -1
|
||||||
|
var block
|
||||||
|
var if_statement
|
||||||
|
var option_statement
|
||||||
|
var assignment
|
||||||
|
var shortcut_option_group
|
||||||
|
var custom_command
|
||||||
|
var line
|
||||||
|
var failed_to_parse = false
|
||||||
|
|
||||||
|
func _init(parent, parser).(parent, parser):
|
||||||
|
if Block.can_parse(parser):
|
||||||
|
block = Block.new(self, parser)
|
||||||
|
type = Type.Block
|
||||||
|
|
||||||
|
elif IfStatement.can_parse(parser):
|
||||||
|
if_statement = IfStatement.new(self, parser)
|
||||||
|
type = Type.IfStatement
|
||||||
|
|
||||||
|
elif OptionStatement.can_parse(parser):
|
||||||
|
option_statement = OptionStatement.new(self, parser)
|
||||||
|
type = Type.OptionStatement
|
||||||
|
|
||||||
|
elif Assignment.can_parse(parser):
|
||||||
|
assignment = Assignment.new(self, parser)
|
||||||
|
type = Type.AssignmentStatement
|
||||||
|
|
||||||
|
elif ShortcutOptionGroup.can_parse(parser):
|
||||||
|
shortcut_option_group = ShortcutOptionGroup.new(self, parser)
|
||||||
|
type = Type.ShortcutOptionGroup
|
||||||
|
|
||||||
|
elif CustomCommand.can_parse(parser):
|
||||||
|
custom_command = CustomCommand.new(self, parser)
|
||||||
|
type = Type.CustomCommand
|
||||||
|
|
||||||
|
elif parser.next_symbol_is([Constants.TokenType.Text]):
|
||||||
|
line = LineNode.new(self, parser)
|
||||||
|
type = Type.Line
|
||||||
|
|
||||||
|
else:
|
||||||
|
parser.compiler.assert(false, 'Expected a statement but got %s instead. (probably an imbalanced if statement)' % parser.tokens.front()._to_string())
|
||||||
|
failed_to_parse = true
|
||||||
|
return
|
||||||
|
|
||||||
|
var tags = []
|
||||||
|
|
||||||
|
while parser.next_symbol_is([Constants.TokenType.TagMarker]):
|
||||||
|
parser.expect_symbol([Constants.TokenType.TagMarker])
|
||||||
|
var tag = parser.expect_symbol([Constants.TokenType.Identifier]).value
|
||||||
|
tags.append(tag)
|
||||||
|
|
||||||
|
if tags.size() > 0:
|
||||||
|
self.tags = tags
|
||||||
|
|
||||||
|
func tree_string(indent_level):
|
||||||
|
var info = []
|
||||||
|
|
||||||
|
match type:
|
||||||
|
Type.Block:
|
||||||
|
info.append(block.tree_string(indent_level))
|
||||||
|
Type.IfStatement:
|
||||||
|
info.append(if_statement.tree_string(indent_level))
|
||||||
|
Type.AssignmentStatement:
|
||||||
|
info.append(assignment.tree_string(indent_level))
|
||||||
|
Type.OptionStatement:
|
||||||
|
info.append(option_statement.tree_string(indent_level))
|
||||||
|
Type.ShortcutOptionGroup:
|
||||||
|
info.append(shortcut_option_group.tree_string(indent_level))
|
||||||
|
Type.CustomCommand:
|
||||||
|
info.append(custom_command.tree_string(indent_level))
|
||||||
|
Type.Line:
|
||||||
|
info.append(line.tree_string(indent_level))
|
||||||
|
_:
|
||||||
|
self.parser.compiler.assert(false, 'Cannot print statement')
|
||||||
|
|
||||||
|
return PoolStringArray(info).join('')
|
||||||
|
|
||||||
class InlineExpression extends ParseNode:
|
class InlineExpression extends ParseNode:
|
||||||
var expression
|
var expression
|
||||||
|
@ -153,7 +251,8 @@ class FormatFunctionNode extends ParseNode:
|
||||||
format_text="["
|
format_text="["
|
||||||
parser.expect_symbol([Constants.TokenType.FormatFunctionStart])
|
parser.expect_symbol([Constants.TokenType.FormatFunctionStart])
|
||||||
|
|
||||||
while !parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]):
|
# FIXME: Add exit condition in case of failure
|
||||||
|
while parser.tokens.size() > 0 and not parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]):
|
||||||
if parser.next_symbol_is([Constants.TokenType.Text]):
|
if parser.next_symbol_is([Constants.TokenType.Text]):
|
||||||
format_text += parser.expect_symbol().value
|
format_text += parser.expect_symbol().value
|
||||||
|
|
||||||
|
@ -173,7 +272,7 @@ class FormatFunctionNode extends ParseNode:
|
||||||
class LineNode extends ParseNode:
|
class LineNode extends ParseNode:
|
||||||
var line_text = ''
|
var line_text = ''
|
||||||
# FIXME: Right now we are putting the formatfunctions and inline expressions in the same
|
# FIXME: Right now we are putting the formatfunctions and inline expressions in the same
|
||||||
# list but if at some point we want to stronly type our sub list we need to make a new
|
# list but if at some point we want to strongly type our sub list we need to make a new
|
||||||
# parse node that can have either an InlineExpression or a FunctionFormat
|
# parse node that can have either an InlineExpression or a FunctionFormat
|
||||||
# .. This is a consideration for Godot4.x
|
# .. This is a consideration for Godot4.x
|
||||||
var substitutions = []
|
var substitutions = []
|
||||||
|
@ -212,7 +311,7 @@ class LineNode extends ParseNode:
|
||||||
if line_id.empty():
|
if line_id.empty():
|
||||||
line_id = tag_token.value
|
line_id = tag_token.value
|
||||||
else:
|
else:
|
||||||
printerr("Too many line_tags @[%s:%d]" %[parser.currentNodeName, tag_token.line_number])
|
parser.compiler.assert(false, 'Too many line_tags @[%s:%d]' % [parser.currentNodeName, tag_token.line_number])
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
tags.append(tag_token.value)
|
tags.append(tag_token.value)
|
||||||
|
@ -229,82 +328,6 @@ class LineNode extends ParseNode:
|
||||||
func tree_string(indent_level):
|
func tree_string(indent_level):
|
||||||
return tab(indent_level, 'Line: (%s)[%d]' % [line_text, substitutions.size()])
|
return tab(indent_level, 'Line: (%s)[%d]' % [line_text, substitutions.size()])
|
||||||
|
|
||||||
class Statement extends ParseNode:
|
|
||||||
var Type = Constants.StatementTypes
|
|
||||||
|
|
||||||
var type = -1
|
|
||||||
var block
|
|
||||||
var if_statement
|
|
||||||
var option_statement
|
|
||||||
var assignment
|
|
||||||
var shortcut_option_group
|
|
||||||
var custom_command
|
|
||||||
var line
|
|
||||||
|
|
||||||
func _init(parent, parser).(parent, parser):
|
|
||||||
if Block.can_parse(parser):
|
|
||||||
block = Block.new(self, parser)
|
|
||||||
type = Type.Block
|
|
||||||
|
|
||||||
elif IfStatement.can_parse(parser):
|
|
||||||
if_statement = IfStatement.new(self, parser)
|
|
||||||
type = Type.IfStatement
|
|
||||||
|
|
||||||
elif OptionStatement.can_parse(parser):
|
|
||||||
option_statement = OptionStatement.new(self, parser)
|
|
||||||
type = Type.OptionStatement
|
|
||||||
|
|
||||||
elif Assignment.can_parse(parser):
|
|
||||||
assignment = Assignment.new(self, parser)
|
|
||||||
type = Type.AssignmentStatement
|
|
||||||
|
|
||||||
elif ShortcutOptionGroup.can_parse(parser):
|
|
||||||
shortcut_option_group = ShortcutOptionGroup.new(self, parser)
|
|
||||||
type = Type.ShortcutOptionGroup
|
|
||||||
|
|
||||||
elif CustomCommand.can_parse(parser):
|
|
||||||
custom_command = CustomCommand.new(self, parser)
|
|
||||||
type = Type.CustomCommand
|
|
||||||
|
|
||||||
elif parser.next_symbol_is([Constants.TokenType.Text]):
|
|
||||||
line = LineNode.new(self, parser)
|
|
||||||
type = Type.Line
|
|
||||||
|
|
||||||
else:
|
|
||||||
printerr('Expected a statement but got %s instead. (probably an imbalanced if statement)' % parser.tokens.front()._to_string())
|
|
||||||
|
|
||||||
var tags = []
|
|
||||||
|
|
||||||
while parser.next_symbol_is([Constants.TokenType.TagMarker]):
|
|
||||||
parser.expect_symbol([Constants.TokenType.TagMarker])
|
|
||||||
var tag = parser.expect_symbol([Constants.TokenType.Identifier]).value
|
|
||||||
tags.append(tag)
|
|
||||||
|
|
||||||
if tags.size() > 0:
|
|
||||||
self.tags = tags
|
|
||||||
|
|
||||||
func tree_string(indent_level):
|
|
||||||
var info = []
|
|
||||||
|
|
||||||
match type:
|
|
||||||
Type.Block:
|
|
||||||
info.append(block.tree_string(indent_level))
|
|
||||||
Type.IfStatement:
|
|
||||||
info.append(if_statement.tree_string(indent_level))
|
|
||||||
Type.AssignmentStatement:
|
|
||||||
info.append(assignment.tree_string(indent_level))
|
|
||||||
Type.OptionStatement:
|
|
||||||
info.append(option_statement.tree_string(indent_level))
|
|
||||||
Type.ShortcutOptionGroup:
|
|
||||||
info.append(shortcut_option_group.tree_string(indent_level))
|
|
||||||
Type.CustomCommand:
|
|
||||||
info.append(custom_command.tree_string(indent_level))
|
|
||||||
Type.Line:
|
|
||||||
info.append(line.tree_string(indent_level))
|
|
||||||
_:
|
|
||||||
printerr('Cannot print statement')
|
|
||||||
|
|
||||||
return PoolStringArray(info).join('')
|
|
||||||
|
|
||||||
class CustomCommand extends ParseNode:
|
class CustomCommand extends ParseNode:
|
||||||
|
|
||||||
|
@ -323,7 +346,8 @@ class CustomCommand extends ParseNode:
|
||||||
var command_tokens = []
|
var command_tokens = []
|
||||||
command_tokens.append(parser.expect_symbol())
|
command_tokens.append(parser.expect_symbol())
|
||||||
|
|
||||||
while not parser.next_symbol_is([Constants.TokenType.EndCommand]):
|
# FIXME: add exit condition
|
||||||
|
while parser.tokens.size() > 0 and not parser.next_symbol_is([Constants.TokenType.EndCommand]):
|
||||||
command_tokens.append(parser.expect_symbol())
|
command_tokens.append(parser.expect_symbol())
|
||||||
|
|
||||||
parser.expect_symbol([Constants.TokenType.EndCommand])
|
parser.expect_symbol([Constants.TokenType.EndCommand])
|
||||||
|
@ -339,9 +363,9 @@ class CustomCommand extends ParseNode:
|
||||||
type = Type.Expression
|
type = Type.Expression
|
||||||
|
|
||||||
else:
|
else:
|
||||||
#otherwise evaluuate command
|
# otherwise evaluate command
|
||||||
type = Type.ClientCommand
|
type = Type.ClientCommand
|
||||||
self.client_command = command_tokens[0].value
|
client_command = command_tokens[0].value
|
||||||
|
|
||||||
func tree_string(indent_level):
|
func tree_string(indent_level):
|
||||||
match type:
|
match type:
|
||||||
|
@ -363,9 +387,7 @@ class ShortcutOptionGroup extends ParseNode:
|
||||||
# parse options until there is no more
|
# parse options until there is no more
|
||||||
# expect one otherwise invalid
|
# expect one otherwise invalid
|
||||||
|
|
||||||
var index = 1
|
var index = 0
|
||||||
options.append(ShortCutOption.new(index, self, parser))
|
|
||||||
index += 1
|
|
||||||
while parser.next_symbol_is([Constants.TokenType.ShortcutOption]):
|
while parser.next_symbol_is([Constants.TokenType.ShortcutOption]):
|
||||||
options.append(ShortCutOption.new(index, self, parser))
|
options.append(ShortCutOption.new(index, self, parser))
|
||||||
index += 1
|
index += 1
|
||||||
|
@ -394,7 +416,7 @@ class ShortCutOption extends ParseNode:
|
||||||
parser.expect_symbol([Constants.TokenType.ShortcutOption])
|
parser.expect_symbol([Constants.TokenType.ShortcutOption])
|
||||||
label = parser.expect_symbol([Constants.TokenType.Text]).value
|
label = parser.expect_symbol([Constants.TokenType.Text]).value
|
||||||
|
|
||||||
# NOTE: Parse the conditional << if $x >> when it exists
|
# FIXME: Parse the conditional << if $x >> when it exists
|
||||||
var tags = []
|
var tags = []
|
||||||
while parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]) \
|
while parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]) \
|
||||||
or parser.next_symbol_is([Constants.TokenType.TagMarker]):
|
or parser.next_symbol_is([Constants.TokenType.TagMarker]):
|
||||||
|
@ -447,7 +469,8 @@ class Block extends ParseNode:
|
||||||
parser.expect_symbol([Constants.TokenType.Indent])
|
parser.expect_symbol([Constants.TokenType.Indent])
|
||||||
|
|
||||||
#keep reading statements until we hit a dedent
|
#keep reading statements until we hit a dedent
|
||||||
while not parser.next_symbol_is([Constants.TokenType.Dedent]):
|
# FIXME: find exit condition
|
||||||
|
while parser.tokens.size() > 0 and not parser.next_symbol_is([Constants.TokenType.Dedent]):
|
||||||
#parse all statements including nested blocks
|
#parse all statements including nested blocks
|
||||||
statements.append(Statement.new(self, parser))
|
statements.append(Statement.new(self, parser))
|
||||||
|
|
||||||
|
@ -633,7 +656,7 @@ class ValueNode extends ParseNode:
|
||||||
Constants.TokenType.NullToken:
|
Constants.TokenType.NullToken:
|
||||||
value = Value.new(null)
|
value = Value.new(null)
|
||||||
_:
|
_:
|
||||||
printerr('%s, Invalid token type' % token.name)
|
self.parser.compiler.assert(false, '%s, Invalid token type' % token.name)
|
||||||
|
|
||||||
func tree_string(indent_level):
|
func tree_string(indent_level):
|
||||||
return tab(indent_level, '%s' % value.value())
|
return tab(indent_level, '%s' % value.value())
|
||||||
|
@ -670,6 +693,8 @@ class ExpressionNode extends ParseNode:
|
||||||
|
|
||||||
# Using Djikstra's shunting-yard algorithm to convert stream of expresions into postfix notation,
|
# Using Djikstra's shunting-yard algorithm to convert stream of expresions into postfix notation,
|
||||||
# & then build a tree of expressions
|
# & then build a tree of expressions
|
||||||
|
|
||||||
|
# TODO: Rework expression parsing
|
||||||
static func parse(parent, parser):
|
static func parse(parent, parser):
|
||||||
var rpn = []
|
var rpn = []
|
||||||
var op_stack = []
|
var op_stack = []
|
||||||
|
@ -698,14 +723,19 @@ class ExpressionNode extends ParseNode:
|
||||||
while parser.tokens.size() > 0 and parser.next_symbol_is(valid_types):
|
while parser.tokens.size() > 0 and parser.next_symbol_is(valid_types):
|
||||||
var next = parser.expect_symbol(valid_types)
|
var next = parser.expect_symbol(valid_types)
|
||||||
|
|
||||||
if next.type == Constants.TokenType.Variable \
|
if next.type in [
|
||||||
or next.type == Constants.TokenType.Number \
|
Constants.TokenType.Variable,
|
||||||
or next.type == Constants.TokenType.Str \
|
Constants.TokenType.Number,
|
||||||
or next.type == Constants.TokenType.FalseToken \
|
Constants.TokenType.Str,
|
||||||
or next.type == Constants.TokenType.TrueToken \
|
Constants.TokenType.FalseToken,
|
||||||
or next.type == Constants.TokenType.NullToken:
|
Constants.TokenType.TrueToken,
|
||||||
|
Constants.TokenType.NullToken
|
||||||
|
]:
|
||||||
|
|
||||||
#output primitives
|
# Output primitives
|
||||||
|
if func_stack.size() != 0:
|
||||||
|
op_stack.append(next)
|
||||||
|
else:
|
||||||
rpn.append(next)
|
rpn.append(next)
|
||||||
elif next.type == Constants.TokenType.Identifier:
|
elif next.type == Constants.TokenType.Identifier:
|
||||||
op_stack.push_back(next)
|
op_stack.push_back(next)
|
||||||
|
@ -713,6 +743,7 @@ class ExpressionNode extends ParseNode:
|
||||||
|
|
||||||
#next token is parent - left
|
#next token is parent - left
|
||||||
next = parser.expect_symbol([Constants.TokenType.LeftParen])
|
next = parser.expect_symbol([Constants.TokenType.LeftParen])
|
||||||
|
if next:
|
||||||
op_stack.push_back(next)
|
op_stack.push_back(next)
|
||||||
|
|
||||||
elif next.type == Constants.TokenType.Comma:
|
elif next.type == Constants.TokenType.Comma:
|
||||||
|
@ -720,20 +751,19 @@ class ExpressionNode extends ParseNode:
|
||||||
while op_stack.back().type != Constants.TokenType.LeftParen:
|
while op_stack.back().type != Constants.TokenType.LeftParen:
|
||||||
var p = op_stack.pop_back()
|
var p = op_stack.pop_back()
|
||||||
if p == null:
|
if p == null:
|
||||||
printerr('unbalanced parenthesis %s ' % next.name)
|
parser.compiler.assert(false, 'unbalanced parenthesis %s' % next.name)
|
||||||
break
|
break
|
||||||
rpn.append(p)
|
rpn.append(p)
|
||||||
|
|
||||||
|
|
||||||
#next token in op_stack left paren
|
#next token in op_stack left paren
|
||||||
# next parser token not allowed to be right paren or comma
|
# next parser token not allowed to be right paren or comma
|
||||||
if parser.next_symbol_is([Constants.TokenType.RightParen,
|
if parser.next_symbol_is([Constants.TokenType.RightParen, Constants.TokenType.Comma]):
|
||||||
Constants.TokenType.Comma]):
|
parser.compiler.assert(false, 'Expected Expression : %s' % parser.tokens.front().name)
|
||||||
printerr('Expected Expression : %s' % parser.tokens.front().name)
|
|
||||||
|
|
||||||
#find the closest function on stack
|
#find the closest function on stack
|
||||||
#increment parameters
|
#increment parameters
|
||||||
func_stack.back().parameter_count+=1
|
func_stack.back().parameter_count += 1
|
||||||
|
|
||||||
elif Operator.is_op(next.type):
|
elif Operator.is_op(next.type):
|
||||||
#this is an operator
|
#this is an operator
|
||||||
|
@ -760,7 +790,7 @@ class ExpressionNode extends ParseNode:
|
||||||
next.type = Constants.TokenType.EqualTo
|
next.type = Constants.TokenType.EqualTo
|
||||||
|
|
||||||
#operator precedence
|
#operator precedence
|
||||||
while (ExpressionNode.is_apply_precedence(next.type, op_stack)):
|
while ExpressionNode.is_apply_precedence(next.type, op_stack, parser):
|
||||||
var op = op_stack.pop_back()
|
var op = op_stack.pop_back()
|
||||||
rpn.append(op)
|
rpn.append(op)
|
||||||
|
|
||||||
|
@ -772,20 +802,28 @@ class ExpressionNode extends ParseNode:
|
||||||
elif next.type == Constants.TokenType.RightParen:
|
elif next.type == Constants.TokenType.RightParen:
|
||||||
#leaving sub expression
|
#leaving sub expression
|
||||||
# resolve order of operations
|
# resolve order of operations
|
||||||
|
var parameters = []
|
||||||
while op_stack.back().type != Constants.TokenType.LeftParen:
|
while op_stack.back().type != Constants.TokenType.LeftParen:
|
||||||
rpn.append(op_stack.pop_back())
|
parameters.append(op_stack.pop_back())
|
||||||
if op_stack.back() == null:
|
|
||||||
printerr('Unbalanced parenthasis #RightParen. Parser.ExpressionNode')
|
parser.compiler.assert(
|
||||||
|
op_stack.back() != null,
|
||||||
|
'Unbalanced parenthasis #RightParen. Parser.ExpressionNode'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
rpn.append_array(parameters)
|
||||||
op_stack.pop_back()
|
op_stack.pop_back()
|
||||||
|
# FIXME: Something is going on with parameter counting, fixed for now
|
||||||
|
# but needs a bigger rework
|
||||||
if op_stack.back().type == Constants.TokenType.Identifier:
|
if op_stack.back().type == Constants.TokenType.Identifier:
|
||||||
#function call
|
#function call
|
||||||
#last token == left paren this == no parameters
|
#last token == left paren this == no parameters
|
||||||
#else
|
#else
|
||||||
#we have more than 1 param
|
#we have more than 1 param
|
||||||
if last.type != Constants.TokenType.LeftParen:
|
# if last.type != Constants.TokenType.LeftParen:
|
||||||
func_stack.back().parameter_count+=1
|
# func_stack.back().parameter_count += 1
|
||||||
|
func_stack.back().parameter_count = parameters.size()
|
||||||
|
|
||||||
rpn.append(op_stack.pop_back())
|
rpn.append(op_stack.pop_back())
|
||||||
func_stack.pop_back()
|
func_stack.pop_back()
|
||||||
|
@ -799,7 +837,7 @@ class ExpressionNode extends ParseNode:
|
||||||
|
|
||||||
#if rpn is empty then this is not expression
|
#if rpn is empty then this is not expression
|
||||||
if rpn.size() == 0:
|
if rpn.size() == 0:
|
||||||
printerr('Error parsing expression: Expression not found!')
|
parser.compiler.assert(false, 'Error parsing expression: Expression not found!')
|
||||||
|
|
||||||
#build expression tree
|
#build expression tree
|
||||||
var first = rpn.front()
|
var first = rpn.front()
|
||||||
|
@ -809,10 +847,17 @@ class ExpressionNode extends ParseNode:
|
||||||
var next = rpn.pop_front()
|
var next = rpn.pop_front()
|
||||||
if Operator.is_op(next.type):
|
if Operator.is_op(next.type):
|
||||||
#operation
|
#operation
|
||||||
var info = Operator.op_info(next.type)
|
var info = Operator.op_info(next.type, parser)
|
||||||
|
|
||||||
if eval_stack.size() < info.arguments:
|
if eval_stack.size() < info.arguments:
|
||||||
printerr('Error parsing : Not enough arguments for %s [ got %s expected - was %s]'%[Constants.token_type_name(next.type), eval_stack.size(), info.arguments])
|
parser.compiler.assert(false,
|
||||||
|
'Error parsing : Not enough arguments for %s [ got %s expected - was %s]' \
|
||||||
|
% [
|
||||||
|
Constants.token_type_name(next.type),
|
||||||
|
eval_stack.size(),
|
||||||
|
info.arguments
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
var function_parameters = []
|
var function_parameters = []
|
||||||
for _i in range(info.arguments):
|
for _i in range(info.arguments):
|
||||||
|
@ -846,10 +891,14 @@ class ExpressionNode extends ParseNode:
|
||||||
eval_stack.append(expression)
|
eval_stack.append(expression)
|
||||||
|
|
||||||
|
|
||||||
#we should have a single root expression left
|
# NOTE: We should have a single root expression left
|
||||||
#if more then we failed ---- NANI
|
# if more then we failed
|
||||||
if eval_stack.size() != 1:
|
parser.compiler.assert(
|
||||||
printerr('[%s] Error parsing expression (stack did not reduce correctly )' % first)
|
eval_stack.size() == 1,
|
||||||
|
'[%s] Error parsing expression (stack did not reduce correctly)' % first,
|
||||||
|
first.line_number,
|
||||||
|
first.column
|
||||||
|
)
|
||||||
|
|
||||||
return eval_stack.pop_back()
|
return eval_stack.pop_back()
|
||||||
|
|
||||||
|
@ -861,20 +910,24 @@ class ExpressionNode extends ParseNode:
|
||||||
return key
|
return key
|
||||||
return string
|
return string
|
||||||
|
|
||||||
static func is_apply_precedence(_type, operator_stack):
|
static func is_apply_precedence(_type, operator_stack, parser):
|
||||||
if operator_stack.size() == 0:
|
if operator_stack.size() == 0:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
if not Operator.is_op(_type):
|
if parser.compiler.assert(Operator.is_op(_type), 'Unable to parse expression!'):
|
||||||
assert(false, 'Unable to parse expression!')
|
return false
|
||||||
|
|
||||||
|
# FIXME: Make sure there can't be a Null value here
|
||||||
|
if parser.compiler.assert(operator_stack.back() != null, 'Something went wrong getting precedence'):
|
||||||
|
return false
|
||||||
|
|
||||||
var second = operator_stack.back().type
|
var second = operator_stack.back().type
|
||||||
|
|
||||||
if not Operator.is_op(second):
|
if not Operator.is_op(second):
|
||||||
return false
|
return false
|
||||||
|
|
||||||
var first_info = Operator.op_info(_type)
|
var first_info = Operator.op_info(_type, parser)
|
||||||
var second_info = Operator.op_info(second)
|
var second_info = Operator.op_info(second, parser)
|
||||||
|
|
||||||
return \
|
return \
|
||||||
(first_info.associativity == Associativity.Left \
|
(first_info.associativity == Associativity.Left \
|
||||||
|
@ -934,9 +987,9 @@ class Operator extends ParseNode:
|
||||||
info.append(tab(indent_level, op_type))
|
info.append(tab(indent_level, op_type))
|
||||||
return info.join('')
|
return info.join('')
|
||||||
|
|
||||||
static func op_info(op):
|
static func op_info(op, parser):
|
||||||
if not Operator.is_op(op) :
|
if parser.compiler.assert(Operator.is_op(op), '%s is not a valid operator' % op):
|
||||||
printerr('%s is not a valid operator' % op.name)
|
return
|
||||||
|
|
||||||
#determine associativity and operands
|
#determine associativity and operands
|
||||||
# each operand has
|
# each operand has
|
||||||
|
@ -960,8 +1013,8 @@ class Operator extends ParseNode:
|
||||||
TokenType.Xor:
|
TokenType.Xor:
|
||||||
return OperatorInfo.new(Associativity.Left, 2, 2)
|
return OperatorInfo.new(Associativity.Left, 2, 2)
|
||||||
_:
|
_:
|
||||||
printerr('Unknown operator: %s' % op.name)
|
parser.compiler.assert(false, 'Unknown operator: %s' % op.name)
|
||||||
return null
|
return
|
||||||
|
|
||||||
static func is_op(type):
|
static func is_op(type):
|
||||||
return type in op_types()
|
return type in op_types()
|
67
addons/Wol/editor/Editor.gd
Normal file
67
addons/Wol/editor/Editor.gd
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
tool
|
||||||
|
extends Panel
|
||||||
|
|
||||||
|
var current_graph_node
|
||||||
|
|
||||||
|
onready var preview = get_node('../Preview')
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
hide()
|
||||||
|
connect('visibility_changed', self, '_on_visibility_changed')
|
||||||
|
$Tools/Left/Play.connect('pressed', self, '_on_play')
|
||||||
|
$Tools/Right/Close.connect('pressed', self, 'close')
|
||||||
|
|
||||||
|
func close():
|
||||||
|
hide()
|
||||||
|
preview.close()
|
||||||
|
|
||||||
|
func open_node(graph_node):
|
||||||
|
current_graph_node = graph_node
|
||||||
|
|
||||||
|
var text_edit = graph_node.get_node('TextEdit')
|
||||||
|
text_edit.get_parent().remove_child(text_edit)
|
||||||
|
$Content.add_child(text_edit)
|
||||||
|
toggle_text_edit(text_edit)
|
||||||
|
|
||||||
|
$Tools/Left/Title.disconnect('text_changed', self, '_on_title_changed')
|
||||||
|
$Tools/Left/Title.text = graph_node.node.title
|
||||||
|
$Tools/Left/Title.connect('text_changed', self, '_on_title_changed')
|
||||||
|
|
||||||
|
show()
|
||||||
|
|
||||||
|
func toggle_text_edit(text_edit):
|
||||||
|
text_edit.anchor_left = 0
|
||||||
|
text_edit.anchor_top = 0
|
||||||
|
text_edit.anchor_bottom = 1
|
||||||
|
text_edit.anchor_right = 1
|
||||||
|
text_edit.margin_left = 0
|
||||||
|
text_edit.margin_right = 0
|
||||||
|
text_edit.margin_bottom = 0
|
||||||
|
text_edit.margin_top = 0
|
||||||
|
text_edit.mouse_filter = MOUSE_FILTER_STOP if text_edit.get_parent().name == 'Content' else MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
|
text_edit.deselect()
|
||||||
|
|
||||||
|
for property in [
|
||||||
|
'highlight_current_line',
|
||||||
|
'show_line_numbers',
|
||||||
|
'draw_tabs',
|
||||||
|
'smooth_scrolling',
|
||||||
|
'wrap_enabled',
|
||||||
|
'minimap_draw'
|
||||||
|
]:
|
||||||
|
text_edit.set(property, not text_edit.get(property))
|
||||||
|
|
||||||
|
func _on_play():
|
||||||
|
preview.open_node(current_graph_node)
|
||||||
|
|
||||||
|
func _on_title_changed():
|
||||||
|
current_graph_node.node.title = $HBoxContainer/TextEdit.text
|
||||||
|
current_graph_node.compile()
|
||||||
|
|
||||||
|
func _on_visibility_changed():
|
||||||
|
if not visible:
|
||||||
|
var text_edit = $Content/TextEdit
|
||||||
|
$Content.remove_child(text_edit)
|
||||||
|
current_graph_node.add_child(text_edit)
|
||||||
|
toggle_text_edit(text_edit)
|
48
addons/Wol/editor/GraphNodeTemplate.tscn
Normal file
48
addons/Wol/editor/GraphNodeTemplate.tscn
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
[gd_scene load_steps=3 format=2]
|
||||||
|
|
||||||
|
[ext_resource path="res://addons/Wol/editor/WolGraphNode.gd" type="Script" id=1]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id=1]
|
||||||
|
content_margin_left = 4.0
|
||||||
|
content_margin_right = 4.0
|
||||||
|
content_margin_top = 26.0
|
||||||
|
content_margin_bottom = 4.0
|
||||||
|
corner_radius_top_left = 4
|
||||||
|
corner_radius_top_right = 4
|
||||||
|
corner_radius_bottom_right = 4
|
||||||
|
corner_radius_bottom_left = 4
|
||||||
|
|
||||||
|
[node name="GraphNodeTemplate" type="GraphNode"]
|
||||||
|
margin_left = 500.0
|
||||||
|
margin_top = 1068.0
|
||||||
|
margin_right = 937.0
|
||||||
|
margin_bottom = 1459.0
|
||||||
|
mouse_filter = 1
|
||||||
|
custom_styles/frame = SubResource( 1 )
|
||||||
|
title = "Hello world"
|
||||||
|
offset = Vector2( 500, 500 )
|
||||||
|
resizable = true
|
||||||
|
slot/0/left_enabled = false
|
||||||
|
slot/0/left_type = 0
|
||||||
|
slot/0/left_color = Color( 1, 1, 1, 1 )
|
||||||
|
slot/0/right_enabled = false
|
||||||
|
slot/0/right_type = 0
|
||||||
|
slot/0/right_color = Color( 1, 1, 1, 1 )
|
||||||
|
script = ExtResource( 1 )
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="TextEdit" type="TextEdit" parent="."]
|
||||||
|
margin_left = 4.0
|
||||||
|
margin_top = 26.0
|
||||||
|
margin_right = 433.0
|
||||||
|
margin_bottom = 387.0
|
||||||
|
mouse_filter = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
syntax_highlighting = true
|
||||||
|
fold_gutter = true
|
||||||
|
context_menu_enabled = false
|
||||||
|
virtual_keyboard_enabled = false
|
||||||
|
wrap_enabled = true
|
82
addons/Wol/editor/Preview.gd
Normal file
82
addons/Wol/editor/Preview.gd
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
extends Panel
|
||||||
|
|
||||||
|
var current_graph_node
|
||||||
|
|
||||||
|
onready var line_template = $Content/List/LineTemplate
|
||||||
|
onready var button_template = $Options/List/ButtonTemplate
|
||||||
|
|
||||||
|
onready var wol_editor = find_parent('WolEditor')
|
||||||
|
|
||||||
|
# TODO: Make sure all focus is lost when clicking this pane
|
||||||
|
# TODO: Add restart button
|
||||||
|
# TODO: Add next button
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
hide()
|
||||||
|
$Wol.connect('line', self, '_on_line')
|
||||||
|
$Wol.connect('options', self, '_on_options')
|
||||||
|
$Tools/Right/Close.connect('pressed', self, 'close')
|
||||||
|
|
||||||
|
func open_node(graph_node):
|
||||||
|
current_graph_node = graph_node
|
||||||
|
|
||||||
|
$Wol.stop()
|
||||||
|
|
||||||
|
for child in $Content/List.get_children():
|
||||||
|
if child != line_template and not 'Padding' in child.name:
|
||||||
|
$Content/List.remove_child(child)
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
for child in $Options/List.get_children():
|
||||||
|
if child != button_template:
|
||||||
|
$Options/List.remove_child(child)
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
$Wol.variable_storage = {}
|
||||||
|
$Wol.set_program(wol_editor.get_program())
|
||||||
|
$Wol.start(current_graph_node.node.title)
|
||||||
|
show()
|
||||||
|
|
||||||
|
func close():
|
||||||
|
hide()
|
||||||
|
current_graph_node = null
|
||||||
|
$Wol.stop()
|
||||||
|
|
||||||
|
func next():
|
||||||
|
$Wol.resume()
|
||||||
|
|
||||||
|
func _on_line(line):
|
||||||
|
var line_node = line_template.duplicate()
|
||||||
|
$Content/List.add_child(line_node)
|
||||||
|
$Content/List/PaddingBottom.raise()
|
||||||
|
# TODO: Add hash() based color from speaker
|
||||||
|
|
||||||
|
line_node.get_node('RichTextLabel').bbcode_text = line.text
|
||||||
|
line_node.show()
|
||||||
|
yield(get_tree(), 'idle_frame')
|
||||||
|
$Content.scroll_vertical = $Content/List.rect_size.y
|
||||||
|
|
||||||
|
func _on_options(options):
|
||||||
|
for option in options:
|
||||||
|
var button = button_template.duplicate()
|
||||||
|
$Options/List.add_child(button)
|
||||||
|
|
||||||
|
button.text = option.line.text
|
||||||
|
button.connect('pressed', self, '_on_option_pressed', [option])
|
||||||
|
button.show()
|
||||||
|
|
||||||
|
func _on_option_pressed(option):
|
||||||
|
$Wol.select_option(option.id)
|
||||||
|
|
||||||
|
for child in $Options/List.get_children():
|
||||||
|
if child != button_template:
|
||||||
|
$Options/List.remove_child(child)
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
func _input(event):
|
||||||
|
if visible \
|
||||||
|
and ( \
|
||||||
|
(event is InputEventMouseButton and event.doubleclick) \
|
||||||
|
or (event is InputEventKey and not event.pressed and event.scancode in [KEY_SPACE, KEY_ENTER]) \
|
||||||
|
):
|
||||||
|
next()
|
183
addons/Wol/editor/WolEditor.gd
Normal file
183
addons/Wol/editor/WolEditor.gd
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
tool
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd')
|
||||||
|
onready var GraphNodeTemplate = $GraphNodeTemplate
|
||||||
|
|
||||||
|
var path
|
||||||
|
var refreshed = false
|
||||||
|
var selected_node
|
||||||
|
|
||||||
|
onready var original_delete_node_dialog = $DeleteNodeDialog.dialog_text
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
for menu_button in [$Menu/File]:
|
||||||
|
menu_button.get_popup().connect('index_pressed', self, '_on_menu_pressed', [menu_button.get_popup()])
|
||||||
|
|
||||||
|
$GraphEdit.connect('gui_input', self, '_on_graph_edit_input')
|
||||||
|
$GraphEdit.connect('node_selected', self, '_on_node_selected', [true])
|
||||||
|
$GraphEdit.connect('node_unselected', self, '_on_node_selected', [false])
|
||||||
|
|
||||||
|
$DeleteNodeDialog.connect('confirmed', self, 'delete_node')
|
||||||
|
|
||||||
|
# TODO: Conditionally load in theme based on Editor or standalone
|
||||||
|
|
||||||
|
path = 'res://dialogue.wol'
|
||||||
|
build_nodes()
|
||||||
|
|
||||||
|
func create_node(position = Vector2.ZERO):
|
||||||
|
print('creating node!')
|
||||||
|
var graph_node = GraphNodeTemplate.duplicate()
|
||||||
|
$GraphEdit.add_child(graph_node)
|
||||||
|
|
||||||
|
var node = {
|
||||||
|
'title': 'NewNode',
|
||||||
|
'body': 'Wol: Hello world',
|
||||||
|
'position': position
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_node.connect('recompiled', self, '_on_graph_node_recompiled', [graph_node])
|
||||||
|
graph_node.connect('gui_input', self, '_on_graph_node_input', [graph_node])
|
||||||
|
|
||||||
|
graph_node.node = node
|
||||||
|
graph_node.show()
|
||||||
|
|
||||||
|
func delete_node(node = selected_node):
|
||||||
|
$GraphEdit.remove_child(node)
|
||||||
|
node.queue_free()
|
||||||
|
|
||||||
|
func build_nodes():
|
||||||
|
for node in Compiler.new(path).get_nodes():
|
||||||
|
var graph_node = GraphNodeTemplate.duplicate()
|
||||||
|
$GraphEdit.add_child(graph_node)
|
||||||
|
|
||||||
|
graph_node.connect('recompiled', self, '_on_graph_node_recompiled', [graph_node])
|
||||||
|
graph_node.connect('gui_input', self, '_on_graph_node_input', [graph_node])
|
||||||
|
|
||||||
|
graph_node.node = node
|
||||||
|
graph_node.show()
|
||||||
|
|
||||||
|
func get_program():
|
||||||
|
return Compiler.new(null, serialize_to_file(), true).compile()
|
||||||
|
|
||||||
|
func serialize_to_file():
|
||||||
|
var buffer = []
|
||||||
|
for graph_node in $GraphEdit.get_children():
|
||||||
|
if not graph_node is GraphNode:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var node = graph_node.node
|
||||||
|
buffer.append('title: %s' % node.title)
|
||||||
|
buffer.append('tags: ')
|
||||||
|
buffer.append('colorID: ')
|
||||||
|
buffer.append('position: %d, %d' % [node.position.x, node.position.y])
|
||||||
|
buffer.append('---')
|
||||||
|
buffer.append(node.body)
|
||||||
|
buffer.append('===')
|
||||||
|
|
||||||
|
return PoolStringArray(buffer).join('\n')
|
||||||
|
|
||||||
|
func save_as(file_path = null):
|
||||||
|
if not file_path:
|
||||||
|
$FileDialog.mode = $FileDialog.MODE_SAVE_FILE
|
||||||
|
# TODO: Set up path based on context (Godot editor, standalone or web)
|
||||||
|
$FileDialog.popup_centered()
|
||||||
|
file_path = yield($FileDialog, 'file_selected')
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
var file = File.new()
|
||||||
|
file.open(file_path, File.WRITE)
|
||||||
|
file.store_string(serialize_to_file())
|
||||||
|
file.close()
|
||||||
|
print('saved file!')
|
||||||
|
|
||||||
|
func open():
|
||||||
|
$FileDialog.mode = $FileDialog.MODE_OPEN_FILE
|
||||||
|
# TODO: Set up path based on context (Godot editor, standalone or web)
|
||||||
|
$FileDialog.popup_centered()
|
||||||
|
path = yield($FileDialog, 'file_selected')
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
for node in $GraphEdit.get_children():
|
||||||
|
if node is GraphNode:
|
||||||
|
$GraphEdit.remove_child(node)
|
||||||
|
node.queue_free()
|
||||||
|
|
||||||
|
yield(get_tree(), 'idle_frame')
|
||||||
|
build_nodes()
|
||||||
|
|
||||||
|
func new():
|
||||||
|
# TODO: add dialog for maybe saving existing file
|
||||||
|
|
||||||
|
for node in $GraphEdit.get_children():
|
||||||
|
if node is GraphNode:
|
||||||
|
$GraphEdit.remove_child(node)
|
||||||
|
node.queue_free()
|
||||||
|
|
||||||
|
path = null
|
||||||
|
|
||||||
|
func _on_menu_pressed(index, node):
|
||||||
|
match(node.get_item_text(index)):
|
||||||
|
'New':
|
||||||
|
new()
|
||||||
|
'Save':
|
||||||
|
save_as(path)
|
||||||
|
'Save as...':
|
||||||
|
save_as()
|
||||||
|
'Open':
|
||||||
|
open()
|
||||||
|
|
||||||
|
# FIXME: Come up with better way of showing connections between nodes
|
||||||
|
func _on_graph_node_recompiled(_graph_node):
|
||||||
|
if refreshed: return
|
||||||
|
$GraphEdit.clear_connections()
|
||||||
|
for graph_node in $GraphEdit.get_children():
|
||||||
|
if not graph_node is GraphNode:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var connections = graph_node.get_connections()
|
||||||
|
graph_node.set_slot_enabled_right(0, connections.size() != 0)
|
||||||
|
graph_node.set_slot_type_right(0, 1)
|
||||||
|
|
||||||
|
for connection in connections:
|
||||||
|
if not $GraphEdit.has_node(connection):
|
||||||
|
continue
|
||||||
|
|
||||||
|
var other_graph_node = $GraphEdit.get_node(connection)
|
||||||
|
other_graph_node.set_slot_enabled_left(0, true)
|
||||||
|
other_graph_node.set_slot_type_left(0, 1)
|
||||||
|
|
||||||
|
$GraphEdit.connect_node(graph_node.name, 0, connection, 0)
|
||||||
|
|
||||||
|
refreshed = true
|
||||||
|
yield(get_tree().create_timer(.3), 'timeout')
|
||||||
|
refreshed = false
|
||||||
|
|
||||||
|
func _on_graph_node_input(event, graph_node):
|
||||||
|
if event is InputEventMouseButton \
|
||||||
|
and event.doubleclick and event.button_index == BUTTON_LEFT:
|
||||||
|
$HBoxContainer/Editor.open_node(graph_node)
|
||||||
|
accept_event()
|
||||||
|
|
||||||
|
func _on_node_selected(node, selected):
|
||||||
|
if not selected:
|
||||||
|
selected_node = null
|
||||||
|
else:
|
||||||
|
selected_node = node
|
||||||
|
|
||||||
|
func _on_graph_edit_input(event):
|
||||||
|
if event is InputEventMouseButton \
|
||||||
|
and event.doubleclick and event.button_index == BUTTON_LEFT:
|
||||||
|
create_node(event.global_position + $GraphEdit.scroll_offset)
|
||||||
|
|
||||||
|
func _input(event):
|
||||||
|
if event is InputEventKey \
|
||||||
|
and not event.pressed and event.scancode == KEY_DELETE \
|
||||||
|
and selected_node \
|
||||||
|
and not $HBoxContainer/Editor.visible:
|
||||||
|
$DeleteNodeDialog.dialog_text = original_delete_node_dialog % selected_node.name
|
||||||
|
$DeleteNodeDialog.popup()
|
||||||
|
|
BIN
addons/Wol/editor/WolEditor.tscn
(Stored with Git LFS)
Normal file
BIN
addons/Wol/editor/WolEditor.tscn
(Stored with Git LFS)
Normal file
Binary file not shown.
91
addons/Wol/editor/WolGraphNode.gd
Normal file
91
addons/Wol/editor/WolGraphNode.gd
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
tool
|
||||||
|
extends GraphNode
|
||||||
|
|
||||||
|
signal recompiled
|
||||||
|
|
||||||
|
const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd')
|
||||||
|
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
||||||
|
|
||||||
|
var node setget set_node
|
||||||
|
|
||||||
|
var error_lines = []
|
||||||
|
var compiler
|
||||||
|
var program
|
||||||
|
|
||||||
|
onready var text_edit = $TextEdit
|
||||||
|
|
||||||
|
# TODO: Add syntax highlighting
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
connect('offset_changed', self, '_on_offset_changed')
|
||||||
|
text_edit.connect('text_changed', self, '_on_text_changed')
|
||||||
|
$TextDebounce.connect('timeout', self, '_on_debounce')
|
||||||
|
|
||||||
|
func get_connections():
|
||||||
|
# NOTE: Program failed to compile
|
||||||
|
if not program:
|
||||||
|
return []
|
||||||
|
|
||||||
|
var program_node = program.nodes.values().front()
|
||||||
|
var connections = []
|
||||||
|
|
||||||
|
# FIXME: Don't rely on checking on 'Label\d'
|
||||||
|
var label_check = RegEx.new()
|
||||||
|
label_check.compile('^Label\\doption_\\d')
|
||||||
|
|
||||||
|
for instruction in program_node.instructions:
|
||||||
|
# NOTE: When next node is explicit
|
||||||
|
if instruction.operation == Constants.ByteCode.RunNode:
|
||||||
|
if instruction.operands.size() > 0 \
|
||||||
|
and instruction.operands.front().value != name:
|
||||||
|
connections.append(instruction.operands.front().value)
|
||||||
|
|
||||||
|
# NOTE: When next node is decided through options
|
||||||
|
if instruction.operation == Constants.ByteCode.AddOption:
|
||||||
|
if instruction.operands.size() == 2 \
|
||||||
|
and not label_check.search(instruction.operands[1].value) \
|
||||||
|
and instruction.operands[1].value != name:
|
||||||
|
connections.append(instruction.operands[1].value)
|
||||||
|
|
||||||
|
return connections
|
||||||
|
|
||||||
|
func _on_text_changed():
|
||||||
|
$TextDebounce.start()
|
||||||
|
|
||||||
|
func _on_debounce():
|
||||||
|
text_edit.get_node('ErrorGutter').hide()
|
||||||
|
node.body = text_edit.text
|
||||||
|
for line in error_lines:
|
||||||
|
text_edit.set_line_as_safe(line - 1, false)
|
||||||
|
compile()
|
||||||
|
|
||||||
|
func _on_offset_changed():
|
||||||
|
node.position = offset
|
||||||
|
|
||||||
|
func _on_error(message, line_number, _column):
|
||||||
|
var error_gutter = text_edit.get_node('ErrorGutter')
|
||||||
|
error_gutter.show()
|
||||||
|
error_gutter.text = message
|
||||||
|
|
||||||
|
error_lines.append(line_number)
|
||||||
|
|
||||||
|
text_edit.set_line_as_safe(line_number - 1, true)
|
||||||
|
|
||||||
|
func set_node(_node):
|
||||||
|
node = _node
|
||||||
|
title = node.title
|
||||||
|
name = node.title
|
||||||
|
text_edit.text = node.body
|
||||||
|
text_edit.clear_undo_history()
|
||||||
|
offset = node.position
|
||||||
|
|
||||||
|
compile()
|
||||||
|
|
||||||
|
func compile():
|
||||||
|
var text = 'title: %s\n---\n%s\n===' % [node.title, text_edit.text]
|
||||||
|
compiler = Compiler.new(null, text, true)
|
||||||
|
compiler.connect('error', self, '_on_error')
|
||||||
|
program = compiler.compile()
|
||||||
|
|
||||||
|
yield(get_tree(), 'idle_frame')
|
||||||
|
emit_signal('recompiled')
|
|
@ -1,6 +1,10 @@
|
||||||
tool
|
tool
|
||||||
extends EditorPlugin
|
extends EditorPlugin
|
||||||
|
|
||||||
|
const WolEditor = preload('res://addons/Wol/editor/WolEditor.tscn')
|
||||||
|
|
||||||
|
var wol_editor_instance
|
||||||
|
|
||||||
func _enter_tree():
|
func _enter_tree():
|
||||||
add_custom_type(
|
add_custom_type(
|
||||||
'Wol',
|
'Wol',
|
||||||
|
@ -9,5 +13,44 @@ func _enter_tree():
|
||||||
load('res://addons/Wol/icon-white.svg')
|
load('res://addons/Wol/icon-white.svg')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
wol_editor_instance = WolEditor.instance()
|
||||||
|
get_editor_interface().get_editor_viewport().add_child(wol_editor_instance)
|
||||||
|
|
||||||
|
make_visible(false)
|
||||||
|
|
||||||
|
call_deferred('move_button')
|
||||||
|
|
||||||
|
func move_button():
|
||||||
|
var buttons = get_editor_interface().get_base_control()
|
||||||
|
var path = [0, 0, 2]
|
||||||
|
for child_number in path:
|
||||||
|
if buttons.get_child_count() > child_number:
|
||||||
|
buttons = buttons.get_child(child_number)
|
||||||
|
|
||||||
|
if buttons.has_node('AssetLib'):
|
||||||
|
buttons.get_node('AssetLib').raise()
|
||||||
|
|
||||||
|
func make_visible(visible):
|
||||||
|
if wol_editor_instance:
|
||||||
|
wol_editor_instance.visible = visible
|
||||||
|
|
||||||
func _exit_tree():
|
func _exit_tree():
|
||||||
remove_custom_type('Wol')
|
remove_custom_type('Wol')
|
||||||
|
|
||||||
|
if wol_editor_instance:
|
||||||
|
wol_editor_instance.queue_free()
|
||||||
|
|
||||||
|
func has_main_screen():
|
||||||
|
return true
|
||||||
|
|
||||||
|
func get_plugin_name():
|
||||||
|
return 'Wol'
|
||||||
|
|
||||||
|
func get_plugin_icon():
|
||||||
|
# FIXME: Change this code so it doesn't show a warning on activation
|
||||||
|
var icon = ImageTexture.new()
|
||||||
|
var image = Image.new()
|
||||||
|
image.load('res://addons/Wol/icon-white.svg')
|
||||||
|
image.resize(34, 34)
|
||||||
|
icon.create_from_image(image)
|
||||||
|
return icon
|
||||||
|
|
|
@ -1,39 +1,11 @@
|
||||||
title: Start
|
|
||||||
tags:
|
|
||||||
colorID: 0
|
|
||||||
position: 0, 0
|
|
||||||
---
|
|
||||||
<<a_custom_command>>
|
|
||||||
<<command_with multiple arguments>>
|
|
||||||
|
|
||||||
// remove "to" to trigger error
|
|
||||||
<<set $direction to 'that'>>
|
|
||||||
<<set $one to 1>>
|
|
||||||
|
|
||||||
// Implement inline expressions
|
|
||||||
<<if visit_count() == 1>>
|
|
||||||
Narrator: You, {$direction} way!
|
|
||||||
<<endif>>
|
|
||||||
Narrator: Do you know you've been here {visit_count()} times?
|
|
||||||
You: Did you know one + one equals {$one + $one}?
|
|
||||||
Narrator: You wanna go somewhere?
|
|
||||||
|
|
||||||
-> Go to the store
|
|
||||||
[[TheStore]]
|
|
||||||
// -> Wait, how many times have I been to the store?
|
|
||||||
// Narrator: You've been to the store {visit_count('TheStore')} times.
|
|
||||||
// [[Start]]
|
|
||||||
-> Lets stay here and talk
|
|
||||||
[[Talk]]
|
|
||||||
===
|
|
||||||
title: TheStore
|
title: TheStore
|
||||||
tags:
|
tags:
|
||||||
colorID: 0
|
colorID:
|
||||||
position: 0, 200
|
position: 400, 200
|
||||||
---
|
---
|
||||||
Guy: Hey what's up I need your help can you come here?
|
Guy: Hey what's up I need your help can you come here?
|
||||||
You: Well I can't I'm buying clothes.
|
You: Well I can't I'm buying clothes.
|
||||||
All right well hurry up and come over here.
|
Guy: All right well hurry up and come over here.
|
||||||
You: I can't find them.
|
You: I can't find them.
|
||||||
Guy: What do you mean you can't find them?
|
Guy: What do you mean you can't find them?
|
||||||
You: I can't find them there's only soup.
|
You: I can't find them there's only soup.
|
||||||
|
@ -56,10 +28,38 @@ Guy: WHY ARE YOU BUYING CLOTHES AT THE SOUP STORE?!
|
||||||
You: FUCK YOU!
|
You: FUCK YOU!
|
||||||
[[Go home|Start]]
|
[[Go home|Start]]
|
||||||
===
|
===
|
||||||
|
title: Start
|
||||||
|
tags:
|
||||||
|
colorID:
|
||||||
|
position: 0, 0
|
||||||
|
---
|
||||||
|
<<a_custom_command>>
|
||||||
|
<<command_with multiple arguments>>
|
||||||
|
|
||||||
|
// remove "to" to trigger error
|
||||||
|
<<set $direction to "that">>
|
||||||
|
<<set $one to 1>>
|
||||||
|
|
||||||
|
// Implement inline expressions
|
||||||
|
<<if visit_count() == 1>>
|
||||||
|
Narrator: You, {$direction} way!
|
||||||
|
<<endif>>
|
||||||
|
Narrator: Do you know you've been here {visit_count()} times?
|
||||||
|
You: Did you know one + one equals {$one + $one}?
|
||||||
|
Narrator: You wanna go somewhere?
|
||||||
|
|
||||||
|
-> Go to the store
|
||||||
|
[[TheStore]]
|
||||||
|
-> How much did I visit the store?
|
||||||
|
Narrator: You've been to the store { visit_count("TheStore") } times.
|
||||||
|
[[Start]]
|
||||||
|
-> Lets stay here and talk
|
||||||
|
[[Talk]]
|
||||||
|
===
|
||||||
title: Talk
|
title: Talk
|
||||||
tags:
|
tags:
|
||||||
colorID: 0
|
colorID:
|
||||||
position: 0, 400
|
position: 800, 400
|
||||||
---
|
---
|
||||||
Narrator: So how are you really?
|
Narrator: So how are you really?
|
||||||
You: I'm good!
|
You: I'm good!
|
|
@ -11,7 +11,7 @@ config_version=4
|
||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Wol"
|
config/name="Wol"
|
||||||
run/main_scene="res://Dialogue.tscn"
|
run/main_scene="res://TestScene.tscn"
|
||||||
config/icon="res://icon.png"
|
config/icon="res://icon.png"
|
||||||
|
|
||||||
[debug]
|
[debug]
|
||||||
|
@ -24,6 +24,11 @@ gdscript/warnings/return_value_discarded=false
|
||||||
|
|
||||||
enabled=PoolStringArray( "res://addons/Wol/plugin.cfg" )
|
enabled=PoolStringArray( "res://addons/Wol/plugin.cfg" )
|
||||||
|
|
||||||
|
[network]
|
||||||
|
|
||||||
|
limits/debugger_stdout/max_chars_per_second=8096
|
||||||
|
limits/debugger_stdout/max_messages_per_frame=200
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
common/enable_pause_aware_picking=true
|
common/enable_pause_aware_picking=true
|
||||||
|
|
Reference in a new issue