Compare commits

...

30 Commits

Author SHA1 Message Date
bfc5ee6ac9
Modify the texture.
修改纹理。
2024-10-06 09:47:36 +08:00
4a4d4a0c37
Fixed an issue where AI could not pick up weapons.
修复AI不能捡起武器的问题。
2024-10-05 20:56:50 +08:00
4e83bca579
Ignore the Android build directory.
忽略安卓的编译目录。
2024-10-05 20:02:14 +08:00
6d9b9ca7ec
Improved the loading range of spells.
改进法术的加载范围。
2024-10-05 17:15:19 +08:00
3f96519ac0
Add the triple shot spell.
加入三重射击法术。
2024-10-05 16:19:31 +08:00
407798b13b
Recoil is calculated only after successful firing.
仅在成功开火后计算后座力。
2024-10-05 10:53:07 +08:00
e718d78137
Fixed item ID and its container being lost when throwing items. Pick up the weapon again can not fire the bullet issue.
修复扔出物品时,物品的ID与自身的物品容器丢失。再次捡起武器无法发射子弹的问题。
2024-10-05 10:20:55 +08:00
b7805c5804
Fixed array subscript out of bounds issue.
修复数组下标越界的问题。
2024-10-05 09:52:53 +08:00
839d60b62a
Allows players to reload projectiles on their weapons.
允许玩家对武器装填抛射体了。
2024-10-05 09:13:14 +08:00
8a7a9bcf5d
Now supports spawning spell items.
支持生成法术物品了。
2024-10-04 20:53:34 +08:00
124c6ce0ba
The UI will be automatically closed when the player leaves the range of the furniture. The spell editor is now able to load information about items placed in it.
玩家离开家具的操作范围时会自动关闭UI了。法术编辑器现在能够加载放入的物品信息。
2024-10-04 16:13:34 +08:00
2d7985010d
Add item types. Item containers now support limiting added item types. Added spell classes for projectile weapons.
加入物品类型,物品容器支持限制添加的物品类型了。加入适用于抛射体武器的法术类。
2024-10-04 10:21:09 +08:00
bb0f582fed
Fixed an issue where the spell Editor could not remove items placed in the second time.
修复法术编辑器二次放入物品无法取出的问题。
2024-10-01 20:53:56 +08:00
81344faa3e
Add animation to spell editor.
加入法术编辑器的动画。
2024-10-01 15:01:34 +08:00
77f2bac36e
Adds UI to spell editor.
加入法术编辑器的UI。
2024-09-30 22:13:51 +08:00
67706ca9d7
Ignore the fleet directory.
忽略fleet目录。
2024-09-30 09:10:34 +08:00
bfec07a0de
Fixed When switching items, players would have multiple items stacked on their hands.
修复切换物品时,玩家手上会叠加多个物品。
2024-09-30 08:56:00 +08:00
c1c3fce58a
Allows players to drag items onto the item container for quick storage.
允许玩家将物品拖到物品容器上以便快速入库。
2024-09-29 23:30:42 +08:00
2d92a92faf
Backpacks are no longer allowed in backpacks.
不允许背包内放置背包了。
2024-09-29 22:12:54 +08:00
31a1d292d8
Fixed an issue where players could pick up items in the backpack after throwing it.Supports placing items across item containers
修复玩家扔出背包后,再捡起背包内的物品消失的问题。支持跨容器替换物品了。
2024-09-28 22:59:25 +08:00
4f2208bd60
Improving the ability to drag items, there are still some issues.
改进拖动物品的功能,仍然存在一些问题。
2024-09-27 23:24:22 +08:00
617164a4bc
Fixed an issue where the inventory icon was not updated after the player threw an item.
修复玩家扔出物品后,物品栏没有更新图标的问题。
2024-09-27 21:16:00 +08:00
efff63ddd4
Item containers no longer store items consecutively.
物品容器不再按照索引存储物品了。
2024-09-27 20:53:04 +08:00
f881d43c3b
Bind the backpack to the ui.
使背包和ui绑定。
2024-09-26 22:43:10 +08:00
b53f5bc2df
Remove the drag and drop item code.
移除拖拽物品的代码。
2024-09-25 23:01:52 +08:00
b9a9349596
Fixed an issue where two items would be selected at the same time when the player picked up the item and re-selected it.
修复玩家捡起物品后,重新选中物品,会有两个物品同时被选择的问题。
2024-09-25 21:56:54 +08:00
b7c3651462
Use placeholders to resolve the problem that an empty slot cannot be selected in the shortcut bar.
使用占位符来解决快捷栏无法选中空槽位的问题。
2024-09-23 23:17:57 +08:00
63680a9410
Add an event where item data changes.
加入物品数据改变的事件。
2024-09-23 21:03:39 +08:00
234241b74a
Bind the item container to the item container display.
将物品容器和物品容器显示器绑定。
2024-09-22 23:13:59 +08:00
32299877c6
Item containers maintain item collections, not item slots.
物品容器将维护物品集合,而不是物品槽。
2024-09-22 16:51:42 +08:00
53 changed files with 2324 additions and 938 deletions

3
.gitignore vendored
View File

@ -3,6 +3,9 @@
.idea/
.vs/
.vscode/
.fleet/
# Ignore the Android build directory
android/
export_presets.cfg
*.translation
*.user

14
data/itemRegs/magics.yaml Normal file
View File

@ -0,0 +1,14 @@
#Register spells used by the projectile here.
#在这里注册抛射体使用的法术。
#Note: The id must be the same as the item id in the scene. Otherwise, an ArgumentException will be thrown.
#备注id必须和场景内的物品id保持一致。否则会抛出ArgumentException。
#After you declare the id of the item, add the corresponding localized text to the csv file in the locals' folder. For example, if the id is a corresponding name is item_a and the corresponding description is item_a_desc.
#当您声明物品的id后请在locals文件夹中的csv文件中添加相应的本地化文本。例如id为a则对应的名称为item_a对应的描述为item_a_desc。
- id: necromancy
scene_path: res://prefab/magics/curseOfTheUndead.tscn
icon_path: res://sprites/projectile/curseOfTheUndead.png
max_stack_value: 1
- id: x3
scene_path: res://prefab/magics/x3.tscn
icon_path: res://sprites/projectile/x3.png
max_stack_value: 1

View File

@ -3,3 +3,7 @@ item_staff_necromancy,死灵法杖,staffNecromancy,ネクロポリスの杖で
item_staff_necromancy_desc,发射诅咒,可将敌人转化为邪恶的怪物。,Cast a curse that transforms enemies into evil monsters.,呪いを発射して、敵を邪悪な怪物に変えることができます。
item_portable_backpacks,便携式背包,PortableBackpacks,ポータブルバックパック
item_portable_backpacks_desc,为玩家提供9个物品槽。,Provides 9 item slots for the player.,プレイヤーに9つのアイテムスロットを提供します。
item_necromancy,死灵法术,necromancy,ネクロマンシー
item_necromancy_desc,法术的实体化弹丸。,The materialized projectile of a spell.,術の実体化した弾丸です。
item_x3,三重射击法术,Triple shot spell,三重射撃術です
item_x3_desc,使发射器一次射出三颗弹丸。,Make the launcher shoot three pellets at a time.,発射機から一度に三つの弾丸を発射させます。
1 id zh en ja
3 item_staff_necromancy_desc 发射诅咒,可将敌人转化为邪恶的怪物。 Cast a curse that transforms enemies into evil monsters. 呪いを発射して、敵を邪悪な怪物に変えることができます。
4 item_portable_backpacks 便携式背包 PortableBackpacks ポータブルバックパック
5 item_portable_backpacks_desc 为玩家提供9个物品槽。 Provides 9 item slots for the player. プレイヤーに9つのアイテムスロットを提供します。
6 item_necromancy 死灵法术 necromancy ネクロマンシー
7 item_necromancy_desc 法术的实体化弹丸。 The materialized projectile of a spell. 術の実体化した弾丸です。
8 item_x3 三重射击法术 Triple shot spell 三重射撃術です
9 item_x3_desc 使发射器一次射出三颗弹丸。 Make the launcher shoot three pellets at a time. 発射機から一度に三つの弾丸を発射させます。

View File

@ -4,7 +4,6 @@ log_missing_parameters,缺少参数。,Missing parameters.,パラメータが不
log_room_root_node_must_be_node2d,房间根节点必须是 Node2D。,Room root node must be an instance of Node2D.,ルートードはNode2Dでなければなりません。
log_width_or_height_of_room_slot_must_be_1,房间槽的宽度或高度必须为1。,The width or height of the room slot must be 1.,部屋の溝の幅または高さは1でなければなりません。
log_connected_room_timeout,连接房间超时。,Timeout when connecting rooms.,接続部屋はタイムアウトです。
log_projectiles_is_empty,未设置抛射体。,The projectile is not set.,射出体は設置されていません。
log_map_generator_missing_parameters,地图生成器缺少参数。,Map generator missing parameters.,マップジェネレータが不足しています。
log_map_generator_attempts_to_parse_empty_layout_diagrams,地图生成器尝试解析空的布局图。,Map generator attempts to parse empty layout diagrams.,マップジェネレータは空のレイアウト図を解析しようとしています。
log_map_generator_has_no_starting_room_data,地图生成器没有起点房间数据。,Map generator has no starting room data.,マップ生成器に起点部屋データはありません。
@ -51,13 +50,7 @@ log_patrol_to_next_point,下一个点{0},当前位置{1},偏移量{2},距
log_patrol_arrival_point,到达巡逻点{0}。,Arrival at patrol point {0}.,巡回ポイント{0}に到着します。
log_patrol_origin_position,巡逻路径的起始位置是{0}。,The starting position of the patrol path is {0}.,巡回路の開始位置は{0}です。
log_patrol_not_on_floor,不能将初始路径设置在空中。,The initial path cannot be set in the air.,初期パスを空中に設定できません。
log_item_container_is_null,物品容器为空。,Item container is null.,アイテム・コンテナが空です。
log_can_add_item,可以添加物品{0}。,Can add item {0}.,アイテム{0}を追加できます。
log_backpack_not_allowed,不允许添加到背包。,Not allowed to add to backpack.,バックパックに追加することは許可されていません。
log_item_is_null,物品为空。,Item is null.,アイテムが空です。
log_item_id_not_same,物品ID不同。,Item ID is different.,アイテムIDが異なります。
log_max_quantity_exceeded,超过最大数量。,Exceeded maximum quantity.,最大数量を超えました。
log_item_slot_is_selected_and_not_allowed,已选择物品槽,不允许添加。,"Item slot is selected, not allowed to add.",アイテムスロットが選択されており、追加は許可されていません。
log_patrol_enemy_detected,检测到敌人。,Enemy detected.,敵を検出しました。
log_attacker_or_target_is_null,攻击者或目标为空。,Attacker or target is null.,攻撃者またはターゲットが空です。
log_in_the_same_camp,在同一阵营。,In the same camp.,同じ陣営です。
@ -119,3 +112,8 @@ log_show_all_node,显示{0}个节点。,Show {0} nodes.,{0}ノードが表示さ
log_enter_the_screen,进入屏幕。,Enter screen,画面に移動。
log_exit_the_screen,退出屏幕。,Exit screen,画面を終了します。
log_failed_to_create_room_preview,创建{0}的房间预览图失败。,Failed to create a room preview of the {0}.,{0}の部屋のプレビューを作成できませんでした。
log_generated_item_is_empty,生成的物品{0}是空的吗{1}。,Generated item {0} is empty {1}.,生成したアイテム{0}は空ですか{1}。
log_projectile_generate_magic_is_null,没有装填可提供抛射体的法术。,There is no reload spell that provides projectiles.,射出体を提供するスペルを装填していません。
log_projectile_scene_is_null,抛射体场景为空。,Projectile scene is empty.,射出体は空です。
log_projectile_is_null,抛射体为空。,Projectile scene is empty.,射出シーンは空です。
log_projectile_weapon_range,加载法术范围{0}顺序模式吗{1}上次发射法术时采用的索引{2}。,Load spell range {0} Sequential mode {1} Index used when the spell was last fired {2}.,スペル範囲{0}順序モードですか{1}前回スペルを送信した時のインデックス{2}。
1 id zh en ja
4 log_room_root_node_must_be_node2d 房间根节点必须是 Node2D。 Room root node must be an instance of Node2D. ルートノードはNode2Dでなければなりません。
5 log_width_or_height_of_room_slot_must_be_1 房间槽的宽度或高度必须为1。 The width or height of the room slot must be 1. 部屋の溝の幅または高さは1でなければなりません。
6 log_connected_room_timeout 连接房间超时。 Timeout when connecting rooms. 接続部屋はタイムアウトです。
log_projectiles_is_empty 未设置抛射体。 The projectile is not set. 射出体は設置されていません。
7 log_map_generator_missing_parameters 地图生成器缺少参数。 Map generator missing parameters. マップジェネレータが不足しています。
8 log_map_generator_attempts_to_parse_empty_layout_diagrams 地图生成器尝试解析空的布局图。 Map generator attempts to parse empty layout diagrams. マップジェネレータは空のレイアウト図を解析しようとしています。
9 log_map_generator_has_no_starting_room_data 地图生成器没有起点房间数据。 Map generator has no starting room data. マップ生成器に起点部屋データはありません。
50 log_patrol_arrival_point 到达巡逻点{0}。 Arrival at patrol point {0}. 巡回ポイント{0}に到着します。
51 log_patrol_origin_position 巡逻路径的起始位置是{0}。 The starting position of the patrol path is {0}. 巡回路の開始位置は{0}です。
52 log_patrol_not_on_floor 不能将初始路径设置在空中。 The initial path cannot be set in the air. 初期パスを空中に設定できません。
log_item_container_is_null 物品容器为空。 Item container is null. アイテム・コンテナが空です。
53 log_can_add_item 可以添加物品{0}。 Can add item {0}. アイテム{0}を追加できます。
log_backpack_not_allowed 不允许添加到背包。 Not allowed to add to backpack. バックパックに追加することは許可されていません。
log_item_is_null 物品为空。 Item is null. アイテムが空です。
log_item_id_not_same 物品ID不同。 Item ID is different. アイテムIDが異なります。
log_max_quantity_exceeded 超过最大数量。 Exceeded maximum quantity. 最大数量を超えました。
log_item_slot_is_selected_and_not_allowed 已选择物品槽,不允许添加。 Item slot is selected, not allowed to add. アイテムスロットが選択されており、追加は許可されていません。
54 log_patrol_enemy_detected 检测到敌人。 Enemy detected. 敵を検出しました。
55 log_attacker_or_target_is_null 攻击者或目标为空。 Attacker or target is null. 攻撃者またはターゲットが空です。
56 log_in_the_same_camp 在同一阵营。 In the same camp. 同じ陣営です。
112 log_enter_the_screen 进入屏幕。 Enter screen 画面に移動。
113 log_exit_the_screen 退出屏幕。 Exit screen 画面を終了します。
114 log_failed_to_create_room_preview 创建{0}的房间预览图失败。 Failed to create a room preview of the {0}. {0}の部屋のプレビューを作成できませんでした。
115 log_generated_item_is_empty 生成的物品{0}是空的吗{1}。 Generated item {0} is empty {1}. 生成したアイテム{0}は空ですか{1}。
116 log_projectile_generate_magic_is_null 没有装填可提供抛射体的法术。 There is no reload spell that provides projectiles. 射出体を提供するスペルを装填していません。
117 log_projectile_scene_is_null 抛射体场景为空。 Projectile scene is empty. 射出体は空です。
118 log_projectile_is_null 抛射体为空。 Projectile scene is empty. 射出シーンは空です。
119 log_projectile_weapon_range 加载法术范围{0}顺序模式吗{1}上次发射法术时采用的索引{2}。 Load spell range {0} Sequential mode {1} Index used when the spell was last fired {2}. スペル範囲{0}順序モードですか{1}前回スペルを送信した時のインデックス{2}。

View File

