第1章:构建一个点击游戏¶
在这个项目中,我们将共同构建一个 点击游戏,完全集成到 Odoo 中。在这个游戏中,目标是积累大量的点击,并自动化系统。有趣的部分是,我们将使用 Odoo 用户界面作为我们的游乐场。例如,我们会在网页客户端的某些随机部分隐藏奖励。
要开始使用,您需要一个正在运行的 Odoo 服务器和一个开发环境。在进行练习之前,请确保您已按照此 教程介绍 中描述的所有步骤操作。
目标

本章每个练习的解决方案都托管在 官方 Odoo 教程仓库 上。
1. Create a systray item¶
首先,我们希望在系统托盘中显示一个计数器。
创建一个包含 hello world Owl 组件的
clicker_systray_item.js
(和xml
)文件。将其注册到系统托盘注册表中,并确保其可见。
更新项目内容,使其显示以下字符串:
Clicks: 0
,并在右侧添加一个按钮以增加该值。

瞧,我们有了一个完全可用的点击游戏!
另请参阅
需要翻译的内容是:
2. Count external clicks¶
老实说,这还不太有趣。所以让我们添加一个新功能:我们希望用户界面中的所有点击都计入,这样用户就有动力尽可能多地使用Odoo!但显然,主计数器上的有意点击仍应计得更多。
使用
useExternalListener
监听document.body
上的所有点击。每次点击应将计数器值增加 1。
修改代码,使每次点击计数器时,值增加 10
确保点击计数器不会使值增加 11!
此外,额外的挑战:确保外部监听器捕获事件,这样我们就不会错过任何点击。
3. Create a client action¶
目前,当前用户界面非常小:它只是一个系统托盘项。我们当然需要更多的空间来展示更多的游戏内容。为此,让我们创建一个客户端动作。客户端动作是由网页客户端管理的主要动作,用于显示一个组件。
创建一个
client_action.js
(和xml
)文件,包含一个 hello world 组件。将该客户端操作注册到操作注册表中,名称为
awesome_clicker.client_action
在系统托盘项上添加一个文本为
Open
的按钮。点击它应打开客户端动作 `awesome_clicker.client_action`(使用动作服务来实现)。为避免干扰员工的工作流程,我们更倾向于让客户端操作在弹出窗口中打开,而不是全屏模式。修改
doAction
调用以在弹出窗口中打开它。小技巧
你可以在
doAction
中使用target: "new"
来在弹出窗口中打开操作:{ type: "ir.actions.client", tag: "awesome_clicker.client_action", target: "new", name: "Clicker" }

另请参阅
4. Move the state to a service¶
目前,我们的客户端操作只是一个 hello world 组件。我们希望它显示我们的游戏状态,但该状态目前仅在系统托盘项中可用。因此,这意味着我们需要更改状态的位置,使其对所有组件可用。这是一个完美的服务用例。
创建一个
clicker_service.js
文件,包含相应的服务。该服务应导出一个响应式值(点击次数)以及一些用于更新它的函数:
const state = reactive({ clicks: 0 }); ... return { state, increment(inc) { state.clicks += inc } };
在系统托盘项和客户端操作中访问状态(别忘了使用
useState
)。修改系统托盘项以移除其自身的本地状态并使用它。此外,你可以移除+10 点击
按钮。在客户端操作中显示状态,并在其中添加一个
+10
点击按钮。

另请参阅
5. Use a custom hook¶
目前,代码中需要使用我们点击器服务的每个部分都必须导入 useService
和 useState
。由于这种情况相当常见,让我们使用一个自定义钩子。同时,将更多重点放在 clicker
部分,而减少对 service
部分的强调也是有益的。
导出一个
useClicker
钩子。将所有当前使用的点击器服务更新到新的钩子:
this.clicker = useClicker();
另请参阅
6. Humanize the displayed value¶
我们将来会显示大数字,所以让我们为此做好准备。有一个 humanNumber
函数,它以一种更容易理解的方式格式化数字:例如,1234
可以格式化为 1.2k
用它来显示我们的计数器(无论是在系统托盘项还是客户端操作中)。
创建一个
ClickValue
组件来显示该值。注解
Owl 允许包含纯文本节点的组件!

