feat: implemented conclusion selection UI

This commit is contained in:
Sara Gerretsen 2025-11-02 15:07:46 +01:00
parent 5e477d57ff
commit c765f7daf9
12 changed files with 241 additions and 40 deletions

View file

@ -9,6 +9,7 @@ size = Vector2(166, 172)
[node name="PinnedPhoto" type="PinnedPhoto"]
input_pickable = true
sync_to_physics = false
[node name="MeshInstance2D" type="MeshInstance2D" parent="."]
position = Vector2(0, 73.49998)

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=13 format=3 uid="uid://o3ri154wpbrx"]
[gd_scene load_steps=15 format=3 uid="uid://o3ri154wpbrx"]
[ext_resource type="PackedScene" uid="uid://btcmnw6q6g0h0" path="res://objects/pinned_photo.tscn" id="1_7cefc"]
[ext_resource type="PackedScene" uid="uid://qmb60kjx6yoe" path="res://objects/victim_file.tscn" id="2_vo7lu"]
@ -17,14 +17,41 @@ gradient = SubResource("Gradient_usqe2")
[sub_resource type="RectangleShape2D" id="RectangleShape2D_87mh6"]
size = Vector2(965, 533)
[sub_resource type="RectangleShape2D" id="RectangleShape2D_7cefc"]
size = Vector2(400, 64)
[sub_resource type="QuadMesh" id="QuadMesh_cnvne"]
[sub_resource type="QuadMesh" id="QuadMesh_thvsl"]
[sub_resource type="CircleShape2D" id="CircleShape2D_cnvne"]
radius = 7.280088
[sub_resource type="ImageTexture" id="ImageTexture_cnvne"]
[sub_resource type="GDScript" id="GDScript_cnvne"]
script/source = "extends Button
func _ready() -> void:
update_can_send()
func _pressed() -> void:
if ServerNode.get_singleton():
ServerNode.get_singleton().send_conclusion(%WeaponSlot.get_current_clue(), %MotiveSlot.get_current_clue(), %MurdererSlot.get_current_clue())
else:
print(\"Conclusion: \", %WeaponSlot.get_current_clue(), \", \", %MotiveSlot.get_current_clue(), \", \", %MurdererSlot.get_current_clue())
func update_can_send():
self.disabled = not (%WeaponSlot.is_filled() and %MotiveSlot.is_filled() and %MurdererSlot.is_filled())
func _on_any_slot_selection_changed(_new_value: int) -> void:
update_can_send()
"
[sub_resource type="TextMesh" id="TextMesh_cnvne"]
flip_faces = true
text = "Method, Motive, Murderer"
depth = 0.0
[sub_resource type="Gradient" id="Gradient_thvsl"]
offsets = PackedFloat32Array(0)
colors = PackedColorArray(0, 0, 0, 1)
[sub_resource type="GradientTexture1D" id="GradientTexture1D_c1nar"]
gradient = SubResource("Gradient_thvsl")
[node name="FlatscreenRoot" type="Node2D"]
@ -32,7 +59,7 @@ size = Vector2(400, 64)
anchor_mode = 0
[node name="Pinboard" type="Pinboard" parent="."]
position = Vector2(312, 19)
position = Vector2(109, 19)
[node name="MeshInstance2D" type="MeshInstance2D" parent="Pinboard"]
position = Vector2(794.49994, 417)
@ -44,9 +71,62 @@ texture = SubResource("GradientTexture1D_87mh6")
position = Vector2(636.5, 465.5)
shape = SubResource("RectangleShape2D_87mh6")
[node name="CollisionShape2D2" type="CollisionShape2D" parent="Pinboard"]
position = Vector2(1340, 701)
shape = SubResource("RectangleShape2D_7cefc")
[node name="Handin" type="Node2D" parent="."]
position = Vector2(1899.9999, -15)
rotation = -0.042017065
[node name="MeshInstance2D" type="MeshInstance2D" parent="Handin"]
position = Vector2(-276.4963, 370.27814)
scale = Vector2(553.9857, 740.51447)
mesh = SubResource("QuadMesh_cnvne")
[node name="TextEdit" type="TextEdit" parent="Handin"]
offset_left = -508.21082
offset_top = 553.0962
offset_right = -24.305176
offset_bottom = 714.85803
backspace_deletes_composite_character_enabled = true
wrap_mode = 1
[node name="WeaponSlot" type="ConclusionField" parent="Handin"]
unique_name_in_owner = true
position = Vector2(-448.5945, 290.4132)
[node name="CollisionShape2D" type="CollisionShape2D" parent="Handin/WeaponSlot"]
position = Vector2(0, 43.237)
shape = SubResource("CircleShape2D_cnvne")
[node name="MotiveSlot" type="ConclusionField" parent="Handin"]
unique_name_in_owner = true
position = Vector2(-268.54337, 292.97845)
[node name="CollisionShape2D" type="CollisionShape2D" parent="Handin/MotiveSlot"]
position = Vector2(0, 43.237)
shape = SubResource("CircleShape2D_cnvne")
[node name="MurdererSlot" type="ConclusionField" parent="Handin"]
unique_name_in_owner = true
position = Vector2(-99.188446, 288.08786)
[node name="CollisionShape2D" type="CollisionShape2D" parent="Handin/MurdererSlot"]
position = Vector2(0, 43.237)
shape = SubResource("CircleShape2D_cnvne")
[node name="SubmitButton" type="Button" parent="Handin"]
unique_name_in_owner = true
offset_left = -307.37988
offset_top = 502.53214
offset_right = -230.37988
offset_bottom = 533.5321
text = "Submit"
script = SubResource("GDScript_cnvne")
[node name="MeshInstance2D2" type="MeshInstance2D" parent="Handin"]
position = Vector2(-273.59598, 222.70421)
rotation = 3.1415927
scale = Vector2(-236.09602, 249.81453)
mesh = SubResource("TextMesh_cnvne")
texture = SubResource("GradientTexture1D_c1nar")
[node name="PhotosParent" type="Node2D" parent="."]
@ -76,25 +156,6 @@ open_position = NodePath("../FilePositionTarget")
position = Vector2(678.00006, 63.999996)
rotation = -0.0034519732
[node name="Handin" type="Node2D" parent="."]
position = Vector2(1544.9998, 15)
rotation = -0.042017065
[node name="MeshInstance2D" type="MeshInstance2D" parent="Handin"]
position = Vector2(169.87817, 238.88794)
scale = Vector2(390.23935, 519.97754)
mesh = SubResource("QuadMesh_cnvne")
[node name="TextEdit" type="TextEdit" parent="Handin"]
offset_left = 3.0
offset_top = 344.75198
offset_right = 342.7721
offset_bottom = 485.0
backspace_deletes_composite_character_enabled = true
wrap_mode = 1
[node name="MeshInstance2D2" type="MeshInstance2D" parent="Handin"]
position = Vector2(296.84607, 57.51972)
scale = Vector2(97.55189, 112.9032)
mesh = SubResource("QuadMesh_thvsl")
texture = SubResource("ImageTexture_cnvne")
[connection signal="selection_changed" from="Handin/WeaponSlot" to="Handin/SubmitButton" method="_on_any_slot_selection_changed"]
[connection signal="selection_changed" from="Handin/MotiveSlot" to="Handin/SubmitButton" method="_on_any_slot_selection_changed"]
[connection signal="selection_changed" from="Handin/MurdererSlot" to="Handin/SubmitButton" method="_on_any_slot_selection_changed"]

View file

@ -0,0 +1,66 @@
#include "conclusion_field.h"
#include "core/config/engine.h"
#include "you_done_it/pinned_photo.h"
#include "you_done_it/ydi_networking.h"
String const ConclusionField::sig_selection_changed{ "selection_changed" };
void ConclusionField::_bind_methods() {
ADD_SIGNAL(MethodInfo(sig_selection_changed, PropertyInfo(Variant::INT, "new_value", PROPERTY_HINT_ENUM, NetworkData::ClueID_hint())));
ClassDB::bind_method(D_METHOD("is_filled"), &self_type::is_filled);
ClassDB::bind_method(D_METHOD("get_current_clue"), &self_type::get_current_clue);
}
void ConclusionField::enter_tree() {
connect("body_entered", callable_mp(this, &self_type::on_body_entered));
connect("body_exited", callable_mp(this, &self_type::on_body_exited));
}
void ConclusionField::on_body_entered(Node2D *node) {
if (PinnedPhoto * photo{ cast_to<PinnedPhoto>(node) }) {
photo->push_conclusion_slot(this);
}
}
void ConclusionField::on_body_exited(Node2D *node) {
if (PinnedPhoto * photo{ cast_to<PinnedPhoto>(node) }) {
photo->pull_conclusion_slot(this);
}
}
void ConclusionField::_notification(int what) {
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (what) {
default:
return;
case NOTIFICATION_ENTER_TREE:
enter_tree();
return;
}
}
void ConclusionField::notify_photo_dropped(PinnedPhoto *photo) {
this->photo = photo;
emit_signal(sig_selection_changed, this->photo->get_clue());
}
void ConclusionField::notify_photo_picked(PinnedPhoto *photo) {
if (photo == this->photo) {
this->photo = nullptr;
emit_signal(sig_selection_changed, NetworkData::CLUE_MAX);
}
}
bool ConclusionField::is_filled() const {
return this->photo;
}
NetworkData::ClueID ConclusionField::get_current_clue() const {
if (!this->photo) {
return NetworkData::CLUE_MAX;
} else {
return photo->get_clue();
}
}

View file

@ -0,0 +1,26 @@
#pragma once
#include "scene/2d/physics/area_2d.h"
#include "you_done_it/ydi_networking.h"
class PinnedPhoto;
class ConclusionField : public Area2D {
GDCLASS(ConclusionField, Area2D);
static void _bind_methods();
void enter_tree();
void on_body_entered(Node2D *body);
void on_body_exited(Node2D *body);
protected:
void _notification(int what);
public:
void notify_photo_dropped(PinnedPhoto *photo);
void notify_photo_picked(PinnedPhoto *photo);
bool is_filled() const;
NetworkData::ClueID get_current_clue() const;
private:
PinnedPhoto *photo{ nullptr };
static String const sig_selection_changed;
};

View file

@ -4,6 +4,7 @@
#include "macros.h"
#include "scene/2d/mesh_instance_2d.h"
#include "you_done_it/clue_db.h"
#include "you_done_it/conclusion_field.h"
void PinnedPhoto::_bind_methods() {
BIND_PROPERTY(Variant::BOOL, can_drop);
@ -29,20 +30,31 @@ void PinnedPhoto::process(double delta) {
void PinnedPhoto::on_input_event(Viewport *viewport, Ref<InputEvent> event, int shape) {
Ref<InputEventMouseButton> button{ event };
if (button.is_valid() && button->is_pressed()) {
if (!this->is_held && button.is_valid() && button->is_pressed()) {
this->is_held = true;
viewport->set_input_as_handled();
if (this->conclusion_field) {
this->conclusion_field->notify_photo_picked(this);
this->conclusion_field = nullptr;
}
}
}
void PinnedPhoto::unhandled_input(Ref<InputEvent> const &event) {
if (!this->is_held) {
return;
}
Ref<InputEventMouseButton> button{ event };
if (button.is_valid() && this->can_drop && button->is_released()) {
this->is_held = false;
get_viewport()->set_input_as_handled();
if (this->is_held && button.is_valid() && button->is_released()) {
if (!this->near_fields.is_empty()) {
this->is_held = false;
this->conclusion_field = this->near_fields[this->near_fields.size() - 1];
this->conclusion_field->notify_photo_dropped(this);
get_viewport()->set_input_as_handled();
set_global_position(this->conclusion_field->get_global_position());
set_global_rotation(this->conclusion_field->get_global_rotation());
} else if (this->can_drop) {
this->is_held = false;
get_viewport()->set_input_as_handled();
set_global_rotation(0.0);
}
}
}
@ -65,6 +77,14 @@ void PinnedPhoto::_notification(int what) {
}
}
void PinnedPhoto::push_conclusion_slot(ConclusionField *field) {
this->near_fields.push_back(field);
}
void PinnedPhoto::pull_conclusion_slot(ConclusionField *field) {
this->near_fields.erase(field);
}
void PinnedPhoto::set_can_drop(bool value) {
this->can_drop = value;
}

View file

@ -4,7 +4,8 @@
#include "scene/2d/mesh_instance_2d.h"
#include "scene/2d/physics/animatable_body_2d.h"
#include "scene/main/viewport.h"
#include "ydi_networking.h"
#include "you_done_it/ydi_networking.h"
class ConclusionField;
class PinnedPhoto : public AnimatableBody2D {
GDCLASS(PinnedPhoto, AnimatableBody2D);
@ -19,6 +20,8 @@ protected:
void _notification(int what);
public:
void push_conclusion_slot(ConclusionField *field);
void pull_conclusion_slot(ConclusionField *field);
void set_can_drop(bool value);
bool get_can_drop() const;
void set_clue(NetworkData::ClueID id);
@ -29,4 +32,6 @@ private:
NetworkData::ClueID clue{ NetworkData::CLUE_MAX };
bool can_drop{ false };
bool is_held{ false };
ConclusionField *conclusion_field{};
Vector<ConclusionField *> near_fields{};
};

View file

@ -5,6 +5,7 @@
#include "you_done_it/clue_data.h"
#include "you_done_it/clue_finder.h"
#include "you_done_it/clue_marker.h"
#include "you_done_it/conclusion_field.h"
#include "you_done_it/file_popup.h"
#include "you_done_it/photo_inbox.h"
#include "you_done_it/pinboard.h"
@ -29,6 +30,7 @@ void initialize_you_done_it_module(ModuleInitializationLevel p_level) {
ClassDB::register_class<PinnedPhoto>();
ClassDB::register_class<PhotoInbox>();
ClassDB::register_class<FilePopup>();
ClassDB::register_class<ConclusionField>();
}
void uninitialize_you_done_it_module(ModuleInitializationLevel p_level) {

View file

@ -15,6 +15,7 @@ void ServerNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("open"), &self_type::open);
ClassDB::bind_method(D_METHOD("close"), &self_type::close);
ClassDB::bind_method(D_METHOD("is_open"), &self_type::is_open);
ClassDB::bind_method(D_METHOD("send_conclusion", "method", "motive", "murderer"), &self_type::send_conclusion);
ClassDB::bind_static_method(self_type::get_class_static(), "get_singleton", &self_type::get_singleton);
}
@ -75,6 +76,10 @@ ServerNode *ServerNode::get_singleton() {
return singleton_instance;
}
void ServerNode::send_conclusion(NetworkData::ClueID method, NetworkData::ClueID motive, NetworkData::ClueID murderer) {
ydi::server::send::conclusion(method, motive, murderer);
}
bool ServerNode::open() {
return ydi::server::open();
}

View file

@ -1,6 +1,7 @@
#pragma once
#include "scene/main/node.h"
#include "you_done_it/ydi_networking.h"
class ServerNode : public Node {
GDCLASS(ServerNode, Node);
@ -15,6 +16,7 @@ protected:
public:
static ServerNode *get_singleton();
void send_conclusion(NetworkData::ClueID method, NetworkData::ClueID motive, NetworkData::ClueID murderer);
bool open();
void close();
bool is_open() const;

View file

@ -23,6 +23,7 @@ public:
MSG_BEAT,
// gameplay messages
MSG_REVEAL,
MSG_CONCLUSION,
// end of messages
MSG_INVALID);

View file

@ -196,4 +196,13 @@ bool new_clues(Vector<NetworkData::ClueID> &out) {
return has_new;
}
} //namespace receive
namespace send {
void conclusion(NetworkData::ClueID method, NetworkData::ClueID motive, NetworkData::ClueID murderer) {
if (!service) {
return;
}
std::scoped_lock lock{ service->mtx };
multipart(*service->client, NetworkData::MSG_CONCLUSION, method, motive, murderer).send(*service->socket);
}
} //namespace send
} //namespace ydi::server

View file

@ -13,4 +13,7 @@ bool is_running();
namespace receive {
bool new_clues(Vector<NetworkData::ClueID> &out);
} //namespace receive
namespace send {
void conclusion(NetworkData::ClueID method, NetworkData::ClueID motive, NetworkData::ClueID murderer);
}
} //namespace ydi::server