@ -4,3 +4,8 @@ slogan_1,Kawaii!,Kawaii!,Kawaii!
slogan_2,魔法是想象的世界。,Magic is an imaginary world.,魔法は想像の世界です。
slogan_3,也试试《Minecraft》,Also try 'Minecraft'!,「Minecraft」もやってみて
slogan_4,也试试《Terraria》,Also try 'Terraria'!,「Terraria」もやってみて
slogan_5,也试试《Terraria》,Also try 'Terraria'!,「Terraria」もやってみて
slogan_6,游戏在bug上运行。,The game runs on a bug.,ゲームはバグで動作します。
slogan_7,こんにちは!,こんにちは!,こんにちは!
slogan_7,你好!,你好!,你好!
slogan_7,Hello,Hello,Hello
1 id zh en ja
4 slogan_2 魔法是想象的世界。 Magic is an imaginary world. 魔法は想像の世界です。
5 slogan_3 也试试《Minecraft》! Also try 'Minecraft'! 「Minecraft」もやってみて!
6 slogan_4 也试试《Terraria》! Also try 'Terraria'! 「Terraria」もやってみて!
7 slogan_5 也试试《Terraria》! Also try 'Terraria'! 「Terraria」もやってみて!
8 slogan_6 游戏在bug上运行。 The game runs on a bug. ゲームはバグで動作します。
9 slogan_7 こんにちは! こんにちは! こんにちは!
10 slogan_7 你好! 你好! 你好!
11 slogan_7 Hello! Hello! Hello!

View File

@ -34,3 +34,4 @@ ui_character_voice,角色配音,character_voice,キャラクターボイスで
ui_translator,翻译,translator,翻訳
ui_unordered_list_tip,排名不分先后,Ranking is not in order,順位は関係ありません
ui_loading,正在加载...,Loading...,読み込み中...
ui_spell_editor,法术编辑器,Spell editor,スペルエディター

1 id zh en ja
34 ui_translator 翻译 translator 翻訳
35 ui_unordered_list_tip 排名不分先后 Ranking is not in order 順位は関係ありません
36 ui_loading 正在加载... Loading... 読み込み中...
37 ui_spell_editor 法术编辑器 Spell editor スペルエディター

View File

@ -1,28 +0,0 @@
[gd_scene load_steps=5 format=3 uid="uid://djsh4unystlf0"]
[ext_resource type="Texture2D" uid="uid://qowlv0viyqbb" path="res://sprites/ui/Null.png" id="1_346je"]
[ext_resource type="Script" path="res://scripts/furniture/GuiFurniture.cs" id="1_t1qdg"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_vx4tg"]
size = Vector2(31, 31)
[sub_resource type="CircleShape2D" id="CircleShape2D_in1f7"]
radius = 52.6118
[node name="RigidBody2D" type="RigidBody2D"]
collision_layer = 256
collision_mask = 160
script = ExtResource("1_t1qdg")
[node name="Null" type="Sprite2D" parent="."]
texture = ExtResource("1_346je")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(-0.5, -0.5)
shape = SubResource("RectangleShape2D_vx4tg")
[node name="OperateArea2D" type="Area2D" parent="."]
visible = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="OperateArea2D"]
shape = SubResource("CircleShape2D_in1f7")

View File

@ -0,0 +1,110 @@
[gd_scene load_steps=16 format=3 uid="uid://djsh4unystlf0"]
[ext_resource type="Script" path="res://scripts/furniture/GuiFurniture.cs" id="1_t1qdg"]
[ext_resource type="Texture2D" uid="uid://cyciw4drjvrs8" path="res://sprites/furnitures/SpellEditor.png" id="2_sbshw"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_vx4tg"]
size = Vector2(31, 31)
[sub_resource type="CircleShape2D" id="CircleShape2D_in1f7"]
radius = 52.6118
[sub_resource type="AtlasTexture" id="AtlasTexture_yq6e3"]
atlas = ExtResource("2_sbshw")
region = Rect2(0, 0, 44, 43)
[sub_resource type="AtlasTexture" id="AtlasTexture_ljyfv"]
atlas = ExtResource("2_sbshw")
region = Rect2(44, 0, 44, 43)
[sub_resource type="AtlasTexture" id="AtlasTexture_46j2i"]
atlas = ExtResource("2_sbshw")
region = Rect2(88, 0, 44, 43)
[sub_resource type="AtlasTexture" id="AtlasTexture_q6a2v"]
atlas = ExtResource("2_sbshw")
region = Rect2(132, 0, 44, 43)
[sub_resource type="AtlasTexture" id="AtlasTexture_lyh2i"]
atlas = ExtResource("2_sbshw")
region = Rect2(0, 43, 44, 43)
[sub_resource type="AtlasTexture" id="AtlasTexture_84sml"]
atlas = ExtResource("2_sbshw")
region = Rect2(44, 43, 44, 43)
[sub_resource type="AtlasTexture" id="AtlasTexture_xncbb"]
atlas = ExtResource("2_sbshw")
region = Rect2(88, 43, 44, 43)
[sub_resource type="AtlasTexture" id="AtlasTexture_eaqhr"]
atlas = ExtResource("2_sbshw")
region = Rect2(132, 43, 44, 43)
[sub_resource type="AtlasTexture" id="AtlasTexture_bcdl4"]
atlas = ExtResource("2_sbshw")
region = Rect2(0, 86, 44, 43)
[sub_resource type="AtlasTexture" id="AtlasTexture_a4mgy"]
atlas = ExtResource("2_sbshw")
region = Rect2(44, 86, 44, 43)
[sub_resource type="SpriteFrames" id="SpriteFrames_mppe5"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": SubResource("AtlasTexture_yq6e3")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_ljyfv")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_46j2i")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_q6a2v")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_lyh2i")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_84sml")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_xncbb")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_eaqhr")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_bcdl4")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_a4mgy")
}],
"loop": true,
"name": &"default",
"speed": 5.0
}]
[node name="RigidBody2D" type="RigidBody2D"]
collision_layer = 256
collision_mask = 160
script = ExtResource("1_t1qdg")
Path = "res://prefab/ui/SpellEditorUI.tscn"
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(-0.5, -0.5)
shape = SubResource("RectangleShape2D_vx4tg")
[node name="OperateArea2D" type="Area2D" parent="."]
visible = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="OperateArea2D"]
shape = SubResource("CircleShape2D_in1f7")
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."]
scale = Vector2(0.7, 0.7)
sprite_frames = SubResource("SpriteFrames_mppe5")
autoplay = "default"
frame_progress = 0.717769

View File

@ -0,0 +1,44 @@
[gd_scene load_steps=6 format=3 uid="uid://crthy8a50a4t"]
[ext_resource type="Script" path="res://scripts/spell/SpellPickAble.cs" id="1_v0grp"]
[ext_resource type="AudioStream" uid="uid://cak6chjjsu7wo" path="res://sounds/fire.wav" id="4_ffr2k"]
[ext_resource type="Texture2D" uid="uid://bbcjkyrsx88av" path="res://sprites/projectile/curseOfTheUndead.png" id="4_y6nkf"]
[sub_resource type="CircleShape2D" id="CircleShape2D_qgdry"]
[sub_resource type="CircleShape2D" id="CircleShape2D_akp3k"]
[node name="curseOfTheUndead" type="RigidBody2D"]
collision_layer = 8
collision_mask = 34
angular_damp = -1.0
script = ExtResource("1_v0grp")
_projectilePath = "res://prefab/projectile/curseOfTheUndead.tscn"
[node name="DamageArea2D" type="Area2D" parent="."]
collision_layer = 8
collision_mask = 102
[node name="CollisionShape2D" type="CollisionShape2D" parent="DamageArea2D"]
position = Vector2(0, -0.5)
shape = SubResource("CircleShape2D_qgdry")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0, -0.625)
shape = SubResource("CircleShape2D_akp3k")
[node name="Marker2D" type="Marker2D" parent="."]
position = Vector2(65, 0)
[node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="Marker2D"]
stream = ExtResource("4_ffr2k")
bus = &"SoundEffect"
[node name="TipLabel" type="Label" parent="."]
offset_left = -19.0
offset_top = 23.0
offset_right = 21.0
offset_bottom = 48.0
[node name="CurseOfTheUndead" type="Sprite2D" parent="."]
texture = ExtResource("4_y6nkf")

46
prefab/magics/x3.tscn Normal file
View File

@ -0,0 +1,46 @@
[gd_scene load_steps=6 format=3 uid="uid://cg75t3fw5c6er"]
[ext_resource type="Script" path="res://scripts/spell/MultipleFireSpell.cs" id="1_cnhod"]
[ext_resource type="Texture2D" uid="uid://mb5yijtw7sw5" path="res://sprites/projectile/x3.png" id="3_b3s8h"]
[ext_resource type="AudioStream" uid="uid://cak6chjjsu7wo" path="res://sounds/fire.wav" id="4_ffr2k"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_3eq4k"]
size = Vector2(30, 30)
[sub_resource type="RectangleShape2D" id="RectangleShape2D_i3lbq"]
size = Vector2(30, 31)
[node name="x3" type="RigidBody2D"]
collision_layer = 8
collision_mask = 34
angular_damp = -1.0
script = ExtResource("1_cnhod")
[node name="DamageArea2D" type="Area2D" parent="."]
collision_layer = 8
collision_mask = 102
[node name="CollisionShape2D" type="CollisionShape2D" parent="DamageArea2D"]
shape = SubResource("RectangleShape2D_3eq4k")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0, -0.5)
shape = SubResource("RectangleShape2D_i3lbq")
[node name="Marker2D" type="Marker2D" parent="."]
position = Vector2(65, 0)
[node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="Marker2D"]
stream = ExtResource("4_ffr2k")
bus = &"SoundEffect"
[node name="TipLabel" type="Label" parent="."]
offset_left = -19.0
offset_top = 23.0
offset_right = 21.0
offset_bottom = 48.0
[node name="CurseOfTheUndead" type="Sprite2D" parent="."]
[node name="X3" type="Sprite2D" parent="."]
texture = ExtResource("3_b3s8h")

View File

@ -15,7 +15,6 @@ _maxDamage = 10
_minDamage = 1
_damageType = 2
Speed = 500.0
_enableTracking = true
_targetDiesDestroyProjectile = true
_repelStrength = 2.5

View File

