diff --git a/assets/export/edge.png b/assets/export/edge.png new file mode 100644 index 0000000..b5ababd Binary files /dev/null and b/assets/export/edge.png differ diff --git a/assets/source/Squares.xcf b/assets/source/Squares.xcf index 141c9b6..6b6d4c9 100644 Binary files a/assets/source/Squares.xcf and b/assets/source/Squares.xcf differ diff --git a/project.godot b/project.godot index eb75ce8..108cd76 100644 --- a/project.godot +++ b/project.godot @@ -8,9 +8,26 @@ config_version=4 -_global_script_classes=[ ] +_global_script_classes=[ { +"base": "Container", +"class": "Game", +"language": "GDScript", +"path": "res://src/Game.gd" +}, { +"base": "Area2D", +"class": "Piece", +"language": "GDScript", +"path": "res://src/Piece.gd" +}, { +"base": "Node2D", +"class": "Square", +"language": "GDScript", +"path": "res://src/Square.gd" +} ] _global_script_class_icons={ - +"Game": "", +"Piece": "", +"Square": "" } [application] diff --git a/src/Game.gd b/src/Game.gd index 355a871..b224cbe 100644 --- a/src/Game.gd +++ b/src/Game.gd @@ -1,5 +1,5 @@ extends Container - +class_name Game # Declare member variables here. Examples: # var a = 2 @@ -8,10 +8,15 @@ extends Container var pf_scale var turn = 1 var is_player_turn = true -# Index: weakref to piece -var pieces = {} +var selected_piece = null +# Holds Vector2(x, y) => { piece, } +var board_squares = {} + var height var width +var landing_piece = null +var landing_pos = null + const piece_types = { "pawn": "res://src/pieces/Pawn.tscn", "rook": "res://src/pieces/Rook.tscn", @@ -38,25 +43,115 @@ func new_piece(piece_type, group, position = null): piece.add_to_group(group) if group == "opponent": piece.get_node("Body").set_modulate( - Color(0.75, 0.1, 0.1, 1.0) + Color(0.8, 0.01, 0.01, 1.0) ) pf.add_child(piece) - print("Created piece", piece_type, " at ", position, " for group ", group) + piece.connect("click", self, "_on_piece_click") + piece.connect("hold_start", self, "_on_piece_click") + piece.connect("hold_stop", self, "_on_hold_stop") + #print("Created piece ", piece, " of type ", piece_type, " at ", position, " for group ", group) return piece -func pos_to_index(position): - return (position.y * self.height) + position.x +func _on_hold_stop(piece, event): + _on_piece_click(piece, null) + # Try to land the piece on the next physics frame so we + # can use raycasts + self.landing_piece = piece + self.landing_pos = event.position + +func get_valid_piece_moves(piece): + var square = square_of_piece(piece) + var possible_moves = piece.get_possible_moves(Vector2(square.x, square.y)) + # @TODO Filter based on game state + var moves = [] + for m in possible_moves: + var target_square = self.board_squares[m['pos']] + if target_square['piece']: + # something here + if pieces_hostile(piece, target_square['piece']): + if not m['attack']: + print("Move to ", m['pos'], " is not valid due to not being an attack move") + else: + moves.append(m) + else: + print("Move to ", m['pos'], " is not valid due to same-team piece is spot") + else: + # empty + if not m['attack_only']: + moves.append(m) + else: + print("Move to ", m['pos'], " is not valid due to missing target") + return moves + +func pieces_hostile(p1, p2): + var p1_player = p1.is_in_group("player") + var p2_player = p2.is_in_group("player") + if p1_player == p2_player: + return false + return true + +func _on_piece_click(piece, event): + if self.selected_piece != null: + # deselect + var p = self.selected_piece + if p.is_in_group("player"): + p.get_node("Body").set_modulate(Color(1, 1, 1, 1)) + else: + p.get_node("Body").set_modulate(Color(0.8, .01, .01, 1)) + var moves = get_valid_piece_moves(p) + clear_square_hilights_for_moves(moves) + if self.selected_piece == piece: + self.selected_piece = null + return + if piece == null: + self.selected_piece = null + return + print("Selected piece: ", piece) + if piece.is_in_group("player"): + piece.get_node("Body").set_modulate(Color(0, 1, 0, 1)) + else: + piece.get_node("Body").set_modulate(Color(1.0, 0, 0, 1)) + self.selected_piece = piece + var square = square_of_piece(piece) + var moves = get_valid_piece_moves(piece) + set_square_hilights_for_moves(moves) + +func clear_square_hilights_for_moves(moves): + var pf = get_node("/root/Game/MarginContainer/Playfield") + for m in moves: + if pf.squares.has(m["pos"]): + var edge = pf.squares[m["pos"]].get_ref().get_node("Body") + edge.set_modulate(Color(1, 1, 1, 1.0)) + +func set_square_hilights_for_moves(moves): + var pf = get_node("/root/Game/MarginContainer/Playfield") + for m in moves: + if pf.squares.has(m["pos"]): + var edge = pf.squares[m["pos"]].get_ref().get_node("Body") + if not m["attack"]: + edge.set_modulate(Color(0, 1, 0, 1)) + else: + edge.set_modulate(Color(1, 0, 0, 1)) + +static func position_in_playfield(x, y, width, height): + return x >= 0 and x < width and y >= 0 and y < height + +func square_of_piece(piece): + for k in self.board_squares.keys(): + if self.board_squares[k]["piece"] == piece: + return self.board_squares[k] + return null func set_piece_position(piece, position, destroy = false): - var idx = pos_to_index(position) - if self.pieces.has(idx): + var square = self.board_squares[position] + if square["piece"] != null: if destroy: - if self.pieces[idx].get_ref(): - self.pieces[idx].get_ref().queue_free() + square["piece"].queue_free() + square["piece"] = null else: - print("Warning, piece collision dur set_piece_position at ", position) + print("Warning, piece collision during set_piece_position at ", position) return false - self.pieces[idx] = weakref(piece) + square["piece"] = piece return true # Called when the node enters the scene tree for the first time. @@ -90,6 +185,19 @@ func reset_game_state(): for p in get_tree().get_nodes_in_group("pieces"): p.queue_free() + self.board_squares = {} + var x = 0; + var y = 0; + while x < self.width: + y = 0 + while y < self.height: + self.board_squares[Vector2(x, y)] = { + "x": x, + "y": y, + "piece": null + } + y += 1 + x += 1 # Create starting pieces # 8 pawns per side, starting at col 0 + (width-8)/2 # on rows 2, height - 2 @@ -98,7 +206,7 @@ func reset_game_state(): var y_player = self.height - 2 var i = 0 while i < 8: - var x = start_x + i + x = start_x + i new_piece("pawn", "player", Vector2(x, y_player)) new_piece("pawn", "opponent", Vector2(x, y_opponent)) i += 1 @@ -122,3 +230,51 @@ func reset_game_state(): # Called every frame. 'delta' is the elapsed time since the previous frame. #func _process(delta): # pass + +func _physics_process(delta): + if self.landing_piece != null: + var piece = self.landing_piece + var square = square_of_piece(piece) + var moves = get_valid_piece_moves(piece) + # Try Land the piece + var matches = get_world_2d().direct_space_state.intersect_point( + self.landing_pos, 32, [], 2147483647, true, true) + var s = null + for m in matches: + if m["collider"].get_parent() is Square: + print(m["collider"].get_parent()) + s = m["collider"].get_parent() + if s == null: + # No valid position, we'll return the piece to where it should be + piece.set_position(Vector2( + square["x"] * 128, square["y"] * 128 + )) + else: + var pf = get_node("/root/Game/MarginContainer/Playfield") + var dest = null + for k in pf.squares.keys(): + if pf.squares[k].get_ref() == s: + dest = k + if dest != null: + print(dest) + var dest_valid = false; + for m in moves: + if m['pos'] == dest: + dest_valid = true + if dest_valid: + var dest_square = self.board_squares[dest] + if dest_square['piece'] != null: + # @TODO If the target doesn't die, we need to bounce back + dest_square['piece'].queue_free() + square['piece'] = null + dest_square['piece'] = piece + piece.set_position(Vector2(dest_square['x']*128, dest_square['y']*128)) + piece.at_spawn = false + else: + # invalid destination bounce back + piece.set_position(Vector2(square['x']*128, square['y']*128)) + else: + piece.set_position(Vector2(square['x']*128, square['y']*128)) + + self.landing_piece = null + diff --git a/src/Game.tscn b/src/Game.tscn index 52c5572..b2c4388 100644 --- a/src/Game.tscn +++ b/src/Game.tscn @@ -24,3 +24,48 @@ size_flags_horizontal = 2 size_flags_vertical = 2 [node name="Playfield" parent="MarginContainer" instance=ExtResource( 2 )] + +[node name="TopBar" type="VBoxContainer" parent="."] +margin_left = 256.0 +margin_right = 1024.0 +margin_bottom = 128.0 + +[node name="Top" type="CenterContainer" parent="TopBar"] +margin_right = 768.0 +margin_bottom = 64.0 +rect_min_size = Vector2( 0, 64 ) + +[node name="HBoxContainer" type="HBoxContainer" parent="TopBar/Top"] +margin_left = 364.0 +margin_top = 25.0 +margin_right = 404.0 +margin_bottom = 39.0 + +[node name="TurnLabel" type="Label" parent="TopBar/Top/HBoxContainer"] +margin_right = 28.0 +margin_bottom = 14.0 +text = "Turn" + +[node name="Turn" type="Label" parent="TopBar/Top/HBoxContainer"] +margin_left = 32.0 +margin_right = 40.0 +margin_bottom = 14.0 +text = "0" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Bottom" type="CenterContainer" parent="TopBar"] +margin_top = 68.0 +margin_right = 768.0 +margin_bottom = 132.0 +rect_min_size = Vector2( 0, 64 ) + +[node name="Instruction" type="Label" parent="TopBar/Bottom"] +margin_left = 344.0 +margin_right = 424.0 +margin_bottom = 64.0 +rect_min_size = Vector2( 0, 64 ) +text = "Do a thing" +valign = 1 +uppercase = true diff --git a/src/Piece.gd b/src/Piece.gd index e640ef2..5085479 100644 --- a/src/Piece.gd +++ b/src/Piece.gd @@ -1,23 +1,95 @@ -extends Node2D - +extends Area2D +class_name Piece # Declare member variables here. Examples: # var a = 2 # var b = "text" +signal hold_start(piece, event) +signal hold_stop(piece, event) +signal click(piece, event) + # Each piece should belong to a group - player or opponent # var health = 1 var damage = 1 +var speed = 8 +var jump = false var abilities = [] var kills = 0 var at_spawn = true +const CLICK_THRESHOLD = 0.15 # seconds +var last_click = null +var hold_started = false # Called when the node enters the scene tree for the first time. func _ready(): - pass # Replace with function body. + pass +func get_possible_moves(position): # implemented by our children + return [] +func _input(event): + if event is InputEventMouseButton and event.button_index == 1: + var pos = self.to_local(event.position) + if (pos.x <= 64.0 and pos.x >= -64.0 and pos.y <= 64.0 and pos.y >= -64.0): + # inside our collision area + if event.pressed: + if self.last_click == null: + self.last_click = 0 + get_tree().set_input_as_handled() + else: + if self.last_click != null and self.last_click <= CLICK_THRESHOLD: + print("Click: ", self, event) + emit_signal("click", self, event) + # Work-around bug where only the last signal is connected + # to the Game + #var game = get_tree().get_root().get_node("/root/Game") + #if game: + # print("Self: ", self) + # game._on_piece_click(self, event) + self.last_click = null + get_tree().set_input_as_handled() + if not event.pressed and self.hold_started: + emit_signal("hold_stop", self, event) + print("Hold stop", self, " ", event) + self.hold_started = false + self.last_click = null + get_tree().set_input_as_handled() + # Called every frame. 'delta' is the elapsed time since the previous frame. -#func _process(delta): -# pass +func _process(delta): + if last_click != null: + self.last_click += delta + if not self.hold_started and self.last_click >= self.CLICK_THRESHOLD: + self.hold_started = true; + print("Hold start", self, " ", null) + emit_signal("hold_start", self, null) + if self.hold_started: + self.set_global_position(get_viewport().get_mouse_position()) + + + +#func _on_Piece_input_event(viewport, event, shape_idx): +# print("piece input event: ", event) +# if event is InputEventMouseButton and event.button_index == 1: +# if event.pressed: +# if self.last_click == null: +# set_process_input(true) +# self.last_click = 0 +# else: +# if self.last_click <= CLICK_THRESHOLD: +# emit_signal("click", self, event) +# else: +# emit_signal("hold_stop", self, event) +# self.hold_started = false +# self.last_click = null + +#static func get_move_dict() { +# var x = { +# "attack": false, +# "pos": null, +# "attack_only": false, +# } +# return x +#} diff --git a/src/Playfield.gd b/src/Playfield.gd index fff1a35..527af5f 100644 --- a/src/Playfield.gd +++ b/src/Playfield.gd @@ -8,15 +8,15 @@ extends Node2D var height var width # holds weakrefs to the square nodes -var squares = [] +var squares = {} func initialize(width: int = 8, height: int = 8): # Remove any Squares beneath us - for i in self.squares: + for i in self.squares.values(): if i.get_ref(): i.get_ref().queue_free() - self.squares = [] + self.squares = {} self.height = height self.width = width @@ -28,7 +28,7 @@ func initialize(width: int = 8, height: int = 8): var instance = ResourceLoader.load("res://src/Square.tscn").instance() # @TODO any tweaks to the node by calling custom function initialize() instance.translate(Vector2(128*i, 128*j)) - self.squares.append(weakref(instance)) + self.squares[Vector2(i, j)] = weakref(instance) add_child(instance) j += 1 i += 1 diff --git a/src/Square.gd b/src/Square.gd new file mode 100644 index 0000000..0d8842a --- /dev/null +++ b/src/Square.gd @@ -0,0 +1,16 @@ +extends Node2D +class_name Square + +# Declare member variables here. Examples: +# var a = 2 +# var b = "text" + + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +#func _process(delta): +# pass diff --git a/src/Square.tscn b/src/Square.tscn index c6d8f36..694fe04 100644 --- a/src/Square.tscn +++ b/src/Square.tscn @@ -1,13 +1,24 @@ -[gd_scene load_steps=3 format=2] +[gd_scene load_steps=5 format=2] [ext_resource path="res://assets/export/square_edge.png" type="Texture" id=1] [ext_resource path="res://assets/export/square_center_0.png" type="Texture" id=2] +[ext_resource path="res://src/Square.gd" type="Script" id=3] + +[sub_resource type="RectangleShape2D" id=1] +extents = Vector2( 60, 60 ) [node name="Square" type="Node2D"] z_index = -1 +script = ExtResource( 3 ) [node name="Body" type="Sprite" parent="."] texture = ExtResource( 2 ) [node name="Edge" type="Sprite" parent="."] +modulate = Color( 0.12549, 0.0980392, 0.0980392, 1 ) texture = ExtResource( 1 ) + +[node name="Area2D" type="Area2D" parent="."] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"] +shape = SubResource( 1 ) diff --git a/src/flash_color.shader b/src/flash_color.shader new file mode 100644 index 0000000..ebbcd6d --- /dev/null +++ b/src/flash_color.shader @@ -0,0 +1,30 @@ +shader_type canvas_item; + +uniform vec4 min_color: hint_color; +uniform vec4 max_color: hint_color; +uniform vec4 offset_color: hint_color; +uniform float frequency : hint_range(0.0, 2.0) = 0.5; +uniform bool enable: hint_bool = false; + +void fragment() { + if (enable) { + vec4 sin_anim = vec4( + min_color.r * sin(2.0*3.14*frequency*TIME), + min_color.g * sin(2.0*3.14*frequency*TIME), + min_color.b * sin(2.0*3.14*frequency*TIME), + 1.0 + ); + vec4 cos_anim = vec4( + max_color.r * cos(1.0*3.14*frequency*TIME), + max_color.g * cos(1.0*3.14*frequency*TIME), + max_color.b * cos(1.0*3.14*frequency*TIME), + 1.0 + ); + vec4 color = texture(TEXTURE, UV); + color = mix(color, sin_anim, color.a); + COLOR = mix(color, cos_anim, color.a); + } + else { + COLOR = texture(TEXTURE, UV); + } +} \ No newline at end of file diff --git a/src/pieces/Pawn.gd b/src/pieces/Pawn.gd new file mode 100644 index 0000000..85c4b74 --- /dev/null +++ b/src/pieces/Pawn.gd @@ -0,0 +1,54 @@ +extends Piece + + +# Declare member variables here. Examples: +# var a = 2 +# var b = "text" + + +# Called when the node enters the scene tree for the first time. +func _ready(): + self.speed = 1 + +func get_possible_moves(position): # implemented by our children + # Pawns only move "forward" with respect to their player + var directions = [] + var is_player = false + var forward = Vector2(0, 1) + var attacks = [Vector2(1, 1), Vector2(-1, 1)] + if self.is_in_group("player"): + forward = Vector2(0, -1) + attacks = [Vector2(1, -1), Vector2(-1, -1)] + is_player = true + + var options = [] + print(self, " at current position: ", position) + print("Is player piece: ", is_player, " and is going in direction: ", forward) + var i = 1; + while i < self.speed+1: + options.append({ + "pos": Vector2(position.x, position.y + i*forward.y), + "attack": false, + "attack_only": false, + }) + i += 1 + if self.at_spawn: + options.append({ + "pos": Vector2(position.x, position.y + (self.speed+1)*forward.y), + "attack": false, + "attack_only": false, + }) + for d in attacks: + i = 1 + while i < self.speed+1: + options.append({ + "pos": Vector2(position.x + i*d.x, position.y + i*d.y), + "attack": true, + "attack_only": true, + }) + i += 1 + return options + +# Called every frame. 'delta' is the elapsed time since the previous frame. +#func _process(delta): +# pass diff --git a/src/pieces/Pawn.tscn b/src/pieces/Pawn.tscn index 0541308..f6add37 100644 --- a/src/pieces/Pawn.tscn +++ b/src/pieces/Pawn.tscn @@ -1,9 +1,25 @@ -[gd_scene load_steps=3 format=2] +[gd_scene load_steps=6 format=2] -[ext_resource path="res://src/pieces/Piece.tscn" type="PackedScene" id=1] +[ext_resource path="res://src/hilight.shader" type="Shader" id=1] +[ext_resource path="res://src/pieces/Pawn.gd" type="Script" id=2] [ext_resource path="res://assets/export/pawn.png" type="Texture" id=3] -[node name="Piece" instance=ExtResource( 1 )] +[sub_resource type="ShaderMaterial" id=1] +shader = ExtResource( 1 ) +shader_param/width = 2.0 +shader_param/color = null -[node name="Body" parent="." index="0"] +[sub_resource type="RectangleShape2D" id=2] +extents = Vector2( 128, 128 ) + +[node name="Piece" type="Area2D"] +script = ExtResource( 2 ) + +[node name="Body" type="Sprite" parent="."] +material = SubResource( 1 ) texture = ExtResource( 3 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +modulate = Color( 0.345098, 0.980392, 0.0862745, 1 ) +shape = SubResource( 2 ) +[connection signal="input_event" from="." to="." method="_on_Piece_input_event"] diff --git a/src/pieces/Piece.gd b/src/pieces/Piece.gd deleted file mode 100644 index fba87df..0000000 --- a/src/pieces/Piece.gd +++ /dev/null @@ -1,39 +0,0 @@ -extends Sprite - - -# Declare member variables here. Examples: -# var a = 2 -# var b = "text" - -const types = { - "pawn": "res://src/pieces/Pawn.tscn", - "rook": "res://src/pieces/Rook.tscn", - "bishop": "res://src/pieces/Bishop.tscn", - "knight": "res://src/pieces/Knight.tscn", - "king": "res://src/pieces/King.tscn", - "queen": "res://src/pieces/Queen.tscn", -} - -# Each piece should belong to a group - player or opponent -# -var health = 1 -var damage = 1 -var abilities = [] -var kills = 0 - -static func new_piece(piece_type): - if not types.has(piece_type): - return null - return ResourceLoader.load(types[piece_type]).instance() - #piece.apply_scale(Vector2(pf.scale, pf.scale)) - #piece.add_to_group(group) - #return piece - -# Called when the node enters the scene tree for the first time. -func _ready(): - pass # Replace with function body. - - -# Called every frame. 'delta' is the elapsed time since the previous frame. -#func _process(delta): -# pass diff --git a/src/pieces/Piece.tscn b/src/pieces/Piece.tscn index 54aaabe..38bd6f8 100644 --- a/src/pieces/Piece.tscn +++ b/src/pieces/Piece.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=4 format=2] +[gd_scene load_steps=5 format=2] [ext_resource path="res://src/Piece.gd" type="Script" id=1] [ext_resource path="res://src/hilight.shader" type="Shader" id=2] @@ -8,8 +8,15 @@ shader = ExtResource( 2 ) shader_param/width = 2.0 shader_param/color = null -[node name="Piece" type="Node2D"] +[sub_resource type="RectangleShape2D" id=2] +extents = Vector2( 128, 128 ) + +[node name="Piece" type="Area2D"] script = ExtResource( 1 ) [node name="Body" type="Sprite" parent="."] material = SubResource( 1 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource( 2 ) +[connection signal="input_event" from="." to="." method="_on_Piece_input_event"]