插件开发实战: 商店系统
Intro - 导语
在你安装了 ToolDelta 之后,你的租赁服的玩家们迫切地需要一个租赁服商店系统,允许他们购买和出售物品!于是你打开了 IDE 并进入了 ToolDelta 目录...
Step.1 - 创建插件框架
没错,万物的起点是新建文件夹:
插件文件/
ToolDelta类式插件/
商店系统/
__init__.py
2
3
4
打开 __init__.py
,先构建一个插件框架:
如果你下载了
类式插件创建器
插件, 那么你可以省去以下步骤, 直接在 ToolDelta 控制台使用create
命令创建插件。
设置插件名,作者名和版本号,并使用 __init__
初始化插件。
__init__
方法将会帮你获取系统框架。
from tooldelta import Plugin, plugins
@plugins.add_plugin
class Shop(Plugin):
name = "商店系统"
author = "作者名"
version = (0, 0, 1)
def __init__(self, frame):
super().__init__(frame)
2
3
4
5
6
7
8
9
10
Step.2 - 添加依赖库、导入前置插件
因为我们可能需要获取分数、获取玩家背包内物品数量等,所以我们需要在导入模块部分额外添加一些库:
from tooldelta import Plugin, plugins, game_utils, Utils
在购买或者出售商品的时候,需要监听菜单触发词以打开商店菜单,前置-聊天栏菜单 提供了监听聊天栏菜单触发词的回调。
注意: 前置-聊天栏菜单 插件并非自带,而是需要在内置的插件市场手动下载。
from tooldelta import Plugin, plugins, game_utils, Utils
@plugins.add_plugin
class Shop(Plugin):
name = "商店系统"
author = "作者名"
version = (0, 0, 1)
def __init__(self, frame):
super().__init__(frame)
def on_def(self):
# 导入前置插件只能在 on_def() 方法内使用
# 这样, 当前置插件不存在的时候
# ToolDelta 会显示前置插件不存在的报错
self.chatbar = plugins.get_plugin_api("聊天栏菜单")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Step.3 - 预设配置
我们需要预设一张写有 货币计分板名、出售商品的价格,收购商品的价格 的字典,作为默认配置(至于如何读取配置,我们后面再教学):
from tooldelta import Plugin, plugins, game_utils, Utils
@plugins.add_plugin
class Shop(Plugin):
name = "商店系统"
author = "作者名"
version = (0, 0, 1)
def __init__(self, frame):
super().__init__(frame)
CONFIG = {
# 假设你的租赁服所使用的货币计分板是 money
"货币计分板名": "money",
"出售": {
"钻石": {
"ID": "diamond",
"价格": 200
},
"绿宝石": {
"ID": "emerald",
"价格": 100
}
},
"收购": {
"铁锭": {
"ID": "iron_ingot",
"价格": 10,
},
"金锭": {
"ID": "gold_ingot",
"价格": 20
}
}
}
self.money_scb_name = CONFIG["货币计分板名"]
self.sells = CONFIG["出售"]
self.buys = CONFIG["收购"]
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Step.4 - 编写购买和收购商品的逻辑
定义 when_player_buy
方法,显示商品列表,然后令玩家选择一项并输入购买数量,完成购买。
from tooldelta import Plugin, plugins, game_utils, Utils
@plugins.add_plugin
class Shop(Plugin):
name = "商店系统"
author = "作者名"
version = (0, 0, 1)
...
def on_inject(self):
# 将玩家购买商品的触发词方法注册进聊天栏菜单
# 输入 ".buy" 或者 "。购买" 或者 ".购买" 等
# 就会执行 when_player_buy 回调 (传参: 玩家名, [])
self.chatbar.add_trigger(["购买", "buy"], None, "打开商店物品购买页面", self.when_player_buy)
def when_player_buy(self, playername: str, _):
# playername 传入玩家名
# 由于触发词不需要参数, 因此可以舍弃这个参数 _
# 先把所有出售的物品名整理成一个有序列表
sells_list = list(self.sells.keys())
if sells_list == []:
self.game_ctrl.say_to(playername, "§c没有可购买的物品, 已退出")
return
# 然后向玩家展示
self.game_ctrl.say_to(playername, "§6你想购买哪件商品?")
for i, good_name in enumerate(sells_list):
self.game_ctrl.say_to(playername, f"{i + 1}. {good_name}")
self.game_ctrl.say_to(playername, "§6请输入选项序号以选择商品:")
# 询问, 获取答复
resp = game_utils.waitMsg(playername)
# 如果超时了或者玩家在中途退出游戏
if resp is None:
self.game_ctrl.say_to(playername, "§c太久没有回应, 已退出")
return
# 回应太长
elif len(resp) > 10:
self.game_ctrl.say_to(playername, "§c输入过长, 已退出")
return
# 如果选项不合法
# 回应不是数字; 或者不在范围内
resp = Utils.try_int(resp)
if resp is None or resp not in range(1, len(sells_list) + 1):
self.game_ctrl.say_to(playername, "§c选项不合法, 已退出")
return
good_to_buy = sells_list[resp - 1]
good_id = self.sells[good_to_buy]["ID"]
good_price = self.sells[good_to_buy]["价格"]
# 询问需要购买多少个商品
self.game_ctrl.say_to(playername, f"§6你需要购买多少个{good_to_buy}?")
buy_count = Utils.try_int(game_utils.waitMsg(playername))
if buy_count is None:
self.game_ctrl.say_to(playername, "§c输入有误, 已退出")
return
elif buy_count < 1:
self.game_ctrl.say_to(playername, "§c商品数量不能小于1, 已退出")
return
# 计算总金额并进行处理
price_total = good_price * buy_count
# 使用 game_utils 提供的 getScore 获取玩家分数
have_money = game_utils.getScore(self.money_scb_name, playername)
if have_money < price_total:
self.game_ctrl.say_to(
playername,
f"§c需要 §e{price_total}金币 §c才可以购买, 你只有 §e{have_money} 金币",
)
else:
self.game_ctrl.sendwocmd(f'give "{playername}" {good_id} {buy_count}')
self.game_ctrl.say_to(
playername, f"§a你成功购买了 {buy_count} 个 {good_to_buy}"
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
商品购买的逻辑完成了!我们可以立刻测试这个插件的效果。
接下来我们需要编收购的逻辑,也是 展示列表-->询问->选择-->判定-->完成收购。
from tooldelta import Plugin, plugins, game_utils, Utils
@plugins.add_plugin
class Shop(Plugin):
name = "商店系统"
author = "作者名"
version = (0, 0, 1)
...
def on_inject(self):
# 将玩家购买商品的触发词方法注册进聊天栏菜单
# 输入 ".buy" 或者 "。购买" 或者 ".购买" 等
# 就会执行 when_player_buy 回调 (传参: 玩家名, [])
self.chatbar.add_trigger(["购买", "buy"], None, "打开商店物品购买页面", self.when_player_buy)
self.chatbar.add_trigger(["收购", "sell"], None, "打开商店物品收购页面", self.when_player_sell)
def when_player_sell(self, playername: str, _):
buys_list = list(self.buys.keys())
if buys_list == []:
self.game_ctrl.say_to(playername, "§c没有可出售的物品, 已退出")
return
self.game_ctrl.say_to(playername, "§6你想出售哪件物品?")
for i, good_name in enumerate(buys_list):
self.game_ctrl.say_to(playername, f"{i + 1}. {good_name}")
self.game_ctrl.say_to(playername, "§6请输入选项序号以选择:")
resp = game_utils.waitMsg(playername)
if resp is None:
self.game_ctrl.say_to(playername, "§c太久没有回应, 已退出")
return
elif len(resp) > 10:
self.game_ctrl.say_to(playername, "§c输入过长, 已退出")
return
resp = Utils.try_int(resp)
if resp is None or resp not in range(1, len(buys_list) + 1):
self.game_ctrl.say_to(playername, "§c输入不合法, 已退出")
return
good_to_buy = buys_list[resp - 1]
good_id = self.buys[good_to_buy]["ID"]
good_price = self.buys[good_to_buy]["价格"]
# 询问需要出售多少个物品
self.game_ctrl.say_to(playername, f"§6你需要出售多少个{good_to_buy}?")
sell_count = Utils.try_int(game_utils.waitMsg(playername))
if sell_count is None:
self.game_ctrl.say_to(playername, "§c输入有误, 已退出")
return
elif sell_count < 1:
self.game_ctrl.say_to(playername, "§c商品数量不能小于1, 已退出")
return
# 获取玩家物品数量
have_good = game_utils.getItem(playername, good_id)
if have_good < sell_count:
self.game_ctrl.say_to(
playername, f"§c你只有 {have_good} 个 {good_to_buy}, 无法出售, 已退出"
)
else:
self.game_ctrl.sendwocmd(f'clear "{playername}" {good_id} 0 {sell_count}')
self.game_ctrl.say_to(
playername,
f"§a你出售了 {sell_count} 个 {good_to_buy}, 获得 §e{good_price * sell_count} 金币",
)
self.game_ctrl.sendwocmd(
f'scoreboard players add "{playername}" {self.money_scb_name} {good_price * sell_count}'
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
至此,商店系统的两个核心逻辑就已经编写完毕了。我们现在要考虑如何继续优化插件的实用性。
Step.5 - 自定义配置
还记得之前在 __init__
方法中写死的配置吗?我们需要把它变成用户可修改的配置。
我们需要定义一个默认配置,以已经写好的为例:
CONFIG_DEFAULT = {
"货币计分板名": "money",
"出售": {
"钻石": {"ID": "diamond", "价格": 200},
"绿宝石": {"ID": "emerald", "价格": 100},
},
"收购": {
"铁锭": {
"ID": "iron_ingot",
"价格": 10,
},
"金锭": {"ID": "gold_ingot", "价格": 20},
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
之前我们没有从 tooldelta 导入配置类 Config, 我们需要在第一行导入:
from tooldelta import Plugin, plugins, Config, game_utils, Utils
为了保证插件能顺利地读取配置,不产生因为类型异常问题而导致的报错,我们还需要提供配置模版:
CONFIG_STD = {
# "货币计分板名" 必须为字符串类型
"货币计分板名": str,
# 因为 "出售" 的键可以是任意的但是值是固定的, 所以使用 AnyKeyValue
"出售": Config.AnyKeyValue({
"ID": str,
# "价格" 必须为正整数类型
"价格": Config.NNInt,
}),
"收购": Config.AnyKeyValue({
"ID": str,
"价格": Config.NNInt,
}),
}
2
3
4
5
6
7
8
9
10
11
12
13
14
接着我们就可以修改 __init__
方法,加入配置文件读取了:
from tooldelta import Plugin, plugins, Config, game_utils, Utils
@plugins.add_plugin
class Shop(Plugin):
name = "商店系统"
author = "作者名"
version = (0, 0, 1)
def __init__(self, frame):
super().__init__(frame)
CONFIG_DEFAULT = {
"货币计分板名": "money",
"出售": {
"钻石": {"ID": "diamond", "价格": 200},
"绿宝石": {"ID": "emerald", "价格": 100},
},
"收购": {
"铁锭": {
"ID": "iron_ingot",
"价格": 10,
},
"金锭": {"ID": "gold_ingot", "价格": 20},
},
}
CONFIG_STD = {
"货币计分板名": str,
"出售": Config.AnyKeyValue(
{
"ID": str,
"价格": Config.NNInt,
}
),
"收购": Config.AnyKeyValue(
{
"ID": str,
"价格": Config.NNInt,
}
),
}
# 读取配置, 没有配置就写入默认配置
# cfg_version 我们不需要用到, 这个是插件配置版本号
# 在这步如果配置文件出现异常, 不需要处理
# ToolDelta 会自动处理并以优雅的方式显示问题
cfg, cfg_version = Config.get_plugin_config_and_version(
self.name, CONFIG_STD, CONFIG_DEFAULT, self.version
)
self.money_scb_name = cfg["货币计分板名"]
self.sells = cfg["出售"]
self.buys = cfg["收购"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
插件编写完成! Congratulations!
至此,插件编写已经圆满完成了!祝贺!
让我们再看看插件的整体代码:
from tooldelta import Plugin, plugins, Config, game_utils, Utils
@plugins.add_plugin
class Shop(Plugin):
name = "商店系统"
author = "作者名"
version = (0, 0, 1)
def __init__(self, frame):
super().__init__(frame)
CONFIG_DEFAULT = {
"货币计分板名": "money",
"出售": {
"钻石": {"ID": "diamond", "价格": 200},
"绿宝石": {"ID": "emerald", "价格": 100},
},
"收购": {
"铁锭": {
"ID": "iron_ingot",
"价格": 10,
},
"金锭": {"ID": "gold_ingot", "价格": 20},
},
}
CONFIG_STD = {
"货币计分板名": str,
"出售": Config.AnyKeyValue(
{
"ID": str,
"价格": Config.NNInt,
}
),
"收购": Config.AnyKeyValue(
{
"ID": str,
"价格": Config.NNInt,
}
),
}
# 读取配置, 没有配置就写入默认配置
# cfg_version 我们不需要用到, 这个是插件配置版本号
# 在这步如果配置文件出现异常, 不需要处理
# ToolDelta 会自动处理并以优雅的方式显示问题
cfg, cfg_version = Config.get_plugin_config_and_version(
self.name, CONFIG_STD, CONFIG_DEFAULT, self.version
)
self.money_scb_name = cfg["货币计分板名"]
self.sells = cfg["出售"]
self.buys = cfg["收购"]
def on_def(self):
# 导入前置插件只能在 on_def() 方法内使用
# 这样, 当前置插件不存在的时候
# ToolDelta 会显示前置插件不存在的报错
self.chatbar = plugins.get_plugin_api("聊天栏菜单")
def on_inject(self):
# 将玩家购买商品的触发词方法注册进聊天栏菜单
# 输入 ".buy" 或者 "。购买" 或者 ".购买"
# 就会执行 when_player_buy 回调 (传参: 玩家名, [])
self.chatbar.add_trigger(
["购买", "buy"], None, "打开商店物品购买页面", self.when_player_buy
)
self.chatbar.add_trigger(
["收购", "sell"], None, "打开商店物品收购页面", self.when_player_sell
)
def when_player_buy(self, playername: str, _):
# playername 传入玩家名
# 由于触发词不需要参数, 因此可以舍弃这个参数 _
# 先把所有出售的物品名整理成一个有序列表
sells_list = list(self.sells.keys())
if sells_list == []:
self.game_ctrl.say_to(playername, "§c没有可购买的物品, 已退出")
return
# 然后向玩家展示
self.game_ctrl.say_to(playername, "§6你想购买哪件商品?")
for i, good_name in enumerate(sells_list):
self.game_ctrl.say_to(playername, f"{i + 1}. {good_name}")
self.game_ctrl.say_to(playername, "§6请输入选项序号以选择商品:")
# 询问, 获取答复
resp = game_utils.waitMsg(playername)
# 如果超时了或者玩家在中途退出游戏
if resp is None:
self.game_ctrl.say_to(playername, "§c太久没有回应, 已退出")
return
# 回应太长
elif len(resp) > 10:
self.game_ctrl.say_to(playername, "§c输入过长, 已退出")
return
# 如果选项不合法
# 回应不是数字; 或者不在范围内
resp = Utils.try_int(resp)
if resp is None or resp not in range(1, len(sells_list) + 1):
self.game_ctrl.say_to(playername, "§c选项不合法, 已退出")
return
good_to_buy = sells_list[resp - 1]
good_id = self.sells[good_to_buy]["ID"]
good_price = self.sells[good_to_buy]["价格"]
# 询问需要购买多少个商品
self.game_ctrl.say_to(playername, f"§6你需要购买多少个{good_to_buy}?")
buy_count = Utils.try_int(game_utils.waitMsg(playername))
if buy_count is None:
self.game_ctrl.say_to(playername, "§c输入有误, 已退出")
return
elif buy_count < 1:
self.game_ctrl.say_to(playername, "§c商品数量不能小于1, 已退出")
return
# 计算总金额并进行处理
price_total = good_price * buy_count
# 使用 game_utils 提供的 getScore 获取玩家分数
have_money = game_utils.getScore(self.money_scb_name, playername)
if have_money < price_total:
self.game_ctrl.say_to(
playername,
f"§c需要 §e{price_total}金币 §c才可以购买, 你只有 §e{have_money} 金币",
)
else:
self.game_ctrl.sendwocmd(f'give "{playername}" {good_id} {buy_count}')
self.game_ctrl.say_to(
playername, f"§a你成功购买了 {buy_count} 个 {good_to_buy}"
)
def when_player_sell(self, playername: str, _):
buys_list = list(self.buys.keys())
if buys_list == []:
self.game_ctrl.say_to(playername, "§c没有可出售的物品, 已退出")
return
self.game_ctrl.say_to(playername, "§6你想出售哪件物品?")
for i, good_name in enumerate(buys_list):
self.game_ctrl.say_to(playername, f"{i + 1}. {good_name}")
self.game_ctrl.say_to(playername, "§6请输入选项序号以选择:")
resp = game_utils.waitMsg(playername)
if resp is None:
self.game_ctrl.say_to(playername, "§c太久没有回应, 已退出")
return
elif len(resp) > 10:
self.game_ctrl.say_to(playername, "§c输入过长, 已退出")
return
resp = Utils.try_int(resp)
if resp is None or resp not in range(1, len(buys_list) + 1):
self.game_ctrl.say_to(playername, "§c输入不合法, 已退出")
return
good_to_buy = buys_list[resp - 1]
good_id = self.buys[good_to_buy]["ID"]
good_price = self.buys[good_to_buy]["价格"]
# 询问需要出售多少个物品
self.game_ctrl.say_to(playername, f"§6你需要出售多少个{good_to_buy}?")
sell_count = Utils.try_int(game_utils.waitMsg(playername))
if sell_count is None:
self.game_ctrl.say_to(playername, "§c输入有误, 已退出")
return
elif sell_count < 1:
self.game_ctrl.say_to(playername, "§c商品数量不能小于1, 已退出")
return
# 获取玩家物品数量
have_good = game_utils.getItem(playername, good_id)
if have_good < sell_count:
self.game_ctrl.say_to(
playername, f"§c你只有 {have_good} 个 {good_to_buy}, 无法出售, 已退出"
)
else:
self.game_ctrl.sendwocmd(f'clear "{playername}" {good_id} 0 {sell_count}')
self.game_ctrl.say_to(
playername,
f"§a你出售了 {sell_count} 个 {good_to_buy}, 获得 §e{good_price * sell_count} 金币",
)
self.game_ctrl.sendwocmd(
f'scoreboard players add "{playername}" {self.money_scb_name} {good_price * sell_count}'
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183