@ -4,7 +4,7 @@
[ext_resource type="Script" path="res://scripts/map/PlayerSpawn.cs" id="2_6p8mv"]
[ext_resource type="Script" path="res://scripts/map/ItemSpawn.cs" id="3_v1tlc"]
[ext_resource type="Texture2D" uid="uid://drw45jlmfo0su" path="res://sprites/light/White_100.png" id="5_4pssd"]
[ext_resource type="PackedScene" uid="uid://djsh4unystlf0" path="res://prefab/furnitures/MagicSeparator.tscn" id="5_7c8bh"]
[ext_resource type="PackedScene" uid="uid://djsh4unystlf0" path="res://prefab/furnitures/SpellEditor.tscn" id="5_7c8bh"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_kiih8"]
size = Vector2(507, 251)
@ -45,6 +45,21 @@ position = Vector2(142, 84)
script = ExtResource("3_v1tlc")
ItemId = "staff_necromancy"
[node name="ItemMarker2D3" type="Marker2D" parent="."]
position = Vector2(214, 83)
script = ExtResource("3_v1tlc")
ItemId = "x3"
[node name="ItemMarker2D4" type="Marker2D" parent="."]
position = Vector2(366, 90)
script = ExtResource("3_v1tlc")
ItemId = "necromancy"
[node name="ItemMarker2D2" type="Marker2D" parent="."]
position = Vector2(321, 118)
script = ExtResource("3_v1tlc")
ItemId = "necromancy"
[node name="NavigationRegion2D" type="NavigationRegion2D" parent="."]
navigation_polygon = SubResource("NavigationPolygon_064c7")

View File

@ -0,0 +1,53 @@
[gd_scene load_steps=3 format=3 uid="uid://h7lvaqqlsi4t"]
[ext_resource type="Script" path="res://scripts/loader/uiLoader/SpellEditorUi.cs" id="1_1pxjs"]
[ext_resource type="PackedScene" uid="uid://d2i4udh0hho41" path="res://prefab/ui/ItemSlot.tscn" id="2_3ut57"]
[node name="SpellEditorUi" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
script = ExtResource("1_1pxjs")
[node name="TitleLabel" type="Label" parent="."]
layout_mode = 1
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 25.0
grow_horizontal = 2
text = "ui_spell_editor"
horizontal_alignment = 1
[node name="ExitButton" type="Button" parent="."]
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -91.0
offset_top = 11.0
offset_right = -24.0
offset_bottom = 44.0
grow_horizontal = 0
text = "ui_close"
[node name="HFlowContainer" type="HFlowContainer" parent="."]
layout_mode = 1
anchors_preset = 10
anchor_right = 1.0
offset_left = 32.0
offset_top = 127.0
offset_right = -20.0
offset_bottom = 629.0
grow_horizontal = 2
mouse_filter = 2
[node name="ItemSlot" parent="." instance=ExtResource("2_3ut57")]
layout_mode = 1
offset_left = 34.0
offset_top = 73.0
offset_right = 72.0
offset_bottom = 111.0

View File

@ -1,8 +1,7 @@
[gd_scene load_steps=8 format=3 uid="uid://dnnn2xyayiehk"]
[gd_scene load_steps=7 format=3 uid="uid://dnnn2xyayiehk"]
[ext_resource type="Texture2D" uid="uid://wt50kx6bup51" path="res://sprites/weapon/StaffNecromancy.png" id="1_ms3us"]
[ext_resource type="Script" path="res://scripts/weapon/ProjectileWeapon.cs" id="1_w8hhv"]
[ext_resource type="PackedScene" uid="uid://c01av43yk1q71" path="res://prefab/projectile/curseOfTheUndead.tscn" id="2_mwli5"]
[ext_resource type="Texture2D" uid="uid://dg5vwprt66w4j" path="res://sprites/weapon/StaffNecromancy_Icon.png" id="3_31iau"]
[ext_resource type="AudioStream" uid="uid://cak6chjjsu7wo" path="res://sounds/fire.wav" id="4_ffr2k"]
@ -17,9 +16,8 @@ collision_layer = 8
collision_mask = 34
angular_damp = -1.0
script = ExtResource("1_w8hhv")
OffsetAngle = 0.087
ProjectileScenes = [ExtResource("2_mwli5")]
Sequentially = true
_numberSlots = 5
_fireSequentially = true
FiringIntervalAsMillisecond = 300
_recoilStrength = 5
UniqueIcon = ExtResource("3_31iau")

View File

@ -84,13 +84,16 @@ text = "ui_re_create_map"
[node name="SeedLabel" type="Label" parent="CanvasLayer/Control"]
layout_mode = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -20.0
offset_right = 20.0
offset_bottom = 25.0
grow_horizontal = 2
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -45.0
offset_top = -16.0
grow_horizontal = 0
grow_vertical = 0
theme_override_font_sizes/font_size = 10
[node name="MapContainer" parent="CanvasLayer/Control" instance=ExtResource("6_ljdj4")]
layout_mode = 1
@ -115,6 +118,8 @@ visible = false
[node name="WeaponContainer" type="Node2D" parent="."]
[node name="SpellContainer" type="Node2D" parent="."]
[node name="PlayerContainer" type="Node2D" parent="."]
[node name="AICharacterContainer" type="Node2D" parent="."]

View File

@ -171,6 +171,12 @@ public static class Config
/// </remarks>
public const string DefaultVersionName = "Default";
/// <summary>
/// <para>EmptyVariant</para>
/// <para>空变量</para>
/// </summary>
public static readonly Variant EmptyVariant = new();
/// <summary>
/// <para>IsDebug</para>
@ -192,6 +198,45 @@ public static class Config
return OS.HasFeature("editor");
}
/// <summary>
/// <para>ItemType</para>
/// <para>物品类型</para>
/// </summary>
public static class ItemType
{
/// <summary>
/// <para>Unknown</para>
/// <para>未知的</para>
/// </summary>
public const int Unknown = 0;
/// <summary>
/// <para>Placeholder</para>
/// <para>占位符</para>
/// </summary>
public const int Placeholder = 1;
/// <summary>
/// <para>Packsack</para>
/// <para>背包</para>
/// </summary>
public const int Packsack = 2;
/// <summary>
/// <para>ProjectileWeapon</para>
/// <para>远程武器</para>
/// </summary>
public const int ProjectileWeapon = 3;
/// <summary>
/// <para>Spell</para>
/// <para>法术</para>
/// </summary>
/// <remarks>
///<para>Type of special item used in Projectile weapons</para>
///<para>用于远程武器内的特殊物品类型</para>
/// </remarks>
public const int Spell = 4;
}
/// <summary>
/// <para>Room Injector ID</para>
/// <para>房间注入器ID</para>
@ -211,6 +256,49 @@ public static class Config
public const string TimeInterval = "TimeInterval";
}
public class ZIndexManager
{
/// <summary>
/// <para>Floating icon</para>
/// <para>悬浮图标</para>
/// </summary>
public const int FloatingIcon = 1;
}
/// <summary>
/// <para>Item data changes the event type</para>
/// <para>物品数据改变事件类型</para>
/// </summary>
public enum ItemDataChangeEventType
{
/// <summary>
/// <para>add</para>
/// <para>添加</para>
/// </summary>
Add,
/// <summary>
/// <para>Quantity Added</para>
/// <para>物品数量增加</para>
/// </summary>
QuantityAdded,
/// <summary>
/// <para>remove</para>
/// <para>移除</para>
/// </summary>
Remove,
/// <summary>
/// <para>Replace</para>
/// <para>被替换</para>
/// </summary>
Replace,
/// <summary>
/// <para>Clear</para>
/// <para>被清空</para>
/// </summary>
Clear
}
public enum OsEnum
{
//unknown

View File

@ -49,6 +49,12 @@ public static class GameSceneDepend
/// </summary>
public static Node2D? ProjectileContainer { get; set; }
/// <summary>
/// <para>SpellContainer</para>
/// <para>法术容器</para>
/// </summary>
public static Node2D? SpellContainer { get; set; }
/// <summary>
/// <para>WeaponContainer</para>
/// <para>武器容器</para>

View File

@ -179,9 +179,8 @@ public sealed partial class AiCharacter : CharacterTemplate
//You must create an item container for the character before you can pick up the item.
//必须为角色创建物品容器后才能拾起物品。
var universalItemContainer = new UniversalItemContainer();
var itemSlotNode = universalItemContainer.AddItemSlot(this);
itemSlotNode?.Hide();
var universalItemContainer = new UniversalItemContainer(1);
universalItemContainer.AllowAddingItemByType(Config.ItemType.ProjectileWeapon);
ProtectedItemContainer = universalItemContainer;
//Add initial weapon
//添加初始武器

View File

@ -9,6 +9,7 @@ using ColdMint.scripts.inventory;
using ColdMint.scripts.utils;
using ColdMint.scripts.loot;
using ColdMint.scripts.pickable;
using ColdMint.scripts.weapon;
using Godot;
using WeaponTemplate = ColdMint.scripts.weapon.WeaponTemplate;
@ -413,14 +414,6 @@ public partial class CharacterTemplate : CharacterBody2D
return false;
}
//Get the currently selected node
//拿到当前选择的节点
var selectItemSlotNode = ItemContainer.GetSelectItemSlotNode();
if (selectItemSlotNode == null)
{
return false;
}
//Check to see if you can fit the item into the container first.
//先检查是否能将物品放入容器。
var canAddItem = ItemContainer.CanAddItem(item);
@ -431,8 +424,8 @@ public partial class CharacterTemplate : CharacterBody2D
//Is it successfully added to the container?
//再检查是否成功的添加到容器内了?
var addSuccess = ItemContainer.AddItem(item);
if (!addSuccess)
var addSuccessNumber = ItemContainer.AddItem(item);
if (addSuccessNumber <= 0)
{
return false;
}
@ -453,8 +446,12 @@ public partial class CharacterTemplate : CharacterBody2D
pickAbleTemplate.Sleeping = true;
}
if (pickAbleItemNode2D is ProjectileWeapon projectileWeapon)
{
projectileWeapon.UpdateSpellCache();
}
if (_currentItem == null && selectItemSlotNode.GetItem() == item)
if (_currentItem == null && ItemContainer.GetSelectItem() == item)
{
//If the selected item slot in the item container is a newly picked item, and there is no item in the hand, then we put the selected item into the hand.
//如果物品容器内选中的物品槽是刚刚捡到的物品,且手里没有物品持有,那么我们将选中的物品放到手上。
@ -484,10 +481,10 @@ public partial class CharacterTemplate : CharacterBody2D
if (_currentItem is IItem item)
{
item.Use(this, position);
return item.Use(this, position);
}
return true;
return false;
}
public override void _Process(double delta)
@ -723,7 +720,7 @@ public partial class CharacterTemplate : CharacterBody2D
return;
}
var len = ItemContainer.GetItemSlotCount();
var len = ItemContainer.GetUsedCapacity();
if (len == 0)
{
return;
@ -762,21 +759,19 @@ public partial class CharacterTemplate : CharacterBody2D
/// </param>
protected void ThrowItem(int index, int number, Vector2 velocity)
{
var itemSlotNode = ItemContainer?.GetItemSlotNode(index);
if (itemSlotNode is null) return;
if (number < 0)
if (number == 0)
{
while (!itemSlotNode.IsEmpty())
{
ThrowOneItem(itemSlotNode, velocity);
return;
}
}
else
var item = ItemContainer?.GetItem(index);
if (item is null) return;
//Less than 0, throw everything
//小于0,扔出所有物品
var actualQuantity = number < 0 ? item.Quantity : Math.Min(item.Quantity, number);
for (var i = 0; i < actualQuantity; i++)
{
for (var i = 0; i < number && !itemSlotNode.IsEmpty(); i++)
{
ThrowOneItem(itemSlotNode, velocity);
}
ThrowOneItem(index, item, velocity);
}
}
@ -784,21 +779,23 @@ public partial class CharacterTemplate : CharacterBody2D
/// <para>Throw item</para>
/// <para>抛出物品</para>
/// </summary>
/// <param name="itemSlotNode"></param>
/// <param name="index"></param>
/// <param name="originalItem"></param>
/// <param name="velocity">
/// <para>The speed to be applied to the item</para>
/// <para>要施加到物品上的速度</para>
/// </param>
private void ThrowOneItem(ItemSlotNode itemSlotNode, Vector2 velocity)
private void ThrowOneItem(int index, IItem originalItem, Vector2 velocity)
{
//Remove the item from the item container
//从物品容器内取出物品
var item = itemSlotNode.CreateItemInstance(1);
originalItem.OnThrow(velocity);
var item = originalItem.CreateItem(1);
if (item is not Node2D node2D)
{
return;
}
item.ItemContainer = null;
NodeUtils.CallDeferredAddChild(NodeUtils.FindContainerNode(node2D, GetNode("/root")), node2D);
switch (item)
{
@ -847,7 +844,8 @@ public partial class CharacterTemplate : CharacterBody2D
break;
}
itemSlotNode.RemoveItem(1);
ProtectedItemContainer?.RemoveItem(index, 1);
originalItem.QueueFreeSelf();
}

View File

@ -72,7 +72,7 @@ public partial class Player : CharacterTemplate
//Subscribe to events when the item container is bound to the player.
//在物品容器与玩家绑定时订阅事件。
itemContainer.SelectedItemSlotChangeEvent += SelectedItemSlotChangeEvent;
itemContainer.SelectedItemChangeEvent += SelectedItemChangeEvent;
}
public override void _ExitTree()
@ -82,14 +82,13 @@ public partial class Player : CharacterTemplate
{
//Unsubscribe to events when this object is destroyed.
//此节点被销毁时,取消订阅事件。
ProtectedItemContainer.SelectedItemSlotChangeEvent -= SelectedItemSlotChangeEvent;
ProtectedItemContainer.SelectedItemChangeEvent -= SelectedItemChangeEvent;
}
}
private void SelectedItemSlotChangeEvent(SelectedItemSlotChangeEvent selectedItemSlotChangeEvent)
private void SelectedItemChangeEvent(SelectedItemChangeEvent selectedItemChangeEvent)
{
var item = selectedItemSlotChangeEvent.NewItemSlotNode?.GetItem();
GameSceneDepend.DynamicUiGroup?.HideAllControl();
var item = selectedItemChangeEvent.NewItem;
if (item is Node2D node2D)
{
CurrentItem = node2D;
@ -229,9 +228,6 @@ public partial class Player : CharacterTemplate
{
UseItem(GetGlobalMousePosition());
}
//Pick up an item
//捡起物品
if (Input.IsActionJustPressed("pick_up"))
@ -305,7 +301,6 @@ public partial class Player : CharacterTemplate
}
ThrowItem(ItemContainer.GetSelectIndex(), 1, GetThrowVelocity());
GameSceneDepend.DynamicUiGroup?.HideAllControl();
CurrentItem = null;
}
}

View File

@ -88,6 +88,12 @@ public static class LogCat
/// <para>房间</para>
/// </summary>
public const string Room = "Room";
/// <summary>
/// <para>ItemSpawn</para>
/// <para>物品生成器</para>
/// </summary>
public const string ItemSpawn = "ItemSpawn";
}

View File

@ -1,3 +1,6 @@
using ColdMint.scripts.character;
using ColdMint.scripts.loader.uiLoader;
using ColdMint.scripts.utils;
using Godot;
namespace ColdMint.scripts.furniture;
@ -18,22 +21,93 @@ public partial class GuiFurniture : Furniture
/// </remarks>
private Area2D? _operateArea2D;
/// <summary>
/// <para>Whether the player is within range of the operation</para>
/// <para>玩家是否在操作范围内</para>
/// </summary>
private bool _playerInRange;
[Export]
public string? Path;
/// <summary>
/// <para>There's a mouse hover</para>
/// <para>有鼠标悬停</para>
/// </summary>
private bool _hasMouseOver;
public override void _Ready()
{
base._Ready();
InputPickable = true;
_operateArea2D = GetNode<Area2D>("OperateArea2D");
_operateArea2D.BodyEntered += OnBodyEntered;
_operateArea2D.BodyExited += OnBodyExited;
_operateArea2D.SetCollisionMaskValue(Config.LayerNumber.Player, true);
if (Path != null)
{
GameSceneDepend.DynamicUiGroup?.RegisterControl(Path, () =>
{
var packedScene = GD.Load<PackedScene>(Path);
return NodeUtils.InstantiatePackedScene<SpellEditorUi>(packedScene);
});
}
}
/// <summary>
/// <para>Use furniture</para>
/// <para>使用家具</para>
/// </summary>
/// <param name="player"></param>
private void Use(Player player)
{
if (Path == null)
{
return;
}
GameSceneDepend.DynamicUiGroup?.ShowControl(Path);
}
public override void _MouseEnter()
{
_hasMouseOver = true;
}
public override void _MouseExit()
{
_hasMouseOver = false;
}
private void OnBodyEntered(Node node)
{
if (node is Player)
{
_playerInRange = true;
}
}
public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
if (GameSceneDepend.Player == null || !_playerInRange || !_hasMouseOver)
{
return;
}
if (Input.IsActionJustPressed("use_item"))
{
Use(GameSceneDepend.Player);
}
}
private void OnBodyExited(Node2D node2D)
{
if (node2D is Player)
{
_playerInRange = false;
if (Path != null)
{
GameSceneDepend.DynamicUiGroup?.HideControl(Path);
}
}
}
public override void _ExitTree()

View File

@ -10,22 +10,18 @@ namespace ColdMint.scripts.inventory;
public partial class HotBar : HBoxContainer
{
private IItemContainer? _itemContainer;
private IItemContainerDisplay? _itemContainerDisplay;
public override void _Ready()
{
base._Ready();
_itemContainer = new UniversalItemContainer();
var universalItemContainer = new UniversalItemContainer(Config.HotBarSize);
_itemContainer = universalItemContainer;
universalItemContainer.AllowItemTypesExceptPlaceholder();
_itemContainer.SupportSelect = true;
_itemContainerDisplay = new ItemSlotContainerDisplay(this);
_itemContainerDisplay.BindItemContainer(_itemContainer);
NodeUtils.DeleteAllChild(this);
for (var i = 0; i < Config.HotBarSize; i++)
{
var itemSlotNode = _itemContainer.AddItemSlot(this);
if (itemSlotNode != null)
{
itemSlotNode.BackpackAllowed = true;
}
}
}
@ -36,14 +32,14 @@ public partial class HotBar : HBoxContainer
{
//Mouse wheel down
//鼠标滚轮向下
_itemContainer?.SelectTheNextItemSlot();
_itemContainer?.SelectNextItem();
}
if (Input.IsActionJustPressed("hotbar_previous"))
{
//Mouse wheel up
//鼠标滚轮向上
_itemContainer?.SelectThePreviousItemSlot();
_itemContainer?.SelectPreviousItem();
}
if (Input.IsActionJustPressed("hotbar_1"))
@ -106,7 +102,7 @@ public partial class HotBar : HBoxContainer
return;
}
_itemContainer.SelectItemSlot(shortcutKeyIndex);
_itemContainer.SelectItem(shortcutKeyIndex);
}
public IItemContainer? GetItemContainer()

View File

@ -4,12 +4,35 @@ namespace ColdMint.scripts.inventory;
public interface IItem
{
/// <summary>
/// <para>The position of the item in the container</para>
/// <para>物品在容器内的位置</para>
/// </summary>
public int Index { get; set; }
/// <summary>
/// <para>ID of current item</para>
/// <para>当前物品的ID</para>
/// </summary>
string Id { get; set; }
/// <summary>
/// <para>ShowSelf</para>
/// <para>显示自身</para>
/// </summary>
void ShowSelf();
/// <summary>
/// <para>QueueFreeSelf</para>
/// <para>销毁自身</para>
/// </summary>
void QueueFreeSelf();
/// <summary>
/// <para>HideSelf</para>
/// <para>隐藏自身</para>
/// </summary>
void HideSelf();
/// <summary>
/// <para>Icon of current item</para>
/// <para>当前项目的图标</para>
@ -40,11 +63,79 @@ public interface IItem
/// </summary>
int MaxQuantity { get; }
/// <summary>
/// <para>ItemType</para>
/// <para>获取物品类型</para>
/// </summary>
int ItemType { get; }
/// <summary>
/// <para>Check or not</para>
/// <para>是否选中</para>
/// </summary>
bool IsSelect { get; set; }
/// <summary>
/// <para>The container in which the item is located</para>
/// <para>物品所在的物品容器</para>
/// </summary>
IItemContainer? ItemContainer { get; set; }
/// <summary>
/// <para>Its own container of items</para>
/// <para>自身的物品容器</para>
/// </summary>
/// <remarks>
/// <para>Returns a non-null value if the item itself can hold other items</para>
/// <para>物品本身可容纳其他物品,则返回非空值</para>
/// </remarks>
public IItemContainer? SelfItemContainer { get; set; }
/// <summary>
/// <para>Calculate how many items can be merged with other items</para>
/// <para>计算当前物品可与其他物品合并多少个</para>
/// </summary>
/// <param name="other"></param>
/// <param name="unallocatedQuantity">
///<para>The amount yet to be allocated(This method doesn't actually change the number of iitems, so you need to allocate an int variable to keep track of how many items remain unallocated.)</para>
///<para>尚未分配的数量在此方法并不会实际改变IItem的数量故您需要分配一个int型变量记录还有多少个物品尚未分配</para>
/// </param>
/// <returns>
///<para>Number of mergable numbers. 0 indicates that the number cannot be merged. Greater than 0 indicates that the number can be merged.</para>
///<para>可合并的数量0为不能合并大于0可合并。</para>
/// </returns>
int MergeableItemCount(IItem other, int unallocatedQuantity);
/// <summary>
/// <para>Create a new item instance</para>
/// <para>创建新的物品实例</para>
/// </summary>
/// <param name="number">
///<para>Quantity (pass in a value less than 0 to create an instance using all the quantities of built-in items)</para>
///<para>数量传入小于0的值使用内置物品的所有数量创建实例</para>
/// </param>
/// <returns>
///<para>Newly created item</para>
///<para>新创建的物品</para>
/// </returns>
IItem? CreateItem(int number);
/// <summary>
/// <para>Execute when current item is used <br/> e.g. when player clicks left mouse button with current item in hand</para>
/// <para>当前项被使用时执行 <br/> e.g. 当玩家用鼠标左键点击当前物品时</para>
/// </summary>
/// <param name="owner">Owner of current item, if any</param>
/// <param name="targetGlobalPosition">Target position, such as the position of the cursor when used by the player</param>
void Use(Node2D? owner, Vector2 targetGlobalPosition);
/// <returns>
///<para>Whether it was successfully executed</para>
///<para>是否成功被执行了</para>
/// </returns>
bool Use(Node2D? owner, Vector2 targetGlobalPosition);
/// <summary>
/// <para>When the item is thrown</para>
/// <para>当物品被抛出时</para>
/// </summary>
/// <param name="velocity"></param>
void OnThrow(Vector2 velocity);
}