另请参阅
7. Add a tooltip in ClickValue
component¶
使用 humanNumber
函数,我们实际上在界面上失去了一些精度。让我们将实际数字显示为工具提示。
Tooltip 需要一个 html 元素。将
ClickValue
更改为将值包裹在<span/>
元素中添加一个动态的
data-tooltip
属性以显示确切值。

另请参阅
8. Buy ClickBots¶
让我们让游戏变得更加有趣:一旦玩家首次达到1000次点击,游戏将解锁一个新功能:玩家可以用1000次点击购买机器人。这些机器人每10秒将生成10次点击。
为我们的状态添加一个
level
数字。这是一个在某些里程碑时会递增的数字,并会解锁新功能。在我们的状态中添加一个
clickBots
数字。它表示已购买的机器人数量。修改客户端操作以显示点击机器人的数量(仅在
level >= 1
时显示),并带有一个Buy
按钮,该按钮在clicks >= 1000
时启用。Buy
按钮应将点击机器人的数量增加 1。在服务中设置一个10秒的间隔,该间隔将使点击次数增加
10*clickBots
。确保如果玩家没有足够的点击次数,购买按钮将被禁用。

9. Refactor to a class model¶
当前代码以某种函数式风格编写。但这样做,我们必须将状态及其所有更新函数导出到我们的点击器对象中。随着项目的增长,这可能会变得越来越复杂。为了简化,让我们将业务逻辑从服务中分离出来,放入一个类中。
创建一个
clicker_model
文件,导出一个响应式类。将所有状态和更新函数从服务移动到模型中。小技巧
你可以使用来自
@web/core/utils/reactive
的Reactive
类来扩展 ClickerModel。Reactive
类将模型包装成一个响应式代理。重写点击器服务以实例化并导出点击器模型类。
另请参阅
10. Notify when a milestone is reached¶
当我们达到 1k 点击时,没有太多反馈表明发生了变化。让我们使用 effect
服务来清晰地传达这一信息。问题在于我们的点击模型无法访问服务。此外,我们希望尽可能将 UI 关注点排除在模型之外。因此,我们可以探索一种新的通信策略:事件总线。
更新点击器模型以实例化一个总线,并在首次达到1000次点击时触发一个
MILESTONE_1k
事件。更改点击器服务以监听模型总线上的相同事件。
当这种情况发生时,使用
effect
服务来显示彩虹人。添加一些文字说明用户现在可以购买点击机器人。

另请参阅
11. Add BigBots¶
显然,我们需要一种方式为玩家提供更多选择。让我们添加一种新的点击机器人:BigBots
,它们更加强大:每10秒提供100次点击,但需要花费5000次点击
当达到5k时,增加
level
(因此应为2)更新状态以跟踪 bigbots
bigbots 应在
level >=2
时可用在客户端操作中显示相应的信息
小技巧
如果需要在模板中使用 <
或 >
作为 JavaScript 表达式,请小心,因为它可能与 XML 解析器冲突。为了解决这个问题,你可以使用特殊的别名之一:gt, gte, lt
或 lte
。请参阅 Owl 文档页面关于模板表达式的部分。

12. Add a new type of resource: power¶
现在,为了增加另一个扩展点,让我们添加一种新的资源:一个力量倍增器。这是一个可以在 level >= 3
时增加的数字,并且会倍增机器人的动作(因此,点击机器人现在为我们提供 multiplier
次点击,而不是一次点击)。
当达到100k时,增加
level
(因此它应该是3)。更新状态以跟踪能量值(初始值为1)。
将机器人更改为使用该数字作为乘数。
更新用户界面以显示并让用户购买新的能量等级(费用:50k)。

