mirror of
https://github.com/git-learning-game/oh-my-git.git
synced 2025-05-09 05:02:22 +02:00
Move all .tscn and .gd files into scenes/ directory
This commit is contained in:
parent
c330524f8e
commit
61304803bc
38 changed files with 88 additions and 77 deletions
scenes
arrow.gdarrow.tscncard.gdcard.tscncard_particles.tscncardgame.gdcardgame.tscncards.tscnchapter.gddrop_area.gddrop_area.tscnfile_browser.gdfile_browser.tscnfile_browser_item.gdfile_browser_item.tscngame.gdhelpers.gdlevel.gdlevel_repo.gdlevels.gdmain.gdmain.tscnnode.gdnode.tscnplayer.tscnrepository.gdrepository.tscnsandbox.gdsandbox.tscnshell.gdtcp_server.gdtcp_server.tscnterminal.gdterminal.tscntext_editor.gdtext_editor.tscn
36
scenes/arrow.gd
Normal file
36
scenes/arrow.gd
Normal file
|
@ -0,0 +1,36 @@
|
|||
extends Node2D
|
||||
|
||||
var source: String
|
||||
var target: String
|
||||
|
||||
var repository: Control
|
||||
|
||||
func _ready():
|
||||
pass
|
||||
|
||||
func _process(_delta):
|
||||
position = Vector2(0,0)
|
||||
|
||||
if not (repository and repository.objects.has(source)):
|
||||
return
|
||||
|
||||
var start = repository.objects[source].position
|
||||
var end = start + Vector2(0, 60)
|
||||
|
||||
if repository and repository.objects.has(target) and repository.objects[target].visible:
|
||||
var t = repository.objects[target]
|
||||
end = t.position
|
||||
$Target.hide()
|
||||
else:
|
||||
$Target.text = target
|
||||
if $Target.text.substr(0, 5) != "refs/":
|
||||
$Target.text = ""
|
||||
$Target.show()
|
||||
$Line.hide()
|
||||
$Tip.hide()
|
||||
|
||||
$Line.points[1] = end - repository.objects[source].position
|
||||
# Move the tip away from the object a bit.
|
||||
$Line.points[1] -= $Line.points[1].normalized()*30
|
||||
$Tip.position = $Line.points[1]
|
||||
$Tip.rotation = PI+$Line.points[0].angle_to($Line.points[1])
|
60
scenes/arrow.tscn
Normal file
60
scenes/arrow.tscn
Normal file
|
@ -0,0 +1,60 @@
|
|||
[gd_scene load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
|
||||
[ext_resource path="res://scenes/arrow.gd" type="Script" id=2]
|
||||
|
||||
|
||||
[node name="Arrow" type="Node2D"]
|
||||
show_behind_parent = true
|
||||
script = ExtResource( 2 )
|
||||
|
||||
[node name="Line" type="Line2D" parent="."]
|
||||
points = PoolVector2Array( -0.480499, -0.11055, 158.301, 0.581757 )
|
||||
default_color = Color( 0.2, 0.2, 0.2, 1 )
|
||||
|
||||
[node name="Tip" type="Node2D" parent="."]
|
||||
position = Vector2( 158.06, 0.290878 )
|
||||
z_index = 1
|
||||
|
||||
[node name="Polygon" type="Polygon2D" parent="Tip"]
|
||||
position = Vector2( -24.7164, -6.37881 )
|
||||
z_index = -1
|
||||
color = Color( 0.2, 0.2, 0.2, 1 )
|
||||
polygon = PoolVector2Array( -8.50021, 20.4619, 36.1874, 8.44903, 0.869781, -21.8232 )
|
||||
|
||||
[node name="Polygon2" type="Polygon2D" parent="Tip"]
|
||||
visible = false
|
||||
position = Vector2( -9.66138, -2.89842 )
|
||||
z_index = -1
|
||||
color = Color( 0.2, 0.2, 0.2, 1 )
|
||||
polygon = PoolVector2Array( -8.50021, 20.4619, 22.2526, 5.80623, 2.31131, -19.9012, -12.104, -23.7453, 4.95413, 1.72188, -21.9546, 16.1372 )
|
||||
|
||||
[node name="Label" type="Node2D" parent="."]
|
||||
visible = false
|
||||
position = Vector2( 102, 46 )
|
||||
|
||||
[node name="ID" type="Label" parent="Label"]
|
||||
margin_left = -19.374
|
||||
margin_top = -5.93085
|
||||
margin_right = 20.626
|
||||
margin_bottom = 8.06915
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
custom_colors/font_color = Color( 1, 1, 1, 1 )
|
||||
text = "label"
|
||||
align = 1
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Target" type="Label" parent="."]
|
||||
margin_left = -230.84
|
||||
margin_top = 42.1225
|
||||
margin_right = 231.16
|
||||
margin_bottom = 68.1225
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
custom_colors/font_color = Color( 0.356863, 0.356863, 0.356863, 1 )
|
||||
text = "label"
|
||||
align = 1
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
120
scenes/card.gd
Normal file
120
scenes/card.gd
Normal file
|
@ -0,0 +1,120 @@
|
|||
extends Node2D
|
||||
|
||||
var hovered = false
|
||||
var dragged = false
|
||||
var drag_offset
|
||||
|
||||
export var arg_number = 0
|
||||
export var command = "" setget set_command
|
||||
export var description = "" setget set_description
|
||||
export var energy = 0 setget set_energy
|
||||
|
||||
var _first_argument = null
|
||||
var _home_position = null
|
||||
var _home_rotation = null
|
||||
|
||||
onready var energy_label = $Sprite/Energy
|
||||
|
||||
func _ready():
|
||||
set_process_unhandled_input(true)
|
||||
set_energy(energy)
|
||||
|
||||
func _process(delta):
|
||||
if game.energy >= energy:
|
||||
energy_label.modulate = Color(0.5, 1, 0.5)
|
||||
else:
|
||||
energy_label.modulate = Color(1, 1, 1)
|
||||
modulate = Color(1, 0.5, 0.5)
|
||||
|
||||
if dragged:
|
||||
var mousepos = get_viewport().get_mouse_position()
|
||||
global_position = mousepos - drag_offset
|
||||
|
||||
var target_scale = 1
|
||||
|
||||
if hovered and not dragged:
|
||||
target_scale = 1.5
|
||||
|
||||
var speed = 5
|
||||
|
||||
scale = lerp(scale, Vector2(target_scale, target_scale), 10*delta)
|
||||
|
||||
func _unhandled_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == BUTTON_LEFT and event.pressed and hovered:
|
||||
dragged = true
|
||||
game.dragged_object = self
|
||||
$PickupSound.play()
|
||||
drag_offset = get_viewport().get_mouse_position() - global_position
|
||||
get_tree().set_input_as_handled()
|
||||
modulate.a = 0.5
|
||||
elif event.button_index == BUTTON_LEFT and !event.pressed and dragged:
|
||||
dragged = false
|
||||
game.dragged_object = null
|
||||
modulate.a = 1
|
||||
|
||||
if get_viewport().get_mouse_position().y < get_viewport().size.y/3*2:
|
||||
if arg_number == 0 :
|
||||
try_play($Label.text)
|
||||
else:
|
||||
move_back()
|
||||
else:
|
||||
move_back()
|
||||
|
||||
func _mouse_entered():
|
||||
hovered = true
|
||||
z_index = 1
|
||||
|
||||
func _mouse_exited():
|
||||
hovered = false
|
||||
z_index = 0
|
||||
|
||||
func set_command(new_command):
|
||||
command = new_command
|
||||
$Label.text = command
|
||||
|
||||
func set_description(new_description):
|
||||
description = new_description
|
||||
$Description.text = description
|
||||
|
||||
func set_energy(new_energy):
|
||||
energy = new_energy
|
||||
if energy_label:
|
||||
energy_label.text = str(energy)
|
||||
|
||||
func move_back():
|
||||
position = _home_position
|
||||
rotation_degrees = _home_rotation
|
||||
$ReturnSound.play()
|
||||
|
||||
func buuurn():
|
||||
move_back()
|
||||
|
||||
func dropped_on(other):
|
||||
var full_command = ""
|
||||
match arg_number:
|
||||
1:
|
||||
var argument = other.id
|
||||
if ($Label.text.begins_with("git checkout") or $Label.text.begins_with("git rebase")) and other.id.begins_with("refs/heads"):
|
||||
argument = Array(other.id.split("/")).pop_back()
|
||||
full_command = $Label.text + " " + argument
|
||||
try_play(full_command)
|
||||
# 2:
|
||||
# if _first_argument:
|
||||
# full_command = $Label.text + " " + _first_argument + " " + other.id
|
||||
# $"../Terminal".send_command(full_command)
|
||||
# buuurn()
|
||||
# else:
|
||||
# _first_argument = other.id
|
||||
|
||||
func try_play(command):
|
||||
if game.energy >= energy:
|
||||
$PlaySound.play()
|
||||
var particles = preload("res://scenes/card_particles.tscn").instance()
|
||||
particles.position = position
|
||||
get_parent().add_child(particles)
|
||||
$"../../..".terminal.send_command(command)
|
||||
buuurn()
|
||||
game.energy -= energy
|
||||
else:
|
||||
move_back()
|
136
scenes/card.tscn
Normal file
136
scenes/card.tscn
Normal file
|
@ -0,0 +1,136 @@
|
|||
[gd_scene load_steps=10 format=2]
|
||||
|
||||
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
|
||||
[ext_resource path="res://nodes/blob.svg" type="Texture" id=2]
|
||||
[ext_resource path="res://scenes/card.gd" type="Script" id=3]
|
||||
[ext_resource path="res://sounds/swish.wav" type="AudioStream" id=4]
|
||||
[ext_resource path="res://sounds/swoosh.wav" type="AudioStream" id=5]
|
||||
[ext_resource path="res://sounds/poof.wav" type="AudioStream" id=6]
|
||||
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=1]
|
||||
bg_color = Color( 0.45098, 0.584314, 0.843137, 1 )
|
||||
border_color = Color( 0.0627451, 0.141176, 0.176471, 1 )
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
shadow_color = Color( 0, 0, 0, 0.392157 )
|
||||
shadow_size = 4
|
||||
shadow_offset = Vector2( -2, 2 )
|
||||
|
||||
[sub_resource type="RectangleShape2D" id=2]
|
||||
extents = Vector2( 105.74, 143.46 )
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=3]
|
||||
bg_color = Color( 1, 1, 1, 0.243137 )
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
|
||||
[node name="Card" type="Node2D" groups=[
|
||||
"cards",
|
||||
]]
|
||||
script = ExtResource( 3 )
|
||||
|
||||
[node name="Panel" type="Panel" parent="."]
|
||||
margin_left = -105.0
|
||||
margin_top = -291.0
|
||||
margin_right = 104.0
|
||||
margin_bottom = -2.0
|
||||
mouse_filter = 2
|
||||
custom_styles/panel = SubResource( 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="ColorRect" type="ColorRect" parent="."]
|
||||
visible = false
|
||||
margin_left = -103.0
|
||||
margin_top = -290.336
|
||||
margin_right = 103.0
|
||||
margin_bottom = -1.33582
|
||||
mouse_filter = 2
|
||||
color = Color( 0.105882, 0.67451, 0.847059, 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Area2D" type="Area2D" parent="."]
|
||||
position = Vector2( 0, -145.336 )
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
|
||||
position = Vector2( -6.10352e-05, 0.00012207 )
|
||||
shape = SubResource( 2 )
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
margin_left = -89.0
|
||||
margin_top = -276.0
|
||||
margin_right = 85.0
|
||||
margin_bottom = -185.0
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "Name"
|
||||
autowrap = true
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="ColorRect2" type="Panel" parent="."]
|
||||
margin_left = -97.0
|
||||
margin_top = -169.0
|
||||
margin_right = 94.0
|
||||
margin_bottom = -10.0
|
||||
mouse_filter = 2
|
||||
custom_styles/panel = SubResource( 3 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Description" type="Label" parent="."]
|
||||
margin_left = -92.0
|
||||
margin_top = -164.0
|
||||
margin_right = 133.0
|
||||
margin_bottom = 23.0
|
||||
rect_scale = Vector2( 0.75, 0.75 )
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "Description"
|
||||
autowrap = true
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Sprite" type="Sprite" parent="."]
|
||||
visible = false
|
||||
position = Vector2( -103.288, -287.778 )
|
||||
scale = Vector2( 0.542341, 0.542341 )
|
||||
texture = ExtResource( 2 )
|
||||
|
||||
[node name="Energy" type="Label" parent="Sprite"]
|
||||
margin_left = -51.1637
|
||||
margin_top = -47.4558
|
||||
margin_right = -17.1637
|
||||
margin_bottom = -16.4558
|
||||
rect_scale = Vector2( 3, 3 )
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
text = "0"
|
||||
align = 1
|
||||
valign = 1
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="PickupSound" type="AudioStreamPlayer" parent="."]
|
||||
stream = ExtResource( 4 )
|
||||
|
||||
[node name="PlaySound" type="AudioStreamPlayer" parent="."]
|
||||
stream = ExtResource( 6 )
|
||||
volume_db = -6.848
|
||||
|
||||
[node name="ReturnSound" type="AudioStreamPlayer" parent="."]
|
||||
stream = ExtResource( 5 )
|
||||
volume_db = -6.848
|
||||
[connection signal="mouse_entered" from="Area2D" to="." method="_mouse_entered"]
|
||||
[connection signal="mouse_exited" from="Area2D" to="." method="_mouse_exited"]
|
67
scenes/card_particles.tscn
Normal file
67
scenes/card_particles.tscn
Normal file
|
@ -0,0 +1,67 @@
|
|||
[gd_scene load_steps=5 format=2]
|
||||
|
||||
[sub_resource type="Curve" id=1]
|
||||
_data = [ Vector2( 0, 1 ), 0.0, 0.0, 0, 0, Vector2( 1, 0 ), -2.75937, 0.0, 0, 0 ]
|
||||
|
||||
[sub_resource type="CurveTexture" id=2]
|
||||
curve = SubResource( 1 )
|
||||
|
||||
[sub_resource type="ParticlesMaterial" id=3]
|
||||
emission_shape = 2
|
||||
emission_box_extents = Vector3( 100, 150, 1 )
|
||||
flag_disable_z = true
|
||||
spread = 180.0
|
||||
gravity = Vector3( 0, 0, 0 )
|
||||
initial_velocity = 232.55
|
||||
initial_velocity_random = 0.52
|
||||
orbit_velocity = 0.0
|
||||
orbit_velocity_random = 0.0
|
||||
scale = 14.95
|
||||
scale_curve = SubResource( 2 )
|
||||
color = Color( 0.223529, 0.592157, 0.772549, 1 )
|
||||
|
||||
[sub_resource type="Animation" id=4]
|
||||
resource_name = "play"
|
||||
length = 2.0
|
||||
tracks/0/type = "method"
|
||||
tracks/0/path = NodePath(".")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/keys = {
|
||||
"times": PoolRealArray( 2 ),
|
||||
"transitions": PoolRealArray( 1 ),
|
||||
"values": [ {
|
||||
"args": [ ],
|
||||
"method": "queue_free"
|
||||
} ]
|
||||
}
|
||||
tracks/1/type = "value"
|
||||
tracks/1/path = NodePath("Particles:emitting")
|
||||
tracks/1/interp = 1
|
||||
tracks/1/loop_wrap = true
|
||||
tracks/1/imported = false
|
||||
tracks/1/enabled = true
|
||||
tracks/1/keys = {
|
||||
"times": PoolRealArray( 0 ),
|
||||
"transitions": PoolRealArray( 1 ),
|
||||
"update": 1,
|
||||
"values": [ true ]
|
||||
}
|
||||
|
||||
[node name="CardParticles" type="Node2D"]
|
||||
|
||||
[node name="Particles" type="Particles2D" parent="."]
|
||||
position = Vector2( -0.539337, -145.087 )
|
||||
emitting = false
|
||||
amount = 32
|
||||
lifetime = 0.2
|
||||
one_shot = true
|
||||
explosiveness = 0.91
|
||||
local_coords = false
|
||||
process_material = SubResource( 3 )
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
|
||||
autoplay = "play"
|
||||
anims/play = SubResource( 4 )
|
224
scenes/cardgame.gd
Normal file
224
scenes/cardgame.gd
Normal file
|
@ -0,0 +1,224 @@
|
|||
extends Control
|
||||
|
||||
var cards = [
|
||||
# {
|
||||
# "command": 'git add .',
|
||||
# "arg_number": 0,
|
||||
# "description": "Add all files in the working directory to the index.",
|
||||
# "energy": 1
|
||||
# },
|
||||
#
|
||||
# {
|
||||
# "command": 'touch "file$RANDOM"',
|
||||
# "arg_number": 0,
|
||||
# "description": "Create a new file.",
|
||||
# "energy": 2
|
||||
# },
|
||||
# {
|
||||
# "command": 'git checkout -b "$RANDOM"',
|
||||
# "arg_number": 0,
|
||||
# "description": "Create a new branch and switch to it.",
|
||||
# "energy": 2
|
||||
# },
|
||||
# {
|
||||
# "command": 'git merge',
|
||||
# "arg_number": 1,
|
||||
# "description": "Merge specified commit into HEAD.",
|
||||
# "energy": 1
|
||||
# },
|
||||
# {
|
||||
# "command": 'git commit --allow-empty -m "$RANDOM"',
|
||||
# "arg_number": 0,
|
||||
# "description": "Add a new commit under HEAD.",
|
||||
# "energy": 1
|
||||
# },
|
||||
{
|
||||
"command": 'git checkout',
|
||||
"arg_number": 1,
|
||||
"description": "Travel to a commit!",
|
||||
#"description": "Point HEAD to a branch or commit, and update the index and the working directory.",
|
||||
"energy": 1
|
||||
},
|
||||
{
|
||||
"command": 'git add .; git commit',
|
||||
"arg_number": 0,
|
||||
"description": "Make a new commit!",
|
||||
"energy": 1
|
||||
},
|
||||
# {
|
||||
# "command": 'git branch new',
|
||||
# "arg_number": 1,
|
||||
# "description": "Create a new timeline.",
|
||||
# "energy": 1
|
||||
# },
|
||||
{
|
||||
"command": 'git merge',
|
||||
"arg_number": 1,
|
||||
"description": "Merge the specified timeline into yours.",
|
||||
"energy": 1
|
||||
},
|
||||
{
|
||||
"command": 'git rebase',
|
||||
"arg_number": 1,
|
||||
"description": "Put the events in your current timeline on top of the specified one.",
|
||||
"energy": 1
|
||||
},
|
||||
{
|
||||
"command": 'git pull',
|
||||
"arg_number": 0,
|
||||
"description": "Get timelines from a colleague.",
|
||||
"energy": 1
|
||||
},
|
||||
{
|
||||
"command": 'git push',
|
||||
"arg_number": 0,
|
||||
"description": "Give timelines to a colleague.",
|
||||
"energy": 1
|
||||
},
|
||||
{
|
||||
"command": 'git rebase -i',
|
||||
"arg_number": 1,
|
||||
"description": "Make changes to the events in your current timeline, back to the commit you drag this to.",
|
||||
"energy": 1
|
||||
},
|
||||
{
|
||||
"command": 'git reset --hard',
|
||||
"arg_number": 1,
|
||||
"description": "Reset current label to the specified commit.",
|
||||
"energy": 1
|
||||
},
|
||||
{
|
||||
"command": 'git cherry-pick',
|
||||
"arg_number": 1,
|
||||
"description": "Repeat the specified action on top of your current timeline.",
|
||||
"energy": 1
|
||||
},
|
||||
# {
|
||||
# "command": 'git update-ref -d',
|
||||
# "arg_number": 1,
|
||||
# "description": "Delete a ref.",
|
||||
# "energy": 1
|
||||
# },
|
||||
# {
|
||||
# "command": 'git reflog expire --expire=now --all; git prune',
|
||||
# "arg_number": 0,
|
||||
# "description": "Delete all unreferenced objects.",
|
||||
# "energy": 1
|
||||
# },
|
||||
# {
|
||||
# "command": 'git rebase',
|
||||
# "arg_number": 1,
|
||||
# "description": "Rebase current branch on top of specified commit.",
|
||||
# "energy": 1
|
||||
# },
|
||||
# {
|
||||
# "command": 'git push -f',
|
||||
# "arg_number": 0,
|
||||
# "description": "Push current branch to the remote, overwriting existing commits. Will make everyone angry.",
|
||||
# "energy": 3
|
||||
# },
|
||||
# {
|
||||
# "command": 'git pull',
|
||||
# "arg_number": 0,
|
||||
# "description": "Pull current branch from the remote.",
|
||||
# "energy": 2
|
||||
# },
|
||||
]
|
||||
|
||||
func _ready():
|
||||
# var path = game.tmp_prefix_inside+"/repos/sandbox/"
|
||||
# helpers.careful_delete(path)
|
||||
#
|
||||
# game.global_shell.run("mkdir " + path)
|
||||
# game.global_shell.cd(path)
|
||||
# game.global_shell.run("git init")
|
||||
# game.global_shell.run("git remote add origin ../remote")
|
||||
# $Repository.path = path
|
||||
# $Terminal.repository = $Repository
|
||||
#
|
||||
# var path2 = game.tmp_prefix_inside+"/repos/remote/"
|
||||
# helpers.careful_delete(path2)
|
||||
#
|
||||
# game.global_shell.run("mkdir " + path2)
|
||||
# game.global_shell.cd(path2)
|
||||
# game.global_shell.run("git init")
|
||||
# game.global_shell.run("git config receive.denyCurrentBranch ignore")
|
||||
# $RepositoryRemote.path = path2
|
||||
|
||||
redraw_all_cards()
|
||||
arrange_cards()
|
||||
pass
|
||||
|
||||
func _process(delta):
|
||||
if $Energy:
|
||||
$Energy.text = str(game.energy)
|
||||
|
||||
#func _update_repo():
|
||||
# $Repository.update_everything()
|
||||
# $RepositoryRemote.update_everything()
|
||||
|
||||
func draw_rand_card():
|
||||
var deck = []
|
||||
|
||||
for card in cards:
|
||||
deck.push_back(card)
|
||||
|
||||
# We want a lot of commit and checkout cards!
|
||||
for i in range(5):
|
||||
deck.push_back(cards[0])
|
||||
deck.push_back(cards[1])
|
||||
|
||||
var card = deck[randi() % deck.size()]
|
||||
draw_card(card)
|
||||
|
||||
func draw_card(card):
|
||||
var new_card = preload("res://scenes/card.tscn").instance()
|
||||
|
||||
new_card.command = card.command
|
||||
new_card.arg_number = card.arg_number
|
||||
new_card.description = card.description
|
||||
new_card.energy = 0 #card.energy
|
||||
new_card.position = Vector2(rect_size.x, rect_size.y*2)
|
||||
add_child(new_card)
|
||||
arrange_cards()
|
||||
|
||||
func arrange_cards():
|
||||
var t = Timer.new()
|
||||
t.wait_time = 0.05
|
||||
add_child(t)
|
||||
t.start()
|
||||
yield(t, "timeout")
|
||||
|
||||
var amount_cards = get_tree().get_nodes_in_group("cards").size()
|
||||
var total_angle = min(50, 45.0/7*amount_cards)
|
||||
var angle_between_cards = 0
|
||||
if amount_cards > 1:
|
||||
angle_between_cards = total_angle / (amount_cards-1)
|
||||
|
||||
var current_angle = -total_angle/2
|
||||
for card in get_tree().get_nodes_in_group("cards"):
|
||||
var target_position = Vector2(rect_size.x/2, rect_size.y + 1500)
|
||||
var target_rotation = current_angle
|
||||
var translation_vec = Vector2(0,-1500).rotated(current_angle/180.0*PI)
|
||||
target_position += translation_vec
|
||||
current_angle += angle_between_cards
|
||||
card._home_position = target_position
|
||||
card._home_rotation = target_rotation
|
||||
|
||||
var tween = Tween.new()
|
||||
tween.interpolate_property(card, "position", card.position, target_position, 0.5, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT)
|
||||
tween.interpolate_property(card, "rotation_degrees", card.rotation_degrees, target_rotation, 0.5, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT)
|
||||
add_child(tween)
|
||||
tween.start()
|
||||
|
||||
func redraw_all_cards():
|
||||
game.energy = 5
|
||||
|
||||
for card in get_tree().get_nodes_in_group("cards"):
|
||||
card.queue_free()
|
||||
|
||||
for card in cards:
|
||||
draw_card(card)
|
||||
|
||||
func add_card(command):
|
||||
draw_card({"command": command, "description": "", "arg_number": 0, "energy": 0})
|
65
scenes/cardgame.tscn
Normal file
65
scenes/cardgame.tscn
Normal file
|
@ -0,0 +1,65 @@
|
|||
[gd_scene load_steps=6 format=2]
|
||||
|
||||
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
|
||||
[ext_resource path="res://scenes/repository.tscn" type="PackedScene" id=3]
|
||||
[ext_resource path="res://scenes/cardgame.gd" type="Script" id=4]
|
||||
[ext_resource path="res://scenes/terminal.tscn" type="PackedScene" id=5]
|
||||
[ext_resource path="res://fonts/big.tres" type="DynamicFont" id=6]
|
||||
|
||||
|
||||
[node name="Cardgame" type="Node2D"]
|
||||
script = ExtResource( 4 )
|
||||
|
||||
[node name="ColorRect" type="ColorRect" parent="."]
|
||||
margin_right = 1922.0
|
||||
margin_bottom = 1083.0
|
||||
mouse_filter = 2
|
||||
color = Color( 0.0823529, 0.0823529, 0.0823529, 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="RepositoryRemote" parent="." instance=ExtResource( 3 )]
|
||||
margin_right = 748.0
|
||||
margin_bottom = 746.0
|
||||
label = "remote"
|
||||
simplified_view = true
|
||||
|
||||
[node name="Repository" parent="." instance=ExtResource( 3 )]
|
||||
margin_left = 762.0
|
||||
margin_right = 1481.0
|
||||
margin_bottom = 744.0
|
||||
label = "yours"
|
||||
simplified_view = true
|
||||
|
||||
[node name="Terminal" parent="." instance=ExtResource( 5 )]
|
||||
margin_left = 1488.0
|
||||
margin_top = 5.0
|
||||
margin_right = 1914.0
|
||||
margin_bottom = 745.0
|
||||
|
||||
[node name="Button" type="Button" parent="."]
|
||||
margin_left = 1719.41
|
||||
margin_top = 814.594
|
||||
margin_right = 1892.41
|
||||
margin_bottom = 856.594
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
text = "Draw new cards"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Energy" type="Label" parent="."]
|
||||
modulate = Color( 0.431373, 0.792157, 0.423529, 1 )
|
||||
margin_left = 35.3364
|
||||
margin_top = 796.429
|
||||
margin_right = 75.3364
|
||||
margin_bottom = 845.429
|
||||
rect_scale = Vector2( 2, 2 )
|
||||
custom_fonts/font = ExtResource( 6 )
|
||||
text = "3"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
[connection signal="command_done" from="Terminal" to="." method="_update_repo"]
|
||||
[connection signal="pressed" from="Button" to="." method="redraw_all_cards"]
|
43
scenes/cards.tscn
Normal file
43
scenes/cards.tscn
Normal file
|
@ -0,0 +1,43 @@
|
|||
[gd_scene load_steps=4 format=2]
|
||||
|
||||
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
|
||||
[ext_resource path="res://fonts/big.tres" type="DynamicFont" id=2]
|
||||
[ext_resource path="res://scenes/cardgame.gd" type="Script" id=3]
|
||||
|
||||
|
||||
[node name="Cards" type="Control"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 2
|
||||
script = ExtResource( 3 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Button" type="Button" parent="."]
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
margin_left = -172.0
|
||||
margin_top = 16.0
|
||||
margin_right = -16.0
|
||||
margin_bottom = 47.0
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
text = "Draw new cards"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Energy" type="Label" parent="."]
|
||||
visible = false
|
||||
modulate = Color( 0.431373, 0.792157, 0.423529, 1 )
|
||||
margin_left = 28.2219
|
||||
margin_top = 16.6766
|
||||
margin_right = 68.2219
|
||||
margin_bottom = 65.6766
|
||||
rect_scale = Vector2( 2, 2 )
|
||||
custom_fonts/font = ExtResource( 2 )
|
||||
text = "3"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
[connection signal="pressed" from="Button" to="." method="redraw_all_cards"]
|
49
scenes/chapter.gd
Normal file
49
scenes/chapter.gd
Normal file
|
@ -0,0 +1,49 @@
|
|||
extends Node
|
||||
class_name Chapter
|
||||
|
||||
var slug
|
||||
var levels
|
||||
|
||||
# Path is an outer path.
|
||||
func load(path):
|
||||
levels = []
|
||||
|
||||
var parts = path.split("/")
|
||||
slug = parts[parts.size()-1]
|
||||
|
||||
var level_names = []
|
||||
var dir = Directory.new()
|
||||
dir.open("res://levels/%s" % slug)
|
||||
dir.list_dir_begin()
|
||||
|
||||
while true:
|
||||
var file = dir.get_next()
|
||||
if file == "":
|
||||
break
|
||||
elif not file.begins_with(".") and file != "sequence":
|
||||
level_names.append(file)
|
||||
|
||||
dir.list_dir_end()
|
||||
level_names.sort()
|
||||
|
||||
var final_level_sequence = []
|
||||
|
||||
var level_sequence = Array(helpers.read_file("res://levels/%s/sequence" % slug, "").split("\n"))
|
||||
|
||||
for level in level_sequence:
|
||||
if level == "":
|
||||
continue
|
||||
if not level_names.has(level):
|
||||
helpers.crash("Level '%s' is specified in the sequence, but could not be found" % level)
|
||||
level_names.erase(level)
|
||||
final_level_sequence.push_back(level)
|
||||
|
||||
final_level_sequence += level_names
|
||||
|
||||
for l in final_level_sequence:
|
||||
var level = Level.new()
|
||||
level.load("res://levels/%s/%s" % [slug, l])
|
||||
levels.push_back(level)
|
||||
|
||||
func _to_string():
|
||||
return str(levels)
|
19
scenes/drop_area.gd
Normal file
19
scenes/drop_area.gd
Normal file
|
@ -0,0 +1,19 @@
|
|||
extends Node2D
|
||||
|
||||
var hovered = false
|
||||
|
||||
func _ready():
|
||||
pass
|
||||
|
||||
func _mouse_entered():
|
||||
hovered = true
|
||||
|
||||
func _mouse_exited():
|
||||
hovered = false
|
||||
|
||||
func _input(event):
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == BUTTON_LEFT and !event.pressed and hovered:
|
||||
if game.dragged_object:
|
||||
print(game.dragged_object)
|
||||
game.dragged_object.dropped_on($"..")
|
18
scenes/drop_area.tscn
Normal file
18
scenes/drop_area.tscn
Normal file
|
@ -0,0 +1,18 @@
|
|||
[gd_scene load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://scenes/drop_area.gd" type="Script" id=1]
|
||||
|
||||
|
||||
[sub_resource type="CircleShape2D" id=1]
|
||||
radius = 23.5871
|
||||
|
||||
[node name="DropArea" type="Node2D"]
|
||||
position = Vector2( -0.197731, 0.0673599 )
|
||||
script = ExtResource( 1 )
|
||||
|
||||
[node name="Area2D" type="Area2D" parent="."]
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
|
||||
shape = SubResource( 1 )
|
||||
[connection signal="mouse_entered" from="Area2D" to="." method="_mouse_entered"]
|
||||
[connection signal="mouse_exited" from="Area2D" to="." method="_mouse_exited"]
|
133
scenes/file_browser.gd
Normal file
133
scenes/file_browser.gd
Normal file
|
@ -0,0 +1,133 @@
|
|||
extends Control
|
||||
|
||||
enum FileBrowserMode {
|
||||
WORKING_DIRECTORY,
|
||||
COMMIT,
|
||||
INDEX
|
||||
}
|
||||
|
||||
export(String) var title setget _set_title
|
||||
export(FileBrowserMode) var mode = FileBrowserMode.WORKING_DIRECTORY setget _set_mode
|
||||
|
||||
var shell
|
||||
var commit setget _set_commit
|
||||
var repository
|
||||
|
||||
var open_file
|
||||
|
||||
onready var grid = $Panel/Margin/Rows/Scroll/Grid
|
||||
onready var text_edit = $Panel/TextEdit
|
||||
onready var save_button = $Panel/TextEdit/SaveButton
|
||||
onready var title_label = $Panel/Margin/Rows/Title
|
||||
|
||||
func _ready():
|
||||
update()
|
||||
_set_mode(mode)
|
||||
_set_title(title)
|
||||
|
||||
func _input(event):
|
||||
if event.is_action_pressed("save"):
|
||||
if text_edit.visible:
|
||||
save()
|
||||
|
||||
func clear():
|
||||
for item in grid.get_children():
|
||||
item.queue_free()
|
||||
|
||||
func update():
|
||||
if grid:
|
||||
clear()
|
||||
match mode:
|
||||
FileBrowserMode.WORKING_DIRECTORY:
|
||||
if shell:
|
||||
var file_string = shell.run("find . -type f")
|
||||
var files = file_string.split("\n")
|
||||
files = Array(files)
|
||||
# The last entry is an empty string, remove it.
|
||||
files.pop_back()
|
||||
files.sort_custom(self, "very_best_sort")
|
||||
for file_path in files:
|
||||
file_path = file_path.substr(2)
|
||||
if file_path.substr(0, 5) == ".git/":
|
||||
continue
|
||||
var item = preload("res://scenes/file_browser_item.tscn").instance()
|
||||
item.label = file_path
|
||||
item.connect("clicked", self, "item_clicked")
|
||||
grid.add_child(item)
|
||||
FileBrowserMode.COMMIT:
|
||||
if commit:
|
||||
var files = Array(commit.repository.shell.run("git ls-tree --name-only -r %s" % commit.id).split("\n"))
|
||||
# The last entry is an empty string, remove it.
|
||||
files.pop_back()
|
||||
for file_path in files:
|
||||
var item = preload("res://scenes/file_browser_item.tscn").instance()
|
||||
item.label = file_path
|
||||
item.connect("clicked", self, "item_clicked")
|
||||
grid.add_child(item)
|
||||
FileBrowserMode.INDEX:
|
||||
if repository:
|
||||
var files = Array(repository.shell.run("git ls-files -s | cut -f2").split("\n"))
|
||||
# The last entry is an empty string, remove it.
|
||||
files.pop_back()
|
||||
for file_path in files:
|
||||
var item = preload("res://scenes/file_browser_item.tscn").instance()
|
||||
item.label = file_path
|
||||
item.connect("clicked", self, "item_clicked")
|
||||
grid.add_child(item)
|
||||
|
||||
func item_clicked(item):
|
||||
print(item.label)
|
||||
open_file = item.label
|
||||
match mode:
|
||||
FileBrowserMode.WORKING_DIRECTORY:
|
||||
text_edit.text = helpers.read_file(shell._cwd + item.label)
|
||||
FileBrowserMode.COMMIT:
|
||||
text_edit.text = commit.repository.shell.run("git show %s:\"%s\"" % [commit.id, item.label])
|
||||
FileBrowserMode.INDEX:
|
||||
text_edit.text = repository.shell.run("git show :\"%s\"" % [item.label])
|
||||
text_edit.show()
|
||||
text_edit.grab_focus()
|
||||
|
||||
func close():
|
||||
text_edit.hide()
|
||||
|
||||
func save():
|
||||
match mode:
|
||||
FileBrowserMode.WORKING_DIRECTORY:
|
||||
var fixme_path = shell._cwd
|
||||
|
||||
# Add a newline to the end of the file if there is none.
|
||||
if text_edit.text.length() > 0 and text_edit.text.substr(text_edit.text.length()-1, 1) != "\n":
|
||||
text_edit.text += "\n"
|
||||
|
||||
helpers.write_file(fixme_path+open_file, text_edit.text)
|
||||
close()
|
||||
|
||||
func _set_commit(new_commit):
|
||||
commit = new_commit
|
||||
update()
|
||||
|
||||
func _set_mode(new_mode):
|
||||
mode = new_mode
|
||||
if save_button:
|
||||
save_button.visible = mode == FileBrowserMode.WORKING_DIRECTORY
|
||||
text_edit.readonly = not mode == FileBrowserMode.WORKING_DIRECTORY
|
||||
text_edit.selecting_enabled = mode == FileBrowserMode.WORKING_DIRECTORY
|
||||
if mode == FileBrowserMode.WORKING_DIRECTORY:
|
||||
text_edit.focus_mode = Control.FOCUS_CLICK
|
||||
else:
|
||||
text_edit.focus_mode = Control.FOCUS_NONE
|
||||
|
||||
func _set_title(new_title):
|
||||
title = new_title
|
||||
if title_label:
|
||||
title_label.text = new_title
|
||||
|
||||
func very_best_sort(a,b):
|
||||
# We're looking at the third character because all entries have the form
|
||||
# "./.git/bla".
|
||||
if a.substr(2, 1) == "." and b.substr(2, 1) != ".":
|
||||
return false
|
||||
if a.substr(2, 1) != "." and b.substr(2, 1) == ".":
|
||||
return true
|
||||
return a.casecmp_to(b) == -1
|
129
scenes/file_browser.tscn
Normal file
129
scenes/file_browser.tscn
Normal file
|
@ -0,0 +1,129 @@
|
|||
[gd_scene load_steps=4 format=2]
|
||||
|
||||
[ext_resource path="res://scenes/file_browser.gd" type="Script" id=1]
|
||||
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=2]
|
||||
[ext_resource path="res://styles/theme.tres" type="Theme" id=3]
|
||||
|
||||
|
||||
[node name="FileBrowser" type="Control"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
rect_min_size = Vector2( 0, 142 )
|
||||
theme = ExtResource( 3 )
|
||||
script = ExtResource( 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Panel" type="Panel" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Margin" type="MarginContainer" parent="Panel"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
custom_constants/margin_right = 8
|
||||
custom_constants/margin_top = 8
|
||||
custom_constants/margin_left = 8
|
||||
custom_constants/margin_bottom = 8
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Rows" type="VBoxContainer" parent="Panel/Margin"]
|
||||
margin_left = 8.0
|
||||
margin_top = 8.0
|
||||
margin_right = 1912.0
|
||||
margin_bottom = 1072.0
|
||||
custom_constants/separation = 0
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Title" type="Label" parent="Panel/Margin/Rows"]
|
||||
margin_right = 1904.0
|
||||
margin_bottom = 25.0
|
||||
text = "title"
|
||||
align = 1
|
||||
|
||||
[node name="Breadcrumbs" type="HBoxContainer" parent="Panel/Margin/Rows"]
|
||||
visible = false
|
||||
margin_right = 1856.0
|
||||
margin_bottom = 50.0
|
||||
rect_min_size = Vector2( 0, 50 )
|
||||
custom_constants/separation = 8
|
||||
|
||||
[node name="Button" type="Button" parent="Panel/Margin/Rows/Breadcrumbs"]
|
||||
margin_right = 55.0
|
||||
margin_bottom = 50.0
|
||||
text = "root"
|
||||
|
||||
[node name="Button2" type="Button" parent="Panel/Margin/Rows/Breadcrumbs"]
|
||||
margin_left = 63.0
|
||||
margin_right = 104.0
|
||||
margin_bottom = 50.0
|
||||
text = "dir"
|
||||
|
||||
[node name="Scroll" type="ScrollContainer" parent="Panel/Margin/Rows"]
|
||||
margin_top = 25.0
|
||||
margin_right = 1904.0
|
||||
margin_bottom = 1064.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="Grid" type="GridContainer" parent="Panel/Margin/Rows/Scroll"]
|
||||
margin_right = 1904.0
|
||||
margin_bottom = 1039.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
custom_constants/vseparation = 16
|
||||
custom_constants/hseparation = 16
|
||||
columns = 4
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="TextEdit" type="TextEdit" parent="Panel"]
|
||||
visible = false
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="SaveButton" type="Button" parent="Panel/TextEdit"]
|
||||
anchor_left = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = -114.396
|
||||
margin_top = -59.399
|
||||
margin_right = -14.3955
|
||||
margin_bottom = -14.399
|
||||
focus_mode = 0
|
||||
custom_fonts/font = ExtResource( 2 )
|
||||
enabled_focus_mode = 0
|
||||
text = "Save"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="CloseButton" type="Button" parent="Panel/TextEdit"]
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
margin_left = -54.3247
|
||||
margin_top = 12.0
|
||||
margin_right = -14.3247
|
||||
margin_bottom = 52.0
|
||||
focus_mode = 0
|
||||
custom_fonts/font = ExtResource( 2 )
|
||||
enabled_focus_mode = 0
|
||||
text = "x"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
[connection signal="pressed" from="Panel/TextEdit/SaveButton" to="." method="save"]
|
||||
[connection signal="pressed" from="Panel/TextEdit/CloseButton" to="." method="close"]
|
19
scenes/file_browser_item.gd
Normal file
19
scenes/file_browser_item.gd
Normal file
|
@ -0,0 +1,19 @@
|
|||
extends Control
|
||||
|
||||
signal clicked(what)
|
||||
|
||||
export var label: String setget _set_label
|
||||
|
||||
onready var label_node = $VBoxContainer/Label
|
||||
|
||||
func _ready():
|
||||
_set_label(label)
|
||||
|
||||
func _set_label(new_label):
|
||||
label = new_label
|
||||
if label_node:
|
||||
label_node.text = helpers.abbreviate(new_label, 30)
|
||||
|
||||
func _gui_input(event):
|
||||
if event is InputEventMouseButton and event.is_pressed() and event.button_index == BUTTON_LEFT:
|
||||
emit_signal("clicked", self)
|
53
scenes/file_browser_item.tscn
Normal file
53
scenes/file_browser_item.tscn
Normal file
|
@ -0,0 +1,53 @@
|
|||
[gd_scene load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://scenes/file_browser_item.gd" type="Script" id=1]
|
||||
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=2]
|
||||
|
||||
|
||||
[node name="Control" type="Control"]
|
||||
anchor_right = 0.052
|
||||
anchor_bottom = 0.093
|
||||
margin_right = 100.16
|
||||
margin_bottom = -0.439995
|
||||
rect_min_size = Vector2( 150, 100 )
|
||||
script = ExtResource( 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 2
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Control" type="Control" parent="VBoxContainer"]
|
||||
margin_right = 200.0
|
||||
margin_bottom = 71.0
|
||||
mouse_filter = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ColorRect" type="ColorRect" parent="VBoxContainer/Control"]
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
margin_left = -25.0
|
||||
margin_top = -25.0
|
||||
margin_right = 25.0
|
||||
margin_bottom = 25.0
|
||||
mouse_filter = 2
|
||||
color = Color( 0.203922, 0.721569, 0.501961, 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer"]
|
||||
margin_top = 75.0
|
||||
margin_right = 200.0
|
||||
margin_bottom = 100.0
|
||||
custom_fonts/font = ExtResource( 2 )
|
||||
text = "filename"
|
||||
align = 1
|
67
scenes/game.gd
Normal file
67
scenes/game.gd
Normal file
|
@ -0,0 +1,67 @@
|
|||
extends Node
|
||||
|
||||
var tmp_prefix_outside = _tmp_prefix_outside()
|
||||
var tmp_prefix_inside = _tmp_prefix_inside()
|
||||
var global_shell
|
||||
var fake_editor
|
||||
|
||||
var dragged_object
|
||||
var energy = 2
|
||||
|
||||
var _file = "user://savegame.json"
|
||||
var state = {}
|
||||
|
||||
func _ready():
|
||||
var dir = Directory.new()
|
||||
var repo_dir = tmp_prefix_outside+"repos/"
|
||||
if not dir.dir_exists(tmp_prefix_outside):
|
||||
var err = dir.make_dir(tmp_prefix_outside)
|
||||
if err != OK:
|
||||
helpers.crash("Could not create temporary directory %s." % tmp_prefix_outside)
|
||||
if not dir.dir_exists(repo_dir):
|
||||
var err = dir.make_dir(repo_dir)
|
||||
if err != OK:
|
||||
helpers.crash("Could not create temporary directory %s." % repo_dir)
|
||||
|
||||
global_shell = Shell.new()
|
||||
fake_editor = copy_file_to_game_env("fake-editor")
|
||||
copy_file_to_game_env("fake-editor-noblock")
|
||||
load_state()
|
||||
|
||||
func _initial_state():
|
||||
return {"history": []}
|
||||
|
||||
func save_state():
|
||||
var savegame = File.new()
|
||||
|
||||
savegame.open(_file, File.WRITE)
|
||||
savegame.store_line(to_json(state))
|
||||
savegame.close()
|
||||
|
||||
func load_state():
|
||||
var savegame = File.new()
|
||||
if not savegame.file_exists(_file):
|
||||
save_state()
|
||||
|
||||
savegame.open(_file, File.READ)
|
||||
|
||||
state = _initial_state()
|
||||
var new_state = parse_json(savegame.get_line())
|
||||
for key in new_state:
|
||||
state[key] = new_state[key]
|
||||
savegame.close()
|
||||
|
||||
func copy_file_to_game_env(filename):
|
||||
# Copy fake-editor to tmp directory (because the original might be in a .pck file).
|
||||
var file_outside = tmp_prefix_outside + filename
|
||||
var file_inside = tmp_prefix_inside + filename
|
||||
var content = helpers.read_file("res://scripts/"+filename)
|
||||
helpers.write_file(file_outside, content)
|
||||
global_shell.run("chmod u+x " + '"'+file_inside+'"')
|
||||
return file_inside
|
||||
|
||||
func _tmp_prefix_inside():
|
||||
return OS.get_user_data_dir() + "/tmp/"
|
||||
|
||||
func _tmp_prefix_outside():
|
||||
return "user://tmp/"
|
134
scenes/helpers.gd
Normal file
134
scenes/helpers.gd
Normal file
|
@ -0,0 +1,134 @@
|
|||
extends Node
|
||||
|
||||
var debug_file_io = false
|
||||
|
||||
# Crash the game and display the error message.
|
||||
func crash(message):
|
||||
push_error(message)
|
||||
print("FATAL ERROR: " + message)
|
||||
get_tree().quit()
|
||||
# Oh, still here? Let's crash more violently, by calling a non-existing method.
|
||||
# Violent delights have violent ends.
|
||||
get_tree().fatal_error()
|
||||
|
||||
# Run a simple command with arguments, blocking, using OS.execute.
|
||||
func exec(command, args=[], crash_on_fail=true):
|
||||
var debug = false
|
||||
|
||||
if debug:
|
||||
print("exec: %s [%s]" % [command, PoolStringArray(args).join(", ")])
|
||||
|
||||
var output = []
|
||||
var exit_code = OS.execute(command, args, true, output, true)
|
||||
output = output[0]
|
||||
|
||||
if exit_code != 0 and crash_on_fail:
|
||||
helpers.crash("OS.execute failed: %s [%s] Output: %s \nExit Code %d" % [command, PoolStringArray(args).join(", "), output, exit_code])
|
||||
elif debug:
|
||||
print("Output: %s" %output)
|
||||
|
||||
return {"output": output, "exit_code": exit_code}
|
||||
|
||||
# Return the contents of a file. If no fallback_string is provided, crash when
|
||||
# the file doesn't exist.
|
||||
func read_file(path, fallback_string=null):
|
||||
if debug_file_io:
|
||||
print("reading " + path)
|
||||
var file = File.new()
|
||||
var open_status = file.open(path, File.READ)
|
||||
if open_status == OK:
|
||||
var content = file.get_as_text()
|
||||
file.close()
|
||||
return content
|
||||
else:
|
||||
if fallback_string != null:
|
||||
return fallback_string
|
||||
else:
|
||||
helpers.crash("File %s could not be read, and has no fallback" % path)
|
||||
|
||||
func write_file(path, content):
|
||||
if debug_file_io:
|
||||
print("writing " + path)
|
||||
var file = File.new()
|
||||
file.open(path, File.WRITE)
|
||||
file.store_string(content)
|
||||
file.close()
|
||||
return true
|
||||
|
||||
func parse_args():
|
||||
var arguments = {}
|
||||
for argument in OS.get_cmdline_args():
|
||||
if argument.substr(0, 2) == "--":
|
||||
# Parse valid command-line arguments into a dictionary
|
||||
if argument.find("=") > -1:
|
||||
var key_value = argument.split("=")
|
||||
arguments[key_value[0].lstrip("--")] = key_value[1]
|
||||
else:
|
||||
arguments[argument.lstrip("--")] = true
|
||||
return arguments
|
||||
|
||||
func careful_delete(path_inside):
|
||||
var expected_prefix
|
||||
|
||||
var os = OS.get_name()
|
||||
|
||||
if os == "X11":
|
||||
expected_prefix = "/home/%s/.local/share/git-hydra/tmp/" % OS.get_environment("USER")
|
||||
elif os == "OSX":
|
||||
expected_prefix = "/Users/%s/Library/Application Support/git-hydra/tmp/" % OS.get_environment("USER")
|
||||
elif os == "Windows":
|
||||
expected_prefix = "C:/Users/%s/AppData/Roaming/git-hydra/tmp/" % OS.get_environment("USERNAME")
|
||||
else:
|
||||
helpers.crash("Unsupported OS: %s" % os)
|
||||
|
||||
if path_inside.substr(0,expected_prefix.length()) != expected_prefix:
|
||||
helpers.crash("Refusing to delete directory %s that does not start with %s" % [path_inside, expected_prefix])
|
||||
else:
|
||||
game.global_shell.cd(game.tmp_prefix_inside)
|
||||
game.global_shell.run("rm -rf '%s'" % path_inside)
|
||||
|
||||
func parse(file):
|
||||
var text = read_file(file)
|
||||
var result = {}
|
||||
var current_section
|
||||
|
||||
var section_regex = RegEx.new()
|
||||
section_regex.compile("^\\[(.*)\\]$")
|
||||
|
||||
var assignment_regex = RegEx.new()
|
||||
assignment_regex.compile("^([a-z ]+)=(.*)$")
|
||||
|
||||
for line in text.split("\n"):
|
||||
# Skip comments.
|
||||
if line.substr(0, 1) == ";":
|
||||
continue
|
||||
|
||||
# Parse a [section name].
|
||||
var m = section_regex.search(line)
|
||||
if m:
|
||||
current_section = m.get_string(1)
|
||||
result[current_section] = ""
|
||||
continue
|
||||
|
||||
# Parse a direct=assignment.
|
||||
m = assignment_regex.search(line)
|
||||
if m:
|
||||
var key = m.get_string(1).strip_edges()
|
||||
var value = m.get_string(2).strip_edges()
|
||||
result[key] = value
|
||||
continue
|
||||
|
||||
# At this point, the line is just content belonging to the current section.
|
||||
if current_section:
|
||||
result[current_section] += line + "\n"
|
||||
|
||||
for key in result:
|
||||
result[key] = result[key].strip_edges()
|
||||
|
||||
return result
|
||||
|
||||
func abbreviate(text, max_length):
|
||||
if text.length() > max_length-3:
|
||||
text = text.substr(0, max_length-3) + "..."
|
||||
|
||||
return text
|
110
scenes/level.gd
Normal file
110
scenes/level.gd
Normal file
|
@ -0,0 +1,110 @@
|
|||
extends Node
|
||||
class_name Level
|
||||
|
||||
var slug
|
||||
var title
|
||||
var description
|
||||
var congrats
|
||||
var repos = {}
|
||||
|
||||
# The path is an outer path.
|
||||
func load(path):
|
||||
var parts = path.split("/")
|
||||
slug = parts[parts.size()-1]
|
||||
|
||||
var dir = Directory.new()
|
||||
if dir.file_exists(path):
|
||||
# This is a new-style level.
|
||||
var config = helpers.parse(path)
|
||||
|
||||
title = config.get("title", slug)
|
||||
description = config.get("description", "(no description)")
|
||||
congrats = config.get("congrats", "Good job, you solved the level!\n\nFeel free to try a few more things or click 'Next Level'.")
|
||||
|
||||
var keys = config.keys()
|
||||
var repo_setups = []
|
||||
for k in keys:
|
||||
if k.begins_with("setup"):
|
||||
repo_setups.push_back(k)
|
||||
var repo_wins = []
|
||||
for k in keys:
|
||||
if k.begins_with("win"):
|
||||
repo_wins.push_back(k)
|
||||
|
||||
for k in repo_setups:
|
||||
var repo
|
||||
if " " in k:
|
||||
repo = Array(k.split(" "))[1]
|
||||
else:
|
||||
repo = "yours"
|
||||
if not repos.has(repo):
|
||||
repos[repo] = LevelRepo.new()
|
||||
repos[repo].setup_commands = config[k]
|
||||
|
||||
for k in repo_wins:
|
||||
var repo
|
||||
if " " in k:
|
||||
repo = Array(k.split(" "))[1]
|
||||
else:
|
||||
repo = "yours"
|
||||
repos[repo].win_commands = config[k]
|
||||
elif dir.file_exists(path+"/description"):
|
||||
# This is an old-style level.
|
||||
description = helpers.read_file(path+"/description", "(no description)")
|
||||
congrats = helpers.read_file(path+"/congrats", "Good job, you solved the level!\n\nFeel free to try a few more things or click 'Next Level'.")
|
||||
|
||||
var yours = LevelRepo.new()
|
||||
yours.setup_commands = helpers.read_file(path+"/start", "")
|
||||
#goal_commands = helpers.read_file(path+"/goal", "")
|
||||
yours.win_commands = helpers.read_file(path+"/win", "")
|
||||
|
||||
repos["yours"] = yours
|
||||
else:
|
||||
helpers.crash("Level %s does not exist." % path)
|
||||
|
||||
for repo in repos:
|
||||
repos[repo].path = game.tmp_prefix_inside+"repos/%s/" % repo
|
||||
repos[repo].slug = repo
|
||||
|
||||
# Surround all lines indented with four spaces with [code] tags.
|
||||
var monospace_regex = RegEx.new()
|
||||
monospace_regex.compile("\\n ([^\\n]*)")
|
||||
description = monospace_regex.sub(description, "\n [code]$1[/code]", true)
|
||||
|
||||
func construct():
|
||||
for r in repos:
|
||||
var repo = repos[r]
|
||||
# We're actually destroying stuff here.
|
||||
# Make sure that active_repository is in a temporary directory.
|
||||
helpers.careful_delete(repo.path)
|
||||
|
||||
game.global_shell.run("mkdir '%s'" % repo.path)
|
||||
game.global_shell.cd(repo.path)
|
||||
game.global_shell.run("git init")
|
||||
game.global_shell.run("git symbolic-ref HEAD refs/heads/main")
|
||||
|
||||
# Add other repos as remotes.
|
||||
for r2 in repos:
|
||||
if r == r2:
|
||||
continue
|
||||
game.global_shell.run("git remote add %s %s" % [r2, repos[r2].path])
|
||||
|
||||
# Allow receiving a push of the checked-out branch.
|
||||
game.global_shell.run("git config receive.denyCurrentBranch ignore")
|
||||
|
||||
for r in repos:
|
||||
var repo = repos[r]
|
||||
game.global_shell.cd(repo.path)
|
||||
game.global_shell.run(repo.setup_commands)
|
||||
|
||||
func check_win():
|
||||
var won = true
|
||||
var any_checked = false
|
||||
for r in repos:
|
||||
var repo = repos[r]
|
||||
if repo.win_commands != "":
|
||||
any_checked = true
|
||||
game.global_shell.cd(repo.path)
|
||||
if not game.global_shell.run("function win { %s\n}; win 2>/dev/null >/dev/null && echo yes || echo no" % repo.win_commands) == "yes\n":
|
||||
won = false
|
||||
return won and any_checked
|
7
scenes/level_repo.gd
Normal file
7
scenes/level_repo.gd
Normal file
|
@ -0,0 +1,7 @@
|
|||
extends Node
|
||||
class_name LevelRepo
|
||||
|
||||
var slug
|
||||
var path
|
||||
var setup_commands = ""
|
||||
var win_commands = ""
|
44
scenes/levels.gd
Normal file
44
scenes/levels.gd
Normal file
|
@ -0,0 +1,44 @@
|
|||
extends Node
|
||||
|
||||
var chapters
|
||||
|
||||
func _ready():
|
||||
reload()
|
||||
|
||||
func reload():
|
||||
chapters = []
|
||||
|
||||
var dir = Directory.new()
|
||||
dir.open("res://levels")
|
||||
dir.list_dir_begin()
|
||||
|
||||
var chapter_names = []
|
||||
|
||||
while true:
|
||||
var file = dir.get_next()
|
||||
if file == "":
|
||||
break
|
||||
elif not file.begins_with(".") and file != "sequence":
|
||||
chapter_names.append(file)
|
||||
|
||||
dir.list_dir_end()
|
||||
chapter_names.sort()
|
||||
|
||||
var final_chapter_sequence = []
|
||||
|
||||
var chapter_sequence = Array(helpers.read_file("res://levels/sequence", "").split("\n"))
|
||||
|
||||
for chapter in chapter_sequence:
|
||||
if chapter == "":
|
||||
continue
|
||||
if not chapter_names.has(chapter):
|
||||
helpers.crash("Chapter '%s' is specified in the sequence, but could not be found" % chapter)
|
||||
chapter_names.erase(chapter)
|
||||
final_chapter_sequence.push_back(chapter)
|
||||
|
||||
final_chapter_sequence += chapter_names
|
||||
|
||||
for c in final_chapter_sequence:
|
||||
var chapter = Chapter.new()
|
||||
chapter.load("res://levels/%s" % c)
|
||||
chapters.push_back(chapter)
|
136
scenes/main.gd
Normal file
136
scenes/main.gd
Normal file
|
@ -0,0 +1,136 @@
|
|||
extends Control
|
||||
|
||||
var dragged = null
|
||||
|
||||
var current_chapter
|
||||
var current_level
|
||||
|
||||
onready var terminal = $Rows/Columns/RightSide/Terminal
|
||||
onready var input = terminal.input
|
||||
onready var output = terminal.output
|
||||
onready var repositories_node = $Rows/Columns/Repositories
|
||||
var repositories = {}
|
||||
onready var level_select = $Rows/Columns/RightSide/TopStuff/Menu/LevelSelect
|
||||
onready var chapter_select = $Rows/Columns/RightSide/TopStuff/Menu/ChapterSelect
|
||||
onready var next_level_button = $Rows/Columns/RightSide/TopStuff/Menu/NextLevelButton
|
||||
onready var level_name = $Rows/Columns/RightSide/TopStuff/LevelPanel/LevelName
|
||||
onready var level_description = $Rows/Columns/RightSide/TopStuff/LevelPanel/Text/LevelDescription
|
||||
onready var level_congrats = $Rows/Columns/RightSide/TopStuff/LevelPanel/Text/LevelCongrats
|
||||
onready var cards = $Rows/Cards
|
||||
|
||||
func _ready():
|
||||
var args = helpers.parse_args()
|
||||
|
||||
if args.has("sandbox"):
|
||||
var err = get_tree().change_scene("res://scenes/sandbox.tscn")
|
||||
if err != OK:
|
||||
helpers.crash("Could not change to sandbox scene")
|
||||
return
|
||||
|
||||
current_chapter = 0
|
||||
current_level = 0
|
||||
|
||||
# Initialize level select.
|
||||
level_select.connect("item_selected", self, "load_level")
|
||||
repopulate_levels()
|
||||
level_select.select(current_level)
|
||||
|
||||
# Initialize chapter select.
|
||||
chapter_select.connect("item_selected", self, "load_chapter")
|
||||
repopulate_chapters()
|
||||
chapter_select.select(current_chapter)
|
||||
|
||||
# Load first chapter.
|
||||
load_chapter(current_chapter)
|
||||
input.grab_focus()
|
||||
|
||||
func load_chapter(id):
|
||||
current_chapter = id
|
||||
repopulate_levels()
|
||||
load_level(0)
|
||||
|
||||
func load_level(level_id):
|
||||
next_level_button.hide()
|
||||
level_congrats.hide()
|
||||
level_description.show()
|
||||
current_level = level_id
|
||||
|
||||
AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), true)
|
||||
|
||||
levels.chapters[current_chapter].levels[current_level].construct()
|
||||
|
||||
var level = levels.chapters[current_chapter].levels[current_level]
|
||||
level_description.bbcode_text = level.description
|
||||
level_congrats.bbcode_text = level.congrats
|
||||
level_name.text = level.title
|
||||
|
||||
for r in repositories_node.get_children():
|
||||
r.queue_free()
|
||||
repositories = {}
|
||||
|
||||
var repo_names = level.repos.keys()
|
||||
repo_names.invert()
|
||||
|
||||
for r in repo_names:
|
||||
var repo = level.repos[r]
|
||||
var new_repo = preload("res://scenes/repository.tscn").instance()
|
||||
new_repo.path = repo.path
|
||||
new_repo.label = repo.slug
|
||||
new_repo.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
repositories_node.add_child(new_repo)
|
||||
repositories[r] = new_repo
|
||||
|
||||
terminal.repository = repositories[repo_names[repo_names.size()-1]]
|
||||
terminal.clear()
|
||||
terminal.find_node("TextEditor").close()
|
||||
|
||||
# Unmute the audio after a while, so that player can hear pop sounds for
|
||||
# nodes they create.
|
||||
var t = Timer.new()
|
||||
t.wait_time = 1
|
||||
add_child(t)
|
||||
t.start()
|
||||
yield(t, "timeout")
|
||||
AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), false)
|
||||
# FIXME: Need to clean these up when switching levels somehow.
|
||||
|
||||
func reload_level():
|
||||
levels.reload()
|
||||
load_level(current_level)
|
||||
|
||||
func load_next_level():
|
||||
current_level = (current_level + 1) % levels.chapters[current_chapter].levels.size()
|
||||
load_level(current_level)
|
||||
|
||||
func show_win_status():
|
||||
if not level_congrats.visible:
|
||||
next_level_button.show()
|
||||
level_description.hide()
|
||||
level_congrats.show()
|
||||
$SuccessSound.play()
|
||||
|
||||
func repopulate_levels():
|
||||
levels.reload()
|
||||
level_select.clear()
|
||||
for level in levels.chapters[current_chapter].levels:
|
||||
level_select.add_item(level.slug)
|
||||
level_select.select(current_level)
|
||||
|
||||
func repopulate_chapters():
|
||||
levels.reload()
|
||||
chapter_select.clear()
|
||||
for c in levels.chapters:
|
||||
chapter_select.add_item(c.slug)
|
||||
chapter_select.select(current_chapter)
|
||||
|
||||
func update_repos():
|
||||
for r in repositories:
|
||||
var repo = repositories[r]
|
||||
repo.update_everything()
|
||||
|
||||
if levels.chapters[current_chapter].levels[current_level].check_win():
|
||||
show_win_status()
|
||||
|
||||
|
||||
func toggle_cards():
|
||||
cards.visible = not cards.visible
|
212
scenes/main.tscn
Normal file
212
scenes/main.tscn
Normal file
|
@ -0,0 +1,212 @@
|
|||
[gd_scene load_steps=9 format=2]
|
||||
|
||||
[ext_resource path="res://scenes/terminal.tscn" type="PackedScene" id=1]
|
||||
[ext_resource path="res://scenes/main.gd" type="Script" id=2]
|
||||
[ext_resource path="res://scenes/cards.tscn" type="PackedScene" id=3]
|
||||
[ext_resource path="res://styles/alert_button.tres" type="StyleBox" id=4]
|
||||
[ext_resource path="res://styles/theme.tres" type="Theme" id=6]
|
||||
[ext_resource path="res://fonts/big.tres" type="DynamicFont" id=7]
|
||||
[ext_resource path="res://sounds/success.wav" type="AudioStream" id=8]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=1]
|
||||
content_margin_left = 10.0
|
||||
content_margin_right = 10.0
|
||||
content_margin_top = 5.0
|
||||
content_margin_bottom = 5.0
|
||||
bg_color = Color( 0.847059, 0.0666667, 0.0666667, 1 )
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[node name="Main" type="Control"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 8.0
|
||||
margin_top = 8.0
|
||||
margin_right = -8.0
|
||||
margin_bottom = -8.0
|
||||
mouse_filter = 2
|
||||
theme = ExtResource( 6 )
|
||||
script = ExtResource( 2 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="CanvasLayer" type="CanvasLayer" parent="."]
|
||||
layer = -1
|
||||
|
||||
[node name="Background" type="ColorRect" parent="CanvasLayer"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 2
|
||||
color = Color( 0.0705882, 0.0705882, 0.0705882, 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Rows" type="VBoxContainer" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 2
|
||||
size_flags_vertical = 3
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Columns" type="HSplitContainer" parent="Rows"]
|
||||
margin_right = 1904.0
|
||||
margin_bottom = 784.0
|
||||
mouse_filter = 2
|
||||
size_flags_vertical = 3
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Repositories" type="HBoxContainer" parent="Rows/Columns"]
|
||||
margin_right = 1136.0
|
||||
margin_bottom = 784.0
|
||||
mouse_filter = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 1.5
|
||||
custom_constants/separation = 8
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="RightSide" type="VSplitContainer" parent="Rows/Columns"]
|
||||
margin_left = 1148.0
|
||||
margin_right = 1904.0
|
||||
margin_bottom = 784.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="TopStuff" type="VBoxContainer" parent="Rows/Columns/RightSide"]
|
||||
margin_right = 756.0
|
||||
margin_bottom = 386.0
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="Menu" type="HBoxContainer" parent="Rows/Columns/RightSide/TopStuff"]
|
||||
margin_right = 756.0
|
||||
margin_bottom = 35.0
|
||||
|
||||
[node name="ChapterSelect" type="OptionButton" parent="Rows/Columns/RightSide/TopStuff/Menu"]
|
||||
margin_right = 168.0
|
||||
margin_bottom = 35.0
|
||||
focus_mode = 0
|
||||
enabled_focus_mode = 0
|
||||
text = "Select chapter..."
|
||||
|
||||
[node name="LevelSelect" type="OptionButton" parent="Rows/Columns/RightSide/TopStuff/Menu"]
|
||||
margin_left = 173.0
|
||||
margin_right = 317.0
|
||||
margin_bottom = 35.0
|
||||
focus_mode = 0
|
||||
enabled_focus_mode = 0
|
||||
text = "Select level..."
|
||||
expand_icon = true
|
||||
|
||||
[node name="ReloadButton" type="Button" parent="Rows/Columns/RightSide/TopStuff/Menu"]
|
||||
margin_left = 322.0
|
||||
margin_right = 401.0
|
||||
margin_bottom = 35.0
|
||||
focus_mode = 0
|
||||
enabled_focus_mode = 0
|
||||
text = "Reload"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="CardsButton" type="Button" parent="Rows/Columns/RightSide/TopStuff/Menu"]
|
||||
margin_left = 406.0
|
||||
margin_right = 478.0
|
||||
margin_bottom = 35.0
|
||||
focus_mode = 0
|
||||
enabled_focus_mode = 0
|
||||
text = "Cards!"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="NextLevelButton" type="Button" parent="Rows/Columns/RightSide/TopStuff/Menu"]
|
||||
margin_left = 483.0
|
||||
margin_right = 593.0
|
||||
margin_bottom = 35.0
|
||||
focus_mode = 0
|
||||
custom_styles/hover = SubResource( 1 )
|
||||
custom_styles/normal = ExtResource( 4 )
|
||||
enabled_focus_mode = 0
|
||||
text = "Next Level"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="LevelPanel" type="VBoxContainer" parent="Rows/Columns/RightSide/TopStuff"]
|
||||
margin_top = 40.0
|
||||
margin_right = 756.0
|
||||
margin_bottom = 386.0
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="LevelName" type="RichTextLabel" parent="Rows/Columns/RightSide/TopStuff/LevelPanel"]
|
||||
margin_right = 756.0
|
||||
margin_bottom = 60.0
|
||||
rect_min_size = Vector2( 0, 60 )
|
||||
custom_fonts/normal_font = ExtResource( 7 )
|
||||
text = "Level name here!"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Text" type="Control" parent="Rows/Columns/RightSide/TopStuff/LevelPanel"]
|
||||
margin_top = 65.0
|
||||
margin_right = 756.0
|
||||
margin_bottom = 346.0
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="LevelDescription" type="RichTextLabel" parent="Rows/Columns/RightSide/TopStuff/LevelPanel/Text"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
size_flags_vertical = 3
|
||||
bbcode_enabled = true
|
||||
bbcode_text = "Level description here!"
|
||||
text = "Level description here!"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="LevelCongrats" type="RichTextLabel" parent="Rows/Columns/RightSide/TopStuff/LevelPanel/Text"]
|
||||
visible = false
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
size_flags_vertical = 3
|
||||
bbcode_enabled = true
|
||||
bbcode_text = "Level description here!"
|
||||
text = "Level description here!"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Terminal" parent="Rows/Columns/RightSide" instance=ExtResource( 1 )]
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
margin_top = 398.0
|
||||
margin_right = 756.0
|
||||
margin_bottom = 784.0
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="Cards" parent="Rows" instance=ExtResource( 3 )]
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
margin_top = 789.0
|
||||
margin_right = 1904.0
|
||||
margin_bottom = 1064.0
|
||||
size_flags_vertical = 3
|
||||
size_flags_stretch_ratio = 0.35
|
||||
|
||||
[node name="SuccessSound" type="AudioStreamPlayer" parent="."]
|
||||
stream = ExtResource( 8 )
|
||||
[connection signal="button_down" from="Rows/Columns/RightSide/TopStuff/Menu/ChapterSelect" to="." method="repopulate_chapters"]
|
||||
[connection signal="button_down" from="Rows/Columns/RightSide/TopStuff/Menu/LevelSelect" to="." method="repopulate_levels"]
|
||||
[connection signal="pressed" from="Rows/Columns/RightSide/TopStuff/Menu/ReloadButton" to="." method="reload_level"]
|
||||
[connection signal="pressed" from="Rows/Columns/RightSide/TopStuff/Menu/CardsButton" to="." method="toggle_cards"]
|
||||
[connection signal="pressed" from="Rows/Columns/RightSide/TopStuff/Menu/NextLevelButton" to="." method="load_next_level"]
|
||||
[connection signal="command_done" from="Rows/Columns/RightSide/Terminal" to="." method="update_repos"]
|
117
scenes/node.gd
Normal file
117
scenes/node.gd
Normal file
|
@ -0,0 +1,117 @@
|
|||
extends Node2D
|
||||
|
||||
var id setget id_set
|
||||
var content setget content_set
|
||||
var type setget type_set
|
||||
var repository: Control
|
||||
|
||||
onready var content_label = $Content/ContentLabel
|
||||
onready var file_browser = $OnTop/FileBrowser
|
||||
|
||||
var children = {} setget children_set
|
||||
var id_always_visible = false
|
||||
var held = false
|
||||
var hovered = false
|
||||
|
||||
var arrow = preload("res://scenes/arrow.tscn")
|
||||
|
||||
func _ready():
|
||||
content_set(content)
|
||||
type_set(type)
|
||||
$Pop.pitch_scale = rand_range(0.8, 1.2)
|
||||
$Pop.play()
|
||||
|
||||
func _process(_delta):
|
||||
if held:
|
||||
if not Input.is_action_pressed("click"):
|
||||
held = false
|
||||
else:
|
||||
global_position = get_global_mouse_position()
|
||||
|
||||
if visible:
|
||||
apply_forces()
|
||||
|
||||
func apply_forces():
|
||||
var offset = Vector2(0, 80)
|
||||
|
||||
for c in children.keys():
|
||||
if repository.objects.has(c):
|
||||
var other = repository.objects[c]
|
||||
if other.visible:
|
||||
var d = other.position.distance_to(position+offset)
|
||||
var dir = (other.position - (position+offset)).normalized()
|
||||
var f = (d*0.03)
|
||||
position += dir*f
|
||||
other.position -= dir*f
|
||||
|
||||
func id_set(new_id):
|
||||
id = new_id
|
||||
$ID.text = id
|
||||
|
||||
func content_set(new_content):
|
||||
content = new_content
|
||||
if content_label:
|
||||
content_label.text = content
|
||||
|
||||
func type_set(new_type):
|
||||
type = new_type
|
||||
if type == "commit" and file_browser:
|
||||
file_browser.commit = self
|
||||
file_browser.title = "Commit"
|
||||
if type != "ref":
|
||||
$ID.text = $ID.text.substr(0,8)
|
||||
match new_type:
|
||||
"blob":
|
||||
$Sprite.texture = preload("res://nodes/blob.svg")
|
||||
"tree":
|
||||
$Sprite.texture = preload("res://nodes/tree.svg")
|
||||
"commit":
|
||||
$Sprite.texture = preload("res://nodes/commit.svg")
|
||||
"tag":
|
||||
$Sprite.texture = preload("res://nodes/blob.svg")
|
||||
"ref":
|
||||
$Sprite.texture = preload("res://nodes/ref.svg")
|
||||
id_always_visible = true
|
||||
"head":
|
||||
$Sprite.texture = preload("res://nodes/ref.svg")
|
||||
id_always_visible = true
|
||||
if id_always_visible:
|
||||
$ID.show()
|
||||
|
||||
func children_set(new_children):
|
||||
for c in $Arrows.get_children():
|
||||
if not new_children.has(c.target):
|
||||
c.queue_free()
|
||||
for c in new_children:
|
||||
if not children.has(c):
|
||||
var a = arrow.instance()
|
||||
a.source = id
|
||||
a.target = c
|
||||
a.repository = repository
|
||||
$Arrows.add_child(a)
|
||||
children = new_children
|
||||
|
||||
func _on_hover():
|
||||
hovered = true
|
||||
if not id_always_visible:
|
||||
content_label.visible = true
|
||||
$ID.visible = true
|
||||
|
||||
func _on_unhover():
|
||||
hovered = false
|
||||
if not id_always_visible:
|
||||
content_label.visible = false
|
||||
$ID.visible = false
|
||||
|
||||
func _input(event):
|
||||
if hovered:
|
||||
if event.is_action_pressed("click"):
|
||||
held = true
|
||||
if type == "commit":
|
||||
file_browser.visible = not file_browser.visible
|
||||
elif event.is_action_pressed("right_click"):
|
||||
var input = get_tree().get_current_scene().find_node("Input")
|
||||
input.text += id
|
||||
input.caret_position = input.text.length()
|
||||
if event.is_action_released("click"):
|
||||
held = false
|
101
scenes/node.tscn
Normal file
101
scenes/node.tscn
Normal file
|
@ -0,0 +1,101 @@
|
|||
[gd_scene load_steps=9 format=2]
|
||||
|
||||
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
|
||||
[ext_resource path="res://scenes/node.gd" type="Script" id=2]
|
||||
[ext_resource path="res://nodes/blob.svg" type="Texture" id=3]
|
||||
[ext_resource path="res://scenes/file_browser.tscn" type="PackedScene" id=4]
|
||||
[ext_resource path="res://nodes/pop.wav" type="AudioStream" id=5]
|
||||
[ext_resource path="res://scenes/drop_area.tscn" type="PackedScene" id=6]
|
||||
|
||||
|
||||
[sub_resource type="CircleShape2D" id=1]
|
||||
radius = 23.6295
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=2]
|
||||
content_margin_left = 5.0
|
||||
content_margin_right = 5.0
|
||||
content_margin_top = 5.0
|
||||
content_margin_bottom = 5.0
|
||||
bg_color = Color( 0, 0, 0, 0.878431 )
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
corner_radius_bottom_right = 5
|
||||
corner_radius_bottom_left = 5
|
||||
|
||||
[node name="Node" type="Node2D"]
|
||||
script = ExtResource( 2 )
|
||||
|
||||
[node name="Arrows" type="Node2D" parent="."]
|
||||
|
||||
[node name="Rect" type="ColorRect" parent="."]
|
||||
visible = false
|
||||
margin_left = -29.0
|
||||
margin_top = -28.0
|
||||
margin_right = 29.0
|
||||
margin_bottom = 29.0
|
||||
mouse_filter = 2
|
||||
color = Color( 1, 1, 1, 0 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Sprite" type="Sprite" parent="."]
|
||||
scale = Vector2( 0.5, 0.5 )
|
||||
texture = ExtResource( 3 )
|
||||
|
||||
[node name="ID" type="Label" parent="."]
|
||||
visible = false
|
||||
margin_left = -19.9265
|
||||
margin_top = -12.0097
|
||||
margin_right = 129.073
|
||||
margin_bottom = 40.9903
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
custom_colors/font_color = Color( 1, 1, 1, 1 )
|
||||
text = "object_id"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Pop" type="AudioStreamPlayer2D" parent="."]
|
||||
stream = ExtResource( 5 )
|
||||
|
||||
[node name="DropArea" parent="." instance=ExtResource( 6 )]
|
||||
|
||||
[node name="Area2D" type="Area2D" parent="."]
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
|
||||
shape = SubResource( 1 )
|
||||
|
||||
[node name="Content" type="Node2D" parent="."]
|
||||
z_index = 1
|
||||
|
||||
[node name="ContentLabel" type="Label" parent="Content"]
|
||||
visible = false
|
||||
margin_left = 31.3944
|
||||
margin_top = -22.8078
|
||||
margin_right = 41.3944
|
||||
margin_bottom = 12.1922
|
||||
custom_styles/normal = SubResource( 2 )
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
custom_colors/font_color = Color( 1, 1, 1, 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="OnTop" type="Node2D" parent="."]
|
||||
z_index = 2
|
||||
|
||||
[node name="FileBrowser" parent="OnTop" instance=ExtResource( 4 )]
|
||||
visible = false
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
margin_left = -460.672
|
||||
margin_top = -23.6409
|
||||
margin_right = -32.6716
|
||||
margin_bottom = 118.359
|
||||
mouse_filter = 1
|
||||
mode = 1
|
||||
[connection signal="mouse_entered" from="Rect" to="." method="_on_hover"]
|
||||
[connection signal="mouse_exited" from="Rect" to="." method="_on_unhover"]
|
||||
[connection signal="mouse_entered" from="Area2D" to="." method="_on_hover"]
|
||||
[connection signal="mouse_exited" from="Area2D" to="." method="_on_unhover"]
|
30
scenes/player.tscn
Normal file
30
scenes/player.tscn
Normal file
|
@ -0,0 +1,30 @@
|
|||
[gd_scene load_steps=3 format=2]
|
||||
|
||||
[sub_resource type="GDScript" id=1]
|
||||
script/source = "extends KinematicBody2D
|
||||
|
||||
export var speed = 800
|
||||
|
||||
func _ready():
|
||||
pass
|
||||
|
||||
func _process(delta):
|
||||
var right = Input.get_action_strength(\"right\") - Input.get_action_strength(\"left\")
|
||||
var down = Input.get_action_strength(\"down\") - Input.get_action_strength(\"up\")
|
||||
move_and_slide(Vector2(right, down).normalized()*speed)
|
||||
"
|
||||
|
||||
[sub_resource type="RectangleShape2D" id=2]
|
||||
extents = Vector2( 50, 50 )
|
||||
|
||||
[node name="Player" type="KinematicBody2D"]
|
||||
script = SubResource( 1 )
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource( 2 )
|
||||
|
||||
[node name="Rect" type="ColorRect" parent="."]
|
||||
margin_left = -50.0
|
||||
margin_top = -50.0
|
||||
margin_right = 50.0
|
||||
margin_bottom = 50.0
|
344
scenes/repository.gd
Normal file
344
scenes/repository.gd
Normal file
|
@ -0,0 +1,344 @@
|
|||
extends Control
|
||||
|
||||
onready var index = $Rows/Browsers/Index
|
||||
onready var nodes = $Rows/RepoVis/Nodes
|
||||
onready var file_browser = $Rows/Browsers/FileBrowser
|
||||
onready var label_node = $Rows/RepoVis/Label
|
||||
onready var path_node = $Rows/RepoVis/Path
|
||||
onready var simplify_checkbox = $Rows/RepoVis/SimplifyCheckbox
|
||||
|
||||
export var label: String setget set_label
|
||||
export var path: String setget set_path, get_path
|
||||
export var file_browser_active = true setget set_file_browser_active
|
||||
export var simplified_view = false setget set_simplified_view
|
||||
export var editable_path = false setget set_editable_path
|
||||
|
||||
var node = preload("res://scenes/node.tscn")
|
||||
|
||||
var shell = Shell.new()
|
||||
var objects = {}
|
||||
var mouse_inside = false
|
||||
|
||||
# We use this for a heuristic of when to hide trees and blobs.
|
||||
var _commit_count = 0
|
||||
|
||||
func _ready():
|
||||
file_browser.shell = shell
|
||||
|
||||
# Trigger these again because nodes were not ready before.
|
||||
set_label(label)
|
||||
set_file_browser_active(file_browser_active)
|
||||
set_simplified_view(simplified_view)
|
||||
set_editable_path(editable_path)
|
||||
set_path(path)
|
||||
index.repository = self
|
||||
|
||||
update_everything()
|
||||
update_node_positions()
|
||||
|
||||
func _process(_delta):
|
||||
nodes.rect_pivot_offset = nodes.rect_size / 2
|
||||
if path:
|
||||
apply_forces()
|
||||
|
||||
func _unhandled_input(event):
|
||||
if event.is_action_pressed("zoom_out") and nodes.rect_scale.x > 0.3:
|
||||
nodes.rect_scale -= Vector2(0.05, 0.05)
|
||||
if event.is_action_pressed("zoom_in") and nodes.rect_scale.x < 2:
|
||||
nodes.rect_scale += Vector2(0.05, 0.05)
|
||||
|
||||
func there_is_a_git():
|
||||
return shell.run("test -d .git && echo yes || echo no") == "yes\n"
|
||||
|
||||
func update_everything():
|
||||
if file_browser:
|
||||
file_browser.update()
|
||||
if there_is_a_git():
|
||||
update_head()
|
||||
update_refs()
|
||||
update_index()
|
||||
update_objects()
|
||||
remove_gone_stuff()
|
||||
else:
|
||||
if index:
|
||||
index.clear()
|
||||
for o in objects:
|
||||
objects[o].queue_free()
|
||||
objects = {}
|
||||
|
||||
func set_path(new_path):
|
||||
path = new_path
|
||||
if path_node:
|
||||
path_node.text = path
|
||||
if new_path != "":
|
||||
shell.cd(new_path)
|
||||
for o in objects.values():
|
||||
o.queue_free()
|
||||
objects = {}
|
||||
if is_inside_tree():
|
||||
update_everything()
|
||||
|
||||
func get_path():
|
||||
return path
|
||||
|
||||
func set_label(new_label):
|
||||
label = new_label
|
||||
if label_node:
|
||||
label_node.text = new_label
|
||||
|
||||
func update_index():
|
||||
index.update()
|
||||
|
||||
func random_position():
|
||||
return Vector2(rand_range(0, rect_size.x), rand_range(0, rect_size.y))
|
||||
|
||||
func update_objects():
|
||||
var all = all_objects()
|
||||
|
||||
# Create new objects, if necessary.
|
||||
for o in all:
|
||||
if objects.has(o):
|
||||
continue
|
||||
|
||||
var type = object_type(o)
|
||||
|
||||
var n = node.instance()
|
||||
n.id = o
|
||||
n.type = object_type(o)
|
||||
n.content = object_content(o)
|
||||
n.repository = self
|
||||
|
||||
match type:
|
||||
"blob":
|
||||
pass
|
||||
"tree":
|
||||
n.children = tree_children(o)
|
||||
n.content = n.content.replacen("\t", " ")
|
||||
"commit":
|
||||
var c = {}
|
||||
c[commit_tree(o)] = ""
|
||||
for p in commit_parents(o):
|
||||
c[p] = ""
|
||||
n.children = c
|
||||
|
||||
_commit_count += 1
|
||||
if _commit_count >= 3 and not simplified_view:
|
||||
set_simplified_view(true)
|
||||
"tag":
|
||||
n.children = tag_target(o)
|
||||
|
||||
n.position = find_position(n)
|
||||
nodes.add_child(n)
|
||||
objects[o] = n
|
||||
|
||||
if simplified_view:
|
||||
if type == "tree" or type == "blob":
|
||||
n.hide()
|
||||
|
||||
func update_node_positions():
|
||||
if there_is_a_git():
|
||||
var graph_text = shell.run("git log --graph --oneline --all --no-abbrev")
|
||||
var graph_lines = Array(graph_text.split("\n"))
|
||||
graph_lines.pop_back()
|
||||
|
||||
for line_count in range(graph_lines.size()):
|
||||
var line = graph_lines[line_count]
|
||||
if "*" in line:
|
||||
var star_idx = line.find("*")
|
||||
var hash_regex = RegEx.new()
|
||||
hash_regex.compile("[a-f0-9]+")
|
||||
var regex_match = hash_regex.search(line)
|
||||
objects[regex_match.get_string()].position = Vector2(star_idx * 100 + 500, line_count * 100 + 500)
|
||||
|
||||
for ref in all_refs():
|
||||
var target_reference = objects[ref].children.keys()[0]
|
||||
var target = objects[target_reference]
|
||||
objects[ref].position = Vector2(target.position.x ,target.position.y - 100)
|
||||
|
||||
var target_reference = objects["HEAD"].children.keys()[0]
|
||||
if objects.has(target_reference):
|
||||
var target = objects[target_reference]
|
||||
objects["HEAD"].position = Vector2(target.position.x ,target.position.y - 100)
|
||||
|
||||
|
||||
|
||||
|
||||
func update_refs():
|
||||
for r in all_refs():
|
||||
if not objects.has(r):
|
||||
var n = node.instance()
|
||||
n.id = r
|
||||
n.type = "ref"
|
||||
n.content = ""
|
||||
n.repository = self
|
||||
objects[r] = n
|
||||
n.children = {ref_target(r): ""}
|
||||
n.position = find_position(n)
|
||||
nodes.add_child(n)
|
||||
var n = objects[r]
|
||||
n.children = {ref_target(r): ""}
|
||||
|
||||
func apply_forces():
|
||||
for o in objects.values():
|
||||
if not o.visible:
|
||||
continue
|
||||
for o2 in objects.values():
|
||||
if o == o2 or not o2.visible:
|
||||
continue
|
||||
var d = o.position.distance_to(o2.position)
|
||||
var dir = (o.global_position - o2.global_position).normalized()
|
||||
var f = 2000/pow(d+0.00001,1.5)
|
||||
o.position += dir*f
|
||||
o2.position -= dir*f
|
||||
var center_of_gravity = nodes.rect_size/2
|
||||
var d = o.position.distance_to(center_of_gravity)
|
||||
var dir = (o.position - center_of_gravity).normalized()
|
||||
var f = (d+0.00001)*(Vector2(nodes.rect_size.y, nodes.rect_size.x/3).normalized()/30)
|
||||
o.position -= dir*f
|
||||
|
||||
func find_position(n):
|
||||
var position = Vector2.ZERO
|
||||
var count = 0
|
||||
for child in n.children:
|
||||
if objects.has(child):
|
||||
position += objects[child].position
|
||||
count += 1
|
||||
if count > 0:
|
||||
position /= count
|
||||
n.position = position + Vector2(0, -150)
|
||||
else:
|
||||
n.position = random_position()
|
||||
return n.position
|
||||
|
||||
func git(args, splitlines = false):
|
||||
var o = shell.run("git --no-replace-objects " + args)
|
||||
|
||||
if splitlines:
|
||||
o = o.split("\n")
|
||||
# Remove last empty line.
|
||||
o.remove(len(o)-1)
|
||||
else:
|
||||
# Remove trailing newline.
|
||||
o = o.substr(0,len(o)-1)
|
||||
return o
|
||||
|
||||
func update_head():
|
||||
if not objects.has("HEAD"):
|
||||
var n = node.instance()
|
||||
n.id = "HEAD"
|
||||
n.type = "head"
|
||||
n.content = ""
|
||||
n.repository = self
|
||||
n.position = find_position(n)
|
||||
|
||||
objects["HEAD"] = n
|
||||
nodes.add_child(n)
|
||||
var n = objects["HEAD"]
|
||||
n.children = {ref_target("HEAD"): ""}
|
||||
|
||||
func all_objects():
|
||||
var obj = git("cat-file --batch-check='%(objectname)' --batch-all-objects", true)
|
||||
var dict = {}
|
||||
for o in obj:
|
||||
dict[o] = ""
|
||||
return dict
|
||||
|
||||
func object_type(id):
|
||||
return git("cat-file -t "+id)
|
||||
|
||||
func object_content(id):
|
||||
return git("cat-file -p "+id)
|
||||
|
||||
func tree_children(id):
|
||||
var children = git("cat-file -p "+id, true)
|
||||
var ids = {}
|
||||
for c in children:
|
||||
var a = c.split(" ")
|
||||
ids[a[2].split("\t")[0]] = a[2].split("\t")[1]
|
||||
return ids
|
||||
|
||||
func commit_tree(id):
|
||||
var c = git("cat-file -p "+id, true)
|
||||
for cc in c:
|
||||
var ccc = cc.split(" ", 2)
|
||||
match ccc[0]:
|
||||
"tree":
|
||||
return ccc[1]
|
||||
return null
|
||||
|
||||
func commit_parents(id):
|
||||
var parents = []
|
||||
var c = git("cat-file -p "+id, true)
|
||||
for cc in c:
|
||||
var ccc = cc.split(" ", 2)
|
||||
match ccc[0]:
|
||||
"parent":
|
||||
parents.push_back(ccc[1])
|
||||
return parents
|
||||
|
||||
func tag_target(id):
|
||||
var c = git("rev-parse %s^{}" % id)
|
||||
return {c: ""}
|
||||
|
||||
func all_refs():
|
||||
var refs = {}
|
||||
# If there are no refs, show-ref will have exit code 1. We don't care.
|
||||
for line in git("show-ref || true", true):
|
||||
line = line.split(" ")
|
||||
var _id = line[0]
|
||||
var name = line[1]
|
||||
refs[name] = ""
|
||||
return refs
|
||||
|
||||
func ref_target(ref):
|
||||
# Test whether this is a symbolic ref.
|
||||
var ret = git("symbolic-ref -q "+ref+" || true")
|
||||
# If it's not, it's probably a regular ref.
|
||||
if ret == "":
|
||||
if ref == "HEAD":
|
||||
ret = git("show-ref --head "+ref).split(" ")[0]
|
||||
else:
|
||||
ret = git("show-ref "+ref).split(" ")[0]
|
||||
return ret
|
||||
|
||||
func set_simplified_view(simplify):
|
||||
simplified_view = simplify
|
||||
if simplify_checkbox:
|
||||
simplify_checkbox.pressed = simplify
|
||||
|
||||
for o in objects:
|
||||
var obj = objects[o]
|
||||
if obj.type == "tree" or obj.type == "blob":
|
||||
obj.visible = not simplify
|
||||
|
||||
func set_editable_path(editable):
|
||||
editable_path = editable
|
||||
if label_node:
|
||||
label_node.visible = not editable
|
||||
if path_node:
|
||||
path_node.visible = editable
|
||||
|
||||
func remove_gone_stuff():
|
||||
# FIXME: Cache the result of all_objects.
|
||||
var all = {}
|
||||
for o in all_objects():
|
||||
all[o] = ""
|
||||
for o in all_refs():
|
||||
all[o] = ""
|
||||
all["HEAD"] = ""
|
||||
# Delete objects, if they disappeared.
|
||||
for o in objects.keys():
|
||||
if not all.has(o):
|
||||
objects[o].queue_free()
|
||||
objects.erase(o)
|
||||
|
||||
func _on_mouse_entered():
|
||||
mouse_inside = true
|
||||
|
||||
func _on_mouse_exited():
|
||||
mouse_inside = false
|
||||
|
||||
func set_file_browser_active(active):
|
||||
file_browser_active = active
|
||||
if file_browser:
|
||||
file_browser.visible = active
|
128
scenes/repository.tscn
Normal file
128
scenes/repository.tscn
Normal file
|
@ -0,0 +1,128 @@
|
|||
[gd_scene load_steps=5 format=2]
|
||||
|
||||
[ext_resource path="res://scenes/repository.gd" type="Script" id=1]
|
||||
[ext_resource path="res://styles/theme.tres" type="Theme" id=2]
|
||||
[ext_resource path="res://fonts/big.tres" type="DynamicFont" id=3]
|
||||
[ext_resource path="res://scenes/file_browser.tscn" type="PackedScene" id=4]
|
||||
|
||||
[node name="Repository" type="Control"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 2
|
||||
theme = ExtResource( 2 )
|
||||
script = ExtResource( 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Rows" type="VSplitContainer" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 2
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="RepoVis" type="Control" parent="Rows"]
|
||||
margin_right = 1920.0
|
||||
margin_bottom = 926.0
|
||||
mouse_filter = 2
|
||||
size_flags_vertical = 3
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Label" type="Label" parent="Rows/RepoVis"]
|
||||
margin_left = 5.60091
|
||||
margin_top = -0.518692
|
||||
margin_right = 204.601
|
||||
margin_bottom = 48.4813
|
||||
custom_fonts/font = ExtResource( 3 )
|
||||
text = "Repo name"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="IndexLabel" type="Label" parent="Rows/RepoVis"]
|
||||
visible = false
|
||||
margin_left = 21.0
|
||||
margin_top = 65.0
|
||||
margin_right = 377.0
|
||||
margin_bottom = 108.0
|
||||
text = "Index:"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Button" type="Button" parent="Rows/RepoVis"]
|
||||
visible = false
|
||||
margin_left = 36.5602
|
||||
margin_top = 67.9891
|
||||
margin_right = 119.56
|
||||
margin_bottom = 109.989
|
||||
text = "Update"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="SimplifyCheckbox" type="CheckBox" parent="Rows/RepoVis"]
|
||||
visible = false
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
margin_left = -208.715
|
||||
margin_top = 17.9594
|
||||
margin_right = -15.7146
|
||||
margin_bottom = 42.9594
|
||||
focus_mode = 0
|
||||
enabled_focus_mode = 0
|
||||
text = "Hide trees and blobs"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Nodes" type="Control" parent="Rows/RepoVis"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_bottom = -6.10352e-05
|
||||
mouse_filter = 2
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Path" type="LineEdit" parent="Rows/RepoVis"]
|
||||
visible = false
|
||||
margin_left = 23.0
|
||||
margin_top = 12.0
|
||||
margin_right = 374.0
|
||||
margin_bottom = 61.0
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Browsers" type="VBoxContainer" parent="Rows"]
|
||||
margin_top = 938.0
|
||||
margin_right = 1920.0
|
||||
margin_bottom = 1080.0
|
||||
|
||||
[node name="Index" parent="Rows/Browsers" instance=ExtResource( 4 )]
|
||||
visible = false
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
margin_right = 1920.0
|
||||
margin_bottom = 142.0
|
||||
size_flags_vertical = 3
|
||||
title = "Index"
|
||||
mode = 2
|
||||
|
||||
[node name="FileBrowser" parent="Rows/Browsers" instance=ExtResource( 4 )]
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
margin_right = 1920.0
|
||||
margin_bottom = 142.0
|
||||
size_flags_vertical = 3
|
||||
title = "Working directory"
|
||||
[connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"]
|
||||
[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"]
|
||||
[connection signal="pressed" from="Rows/RepoVis/Button" to="." method="update_everything"]
|
||||
[connection signal="toggled" from="Rows/RepoVis/SimplifyCheckbox" to="." method="set_simplified_view"]
|
||||
[connection signal="text_entered" from="Rows/RepoVis/Path" to="." method="set_path"]
|
33
scenes/sandbox.gd
Normal file
33
scenes/sandbox.gd
Normal file
|
@ -0,0 +1,33 @@
|
|||
extends Control
|
||||
|
||||
func _ready():
|
||||
var path = null
|
||||
|
||||
var args = helpers.parse_args()
|
||||
if args.has("sandbox"):
|
||||
if args["sandbox"] is String:
|
||||
if args["sandbox"] == ".":
|
||||
args["sandbox"] = OS.get_environment("PWD")
|
||||
var dir = Directory.new()
|
||||
if dir.dir_exists(args["sandbox"]):
|
||||
path = args["sandbox"]
|
||||
else:
|
||||
helpers.crash("Directory %s does not exist" % args["sandbox"])
|
||||
|
||||
if path == null:
|
||||
path = game.tmp_prefix_inside+"/repos/sandbox/"
|
||||
helpers.careful_delete(path)
|
||||
|
||||
game.global_shell.run("mkdir " + path)
|
||||
game.global_shell.cd(path)
|
||||
game.global_shell.run("git init")
|
||||
game.global_shell.run("git symbolic-ref HEAD refs/heads/main")
|
||||
|
||||
$Columns/Repository.path = path
|
||||
|
||||
get_tree().set_screen_stretch(SceneTree.STRETCH_MODE_2D, SceneTree.STRETCH_ASPECT_KEEP, Vector2(1920, 1080), 1.5)
|
||||
|
||||
$Columns/Terminal.repository = $Columns/Repository
|
||||
|
||||
func update_repo():
|
||||
$Columns/Repository.update_everything()
|
53
scenes/sandbox.tscn
Normal file
53
scenes/sandbox.tscn
Normal file
|
@ -0,0 +1,53 @@
|
|||
[gd_scene load_steps=5 format=2]
|
||||
|
||||
[ext_resource path="res://scenes/terminal.tscn" type="PackedScene" id=1]
|
||||
[ext_resource path="res://scenes/repository.tscn" type="PackedScene" id=2]
|
||||
[ext_resource path="res://styles/theme.tres" type="Theme" id=3]
|
||||
[ext_resource path="res://sandbox.gd" type="Script" id=4]
|
||||
|
||||
|
||||
[node name="Sandbox" type="Control"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
theme = ExtResource( 3 )
|
||||
script = ExtResource( 4 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Background" type="ColorRect" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 2
|
||||
color = Color( 0.0705882, 0.0705882, 0.0705882, 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Columns" type="HSplitContainer" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 5.0
|
||||
margin_top = 5.0
|
||||
margin_right = -5.0
|
||||
margin_bottom = -5.0
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Repository" parent="Columns" instance=ExtResource( 2 )]
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
margin_right = 949.0
|
||||
margin_bottom = 1070.0
|
||||
size_flags_horizontal = 3
|
||||
editable_path = true
|
||||
|
||||
[node name="Terminal" parent="Columns" instance=ExtResource( 1 )]
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
margin_left = 961.0
|
||||
margin_right = 1910.0
|
||||
margin_bottom = 1070.0
|
||||
size_flags_horizontal = 3
|
||||
[connection signal="command_done" from="Columns/Terminal" to="." method="update_repo"]
|
110
scenes/shell.gd
Normal file
110
scenes/shell.gd
Normal file
|
@ -0,0 +1,110 @@
|
|||
extends Node
|
||||
class_name Shell
|
||||
|
||||
var exit_code
|
||||
|
||||
var _cwd
|
||||
var _os = OS.get_name()
|
||||
|
||||
func _init():
|
||||
_cwd = game.tmp_prefix_inside
|
||||
|
||||
func cd(dir):
|
||||
_cwd = dir
|
||||
|
||||
# Run a shell command given as a string. Run this if you're interested in the
|
||||
# output of the command.
|
||||
func run(command, crash_on_fail=true):
|
||||
var debug = false
|
||||
|
||||
if debug:
|
||||
print("$ %s" % command)
|
||||
|
||||
var env = {}
|
||||
if game.fake_editor:
|
||||
env["GIT_EDITOR"] = game.fake_editor.replace(" ", "\\ ")
|
||||
env["GIT_AUTHOR_NAME"] = "You"
|
||||
env["GIT_COMMITTER_NAME"] = "You"
|
||||
env["GIT_AUTHOR_EMAIL"] = "you@example.com"
|
||||
env["GIT_COMMITTER_EMAIL"] = "you@example.com"
|
||||
env["GIT_TEMPLATE_DIR"] = ""
|
||||
|
||||
var hacky_command = ""
|
||||
for variable in env:
|
||||
hacky_command += "export %s='%s';" % [variable, env[variable]]
|
||||
hacky_command += "cd '%s' || exit 1;" % _cwd
|
||||
hacky_command += command
|
||||
|
||||
var result
|
||||
if _os == "X11" or _os == "OSX":
|
||||
# Godot's OS.execute wraps each argument in double quotes before executing
|
||||
# on Linux and macOS.
|
||||
# Because we want to be in a single-quote context, where nothing is evaluated,
|
||||
# we end those double quotes and start a single quoted string. For each single
|
||||
# quote appearing in our string, we close the single quoted string, and add
|
||||
# a double quoted string containing the single quote. Ooooof!
|
||||
#
|
||||
# Example: The string
|
||||
#
|
||||
# test 'fu' "bla" blubb
|
||||
#
|
||||
# becomes
|
||||
#
|
||||
# "'test '"'"'fu'"'"' "bla" blubb"
|
||||
|
||||
hacky_command = '"\''+hacky_command.replace("'", "'\"'\"'")+'\'"'
|
||||
result = helpers.exec(_shell_binary(), ["-c", hacky_command], crash_on_fail)
|
||||
elif _os == "Windows":
|
||||
# On Windows, if the command contains a newline (even if inside a string),
|
||||
# execution will end. To avoid that, we first write the command to a file,
|
||||
# and run that file with bash.
|
||||
var script_path = game.tmp_prefix_inside + "command" + str(randi())
|
||||
helpers.write_file(script_path, hacky_command)
|
||||
result = helpers.exec(_shell_binary(), [script_path], crash_on_fail)
|
||||
else:
|
||||
helpers.crash("Unimplemented OS: %s" % _os)
|
||||
|
||||
if debug:
|
||||
print(result["output"])
|
||||
|
||||
exit_code = result["exit_code"]
|
||||
return result["output"]
|
||||
|
||||
func _shell_binary():
|
||||
if _os == "X11" or _os == "OSX":
|
||||
return "bash"
|
||||
elif _os == "Windows":
|
||||
return "dependencies\\windows\\git\\bin\\bash.exe"
|
||||
else:
|
||||
helpers.crash("Unsupported OS: %s" % _os)
|
||||
|
||||
var _t
|
||||
func run_async(command):
|
||||
_t = Thread.new()
|
||||
_t.start(self, "run_async_thread", command)
|
||||
|
||||
func run_async_thread(command):
|
||||
var port = 1000 + (randi() % 1000)
|
||||
var s = TCP_Server.new()
|
||||
s.listen(port)
|
||||
var _pid = OS.execute("ncat", ["127.0.0.1", str(port), "-c", command], false, [], true)
|
||||
while not s.is_connection_available():
|
||||
pass
|
||||
var c = s.take_connection()
|
||||
while c.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
||||
read_from(c)
|
||||
OS.delay_msec(1000/30)
|
||||
read_from(c)
|
||||
c.disconnect_from_host()
|
||||
s.stop()
|
||||
|
||||
func read_from(c):
|
||||
var total_available = c.get_available_bytes()
|
||||
print(str(total_available)+" bytes available")
|
||||
while total_available > 0:
|
||||
var available = min(1024, total_available)
|
||||
total_available -= available
|
||||
print("reading "+str(available))
|
||||
var data = c.get_utf8_string(available)
|
||||
#emit_signal("output", data)
|
||||
print(data.size())
|
41
scenes/tcp_server.gd
Normal file
41
scenes/tcp_server.gd
Normal file
|
@ -0,0 +1,41 @@
|
|||
extends Node
|
||||
|
||||
signal data_received(string)
|
||||
|
||||
export var port: int
|
||||
|
||||
var _s = TCP_Server.new()
|
||||
var _c
|
||||
var _connected = false
|
||||
|
||||
func _ready():
|
||||
start()
|
||||
|
||||
func start():
|
||||
_s.listen(port)
|
||||
|
||||
func _process(_delta):
|
||||
if _s.is_connection_available():
|
||||
if _connected:
|
||||
_c.disconnect_from_host()
|
||||
helpers.crash("Dropping active connection")
|
||||
_c = _s.take_connection()
|
||||
_connected = true
|
||||
print("connected!")
|
||||
|
||||
if _connected:
|
||||
if _c.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
||||
_connected = false
|
||||
print("disconnected")
|
||||
var available = _c.get_available_bytes()
|
||||
while available > 0:
|
||||
var data = _c.get_utf8_string(available)
|
||||
emit_signal("data_received", data)
|
||||
available = _c.get_available_bytes()
|
||||
|
||||
func send(text):
|
||||
if _connected:
|
||||
text += "\n"
|
||||
_c.put_data(text.to_utf8())
|
||||
else:
|
||||
helpers.crash("Trying to send data on closed connection")
|
7
scenes/tcp_server.tscn
Normal file
7
scenes/tcp_server.tscn
Normal file
|
@ -0,0 +1,7 @@
|
|||
[gd_scene load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://scenes/tcp_server.gd" type="Script" id=1]
|
||||
|
||||
[node name="TCPServer" type="Node"]
|
||||
script = ExtResource( 1 )
|
||||
port = 6666
|
212
scenes/terminal.gd
Normal file
212
scenes/terminal.gd
Normal file
|
@ -0,0 +1,212 @@
|
|||
extends Control
|
||||
|
||||
signal command_done
|
||||
|
||||
var thread
|
||||
|
||||
var history_position = 0
|
||||
var git_commands = ["add", "am", "archive", "bisect", "branch", "bundle", "checkout", "cherry-pick", "citool", "clean", "clone", "commit", "describe", "diff", "fetch", "format-patch", "gc", "gitk", "grep", "gui", "init", "log", "merge", "mv", "notes", "pull", "push", "range-diff", "rebase", "reset", "restore", "revert", "rm", "shortlog", "show", "sparse-checkout", "stash", "status", "submodule", "switch", "tag", "worktree", "config", "fast-export", "fast-import", "filter-branch", "mergetool", "pack-refs", "prune", "reflog", "remote", "repack", "replace", "annotate", "blame", "bugreport", "count-objects", "difftool", "fsck", "gitweb", "help", "instaweb", "merge-tree", "rerere", "show-branch", "verify-commit", "verify-tag", "whatchanged", "archimport", "cvsexportcommit", "cvsimport", "cvsserver", "imap-send", "p", "quiltimport", "request-pull", "send-email", "svn", "apply", "checkout-index", "commit-graph", "commit-tree", "hash-object", "index-pack", "merge-file", "merge-index", "mktag", "mktree", "multi-pack-index", "pack-objects", "prune-packed", "read-tree", "symbolic-ref", "unpack-objects", "update-index", "update-ref", "write-tree", "cat-file", "cherry", "diff-files", "diff-index", "diff-tree", "for-each-ref", "get-tar-commit-id", "ls-files", "ls-remote", "ls-tree", "merge-base", "name-rev", "pack-redundant", "rev-list", "rev-parse", "show-index", "show-ref", "unpack-file", "var", "verify-pack", "daemon", "fetch-pack", "http-backend", "send-pack", "update-server-info", "check-attr", "check-ignore", "check-mailmap", "check-ref-format", "column", "credential", "credential-cache", "credential-store", "fmt-merge-msg", "interpret-trailers", "mailinfo", "mailsplit", "merge-one-file", "patch-id", "sh-i", "sh-setup"]
|
||||
|
||||
var git_commands_help = []
|
||||
|
||||
onready var input = $Rows/InputLine/Input
|
||||
onready var output = $Rows/TopHalf/Output
|
||||
onready var completions = $Rows/TopHalf/Completions
|
||||
var repository
|
||||
onready var main = get_tree().get_root().get_node("Main")
|
||||
|
||||
var premade_commands = [
|
||||
'git commit --allow-empty -m "empty"',
|
||||
'echo $RANDOM | git hash-object -w --stdin',
|
||||
'git switch -c $RANDOM',
|
||||
]
|
||||
|
||||
func _ready():
|
||||
var error = $TextEditor.connect("hide", self, "editor_closed")
|
||||
if error != OK:
|
||||
helpers.crash("Could not connect TextEditor's hide signal")
|
||||
input.grab_focus()
|
||||
|
||||
for subcommand in git_commands:
|
||||
git_commands_help.push_back("")
|
||||
|
||||
completions.hide()
|
||||
history_position = game.state["history"].size()
|
||||
|
||||
func _input(event):
|
||||
if game.state["history"].size() > 0:
|
||||
if event.is_action_pressed("ui_up"):
|
||||
if history_position > 0:
|
||||
history_position -= 1
|
||||
input.text = game.state["history"][history_position]
|
||||
input.caret_position = input.text.length()
|
||||
# This prevents the Input taking the arrow as a "skip to beginning" command.
|
||||
get_tree().set_input_as_handled()
|
||||
if event.is_action_pressed("ui_down"):
|
||||
if history_position < game.state["history"].size()-1:
|
||||
history_position += 1
|
||||
input.text = game.state["history"][history_position]
|
||||
input.caret_position = input.text.length()
|
||||
get_tree().set_input_as_handled()
|
||||
|
||||
if event.is_action_pressed("tab_complete"):
|
||||
if completions.visible:
|
||||
completions.get_root().get_children().select(0)
|
||||
get_tree().set_input_as_handled()
|
||||
if event.is_action_pressed("delete_word"):
|
||||
var first_half = input.text.substr(0,input.caret_position)
|
||||
var second_half = input.text.substr(input.caret_position)
|
||||
|
||||
var idx = first_half.strip_edges(false, true).find_last(" ")
|
||||
if idx > 0:
|
||||
input.text = first_half.substr(0,idx+1) + second_half
|
||||
input.caret_position = idx+1
|
||||
else:
|
||||
input.text = "" + second_half
|
||||
|
||||
func load_command(id):
|
||||
input.text = premade_commands[id]
|
||||
input.caret_position = input.text.length()
|
||||
|
||||
func send_command(command):
|
||||
game.state["history"].push_back(command)
|
||||
game.save_state()
|
||||
history_position = game.state["history"].size()
|
||||
|
||||
input.editable = false
|
||||
completions.hide()
|
||||
|
||||
if thread != null:
|
||||
thread.wait_to_finish()
|
||||
thread = Thread.new()
|
||||
thread.start(self, "run_command_in_a_thread", command)
|
||||
|
||||
func send_command_async(command):
|
||||
input.text = ""
|
||||
$TCPServer.send(command+"\n")
|
||||
|
||||
func run_command_in_a_thread(command):
|
||||
var o = repository.shell.run(command, false)
|
||||
|
||||
if repository.shell.exit_code == 0:
|
||||
$OkSound.pitch_scale = rand_range(0.8, 1.2)
|
||||
$OkSound.play()
|
||||
else:
|
||||
$ErrorSound.play()
|
||||
|
||||
input.text = ""
|
||||
input.editable = true
|
||||
|
||||
if o.length() <= 1000:
|
||||
output.text = output.text + "$ " + command + "\n" + o
|
||||
else:
|
||||
$Pager/Text.text = o
|
||||
$Pager.popup()
|
||||
|
||||
emit_signal("command_done")
|
||||
|
||||
func receive_output(text):
|
||||
output.text += text
|
||||
repository.update_everything()
|
||||
|
||||
func clear():
|
||||
output.text = ""
|
||||
|
||||
func editor_closed():
|
||||
input.grab_focus()
|
||||
|
||||
func regenerate_completions_menu(new_text):
|
||||
var comp = generate_completions(new_text)
|
||||
|
||||
completions.clear()
|
||||
|
||||
|
||||
var filtered_comp = []
|
||||
for c in comp:
|
||||
if c != new_text:
|
||||
filtered_comp.push_back(c)
|
||||
|
||||
if filtered_comp.size() == 0:
|
||||
completions.hide()
|
||||
else:
|
||||
completions.show()
|
||||
|
||||
var _root = completions.create_item()
|
||||
for c in filtered_comp:
|
||||
var child = completions.create_item()
|
||||
child.set_text(0, c)
|
||||
if c.split(" ").size() >= 2:
|
||||
var subcommand = c.split(" ")[1]
|
||||
var idx = git_commands.find(subcommand)
|
||||
if idx >= 0:
|
||||
child.set_text(1, git_commands_help[idx])
|
||||
|
||||
completions.margin_top = -min(filtered_comp.size() * 35 + 10, 210)
|
||||
|
||||
func relevant_subcommands():
|
||||
var result = {}
|
||||
for h in game.state["history"]:
|
||||
var parts = Array(h.split(" "))
|
||||
if parts[0] == "git":
|
||||
var subcommand = parts[1]
|
||||
if git_commands.has(subcommand):
|
||||
if not result.has(subcommand):
|
||||
result[subcommand] = 0
|
||||
result[subcommand] += 1
|
||||
|
||||
# Convert to format [["add", 3], ["pull", 5]].
|
||||
var result_array = []
|
||||
for r in result:
|
||||
result_array.push_back([r, result[r]])
|
||||
|
||||
result_array.sort_custom(self, "sort_by_frequency_desc")
|
||||
|
||||
var plain_result = []
|
||||
for r in result_array:
|
||||
plain_result.push_back(r[0])
|
||||
return plain_result
|
||||
|
||||
func sort_by_frequency_desc(a, b):
|
||||
return a[1] > b[1]
|
||||
|
||||
func generate_completions(command):
|
||||
var results = []
|
||||
|
||||
# Collect git commands.
|
||||
if command.substr(0, 4) == "git ":
|
||||
var rest = command.substr(4)
|
||||
var subcommands = relevant_subcommands()
|
||||
|
||||
for sc in subcommands:
|
||||
if sc.substr(0, rest.length()) == rest:
|
||||
results.push_back("git "+sc)
|
||||
|
||||
# Part 1: Only autocomplete after git subcommand.
|
||||
# Part2: Prevent autocompletion to only show filename at the beginning of a command.
|
||||
if !(command.substr(0,4) == "git " and command.split(" ").size() <= 2) and command.split(" ").size() > 1:
|
||||
var last_word = Array(command.split(" ")).pop_back()
|
||||
var file_string = repository.shell.run("find . -type f")
|
||||
var files = file_string.split("\n")
|
||||
files = Array(files)
|
||||
# The last entry is an empty string, remove it.
|
||||
files.pop_back()
|
||||
for file_path in files:
|
||||
file_path = file_path.substr(2)
|
||||
if file_path.substr(0,4) != ".git" and file_path.substr(0,last_word.length()) == last_word:
|
||||
results.push_back(command+file_path.substr(last_word.length()))
|
||||
|
||||
return results
|
||||
|
||||
func _input_changed(new_text):
|
||||
call_deferred("regenerate_completions_menu", new_text)
|
||||
|
||||
func _completion_selected():
|
||||
var item = completions.get_selected()
|
||||
input.text = item.get_text(0)
|
||||
input.emit_signal("text_changed", input.text)
|
||||
#completions.hide()
|
||||
input.grab_focus()
|
||||
input.caret_position = input.text.length()
|
||||
|
||||
func editor_saved():
|
||||
emit_signal("command_done")
|
173
scenes/terminal.tscn
Normal file
173
scenes/terminal.tscn
Normal file
|
@ -0,0 +1,173 @@
|
|||
[gd_scene load_steps=10 format=2]
|
||||
|
||||
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
|
||||
[ext_resource path="res://sounds/typewriter_ding.wav" type="AudioStream" id=2]
|
||||
[ext_resource path="res://fonts/monospace.tres" type="DynamicFont" id=3]
|
||||
[ext_resource path="res://scenes/terminal.gd" type="Script" id=4]
|
||||
[ext_resource path="res://scenes/text_editor.tscn" type="PackedScene" id=5]
|
||||
[ext_resource path="res://scenes/tcp_server.tscn" type="PackedScene" id=6]
|
||||
[ext_resource path="res://sounds/buzzer.wav" type="AudioStream" id=7]
|
||||
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=1]
|
||||
content_margin_left = 5.0
|
||||
content_margin_right = 5.0
|
||||
content_margin_top = 5.0
|
||||
content_margin_bottom = 5.0
|
||||
bg_color = Color( 0, 0, 0, 1 )
|
||||
border_color = Color( 0.415686, 0.333333, 1, 1 )
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
|
||||
[sub_resource type="GDScript" id=2]
|
||||
script/source = "extends Button
|
||||
|
||||
func _ready():
|
||||
pass
|
||||
|
||||
func pressed():
|
||||
$\"../../..\".send_command(text)
|
||||
"
|
||||
|
||||
[node name="Terminal" type="Control"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 1
|
||||
script = ExtResource( 4 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Rows" type="VBoxContainer" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="TopHalf" type="Control" parent="Rows"]
|
||||
margin_right = 1920.0
|
||||
margin_bottom = 1052.0
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="Output" type="RichTextLabel" parent="Rows/TopHalf"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_top = -1.92206
|
||||
margin_bottom = -1.92212
|
||||
size_flags_vertical = 3
|
||||
custom_styles/normal = SubResource( 1 )
|
||||
custom_fonts/normal_font = ExtResource( 3 )
|
||||
scroll_following = true
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Completions" type="Tree" parent="Rows/TopHalf"]
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_top = -311.0
|
||||
columns = 2
|
||||
hide_root = true
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Rows"]
|
||||
visible = false
|
||||
margin_top = 984.0
|
||||
margin_right = 1920.0
|
||||
margin_bottom = 1052.0
|
||||
|
||||
[node name="Button" type="Button" parent="Rows/VBoxContainer"]
|
||||
margin_right = 1920.0
|
||||
margin_bottom = 20.0
|
||||
text = "git commit --allow-empty -m \"$RANDOM\""
|
||||
script = SubResource( 2 )
|
||||
|
||||
[node name="Button2" type="Button" parent="Rows/VBoxContainer"]
|
||||
margin_top = 24.0
|
||||
margin_right = 1920.0
|
||||
margin_bottom = 44.0
|
||||
text = "git checkout HEAD^"
|
||||
script = SubResource( 2 )
|
||||
|
||||
[node name="Button3" type="Button" parent="Rows/VBoxContainer"]
|
||||
margin_top = 48.0
|
||||
margin_right = 1920.0
|
||||
margin_bottom = 68.0
|
||||
text = "git checkout -b \"$RANDOM\""
|
||||
script = SubResource( 2 )
|
||||
|
||||
[node name="InputLine" type="HBoxContainer" parent="Rows"]
|
||||
margin_top = 1056.0
|
||||
margin_right = 1920.0
|
||||
margin_bottom = 1080.0
|
||||
|
||||
[node name="Input" type="LineEdit" parent="Rows/InputLine"]
|
||||
margin_right = 1920.0
|
||||
margin_bottom = 24.0
|
||||
size_flags_horizontal = 3
|
||||
caret_blink = true
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="ClearButton" type="Button" parent="."]
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
margin_left = -88.0
|
||||
margin_top = 5.0
|
||||
margin_right = -5.0
|
||||
margin_bottom = 36.0
|
||||
focus_mode = 0
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
enabled_focus_mode = 0
|
||||
text = "Clear"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="TextEditor" parent="." instance=ExtResource( 5 )]
|
||||
visible = false
|
||||
mouse_filter = 1
|
||||
syntax_highlighting = false
|
||||
|
||||
[node name="TCPServer" parent="." instance=ExtResource( 6 )]
|
||||
|
||||
[node name="Pager" type="WindowDialog" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 18.0
|
||||
margin_top = 39.0
|
||||
margin_right = -687.0
|
||||
margin_bottom = -48.0
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Text" type="RichTextLabel" parent="Pager"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
custom_fonts/normal_font = ExtResource( 3 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="ErrorSound" type="AudioStreamPlayer" parent="."]
|
||||
stream = ExtResource( 7 )
|
||||
|
||||
[node name="OkSound" type="AudioStreamPlayer" parent="."]
|
||||
stream = ExtResource( 2 )
|
||||
[connection signal="item_selected" from="Rows/TopHalf/Completions" to="." method="_completion_selected"]
|
||||
[connection signal="pressed" from="Rows/VBoxContainer/Button" to="Rows/VBoxContainer/Button" method="pressed"]
|
||||
[connection signal="pressed" from="Rows/VBoxContainer/Button2" to="Rows/VBoxContainer/Button2" method="pressed"]
|
||||
[connection signal="pressed" from="Rows/VBoxContainer/Button3" to="Rows/VBoxContainer/Button3" method="pressed"]
|
||||
[connection signal="text_changed" from="Rows/InputLine/Input" to="." method="_input_changed"]
|
||||
[connection signal="text_entered" from="Rows/InputLine/Input" to="." method="send_command"]
|
||||
[connection signal="pressed" from="ClearButton" to="." method="clear"]
|
||||
[connection signal="saved" from="TextEditor" to="." method="editor_saved"]
|
||||
[connection signal="data_received" from="TCPServer" to="." method="receive_output"]
|
53
scenes/text_editor.gd
Normal file
53
scenes/text_editor.gd
Normal file
|
@ -0,0 +1,53 @@
|
|||
extends TextEdit
|
||||
|
||||
signal saved
|
||||
|
||||
var path
|
||||
|
||||
var _server
|
||||
var _client_connection
|
||||
|
||||
func _ready():
|
||||
# Initialize TCP server for fake editor.
|
||||
_server = TCP_Server.new()
|
||||
_server.listen(1234)
|
||||
|
||||
func _process(_delta):
|
||||
if _server.is_connection_available():
|
||||
_client_connection = _server.take_connection()
|
||||
var length = _client_connection.get_u8()
|
||||
var filename = _client_connection.get_string(length)
|
||||
filename = filename.replace("%srepos/" % game.tmp_prefix_inside, "")
|
||||
open(filename)
|
||||
|
||||
func _input(event):
|
||||
if event.is_action_pressed("save"):
|
||||
save()
|
||||
|
||||
func open(filename):
|
||||
path = filename
|
||||
|
||||
var fixme_path = game.tmp_prefix_outside+"repos/"
|
||||
var content = helpers.read_file(fixme_path+filename)
|
||||
text = content
|
||||
|
||||
show()
|
||||
grab_focus()
|
||||
|
||||
func save():
|
||||
if visible:
|
||||
var fixme_path = game.tmp_prefix_outside+"repos/"
|
||||
|
||||
# Add a newline to the end of the file if there is none.
|
||||
if text.length() > 0 and text.substr(text.length()-1, 1) != "\n":
|
||||
text += "\n"
|
||||
|
||||
helpers.write_file(fixme_path+path, text)
|
||||
close()
|
||||
emit_signal("saved")
|
||||
|
||||
func close():
|
||||
if _client_connection and _client_connection.is_connected_to_host():
|
||||
_client_connection.disconnect_from_host()
|
||||
text = ""
|
||||
hide()
|
50
scenes/text_editor.tscn
Normal file
50
scenes/text_editor.tscn
Normal file
|
@ -0,0 +1,50 @@
|
|||
[gd_scene load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
|
||||
[ext_resource path="res://scenes/text_editor.gd" type="Script" id=2]
|
||||
|
||||
|
||||
[node name="TextEditor" type="TextEdit"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
custom_colors/background_color = Color( 0, 0, 0, 1 )
|
||||
text = "Text here"
|
||||
syntax_highlighting = true
|
||||
script = ExtResource( 2 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="SaveButton" type="Button" parent="."]
|
||||
anchor_left = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = -114.396
|
||||
margin_top = -59.399
|
||||
margin_right = -14.3955
|
||||
margin_bottom = -14.399
|
||||
focus_mode = 0
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
enabled_focus_mode = 0
|
||||
text = "Save"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="CloseButton" type="Button" parent="."]
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
margin_left = -54.3247
|
||||
margin_top = 12.0
|
||||
margin_right = -14.3247
|
||||
margin_bottom = 52.0
|
||||
focus_mode = 0
|
||||
custom_fonts/font = ExtResource( 1 )
|
||||
enabled_focus_mode = 0
|
||||
text = "x"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
[connection signal="pressed" from="SaveButton" to="." method="save"]
|
||||
[connection signal="pressed" from="CloseButton" to="." method="close"]
|
Loading…
Add table
Add a link
Reference in a new issue