View File

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using ColdMint.scripts.map.events;
using Godot;
namespace ColdMint.scripts.inventory;
@ -9,17 +7,33 @@ namespace ColdMint.scripts.inventory;
/// <para>item container</para>
/// <para>物品容器</para>
/// </summary>
/// <remarks>
///<para>Item containers can store items. Things like backpacks and Hotbars are containers with visual pages.</para>
///<para>物品容器可以储存物品。像背包和hotbar是具有可视化页面的容器。</para>
/// </remarks>
public interface IItemContainer : IEnumerable<ItemSlotNode>
public interface IItemContainer
{
/// <summary>
/// <para>This event is triggered when the selected item slot changes</para>
/// <para>当选中的物品改变时,触发此事件</para>
/// <para>This event is triggered when the selected item changes</para>
/// <para>当选中的物品改变时,触发此事件</para>
/// </summary>
Action<SelectedItemSlotChangeEvent>? SelectedItemSlotChangeEvent { get; set; }
Action<SelectedItemChangeEvent>? SelectedItemChangeEvent { get; set; }
/// <summary>
/// <para>This event is triggered when the item's data changes, such as the number increases, decreases, or new items are added to the container</para>
/// <para>当物品的数据发生改变时,例如数量增加,减少,或者新物品被添加到容器内触发此事件</para>
/// </summary>
Action<ItemDataChangeEvent>? ItemDataChangeEvent { get; set; }
/// <summary>
/// <para>Allow Adding Item By Type</para>
/// <para>允许添加指定类型的物品</para>
/// </summary>
/// <param name="itemType"></param>
void AllowAddingItemByType(int itemType);
/// <summary>
/// <para>Disallow Adding Item By Type</para>
/// <para>禁止添加指定类型的物品</para>
/// </summary>
/// <param name="itemType"></param>
void DisallowAddingItemByType(int itemType);
/// <summary>
/// <para>Can the specified item be added to the container?</para>
@ -34,15 +48,28 @@ public interface IItemContainer : IEnumerable<ItemSlotNode>
/// <para>实现添加物品的方法</para>
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
bool AddItem(IItem item);
/// <returns>
///<para>How many items were successfully added. The addition failed, and 0 was returned.</para>
///<para>有多少个物品被成功添加了。添加失败返回0</para>
/// </returns>
int AddItem(IItem item);
/// <summary>
/// <para>Whether this item container supports checking</para>
/// <para>此物品容器是否支持选中</para>
/// </summary>
public bool SupportSelect { get; set; }
bool SupportSelect { get; set; }
/// <summary>
/// <para>Gets a placeholder object</para>
/// <para>获取占位符对象</para>
/// </summary>
/// <param name="index">
///<para>index</para>
///<para>占位符代替的索引</para>
/// </param>
/// <returns></returns>
IItem GetPlaceHolderItem(int index);
/// <summary>
/// <para>Gets the selected location</para>
@ -52,104 +79,118 @@ public interface IItemContainer : IEnumerable<ItemSlotNode>
int GetSelectIndex();
/// <summary>
/// <para>Gets the currently selected node</para>
/// <para>获取当前选中的节点</para>
/// <para>Gets the currently selected item</para>
/// <para>获取当前选中的物品</para>
/// </summary>
/// <returns></returns>
ItemSlotNode? GetSelectItemSlotNode();
IItem? GetSelectItem();
/// <summary>
/// <para>Removes an item from the inventory at the currently selected location</para>
/// <para>移除当前选中位置物品栏内的物品</para>
/// </summary>
/// <param name="number">
/// <para>Quantity to be removed, inputs below zero represent all items</para>
/// <para>要删除的数量小于0的输入代表全部物品</para>
/// </param>
/// <returns>
/// <para>The remaining number, if the number of items in the current item stack is less than the specified number. Otherwise,0</para>
/// <para>若物品槽内物品少于指定的数量返回相差的数量。否则返回0</para>
/// </returns>
int RemoveItemFromItemSlotBySelectIndex(int number);
/// <summary>
/// <para>Gets the number of item slots</para>
/// <para>获取物品槽的数量</para>
/// </summary>
/// <returns></returns>
int GetItemSlotCount();
/// <summary>
/// <para>Gets the item slot for the specified location</para>
/// <para>获取指定位置的物品槽</para>
/// <para>Gets the item in the specified location</para>
/// <para>获取指定位置的物品</para>
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
ItemSlotNode? GetItemSlotNode(int index);
IItem? GetItem(int index);
/// <summary>
/// <para>Gets the item slot for the specified location, equals to <see cref="GetItemSlotNode"/></para>
/// <para>获取指定位置的物品槽,等同于<see cref="GetItemSlotNode"/></para>
/// <para>ReplaceItem</para>
/// <para>替换物品</para>
/// </summary>
/// <remarks>
///<para>Even if the item corresponding to the index is null, it can be successfully replaced.</para>
///<para>即使索引对应的物品为null也可以成功的替换。</para>
/// </remarks>
/// <param name="index"></param>
/// <returns></returns>
ItemSlotNode? this[int index] => GetItemSlotNode(index);
/// <summary>
/// <para>Removes an item from the item slot in the specified location</para>
/// <para>在指定位置的物品槽内移除物品</para>
/// </summary>
/// <param name="itemSlotIndex"></param>
/// <param name="number">
/// <para>Quantity to be removed, inputs below zero represent all items</para>
/// <para>要删除的数量小于0的输入代表全部物品</para>
/// </param>
/// <returns>
/// <para>The remaining number, if the number of items in the current item stack is less than the specified number. Otherwise,0</para>
/// <para>若物品槽内物品少于指定的数量返回相差的数量。否则返回0</para>
/// </returns>
int RemoveItemFromItemSlot(int itemSlotIndex, int number);
/// <summary>
/// <para>Based on the given item, match the item slots where it can be added to </para>
/// <para>根据给定的物品,匹配可放置它的物品槽</para>
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
bool ReplaceItem(int index, IItem item);
/// <summary>
/// <para>Whether items are replaceable</para>
/// <para>是否可替换物品</para>
/// </summary>
/// <param name="index"></param>
/// <param name="item"></param>
/// <returns></returns>
bool CanReplaceItem(int index, IItem item);
/// <summary>
/// <para>ClearItem</para>
/// <para>清理物品</para>
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
bool ClearItem(int index);
/// <summary>
/// <para>Gets the item in the specified location, equivalent to <see cref="GetItem"/></para>
/// <para>获取指定位置的物品,等同于<see cref="GetItem"/></para>
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
IItem? this[int index] => GetItem(index);
/// <summary>
/// <para>Removes the item from the currently selected location</para>
/// <para>移除当前选中位置的物品</para>
/// </summary>
/// <param name="number">
/// <para>Quantity to be removed, inputs below zero represent all items</para>
/// <para>要删除的数量小于0的输入代表全部物品</para>
/// </param>
/// <returns>
/// <para>Return null if there is no slot to place the item in</para>
/// <para>若没有槽可放置此物品则返回null</para>
///<para>How many items were actually removed</para>
///<para>实际移除了多少个物品</para>
/// </returns>
ItemSlotNode? Match(IItem item);
int RemoveSelectItem(int number);
/// <summary>
/// <para>AddItemSlot</para>
/// <para>添加物品槽</para>
/// <para>Deduct the number of items in the specified location</para>
/// <para>扣除指定位置的物品数量</para>
/// </summary>
/// <param name="rootNode"></param>
ItemSlotNode? AddItemSlot(Node rootNode);
/// <param name="itemIndex"></param>
/// <param name="number">
/// <para>Quantity to be removed, inputs below zero represent all items</para>
/// <para>要删除的数量小于0的输入代表全部物品</para>
/// </param>
/// <returns>
///<para>How many items were actually removed</para>
///<para>实际移除了多少个物品</para>
/// </returns>
int RemoveItem(int itemIndex, int number);
/// <summary>
/// <para>SelectTheNextItemSlot</para>
/// <para>选择下一个物品槽</para>
/// <para>Gets the used capacity of the item container</para>
/// <para>获取物品容器已使用的容量</para>
/// </summary>
void SelectTheNextItemSlot();
/// <returns></returns>
int GetUsedCapacity();
/// <summary>
/// <para>SelectThePreviousItemSlot</para>
/// <para>选择上一个物品槽</para>
/// <para>Gets the total capacity of the item container</para>
/// <para>获取物品容器的总容量</para>
/// </summary>
void SelectThePreviousItemSlot();
/// <returns></returns>
int GetTotalCapacity();
/// <summary>
/// <para>选择物品槽</para>
/// <para>SelectItemSlot</para>
/// <para>Select the next item</para>
/// <para>选择下一个物品</para>
/// </summary>
/// <param name="newSelectIndex"></param>
void SelectItemSlot(int newSelectIndex);
void SelectNextItem();
/// <summary>
/// <para>Select the previous item</para>
/// <para>选择上一个物品</para>
/// </summary>
void SelectPreviousItem();
/// <summary>
/// <para>选择物品</para>
/// <para>Select item</para>
/// </summary>
/// <param name="index"></param>
void SelectItem(int index);
}

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace ColdMint.scripts.inventory;
public interface IItemContainerDisplay : IEnumerable<IItemDisplay>
{
/// <summary>
/// <para>Bind an item container to the item container display</para>
/// <para>为物品容器显示器绑定物品容器</para>
/// </summary>
/// <param name="itemContainer"></param>
void BindItemContainer(IItemContainer? itemContainer);
}

View File

@ -0,0 +1,40 @@
namespace ColdMint.scripts.inventory;
/// <summary>
/// <para>IItemDisplay</para>
/// <para>物品显示器</para>
/// </summary>
public interface IItemDisplay
{
/// <summary>
/// <para>Call this method to refresh the display when the item's information changes</para>
/// <para>物品的信息发生变更时,调用此方法刷新显示器</para>
/// </summary>
/// <remarks>
///<param name="item">
///<para>New data for items after changes</para>
///<para>发生改变后的物品新数据</para>
/// </param>
/// </remarks>
void Update(IItem? item);
/// <summary>
/// <para>Gets the item that is being displayed</para>
/// <para>获取正在显示的物品</para>
/// </summary>
IItem? Item { get; }
/// <summary>
/// <para>Show item Display</para>
/// <para>显示物品显示器</para>
/// </summary>
void ShowSelf();
/// <summary>
/// <para>Hide item Display</para>
/// <para>隐藏物品显示器</para>
/// </summary>
void HideSelf();
}

View File