13. Define some random rewards¶
我们希望用户有时能获得奖励,以鼓励使用Odoo。
在
click_rewards.js
中定义奖励列表。奖励是一个对象,包含:- 一个description
字符串。- 一个apply
函数,该函数接收游戏状态作为参数并可以修改它。- 一个minLevel
数字(可选),描述奖励在哪个解锁等级可用。- 一个maxLevel
数字(可选),描述奖励在哪个解锁等级不再可用。例如:
export const rewards = [ { description: "Get 1 click bot", apply(clicker) { clicker.increment(1); }, maxLevel: 3, }, { description: "Get 10 click bot", apply(clicker) { clicker.increment(10); }, minLevel: 3, maxLevel: 4, }, { description: "Increase bot power!", apply(clicker) { clicker.multipler += 1; }, minLevel: 3, }, ];
你可以在这个列表中添加任何你想要的内容!
定义一个函数
getReward
,该函数将从与当前解锁级别匹配的奖励列表中随机选择一个奖励。将随机选择数组中元素的代码提取到一个名为
choose
的函数中,并将其移至另一个utils.js
文件中。
14. Provide a reward when opening a form view¶
修补表单控制器。每次创建表单控制器时,它应随机决定(1% 的概率)是否应给予奖励。
如果答案是肯定的,调用模型上的方法
getReward
。该方法应选择一个奖励,发送一个带有按钮
Collect
的粘性通知,该按钮将应用奖励,最后,它应打开clicker
客户端操作。

另请参阅
15. Add commands in command palette¶
添加一个命令
Open Clicker Game
到命令面板中。添加另一个命令:
购买 1 个点击机器人
。

另请参阅
16. Add yet another resource: trees¶
现在是时候引入一种全新的资源类型了。这里有一个应该不会引起太大争议的资源:树木。我们现在允许用户种植(收集?)果树。一棵树需要花费100万次点击,但它会为我们提供水果(梨或樱桃)。
更新状态以跟踪不同类型的树:梨树/樱桃树及其果实。
添加一个计算树木和果实总数的函数。
定义一个新的解锁等级,条件是
clicks >= 1 000 000
。更新客户端用户界面以显示树木和水果的数量,并且还可以购买树木。
每棵树每30秒水果数量增加1。

18. Use a Notebook component¶
我们现在追踪的信息更多了。让我们通过使用 Notebook
组件,将信息和功能组织到不同的标签页中,来改进客户端界面:
使用
Notebook
组件。所有
click
内容应显示在一个标签页中。所有
tree/fruits
内容应在另一个标签页中显示。

19. Persist the game state¶
你一定注意到了我们游戏中的一个重大缺陷:它是临时的。每次用户关闭浏览器标签页时,游戏状态都会丢失。让我们来解决这个问题。我们将使用本地存储来持久化状态。
从
@web/core/browser/browser
导入browser
以访问本地存储。每 10 秒序列化状态(在同一间隔代码中)并将其存储在本地存储中。
当
clicker
服务启动时,它应从本地存储加载状态(如果有的话),否则应自行初始化。
20. Introduce state migration system¶
一旦你将状态持久化到某个地方,新的问题就会出现:当你更新代码时,状态的结构发生了变化,而用户打开浏览器时使用的是旧版本创建的状态,会发生什么情况呢?欢迎来到迁移问题的世界!
尽早解决这个问题可能是明智的。我们在这里要做的是为状态添加一个版本号,并引入一个系统,以便在状态不是最新时自动更新状态。
为状态添加版本号。
定义一个(空的)迁移列表。迁移是一个包含
fromVersion
数字、toVersion
数字和一个apply
函数的对象。每当代码从本地存储加载状态时,都应检查版本号。如果状态不是最新的,则应应用所有必要的迁移。
21. Add another type of trees¶
为了测试我们的迁移系统,让我们添加一种新的树:桃树。
添加
peach
树。增加状态版本号。
定义迁移。