@ -0,0 +1,161 @@
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using ColdMint.scripts.map.events;
namespace ColdMint.scripts.inventory;
public abstract class ItemContainerDisplayTemplate : IItemContainerDisplay
{
protected readonly List<IItemDisplay> ItemDisplayList = [];
private IItemContainer? _itemContainer;
public async void BindItemContainer(IItemContainer? itemContainer)
{
if (_itemContainer == itemContainer)
{
return;
}
if (_itemContainer != null)
{
_itemContainer.SelectedItemChangeEvent -= OnSelectedItemChangeEvent;
_itemContainer.ItemDataChangeEvent -= OnItemDataChangeEvent;
}
_itemContainer = itemContainer;
if (itemContainer == null)
{
//Set empty items container to hide all ui.
//设置空物品容器隐藏全部ui。
foreach (var itemDisplay in ItemDisplayList)
{
itemDisplay.Update(null);
itemDisplay.HideSelf();
}
return;
}
itemContainer.SelectedItemChangeEvent += OnSelectedItemChangeEvent;
itemContainer.ItemDataChangeEvent += OnItemDataChangeEvent;
var totalCapacity = itemContainer.GetTotalCapacity();
var currentCapacity = ItemDisplayList.Count;
var capacityDifference = totalCapacity - currentCapacity;
var adjustedEndIndex = totalCapacity;
if (capacityDifference > 0)
{
//There are those that need to be added, and we create them.
//有需要添加的,我们创建他们。
for (var i = 0; i < capacityDifference; i++)
{
AddItemDisplay();
}
}
else if (capacityDifference < 0)
{
//There are things that need to be hidden
//有需要被隐藏的
adjustedEndIndex += capacityDifference;
var loopEndIndex = currentCapacity + capacityDifference;
for (var i = currentCapacity - 1; i >= loopEndIndex; i--)
{
var itemDisplay = ItemDisplayList[i];
itemDisplay.Update(null);
itemDisplay.HideSelf();
}
}
await Task.Yield();
UpdateData(itemContainer, adjustedEndIndex);
}
private void OnItemDataChangeEvent(ItemDataChangeEvent itemDataChangeEvent)
{
if (_itemContainer == null)
{
return;
}
UpdateDataForSingleLocation(_itemContainer, itemDataChangeEvent.NewIndex);
}
private void OnSelectedItemChangeEvent(SelectedItemChangeEvent selectedItemChangeEvent)
{
if (_itemContainer == null)
{
return;
}
UpdateDataForSingleLocation(_itemContainer, selectedItemChangeEvent.OldIndex);
UpdateDataForSingleLocation(_itemContainer, selectedItemChangeEvent.NewIndex);
}
/// <summary>
/// <para>Update data</para>
/// <para>更新数据</para>
/// </summary>
/// <remarks>
///<para>Used to batch update the Item data in itemContainer to the display.</para>
///<para>用于将itemContainer内的Item数据批量更新到显示器内。</para>
/// </remarks>
/// <param name="itemContainer">
///<para>Item container data</para>
///<para>物品容器数据</para>
/// </param>
/// <param name="endIndex">
///<para>endIndex</para>
///<para>结束位置</para>
/// </param>
/// <param name="startIndex">
///<para>startIndex</para>
///<para>起始位置</para>
/// </param>
private void UpdateData(IItemContainer itemContainer, int endIndex, int startIndex = 0)
{
for (var i = startIndex; i < endIndex; i++)
{
UpdateDataForSingleLocation(itemContainer, i);
}
}
/// <summary>
/// <para>Update data for a single location</para>
/// <para>更新单个位置的数据</para>
/// </summary>
/// <param name="itemContainer"></param>
/// <param name="index"></param>
private void UpdateDataForSingleLocation(IItemContainer itemContainer, int index)
{
var itemDisplay = ItemDisplayList[index];
var item = itemContainer.GetItem(index);
if (item == null)
{
item = itemContainer.GetPlaceHolderItem(index);
}
if (itemContainer.SupportSelect)
{
item.IsSelect = index == itemContainer.GetSelectIndex();
}
else
{
item.IsSelect = false;
}
itemDisplay.Update(item);
itemDisplay.ShowSelf();
}
/// <summary>
/// <para>Add item display</para>
/// <para>添加物品显示器</para>
/// </summary>
protected abstract void AddItemDisplay();
public IEnumerator<IItemDisplay> GetEnumerator()
{
return ItemDisplayList.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@ -0,0 +1,26 @@
using ColdMint.scripts.utils;
using Godot;
namespace ColdMint.scripts.inventory;
public class ItemSlotContainerDisplay(Node rootNode) : ItemContainerDisplayTemplate
{
private readonly PackedScene? _packedScene = GD.Load<PackedScene>("res://prefab/ui/ItemSlot.tscn");
protected override void AddItemDisplay()
{
if (_packedScene == null)
{
return;
}
var itemSlotNode = NodeUtils.InstantiatePackedScene<ItemSlotNode>(_packedScene);
if (itemSlotNode == null)
{
return;
}
ItemDisplayList.Add(itemSlotNode);
NodeUtils.CallDeferredAddChild(rootNode, itemSlotNode);
}
}

View File

@ -1,6 +1,3 @@
using System;
using ColdMint.scripts.debug;
using ColdMint.scripts.pickable;
using ColdMint.scripts.utils;
using Godot;
@ -10,27 +7,16 @@ namespace ColdMint.scripts.inventory;
/// <para>A slot in the inventory</para>
/// <para>物品栏内的一个插槽</para>
/// </summary>
public partial class ItemSlotNode : MarginContainer
public partial class ItemSlotNode : MarginContainer, IItemDisplay
{
private TextureRect? _backgroundTextureRect;
private TextureRect? _iconTextureRect;
private Label? _quantityLabel;
private Control? _control;
private bool _isSelect;
private Texture2D? _backgroundTexture;
private Texture2D? _backgroundTextureWhenSelect;
private IItem? _item;
public IItem? Item { get; private set; }
public IItem? Item
{
set
{
//Set item
//设置物品
_item = value;
UpdateAllDisplay();
}
}
public override void _Ready()
{
@ -42,253 +28,154 @@ public partial class ItemSlotNode : MarginContainer
_quantityLabel = GetNode<Label>("Control/QuantityLabel");
_control = GetNode<Control>("Control");
_quantityLabel.Hide();
UpdateBackground(_isSelect);
}
public override Variant _GetDragData(Vector2 atPosition)
{
if (_isSelect || _iconTextureRect == null)
switch (Item)
{
//Drag is not allowed if there is no icon or no pile of items.
//如果没有图标或者没有物品堆,那么不允许拖动。
return new Variant();
case null:
return Config.EmptyVariant;
case PlaceholderItem:
return Config.EmptyVariant;
}
if (_iconTextureRect == null)
{
return Config.EmptyVariant;
}
var textureRect = new TextureRect();
textureRect.ExpandMode = _iconTextureRect.ExpandMode;
textureRect.Size = _iconTextureRect.Size;
textureRect.Texture = _iconTextureRect.Texture;
textureRect.ZIndex = Config.ZIndexManager.FloatingIcon;
SetDragPreview(textureRect);
return Variant.CreateFrom(this);
}
public override bool _CanDropData(Vector2 atPosition, Variant data)
{
if (_isSelect)
{
//Do not place items in the selected item slot, even if the item slot is empty.
//禁止在已选中的物品槽内放置物品,即使物品槽是空的。
LogCat.Log("item_slot_is_selected_and_not_allowed");
return false;
}
//If the preplaced slot does not have an icon, the preplaced slot is not allowed.
//如果预放置的槽位没有图标,那么不允许放置。
if (_iconTextureRect == null)
{
return false;
}
var type = data.VariantType;
if (type == Variant.Type.Nil)
{
//The preplaced data is null.
//预放置的数据为null。
return false;
}
var itemSlotNode = data.As<ItemSlotNode>();
var item = itemSlotNode.GetItem();
if (item == null)
var sourceItem = itemSlotNode.Item;
if (sourceItem == null)
{
//Return null when trying to get the source item.
//尝试获取源物品时返回null。
return false;
}
if (item is Packsack packsack)
switch (Item)
{
if (_item == null)
case null:
return true;
case PlaceholderItem placeholderItem:
var placeholderItemContainer = placeholderItem.ItemContainer;
if (placeholderItemContainer == null)
{
//If the dragged item is a backpack and there are no items in the current slot, return whether the backpack is allowed.
//如果拖拽的物品是背包,且当前槽位没有物品,那么返回是否允许放置背包。
return BackpackAllowed;
return true;
}
if (packsack.ItemContainer == null)
return placeholderItemContainer.CanReplaceItem(placeholderItem.Index, sourceItem);
default:
var sourceItemSelfContainer = sourceItem.SelfItemContainer;
if (sourceItemSelfContainer != null)
{
LogCat.Log("item_container_is_null");
return false;
//Place the container on the item.
//将容器放在物品上。
return sourceItemSelfContainer.CanAddItem(Item);
}
return packsack.ItemContainer.CanAddItem(_item);
}
if (_item is Packsack currentPacksack)
var itemSelfContainer = Item.SelfItemContainer;
if (itemSelfContainer != null)
{
if (currentPacksack.ItemContainer == null)
{
LogCat.Log("item_container_is_null");
return false;
//Drag the item onto the container.
//将物品拖到容器上。
return itemSelfContainer.CanAddItem(sourceItem);
}
return currentPacksack.ItemContainer.CanAddItem(item);
return Item.MergeableItemCount(sourceItem, sourceItem.Quantity) > 0;
}
return CanAddItem(item);
}
/// <summary>
/// <para>Get the items in the item container</para>
/// <para>获取物品容器内的物品</para>
/// </summary>
/// <returns>
///<para>There may be multiple quantities</para>
///<para>数量可能有多个</para>
/// </returns>
public IItem? GetItem()
{
return _item;
}
/// <summary>
/// <para>CreateItemInstance</para>
/// <para>创建物品槽内的物品实例</para>
/// </summary>
/// <param name="number">
///<para>Quantity (pass in a value less than 0 to create an instance using all the quantities of built-in items)</para>
///<para>数量传入小于0的值使用内置物品的所有数量创建实例</para>
/// </param>
/// <returns>
///<para>Newly created item</para>
///<para>新创建的物品</para>
/// </returns>
public IItem? CreateItemInstance(int number)
{
if (number == 0)
{
return null;
}
if (_item is not Node2D node2D)
{
return null;
}
var duplicate = node2D.Duplicate();
if (node2D is PickAbleTemplate pickAbleTemplate)
{
pickAbleTemplate.CopyAttributes(duplicate);
}
if (duplicate is not Node2D newNode2D)
{
return null;
}
newNode2D.GlobalPosition = node2D.GlobalPosition;
if (duplicate is not IItem newItem)
{
return null;
}
if (number < 0)
{
newItem.Quantity = _item.Quantity;
}
else
{
newItem.Quantity = number >= _item.Quantity ? _item.Quantity : number;
}
return newItem;
}
/// <summary>
/// <para>Clean out the items in the item slot</para>
/// <para>清理物品槽内的物品</para>
/// </summary>
/// <param name="queueFree">
///<para>Whether to release a node</para>
///<para>是否释放节点</para>
/// </param>
/// <remarks>
///<para>Clean up item object references in item slots.</para>
///<para>清理物品槽内的物品对象引用。</para>
/// </remarks>
public void ClearItem(bool queueFree = true)
{
if (_item == null)
{
return;
}
if (queueFree && _item is Node node)
{
node.QueueFree();
}
_item = null;
UpdateAllDisplay();
}
public override void _DropData(Vector2 atPosition, Variant data)
{
if (_iconTextureRect == null)
//The item is empty and the corresponding item container cannot be retrieved.
//物品为空,无法获取对应的物品容器。
if (Item is null)
{
return;
}
var type = data.VariantType;
if (type == Variant.Type.Nil)
{
//The passed variable is null.
//传入的变量为null。
return;
}
var itemSlotNode = data.As<ItemSlotNode>();
var sourceItem = itemSlotNode.GetItem();
var sourceItem = itemSlotNode.Item;
if (sourceItem == null)
{
//Return null when trying to get the source item.
//尝试获取源物品时返回null。
return;
}
if (sourceItem is Packsack packsack)
if (Item.SelfItemContainer != null && Item.SelfItemContainer.CanAddItem(sourceItem))
{
//If the source item is a backpack.
//如果源物品是背包。
if (packsack.ItemContainer != null && _item != null)
//Use items and place them on the container.
//用物品,在物品容器上放置。
var oldIndex = sourceItem.Index;
var oldItemContainer = sourceItem.ItemContainer;
var addNumber = Item.SelfItemContainer.AddItem(sourceItem);
if (addNumber >= 0)
{
packsack.ItemContainer.AddItem(_item);
ClearItem(false);
return;
if (addNumber == sourceItem.Quantity)
{
oldItemContainer?.ClearItem(oldIndex);
}
else
{
oldItemContainer?.RemoveItem(oldIndex, addNumber);
}
}
if (_item is Packsack customPacksack)
{
if (customPacksack.ItemContainer == null)
{
return;
}
customPacksack.ItemContainer.AddItem(sourceItem);
itemSlotNode.ClearItem(false);
if (sourceItem.SelfItemContainer != null && sourceItem.SelfItemContainer.CanAddItem(Item))
{
//Use containers and place on top of items.
//用容器物品,在物品上放置。
var oldIndex = Item.Index;
var oldItemContainer = Item.ItemContainer;
var addNumber = sourceItem.SelfItemContainer.AddItem(Item);
if (addNumber >= 0)
{
if (addNumber == Item.Quantity)
{
oldItemContainer?.ClearItem(oldIndex);
}
else
{
oldItemContainer?.RemoveItem(oldIndex, addNumber);
}
}
return;
}
AddItem(sourceItem);
itemSlotNode.ClearItem(false);
if (Item is PlaceholderItem placeholderItem)
{
var placeholderItemContainer = placeholderItem.ItemContainer;
var sourceItemContainer = sourceItem.ItemContainer;
var sourceItemIndex = sourceItem.Index;
var replaceResult = false;
if (placeholderItemContainer != null)
{
replaceResult = placeholderItemContainer.ReplaceItem(placeholderItem.Index, sourceItem);
}
if (replaceResult && sourceItemContainer != null)
{
sourceItemContainer.ClearItem(sourceItemIndex);
}
}
}
/// <summary>
/// <para>Whether to place a backpack in the current slot</para>
/// <para>当前槽位是否允许放置背包</para>
/// </summary>
public bool BackpackAllowed { get; set; }
public bool IsSelect
{
get => _isSelect;
set
{
UpdateBackground(value);
_isSelect = value;
}
}
private void UpdateBackground(bool isSelect)
{
@ -302,24 +189,6 @@ public partial class ItemSlotNode : MarginContainer
public TextureRect? BackgroundTextureRect => _backgroundTextureRect;
/// <summary>
/// <para>Whether the item in this node is empty</para>
/// <para>此节点内的物品是否为空的</para>
/// </summary>
/// <returns>
///<para>Return true if the number of items is 0 or the item object does not exist</para>
///<para>当物品数量为0或物品对象不存在时返回true</para>
/// </returns>
public bool IsEmpty()
{
if (_item == null || _item.Quantity == 0)
{
return true;
}
return false;
}
/// <summary>
/// <para>Update all displays of this slot</para>
@ -338,7 +207,7 @@ public partial class ItemSlotNode : MarginContainer
/// </summary>
private void UpdateTooltipText()
{
if (_item == null)
if (Item is PlaceholderItem or null)
{
TooltipText = null;
return;
@ -349,16 +218,16 @@ public partial class ItemSlotNode : MarginContainer
var debugText = TranslationServerUtils.Translate("item_prompt_debug");
if (debugText != null)
{
TooltipText = string.Format(debugText, _item.Id,
TranslationServerUtils.Translate(_item.Name),
_item.Quantity, _item.MaxQuantity, _item.GetType().Name,
TranslationServerUtils.Translate(_item.Description));
TooltipText = string.Format(debugText, Item.Id,
TranslationServerUtils.Translate(Item.Name),
Item.Quantity, Item.MaxQuantity, Item.GetType().Name,
TranslationServerUtils.Translate(Item.Description));
}
}
else
{
TooltipText = TranslationServerUtils.Translate(_item.Name) + "\n" +
TranslationServerUtils.Translate(_item.Description);
TooltipText = TranslationServerUtils.Translate(Item.Name) + "\n" +
TranslationServerUtils.Translate(Item.Description);
}
}
@ -373,7 +242,13 @@ public partial class ItemSlotNode : MarginContainer
return;
}
switch (_item?.Quantity)
if (Item is PlaceholderItem or null)
{
_quantityLabel.Hide();
return;
}
switch (Item?.Quantity)
{
case null or 1:
_quantityLabel.Hide();
@ -381,7 +256,7 @@ public partial class ItemSlotNode : MarginContainer
default:
//When the quantity is not null or 1, we display the quantity.
//当数量不为null或1时我们显示数量
_quantityLabel.Text = _item?.Quantity.ToString();
_quantityLabel.Text = Item?.Quantity.ToString();
_quantityLabel.Show();
break;
}
@ -396,94 +271,24 @@ public partial class ItemSlotNode : MarginContainer
{
if (_iconTextureRect != null)
{
_iconTextureRect.Texture = _item?.Icon;
_iconTextureRect.Texture = Item?.Icon;
}
}
public bool CanAddItem(IItem item)
{
if (!BackpackAllowed && item is Packsack)
{
//如果禁止放置背包,且新物品是背包
LogCat.Log("backpack_not_allowed");
return false;
}
if (_item == null)
{
//If there is no item in the current item slot, it is allowed to add.
//如果当前物品槽内没物品,那么允许添加。
LogCat.Log("item_is_null");
return true;
}
if (item.Id != _item.Id)
{
//If the item ID you want to add is different from the current item ID, disable.
//如果要添加的物品ID和当前的物品ID不一样那么禁止。
LogCat.Log("item_id_not_same");
return false;
}
var newQuantity = item.Quantity + _item.Quantity;
if (newQuantity > _item.MaxQuantity)
{
//The maximum number is exceeded and items cannot be added.
//超过了最大数量,无法添加物品。
LogCat.Log("max_quantity_exceeded");
return false;
}
return true;
}
/// <summary>
/// <para>Remove items from the item slot</para>
/// <para>从物品槽内移除物品</para>
/// </summary>
/// <param name="number">
///<para>number(Less than 0, remove all items)</para>
///<para>物品数量(小于0则移除全部物品)</para>
/// </param>
/// <returns>
///<para>How many items were actually removed</para>
///<para>实际移除了多少个物品</para>
/// </returns>
public int RemoveItem(int number)
{
if (_item == null)
{
return 0;
}
//The number of actual removals
//实际移除的数量
var removeNumber = number < 0 ? _item.Quantity : number;
_item.Quantity -= removeNumber;
if (_item.Quantity <= 0)
{
ClearItem();
}
return removeNumber;
}
public bool AddItem(IItem item)
{
if (_item == null)
public void Update(IItem? item)
{
Item = item;
return true;
UpdateAllDisplay();
UpdateBackground(item is { IsSelect: true });
}
var newQuantity = item.Quantity + _item.Quantity;
_item.Quantity = Math.Min(newQuantity, _item.MaxQuantity);
if (item is Node2D node2D)
public void ShowSelf()
{
node2D.QueueFree();
Show();
}
UpdateQuantityLabel();
return true;
public void HideSelf()
{
Hide();
}
}

View File

@ -97,16 +97,4 @@ public static class ItemTypeManager
Registry.TryGetValue(id, out var itemType)
? itemType.Icon ?? DefaultTexture
: DefaultTexture;
/// <summary>
/// <para>Gets the maximum number of stacks for an item</para>
/// <para>获取某个物品的最大堆叠数量</para>
/// </summary>
/// <param name="id">
///<para>id</para>
///<para>物品ID</para>
/// </param>
/// <returns></returns>
public static int MaxStackQuantityOf(string id) =>
Registry.TryGetValue(id, out var itemType) ? itemType.MaxStackQuantity : 0;
}

View File

@ -1,4 +1,4 @@
using ColdMint.scripts.pickable;
using ColdMint.scripts.pickable;
using ColdMint.scripts.utils;
using Godot;
using PacksackUi = ColdMint.scripts.loader.uiLoader.PacksackUi;
@ -12,55 +12,54 @@ namespace ColdMint.scripts.inventory;
public partial class Packsack : PickAbleTemplate
{
private const string Path = "res://prefab/ui/packsackUI.tscn";
[Export] public int NumberSlots { get; set; }
/// <summary>
/// <para>Whether to allow backpacks</para>
/// <para>是否允许放置背包</para>
/// </summary>
/// <remarks>
///<para>Can a new backpack be placed in the slot of the backpack?</para>
///<para>即此背包的槽位内是否可以再放置新的背包?</para>
/// </remarks>
[Export]
public bool BackpackAllowed { get; set; }
public override bool CanPutInPack => false;
public override void Use(Node2D? owner, Vector2 targetGlobalPosition)
public override int ItemType
{
GameSceneDepend.DynamicUiGroup?.ShowControl(Path, control =>
get => Config.ItemType.Packsack;
}
[Export] public int NumberSlots { get; set; }
public override bool Use(Node2D? owner, Vector2 targetGlobalPosition)
{
if (GameSceneDepend.DynamicUiGroup == null)
{
return false;
}
return GameSceneDepend.DynamicUiGroup.ShowControl(Path, control =>
{
if (control is PacksackUi packsackUi)
{
packsackUi.Title = Name;
packsackUi.ItemContainer = ItemContainer;
packsackUi.ItemContainer = SelfItemContainer;
}
});
}
public IItemContainer? ItemContainer { get; private set; }
protected override void OnSelectChange(bool isSelected)
{
if (isSelected)
{
return;
}
GameSceneDepend.DynamicUiGroup?.HideControl(Path);
}
public override void OnThrow(Vector2 velocity)
{
GameSceneDepend.DynamicUiGroup?.HideControl(Path);
}
public override void _Ready()
{
base._Ready();
ItemContainer = new UniversalItemContainer();
ItemContainer.SupportSelect = false;
//When the backpack is created, the item slot is generated.
//当背包被创建时,物品槽就被生成出来了。
for (var i = 0; i < NumberSlots; i++)
if (SelfItemContainer == null)
{
var itemSlotNode = ItemContainer.AddItemSlot(this);
if (itemSlotNode == null)
{
continue;
var universalItemContainer = new UniversalItemContainer(NumberSlots);
universalItemContainer.AllowItemTypesExceptPlaceholder();
universalItemContainer.DisallowAddingItemByType(Config.ItemType.Packsack);
SelfItemContainer = universalItemContainer;
SelfItemContainer.SupportSelect = false;
}
itemSlotNode.BackpackAllowed = BackpackAllowed;
itemSlotNode.Hide();
}
GameSceneDepend.DynamicUiGroup?.RegisterControl(Path, () =>
{
var packedScene = GD.Load<PackedScene>(Path);

View File

@ -0,0 +1,61 @@
using Godot;
namespace ColdMint.scripts.inventory;
/// <summary>
/// <para>PlaceholderItem</para>
/// <para>占位物品</para>
/// </summary>
public class PlaceholderItem : IItem
{
public int Index { get; set; }
public string Id { get; set; }
public void ShowSelf()
{
}
public void QueueFreeSelf()
{
}
public void HideSelf()
{
}
public Texture2D Icon { get; }
public string Name { get; }
public string? Description { get; } = null;
public int Quantity { get; set; } = 1;
public int MaxQuantity { get; } = 1;
public int ItemType
{
get => Config.ItemType.Placeholder;
}
public bool IsSelect { get; set; }
public bool CanContainItems { get; set; } = false;
public IItemContainer? ItemContainer { get; set; }
public IItemContainer? SelfItemContainer { get; set; }
public int MergeableItemCount(IItem other, int unallocatedQuantity)
{
return 0;
}
public IItem? CreateItem(int number)
{
return null;
}
public bool Use(Node2D? owner, Vector2 targetGlobalPosition)
{
return false;
}
public void OnThrow(Vector2 velocity)
{
}
}

View File

@ -1,11 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ColdMint.scripts.map.events;
using ColdMint.scripts.utils;
using Godot;
using JetBrains.Annotations;
namespace ColdMint.scripts.inventory;
@ -13,11 +9,9 @@ namespace ColdMint.scripts.inventory;
/// <para>UniversalItemContainer</para>
/// <para>通用的物品容器</para>
/// </summary>
public class UniversalItemContainer : IItemContainer
public class UniversalItemContainer(int totalCapacity) : IItemContainer
{
private readonly List<ItemSlotNode>? _itemSlotNodes = [];
private readonly PackedScene? _itemSlotPackedScene = GD.Load<PackedScene>("res://prefab/ui/ItemSlot.tscn");
private readonly Dictionary<int, IItem> _itemDictionary = [];
/// <summary>
/// <para>UnknownIndex</para>
@ -25,167 +19,383 @@ public class UniversalItemContainer : IItemContainer
/// </summary>
private const int UnknownIndex = -1;
//_selectIndex defaults to 0.
//_selectIndex默认为0.
private int _selectIndex;
[MustDisposeResource]
public IEnumerator<ItemSlotNode> GetEnumerator()
/// <summary>
/// <para>The next available index</para>
/// <para>下个可用的索引</para>
/// </summary>
/// <remarks>
///<para>For example, the variable [1,2,3,5,6] represents 4, or the variable [1,2,3,4,5,6,7] represents 8.</para>
///<para>例如[1,2,3,5,6]这个变量表示4再或者[1,2,3,4,5,6,7]这个变量表示8。</para>
/// </remarks>
private int _nextAvailableIndex;
/// <summary>
/// <para>The type of item that can be added to the item container</para>
/// <para>物品容器允许添加的物品类型</para>
/// </summary>
private readonly HashSet<int> _allowedItemTypes = new();
public Action<SelectedItemChangeEvent>? SelectedItemChangeEvent { get; set; }
public Action<ItemDataChangeEvent>? ItemDataChangeEvent { get; set; }
/// <summary>
/// <para>Allow Item Types Except Placeholder</para>
/// <para>允许添加除占位符以外的所有物品类型</para>
/// </summary>
public void AllowItemTypesExceptPlaceholder()
{
return _itemSlotNodes?.GetEnumerator() ?? Enumerable.Empty<ItemSlotNode>().GetEnumerator();
var itemTypeType = typeof(Config.ItemType);
//Get all fields
//获取所有字段
var fields = itemTypeType.GetFields(BindingFlags.Public | BindingFlags.Static);
//Traversal field
//遍历字段
foreach (var field in fields)
{
//Gets the value of the field
//获取字段的值
var value = field.GetValue(null);
if (value == null)
{
continue;
}
var intValue = (int)value;
if (intValue == Config.ItemType.Placeholder)
{
continue;
}
_allowedItemTypes.Add(intValue);
}
}
[MustDisposeResource]
IEnumerator IEnumerable.GetEnumerator()
public void AllowAddingItemByType(int itemType)
{
return GetEnumerator();
_allowedItemTypes.Add(itemType);
}
public Action<SelectedItemSlotChangeEvent>? SelectedItemSlotChangeEvent { get; set; }
public void DisallowAddingItemByType(int itemType)
{
_allowedItemTypes.Remove(itemType);
}
public bool CanAddItem(IItem item)
{
return Match(item) != null;
}
public bool AddItem(IItem item)
{
var itemSlotNode = Match(item);
if (itemSlotNode == null)
if (!_allowedItemTypes.Contains(item.ItemType))
{
return false;
}
//If the capacity is not full, directly return to add items
//如果未占满容量,直接返回可添加物品
if (GetUsedCapacity() < totalCapacity)
{
return true;
}
return itemSlotNode.AddItem(item);
if (item.MaxQuantity == 1)
{
//New items do not support overlay, capacity is full, return cannot add.
//新物品不支持叠加,容量已满,返回不能添加。
return false;
}
//If the capacity is full, we calculate whether we can spread the new items evenly among the existing items.
//如果容量占满了,我们计算是否能将新物品均摊在已有的物品内。
var unallocatedQuantity = item.Quantity;
foreach (var unitItem in _itemDictionary.Values)
{
var number = unitItem.MergeableItemCount(item, unallocatedQuantity);
if (number == 0)
{
continue;
}
unallocatedQuantity -= number;
if (unallocatedQuantity < 1)
{
return true;
}
}
return unallocatedQuantity < 1;
}
private void UpdateSelectStatus(int index, IItem item)
{
item.IsSelect = index == _selectIndex;
}
/// <summary>
/// <para>Update the next available index location</para>
/// <para>更新下个可用的索引位置</para>
/// </summary>
private void UpdateNextAvailableIndex()
{
_nextAvailableIndex = UnknownIndex;
if (totalCapacity <= 0)
{
return;
}
for (var i = 0; i < totalCapacity; i++)
{
var contains = _itemDictionary.ContainsKey(i);
if (!contains)
{
_nextAvailableIndex = i;
return;
}
}
}
public int AddItem(IItem item)
{
if (item.MaxQuantity == 1)
{
if (_nextAvailableIndex == UnknownIndex)
{
return 0;
}
var nextAvailableIndex = _nextAvailableIndex;
_itemDictionary[nextAvailableIndex] = item;
item.Index = nextAvailableIndex;
item.ItemContainer = this;
if (nextAvailableIndex != _selectIndex)
{
item.HideSelf();
}
UpdateNextAvailableIndex();
UpdateSelectStatus(nextAvailableIndex, item);
ItemDataChangeEvent?.Invoke(new ItemDataChangeEvent
{
NewItem = item,
NewIndex = nextAvailableIndex,
Type = Config.ItemDataChangeEventType.QuantityAdded
});
return item.Quantity;
}
//There can be more than one item, try to share equally.
//物品可有多个,尝试均摊。
var originalQuantity = item.Quantity;
var temporarilyQuantity = item.Quantity;
var index = 0;
foreach (var unitItem in _itemDictionary.Values)
{
var number = unitItem.MergeableItemCount(item, temporarilyQuantity);
if (number == 0)
{
continue;
}
temporarilyQuantity -= number;
unitItem.Quantity += number;
ItemDataChangeEvent?.Invoke(new ItemDataChangeEvent
{
NewItem = unitItem,
NewIndex = index,
Type = Config.ItemDataChangeEventType.QuantityAdded
});
if (item.Quantity < 1)
{
//New items are fully shared.
//新物品完全被均摊。
item.HideSelf();
return originalQuantity;
}
index++;
}
//New items have some left over.
//新物品有一些剩余。
if (GetUsedCapacity() >= totalCapacity)
{
//The capacity is full. The remaining capacity cannot be stored.
//容量已满,无法存放剩余。
return originalQuantity - temporarilyQuantity;
}
//Add the rest to the container.
//添加剩余到容器内。
if (_nextAvailableIndex == UnknownIndex)
{
return 0;
}
var finalNextAvailableIndex = _nextAvailableIndex;
_itemDictionary[finalNextAvailableIndex] = item;
item.Index = finalNextAvailableIndex;
item.ItemContainer = this;
if (finalNextAvailableIndex != _selectIndex)
{
item.HideSelf();
}
UpdateNextAvailableIndex();
UpdateSelectStatus(finalNextAvailableIndex, item);
ItemDataChangeEvent?.Invoke(new ItemDataChangeEvent
{
NewItem = item,
NewIndex = finalNextAvailableIndex,
Type = Config.ItemDataChangeEventType.Add
});
return originalQuantity;
}
public bool SupportSelect { get; set; }
public IItem GetPlaceHolderItem(int index)
{
var placeholderItem = new PlaceholderItem
{
Index = index,
ItemContainer = this
};
return placeholderItem;
}
public int GetSelectIndex()
{
return _selectIndex;
}
public ItemSlotNode? GetSelectItemSlotNode()
public IItem? GetSelectItem()
{
if (_itemSlotNodes == null || _itemSlotNodes.Count == 0)
{
return null;
return _itemDictionary.TryGetValue(_selectIndex, out var item) ? item : null;
}
if (_selectIndex < _itemSlotNodes.Count)
public IItem? GetItem(int index)
{
//Prevent subscripts from going out of bounds.
//防止下标越界。
return _itemSlotNodes[_selectIndex];
return _itemDictionary.TryGetValue(index, out var item) ? item : null;
}
return null;
public bool ReplaceItem(int index, IItem item)
{
var oldItem = GetItem(index);
_itemDictionary[index] = item;
item.Index = index;
item.ItemContainer = this;
ItemDataChangeEvent?.Invoke(new ItemDataChangeEvent
{
NewItem = item,
NewIndex = index,
OldIndex = index,
OldItem = oldItem,
Type = Config.ItemDataChangeEventType.Replace
});
return true;
}
public int RemoveItemFromItemSlotBySelectIndex(int number) => RemoveItemFromItemSlot(_selectIndex, number);
public int GetItemSlotCount()
public bool CanReplaceItem(int index, IItem item)
{
if (_itemSlotNodes == null)
if (!_allowedItemTypes.Contains(item.ItemType))
{
return false;
}
return true;
}
public bool ClearItem(int index)
{
if (!_itemDictionary.TryGetValue(index, out var item))
{
return false;
}
var result = _itemDictionary.Remove(index);
if (result)
{
ItemDataChangeEvent?.Invoke(new ItemDataChangeEvent
{
NewItem = null,
NewIndex = index,
OldIndex = index,
OldItem = null,
Type = Config.ItemDataChangeEventType.Clear
});
if (SupportSelect && index == _selectIndex)
{
item.HideSelf();
SelectedItemChangeEvent?.Invoke(new SelectedItemChangeEvent()
{
NewIndex = index,
OldIndex = index,
NewItem = null,
OldItem = null
});
}
}
return result;
}
public int RemoveSelectItem(int number)
{
return RemoveItem(_selectIndex, number);
}
public int RemoveItem(int itemIndex, int number)
{
if (number == 0)
{
return 0;
}
return _itemSlotNodes.Count;
if (!_itemDictionary.TryGetValue(itemIndex, out var item))
{
return 0;
}
public ItemSlotNode? GetItemSlotNode(int index)
var originalQuantity = item.Quantity;
if (number < 0)
{
if (_itemSlotNodes == null)
//If the number entered is less than 0, all items are removed.
//输入的数量小于0,则移除全部物品。
item.Quantity = 0;
_itemDictionary.Remove(itemIndex);
UpdateNextAvailableIndex();
ItemDataChangeEvent?.Invoke(new ItemDataChangeEvent
{
return null;
NewItem = item,
NewIndex = itemIndex,
Type = Config.ItemDataChangeEventType.Remove
});
return originalQuantity;
}
var safeIndex = GetSafeIndex(index);
return _itemSlotNodes[safeIndex];
var removed = Math.Min(number, item.Quantity);
item.Quantity -= removed;
if (item.Quantity < 1)
{
_itemDictionary.Remove(itemIndex);
UpdateNextAvailableIndex();
ItemDataChangeEvent?.Invoke(new ItemDataChangeEvent
{
NewItem = item,
NewIndex = itemIndex,
Type = Config.ItemDataChangeEventType.Remove
});
}
public int RemoveItemFromItemSlot(int itemSlotIndex, int number)
{
if (_itemSlotNodes == null) return number;
var safeIndex = GetSafeIndex(itemSlotIndex);
if (safeIndex == UnknownIndex)
{
return number;
return removed;
}
var itemSlot = _itemSlotNodes[safeIndex];
return itemSlot.RemoveItem(number);
public int GetUsedCapacity()
{
return _itemDictionary.Count;
}
/// <summary>
/// <para>Gets a secure subscript index</para>
/// <para>获取安全的下标索引</para>
/// </summary>
/// <param name="itemSlotIndex"></param>
/// <returns>
/// <para>-1 is returned on failure, and the index that does not result in an out-of-bounds subscript is returned on success</para>
/// <para>失败返回-1成功返回不会导致下标越界的索引</para>
/// </returns>
private int GetSafeIndex(int itemSlotIndex)
public int GetTotalCapacity()
{
if (_itemSlotNodes == null)
{
return UnknownIndex;
return totalCapacity;
}
var count = _itemSlotNodes.Count;
if (count == 0)
{
//Prevents the dividend from being 0
//防止被除数为0
return UnknownIndex;
}
return itemSlotIndex % count;
}
public ItemSlotNode? Match(IItem item)
public void SelectNextItem()
{
//Find and return the first slot that can hold this item, if the list is null or not found, return null
//寻找并返回第一个遇到的可放置此物品的物品槽若列表为空或不存在将返回null
return _itemSlotNodes?.FirstOrDefault(itemSlotNode => itemSlotNode.CanAddItem(item));
}
public ItemSlotNode? AddItemSlot(Node rootNode)
{
if (_itemSlotNodes == null || _itemSlotPackedScene == null)
{
return null;
}
var itemSlotNode = NodeUtils.InstantiatePackedScene<ItemSlotNode>(_itemSlotPackedScene);
if (itemSlotNode == null)
{
return null;
}
NodeUtils.CallDeferredAddChild(rootNode, itemSlotNode);
if (SupportSelect)
{
itemSlotNode.IsSelect = _itemSlotNodes.Count == _selectIndex;
}
else
{
itemSlotNode.IsSelect = false;
}
_itemSlotNodes.Add(itemSlotNode);
return itemSlotNode;
}
public void SelectTheNextItemSlot()
{
if (_itemSlotNodes == null)
{
return;
}
var count = _itemSlotNodes.Count;
var count = totalCapacity;
if (count == 0)
{
return;
@ -198,17 +408,12 @@ public class UniversalItemContainer : IItemContainer
newSelectIndex = 0;
}
PrivateSelectItemSlot(oldSelectIndex, newSelectIndex);
PrivateSelectItem(oldSelectIndex, newSelectIndex);
}
public void SelectThePreviousItemSlot()
public void SelectPreviousItem()
{
if (_itemSlotNodes == null)
{
return;
}
var count = _itemSlotNodes.Count;
var count = totalCapacity;
if (count == 0)
{
return;
@ -221,77 +426,57 @@ public class UniversalItemContainer : IItemContainer
newSelectIndex = count - 1;
}
PrivateSelectItemSlot(oldSelectIndex, newSelectIndex);
PrivateSelectItem(oldSelectIndex, newSelectIndex);
}
/// <summary>
/// <para>Select an item slot</para>
/// <para>选中某个物品槽</para>
/// <para>Private methods for selecting items</para>
/// <para>选择物品的私有方法</para>
/// </summary>
private void PrivateSelectItemSlot(int oldSelectIndex, int newSelectIndex)
/// <param name="oldIndex"></param>
/// <param name="newIndex"></param>
private void PrivateSelectItem(int oldIndex, int newIndex)
{
if (!SupportSelect || _itemSlotNodes == null || oldSelectIndex == newSelectIndex)
if (!SupportSelect || oldIndex == newIndex)
{
return;
}
var oldItemSlotNode = _itemSlotNodes[oldSelectIndex];
oldItemSlotNode.IsSelect = false;
var newItemSlotNode = _itemSlotNodes[newSelectIndex];
newItemSlotNode.IsSelect = true;
HideItem(oldSelectIndex);
DisplayItem(newSelectIndex);
SelectedItemSlotChangeEvent?.Invoke(new SelectedItemSlotChangeEvent
//There is no need to broadcast placeholders when an event is invoked.
//在调用事件时,无需广播占位符。
var oldItem = GetItem(oldIndex);
if (oldItem != null)
{
NewItemSlotNode = newItemSlotNode,
OldItemSlotNode = oldItemSlotNode
oldItem.HideSelf();
oldItem.IsSelect = false;
}
//There is no need to broadcast placeholders when an event is invoked.
//在调用事件时,无需广播占位符。
var newItem = GetItem(newIndex);
if (newItem != null)
{
newItem.ShowSelf();
newItem.IsSelect = true;
}
_selectIndex = newIndex;
SelectedItemChangeEvent?.Invoke(new SelectedItemChangeEvent
{
NewIndex = newIndex,
OldIndex = oldIndex,
NewItem = newItem,
OldItem = oldItem
});
_selectIndex = newSelectIndex;
}
/// <summary>
/// <para>HideItem</para>
/// <para>隐藏某个物品</para>
/// </summary>
/// <param name="index"></param>
private void HideItem(int index)
{
var oldItem = _itemSlotNodes?[index].GetItem();
if (oldItem is not Node2D oldNode2D) return;
oldNode2D.ProcessMode = Node.ProcessModeEnum.Disabled;
oldNode2D.Hide();
}
/// <summary>
/// <para>Displays the items in an item slot</para>
/// <para>显示某个物品槽内的物品</para>
/// </summary>
/// <remarks>
///<para>This method can also be used to refresh items held by the character, for example when a new item is dragged to the current display location, then call this method to refresh items held by the character.</para>
///<para>此方法也可用于刷新角色手上持有的物品,例如当新的物品被拖动到当前显示位置,那么请调用此方法刷新角色持有的物品。</para>
/// </remarks>
/// <param name="index"></param>
private void DisplayItem(int index)
public void SelectItem(int index)
{
var item = _itemSlotNodes?[index].GetItem();
if (item is not Node2D newNode2D) return;
newNode2D.ProcessMode = Node.ProcessModeEnum.Inherit;
newNode2D.Show();
}
public void SelectItemSlot(int newSelectIndex)
{
if (newSelectIndex == _selectIndex)
if (totalCapacity == 0 || index < 0)
{
return;
}
var safeIndex = GetSafeIndex(newSelectIndex);
if (safeIndex == UnknownIndex)
{
return;
}
PrivateSelectItemSlot(_selectIndex, newSelectIndex);
PrivateSelectItem(_selectIndex, index % totalCapacity);
}
}

View File

@ -45,6 +45,10 @@ public partial class GameSceneLoader : SceneLoaderTemplate
//加载抛射体容器
var projectileContainer = GetNode<Node2D>("ProjectileContainer");
GameSceneDepend.ProjectileContainer = projectileContainer;
//Load magic container
//加载魔术容器
var magicContainer = GetNode<Node2D>("SpellContainer");
GameSceneDepend.SpellContainer = magicContainer;
//Load Packsack container
//加载背包容器
var packsackContainer = GetNode<Node2D>("PacksackContainer");

View File

@ -1,5 +1,4 @@
using ColdMint.scripts.inventory;
using ColdMint.scripts.utils;
using Godot;
namespace ColdMint.scripts.loader.uiLoader;
@ -22,6 +21,8 @@ public partial class PacksackUi : UiLoaderTemplate
private Button? _exitButton;
private IItemContainerDisplay? _itemContainerDisplay;
/// <summary>
/// <para>title</para>
/// <para>标题</para>
@ -46,29 +47,10 @@ public partial class PacksackUi : UiLoaderTemplate
set
{
_itemContainer = value;
PlaceItemSlot(value);
BindItemContainer();
}
}
/// <summary>
/// <para>Place item slots according to item information</para>
/// <para>根据物品信息放置物品槽</para>
/// </summary>
/// <param name="itemContainer"></param>
private void PlaceItemSlot(IItemContainer? itemContainer)
{
if (_hFlowContainer == null || itemContainer == null)
{
return;
}
NodeUtils.DeleteAllChild(_hFlowContainer);
foreach (var itemSlotNode in itemContainer)
{
itemSlotNode.Reparent(_hFlowContainer);
itemSlotNode.Show();
}
}
/// <summary>
/// <para>SetTile</para>
@ -90,14 +72,24 @@ public partial class PacksackUi : UiLoaderTemplate
_packedScene = GD.Load<PackedScene>("res://prefab/ui/ItemSlot.tscn");
}
private void BindItemContainer()
{
if (ItemContainer == null)
{
return;
}
_itemContainerDisplay?.BindItemContainer(ItemContainer);
}
public override void InitializeUi()
{
_hFlowContainer = GetNode<HFlowContainer>("HFlowContainer");
_titleLabel = GetNode<Label>("TitleLabel");
_exitButton = GetNode<Button>("ExitButton");
_itemContainerDisplay = new ItemSlotContainerDisplay(_hFlowContainer);
//If the item container was set before this node was placed in the node tree, load it here.
//若物品容器在此节点放置到节点树之前被设置了,那么在这里加载。
PlaceItemSlot(_itemContainer);
BindItemContainer();
SetTile(_title);
}

View File

@ -0,0 +1,66 @@
using ColdMint.scripts.inventory;
using ColdMint.scripts.map.events;
using Godot;
namespace ColdMint.scripts.loader.uiLoader;
/// <summary>
/// <para>SpellEditorUi</para>
/// <para>法术编辑器UI</para>
/// </summary>
public partial class SpellEditorUi : UiLoaderTemplate
{
private Button? _exitButton;
private IItemContainer? _projectileWeaponContainer;
private ItemSlotNode? _itemSlot;
private HFlowContainer? _flowContainer;
private IItemContainerDisplay? _itemContainerDisplay;
public override void InitializeUi()
{
_exitButton = GetNode<Button>("ExitButton");
_itemSlot = GetNode<ItemSlotNode>("ItemSlot");
_projectileWeaponContainer = new UniversalItemContainer(1);
_projectileWeaponContainer.AllowAddingItemByType(Config.ItemType.ProjectileWeapon);
_projectileWeaponContainer.ItemDataChangeEvent += OnItemDataChangeEvent;
_itemSlot.Update(_projectileWeaponContainer.GetPlaceHolderItem(0));
_flowContainer = GetNode<HFlowContainer>("HFlowContainer");
_itemContainerDisplay = new ItemSlotContainerDisplay(_flowContainer);
}
private void OnItemDataChangeEvent(ItemDataChangeEvent itemDataChangeEvent)
{
if (_itemSlot == null || _projectileWeaponContainer == null)
{
return;
}
var item = itemDataChangeEvent.NewItem;
if (item == null)
{
item = _projectileWeaponContainer.GetPlaceHolderItem(itemDataChangeEvent.NewIndex);
}
item.IsSelect = false;
_itemSlot.Update(item);
_itemContainerDisplay?.BindItemContainer(item.SelfItemContainer);
}
public override void _ExitTree()
{
base._ExitTree();
if (_projectileWeaponContainer != null)
{
_projectileWeaponContainer.ItemDataChangeEvent -= OnItemDataChangeEvent;
}
}
public override void LoadUiActions()
{
if (_exitButton != null)
{
_exitButton.Pressed += () =>
{
GameSceneDepend.DynamicUiGroup?.HideControl(this);
};
}
}
}

View File

@ -1,4 +1,5 @@
using ColdMint.scripts.inventory;
using ColdMint.scripts.debug;
using ColdMint.scripts.inventory;
using ColdMint.scripts.map.events;
using Godot;
@ -15,7 +16,6 @@ public partial class ItemSpawn : Marker2D
public override void _Ready()
{
base._Ready();
EventBus.MapGenerationCompleteEvent += MapGenerationCompleteEvent;
}
@ -29,6 +29,7 @@ public partial class ItemSpawn : Marker2D
}
var item = ItemTypeManager.CreateItem(ItemId, this);
LogCat.LogWithFormat("generated_item_is_empty",LogCat.LogLabel.ItemSpawn,true,ItemId,item == null);
if (item is Node2D node2D)
{
node2D.GlobalPosition = GlobalPosition;

View File

@ -0,0 +1,16 @@
using ColdMint.scripts.inventory;
namespace ColdMint.scripts.map.events;
/// <summary>
/// <para>Item Data Change Event</para>
/// <para>项目数据改变事件</para>
/// </summary>
public class ItemDataChangeEvent
{
public Config.ItemDataChangeEventType Type { get; set; }
public int OldIndex { get; set; }
public int NewIndex { get; set; }
public IItem? NewItem { get; set; }
public IItem? OldItem { get; set; }
}

View File

@ -0,0 +1,26 @@
using ColdMint.scripts.inventory;
namespace ColdMint.scripts.map.events;
/// <summary>
/// <para>Selected item slot changes event</para>
/// <para>选中的物品槽改变事件</para>
/// </summary>
public class SelectedItemChangeEvent
{
public int NewIndex { get; set; }
public int OldIndex { get; set; }
/// <summary>
/// <para>Newly selected item</para>
/// <para>新选中的物品</para>
/// </summary>
public IItem? NewItem { get; set; }
/// <summary>
/// <para>Lost the selected item</para>
/// <para>失去选中的物品</para>
/// </summary>
public IItem? OldItem { get; set; }
}

View File

@ -1,24 +0,0 @@
using ColdMint.scripts.inventory;
namespace ColdMint.scripts.map.events;
/// <summary>
/// <para>Selected item slot changes event</para>
/// <para>选中的物品槽改变事件</para>
/// </summary>
public class SelectedItemSlotChangeEvent
{
/// <summary>
/// <para></para>
/// <para>新选中的物品槽</para>
/// </summary>
public ItemSlotNode? NewItemSlotNode { get; set; }
/// <summary>
/// <para>Lost the selected item slot</para>
/// <para>失去选中的物品槽</para>
/// </summary>
// ReSharper disable UnusedAutoPropertyAccessor.Global
public ItemSlotNode? OldItemSlotNode { get; set; }
// ReSharper restore UnusedAutoPropertyAccessor.Global
}

View File

@ -15,10 +15,27 @@ namespace ColdMint.scripts.pickable;
/// </summary>
public partial class PickAbleTemplate : RigidBody2D, IItem
{
public int Index { get; set; }
//Do not export this field because the ID is specified within yaml.
//不要导出此字段因为ID是在yaml内指定的。
public virtual string Id { get; set; } = "ID";
[Export] protected Texture2D? UniqueIcon { get; set; }
public void ShowSelf()
{
Show();
}
public void QueueFreeSelf()
{
QueueFree();
}
public void HideSelf()
{
Hide();
}
public Texture2D Icon => UniqueIcon ?? ItemTypeManager.DefaultIconOf(Id);
public new string Name
@ -30,8 +47,6 @@ public partial class PickAbleTemplate : RigidBody2D, IItem
}
}
public virtual bool CanPutInPack => true;
/// <summary>
/// <para>Owner</para>
/// <para>主人</para>
@ -76,12 +91,105 @@ public partial class PickAbleTemplate : RigidBody2D, IItem
/// </summary>
public bool Picked { get; set; }
public int MaxQuantity { get; set; }
public int MaxQuantity { get; set; } = 1;
public virtual int ItemType
{
get => Config.ItemType.Unknown;
}
private bool _isSelected;
public bool IsSelect
{
get => _isSelected;
set
{
if (_isSelected == value)
{
return;
}
_isSelected = value;
OnSelectChange(value);
}
}
public IItemContainer? ItemContainer { get; set; }
public IItemContainer? SelfItemContainer { get; set; }
private Label? _tipLabel;
public virtual void Use(Node2D? owner, Vector2 targetGlobalPosition)
/// <summary>
/// <para></para>
/// <para>当选中状态发生改变时</para>
/// </summary>
/// <param name="isSelected"></param>
protected virtual void OnSelectChange(bool isSelected)
{
}
public IItem? CreateItem(int number)
{
if (number == 0)
{
return null;
}
var duplicate = Duplicate();
if (duplicate is PickAbleTemplate pickAbleTemplate)
{
pickAbleTemplate.CopyAttributes(this);
}
if (duplicate is not Node2D newNode2D)
{
return null;
}
newNode2D.GlobalPosition = GlobalPosition;
if (duplicate is not IItem newItem)
{
duplicate.QueueFree();
return null;
}
if (number < 0)
{
newItem.Quantity = Quantity;
}
else
{
newItem.Quantity = Math.Min(Quantity, number);
}
return newItem;
}
public int MergeableItemCount(IItem other, int unallocatedQuantity)
{
var freeQuantity = MaxQuantity - Quantity;
if (freeQuantity == 0)
{
return 0;
}
if (other.Id != Id)
{
return 0;
}
return Math.Min(freeQuantity, unallocatedQuantity);
}
public virtual bool Use(Node2D? owner, Vector2 targetGlobalPosition)
{
return false;
}
public virtual void OnThrow(Vector2 velocity)
{
}
public override void _Ready()
@ -235,21 +343,14 @@ public partial class PickAbleTemplate : RigidBody2D, IItem
/// <para>Please copy node properties within this function</para>
/// <para>请在此函数内复制节点属性</para>
/// </summary>
/// <param name="node"></param>
public void CopyAttributes(Node node)
/// <param name="originalNode"></param>
public void CopyAttributes(Node originalNode)
{
if (node is not PickAbleTemplate pickAbleTemplate)
if (originalNode is not PickAbleTemplate originalPickAbleTemplate)
{
return;
}
pickAbleTemplate.Id = Id;
Id = originalPickAbleTemplate.Id;
SelfItemContainer = originalPickAbleTemplate.SelfItemContainer;
}
public virtual void Destroy()
{
QueueFree();
}
public bool CanStackWith(IItem item) => false;
}

View File

@ -0,0 +1,55 @@
using ColdMint.scripts.weapon;
using Godot;
namespace ColdMint.scripts.projectile;
/// <summary>
/// <para>Spell</para>
/// <para>法术</para>
/// </summary>
/// <remarks>
///<para>For projectile weapons</para>
///<para>用于抛射体武器</para>
/// </remarks>
public interface ISpell
{
/// <summary>
/// <para>GetProjectile</para>
/// <para>获取抛射体</para>
/// </summary>
/// <returns></returns>
PackedScene? GetProjectile();
/// <summary>
/// <para>Modify Weapon</para>
/// <para>修改武器</para>
/// </summary>
/// <param name="projectileWeapon"></param>
void ModifyWeapon(ProjectileWeapon projectileWeapon);
/// <summary>
/// <para>Restores the modified weapon properties</para>
/// <para>还原修改的武器属性</para>
/// </summary>
/// <param name="projectileWeapon"></param>
void RestoreWeapon(ProjectileWeapon projectileWeapon);
/// <summary>
/// <para>Modify the projectile</para>
/// <para>修改抛射体</para>
/// </summary>
/// <param name="index">
///<para>What is the current projectile? For example, a weapon can fire three projectiles at once, with indexes 0,1,2</para>
///<para>当前抛射体是第几个例如武器可一下发射3个抛射体索引为0,1,2</para>
/// </param>
/// <param name="projectile">
///<para>Projectile object</para>
///<para>抛射体对象</para>
/// </param>
/// <param name="velocity">
///<para>The velocity of the projectile</para>
///<para>抛射体的飞行速度</para>
/// </param>
void ModifyProjectile(int index,Projectile projectile, ref Vector2 velocity);
}

View File

@ -0,0 +1,82 @@
using ColdMint.scripts.projectile;
using ColdMint.scripts.utils;
using ColdMint.scripts.weapon;
using Godot;
namespace ColdMint.scripts.spell;
/// <summary>
/// <para>MultipleFireSpell</para>
/// <para>多重射击法术</para>
/// </summary>
/// <remarks>
///<para>Use this spell to create shotgun effects</para>
///<para>通过此法术打造霰弹枪的效果</para>
/// </remarks>
public partial class MultipleFireSpell : SpellPickAble
{
/// <summary>
/// <para>How many projectiles are generated per fire</para>
/// <para>每次开火生成多少个抛射体</para>
/// </summary>
[Export]
public int NumberOfProjectiles { get; set; } = 3;
/// <summary>
/// <para>RandomAngle</para>
/// <para>随机角度</para>
/// </summary>
[Export]
public bool RandomAngle { get; set; }
/// <summary>
/// <para>Unit radian</para>
/// <para>单位弧度</para>
/// </summary>
/// <remarks>
///<para>Unit radian of correction for the projectile Angle.Suppose there are three bullets fired at once, and this is the arc between the two bullets.</para>
///<para>对抛射体角度修正的单位弧度。假定有三颗子弹一次发射,这是两颗子弹之间的弧度。</para>
/// </remarks>
[Export]
public float UnitRadian { get; set; } = 0.069813f;
/// <summary>
/// <para>initial Radian</para>
/// <para>起始弧度</para>
/// </summary>
///<remarks>
///<para>The Angle of the first bullet, and subsequent bullets will be offset in unit radians.</para>
///<para>第一颗子弹的角度,随后的子弹会以单位弧度偏移。</para>
/// </remarks>
private float _initialRadian;
private float _maxRadian;
private int _oldNumberOfProjectiles;
public override void ModifyWeapon(ProjectileWeapon projectileWeapon)
{
base.ModifyWeapon(projectileWeapon);
_oldNumberOfProjectiles = projectileWeapon.NumberOfProjectiles;
projectileWeapon.NumberOfProjectiles = NumberOfProjectiles;
_initialRadian = -(NumberOfProjectiles / 2f * UnitRadian);
_maxRadian = NumberOfProjectiles * UnitRadian;
}
public override void RestoreWeapon(ProjectileWeapon projectileWeapon)
{
base.RestoreWeapon(projectileWeapon);
projectileWeapon.NumberOfProjectiles = _oldNumberOfProjectiles;
}
public override void ModifyProjectile(int index, Projectile projectile, ref Vector2 velocity)
{
base.ModifyProjectile(index, projectile, ref velocity);
if (RandomAngle)
{
velocity = velocity.Rotated(_initialRadian + _maxRadian * RandomUtils.Instance.NextSingle());
}
else
{
velocity = velocity.Rotated(_initialRadian + UnitRadian * index);
}
}
}

View File

@ -0,0 +1,55 @@
using ColdMint.scripts.pickable;
using ColdMint.scripts.projectile;
using ColdMint.scripts.weapon;
using Godot;
namespace ColdMint.scripts.spell;
/// <summary>
/// <para>magic</para>
/// <para>法术</para>
/// </summary>
/// <remarks>
///<para>For projectile weapons</para>
///<para>用于抛射体武器</para>
/// </remarks>
public partial class SpellPickAble : PickAbleTemplate, ISpell
{
[Export]
private string? _projectilePath;
private PackedScene? _projectileScene;
public override void _Ready()
{
base._Ready();
if (_projectilePath != null)
{
_projectileScene = GD.Load<PackedScene>(_projectilePath);
}
}
public override int ItemType
{
get => Config.ItemType.Spell;
}
public PackedScene? GetProjectile()
{
return _projectileScene;
}
public virtual void ModifyWeapon(ProjectileWeapon projectileWeapon)
{
}
public virtual void RestoreWeapon(ProjectileWeapon projectileWeapon)
{
}
public virtual void ModifyProjectile(int index, Projectile projectile, ref Vector2 velocity)
{
}
}

View File

@ -235,6 +235,11 @@ public static class NodeUtils
return GameSceneDepend.ProjectileContainer;
}
if (GameSceneDepend.SpellContainer != null && childNode is ISpell)
{
return GameSceneDepend.SpellContainer;
}
if (GameSceneDepend.WeaponContainer != null && childNode is WeaponTemplate)
{
return GameSceneDepend.WeaponContainer;

View File

@ -1,6 +1,8 @@
using System.Collections.Generic;
using ColdMint.scripts.debug;
using ColdMint.scripts.inventory;
using ColdMint.scripts.map.events;
using ColdMint.scripts.projectile;
using ColdMint.scripts.projectile.decorator;
using ColdMint.scripts.utils;
using Godot;
@ -23,157 +25,251 @@ public partial class ProjectileWeapon : WeaponTemplate
private Marker2D? _marker2D;
/// <summary>
/// <para>Scattering radians</para>
/// <para>散射弧度</para>
/// </summary>
[Export] protected float OffsetAngle;
/// <summary>
/// <para>Offset angle mode</para>
/// <para>偏移角度模式</para>
/// </summary>
[Export] protected int OffsetAngleMode = Config.OffsetAngleMode.Random;
/// <summary>
/// <para>Whether the last offset angle is positive</para>
/// <para>上次的偏移角度是否为正向的</para>
/// </summary>
private bool _positiveOffsetAngle = true;
/// <summary>
/// <para>The number of projectiles fired at once</para>
/// <para>一次可以发射多少个子弹</para>
/// </summary>
[Export] protected float NumberOfProjectiles = 1;
[Export] protected PackedScene[] ProjectileScenes { get; set; } = [];
/// <summary>
/// <para>Whether to launch in the order of the projectile list</para>
/// <para>是否按照抛射体列表的循序发射</para>
/// <para>Number of slots for ranged weapons</para>
/// <para>远程武器的槽位数量</para>
/// </summary>
[Export]
protected bool Sequentially { get; set; }
private int _numberSlots;
private int _projectileIndex;
/// <summary>
/// <para>How many projectiles are generated per fire</para>
/// <para>每次开火生成多少个抛射体</para>
/// </summary>
public int NumberOfProjectiles { get; set; } = 1;
private readonly List<ISpell> _spells = new();
/// <summary>
/// <para>Saves the position of a spell with projectile generation</para>
/// <para>保存具有抛射体生成能力的法术位置</para>
/// </summary>
private readonly List<int> _spellProjectileIndexes = new();
/// <summary>
/// <para>Whether to fire spells in sequence</para>
/// <para>是否按顺序发射法术</para>
/// </summary>
[Export]
private bool _fireSequentially;
/// <summary>
/// <para>The index used the last time a spell was cast</para>
/// <para>上次发射法术时采用的索引</para>
/// </summary>
private int _lastUsedProjectileMagicIndex = -1;
/// <summary>
/// <para>Get next index</para>
/// <para>获取下次索引</para>
/// </summary>
/// <returns></returns>
private int GetNextProjectileMagicIndex()
{
if (_fireSequentially)
{
_lastUsedProjectileMagicIndex++;
if (_lastUsedProjectileMagicIndex >= _spellProjectileIndexes.Count)
{
_lastUsedProjectileMagicIndex = 0;
}
return _lastUsedProjectileMagicIndex;
}
else
{
return RandomUtils.Instance.Next(0, _spellProjectileIndexes.Count);
}
}
/// <summary>
/// <para>Gets the loading range of the spell</para>
/// <para>获取法术的加载范围</para>
/// </summary>
/// <returns>
///<para>Return array meaning: 0, starting position 1, ending position 2, with projectile generated spell position, length 3.</para>
///<para>返回数组的含义为0,起始位置1,结束位置2,带有抛射体生成的法术位置长度为3。</para>
/// </returns>
private int[] GetSpellScope()
{
var index = GetNextProjectileMagicIndex();
var projectileSpellPosition = _spellProjectileIndexes[index];
var endIndex = projectileSpellPosition;
var startIndex = 0;
if (index > 0)
{
//And the previous index can set the starting position.(The starting position is increased by 1 to avoid using spells with projectile generation as starting points.)
//还有前面的索引可设定起始位置。(这里起始位置加1是为了避免 具有抛射体生成能力的法术 作为起点。)
startIndex = _spellProjectileIndexes[index - 1] + 1;
}
if (index == _spellProjectileIndexes.Count - 1)
{
endIndex = _spells.Count - 1;
}
return [startIndex, endIndex, projectileSpellPosition];
}
public override int ItemType
{
get => Config.ItemType.ProjectileWeapon;
}
public override void _Ready()
{
base._Ready();
_marker2D = GetNode<Marker2D>("Marker2D");
if (SelfItemContainer == null)
{
SelfItemContainer = new UniversalItemContainer(_numberSlots);
SelfItemContainer.AllowAddingItemByType(Config.ItemType.Spell);
}
}
/// <summary>
/// <para>GetNextProjectileScene</para>
/// <para>获取下一个抛射体</para>
/// <para>Update spell cache</para>
/// <para>更新法术缓存</para>
/// </summary>
/// <returns></returns>
private PackedScene GetNextProjectileScene()
/// <remarks>
///<para>This will parse available spells from inside the item container.</para>
///<para>这将从物品容器内解析可用的法术。</para>
/// </remarks>
public void UpdateSpellCache()
{
if (Sequentially)
if (SelfItemContainer == null)
{
_projectileIndex = (_projectileIndex + 1) % ProjectileScenes.Length;
return ProjectileScenes[_projectileIndex];
return;
}
else
_spells.Clear();
_spellProjectileIndexes.Clear();
var totalCapacity = SelfItemContainer.GetTotalCapacity();
for (var i = 0; i < totalCapacity; i++)
{
return ProjectileScenes[RandomUtils.Instance.Next(ProjectileScenes.Length)];
var item = SelfItemContainer.GetItem(i);
if (item == null)
{
continue;
}
if (item is not ISpell spell)
{
continue;
}
_spells.Add(spell);
var packedScene = spell.GetProjectile();
if (packedScene != null)
{
//Has the ability to generate projectiles.
//拥有抛射体生成能力。
_spellProjectileIndexes.Add(_spells.Count - 1);
}
}
}
/// <summary>
/// <para>GetRandomAngle</para>
/// <para>获取随机的偏移弧度</para>
/// </summary>
/// <returns></returns>
private float GetRandomAngle()
private void OnItemDataChangeEvent(ItemDataChangeEvent itemDataChangeEvent)
{
if (OffsetAngle == 0)
{
//If the offset angle is 0, then return 0
//弧度为0,不用偏移。
return 0;
UpdateSpellCache();
}
if (OffsetAngleMode == Config.OffsetAngleMode.Cross)
public override void _EnterTree()
{
float result;
if (_positiveOffsetAngle)
base._EnterTree();
if (SelfItemContainer != null)
{
result = -OffsetAngle / 2;
SelfItemContainer.ItemDataChangeEvent += OnItemDataChangeEvent;
}
else
}
public override void _ExitTree()
{
result = OffsetAngle / 2;
}
_positiveOffsetAngle = !_positiveOffsetAngle;
return result;
}
if (OffsetAngleMode == Config.OffsetAngleMode.AlwaysSame)
base._ExitTree();
if (SelfItemContainer != null)
{
return OffsetAngle;
SelfItemContainer.ItemDataChangeEvent -= OnItemDataChangeEvent;
}
}
var min = -OffsetAngle / 2;
return min + RandomUtils.Instance.NextSingle() * OffsetAngle;
}
protected override void DoFire(Node2D? owner, Vector2 enemyGlobalPosition)
protected override bool DoFire(Node2D? owner, Vector2 enemyGlobalPosition)
{
if (owner == null)
{
LogCat.LogError("owner_is_null");
return;
return false;
}
if (_marker2D == null)
{
LogCat.LogError("marker2d_is_null");
return;
return false;
}
if (GameSceneDepend.ProjectileContainer == null)
{
LogCat.LogError("projectile_container_is_null");
return;
return false;
}
//Empty list check
//空列表检查
if (ProjectileScenes is [])
if (_spellProjectileIndexes.Count == 0)
{
LogCat.LogError("projectiles_is_empty");
return;
LogCat.LogError("projectile_generate_magic_is_null");
return false;
}
//Get the first projectile
//获取第一个抛射体
var projectileScene = GetNextProjectileScene();
for (int i = 0; i < NumberOfProjectiles; i++)
var spellScope = GetSpellScope();
LogCat.LogWithFormat("projectile_weapon_range", LogCat.LogLabel.Default, true, string.Join(",", spellScope), _fireSequentially, _lastUsedProjectileMagicIndex);
//The final spell is a projectile generator.
//最后的法术是拥有抛射体生成能力的。
var spellProjectile = _spells[spellScope[2]];
var packedScene = spellProjectile.GetProjectile();
if (packedScene == null)
{
var projectile = NodeUtils.InstantiatePackedScene<Projectile>(projectileScene);
if (projectile == null) return;
if (Config.IsDebug())
{
var nodeSpawnOnKillCharacterDecorator = new NodeSpawnOnKillCharacterDecorator
{
DefaultParentNode = this,
PackedScenePath = "res://prefab/entitys/BlackenedAboriginalWarrior.tscn"
};
projectile.AddProjectileDecorator(nodeSpawnOnKillCharacterDecorator);
LogCat.LogError("projectile_scene_is_null");
return false;
}
ModifyWeapon(spellScope);
for (var i = 0; i < NumberOfProjectiles; i++)
{
var projectile = NodeUtils.InstantiatePackedScene<Projectile>(packedScene);
if (projectile == null)
{
LogCat.LogError("projectile_is_null");
RestoreWeapon(spellScope);
return false;
}
var velocity = _marker2D.GlobalPosition.DirectionTo(enemyGlobalPosition) * projectile.Speed;
for (var s = spellScope[0]; s <= spellScope[1]; s++)
{
var spell = _spells[s];
spell.ModifyProjectile(i, projectile, ref velocity);
}
NodeUtils.CallDeferredAddChild(GameSceneDepend.ProjectileContainer, projectile);
projectile.Owner = owner;
projectile.TargetNode = GameSceneDepend.TemporaryTargetNode;
projectile.Velocity =
(_marker2D.GlobalPosition.DirectionTo(enemyGlobalPosition) * projectile.Speed)
.Rotated(GetRandomAngle());
projectile.Velocity = velocity;
projectile.Position = _marker2D.GlobalPosition;
}
RestoreWeapon(spellScope);
return true;
}
/// <summary>
/// <para>Modify weapon attributes</para>
/// <para>修改武器属性</para>
/// </summary>
/// <param name="spellScope"></param>
private void ModifyWeapon(int[] spellScope)
{
for (var i = spellScope[0]; i <= spellScope[1]; i++)
{
var spell = _spells[i];
spell.ModifyWeapon(this);
}
}
/// <summary>
/// <para>Restores modifications to weapons</para>
/// <para>恢复对武器的修改</para>
/// </summary>
/// <param name="spellScope"></param>
private void RestoreWeapon(int[] spellScope)
{
for (var i = spellScope[0]; i <= spellScope[1]; i++)
{
var spell = _spells[i];
spell.RestoreWeapon(this);
}
}
}

View File

@ -25,9 +25,9 @@ public abstract partial class WeaponTemplate : PickAbleTemplate
_audioStreamPlayer2D = GetNodeOrNull<AudioStreamPlayer2D>("Marker2D/AudioStreamPlayer2D");
}
public override void Use(Node2D? owner, Vector2 targetGlobalPosition)
public override bool Use(Node2D? owner, Vector2 targetGlobalPosition)
{
Fire(owner, targetGlobalPosition);
return Fire(owner, targetGlobalPosition);
}
@ -77,34 +77,38 @@ public abstract partial class WeaponTemplate : PickAbleTemplate
///<para>敌人所在位置</para>
/// </param>
/// </remarks>
public void Fire(Node2D? owner, Vector2 enemyGlobalPosition)
public bool Fire(Node2D? owner, Vector2 enemyGlobalPosition)
{
var nowTime = DateTime.Now;
//If the present time minus the time of the last fire is less than the interval between fires, it means that the fire cannot be fired yet.
//如果现在时间减去上次开火时间小于开火间隔,说明还不能开火。
if (_lastFiringTime != null && nowTime - _lastFiringTime < _firingInterval)
{
return;
return false;
}
_lastFiringTime = nowTime;
var result = DoFire(owner, enemyGlobalPosition);
if (result)
{
if (owner is CharacterTemplate characterTemplate)
{
//We check the recoil of the weapon before each firing.
//我们在每次开火之前,检查武器的后坐力。
if (_recoilStrength != 0)
{
characterTemplate.AddForce(enemyGlobalPosition.DirectionTo(characterTemplate.GlobalPosition) * _recoilStrength * Config.CellSize);
}
}
_audioStreamPlayer2D?.Play();
DoFire(owner, enemyGlobalPosition);
_lastFiringTime = nowTime;
}
return result;
}
/// <summary>
/// <para>Execute fire</para>
/// <para>执行开火</para>
/// </summary>
protected abstract void DoFire(Node2D? owner, Vector2 enemyGlobalPosition);
/// <returns>
///<para>Return Is the fire successful?</para>
///<para>返回是否成功开火?</para>
/// </returns>
protected abstract bool DoFire(Node2D? owner, Vector2 enemyGlobalPosition);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cyciw4drjvrs8"
path="res://.godot/imported/SpellEditor.png-a8650f7f0135f5be6d7d33b867de82ab.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://sprites/furnitures/SpellEditor.png"
dest_files=["res://.godot/imported/SpellEditor.png-a8650f7f0135f5be6d7d33b867de82ab.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 B

After

Width:  |  Height:  |  Size: 383 B

BIN
sprites/projectile/x3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

View File

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://mb5yijtw7sw5"
path="res://.godot/imported/x3.png-096b2fc27f9da2412a5f72aad28677de.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://sprites/projectile/x3.png"
dest_files=["res://.godot/imported/x3.png-096b2fc27f9da2412a5f72aad28677de.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1