第 1 章:构建点击游戏

在这个项目中,我们将一起构建一个完全集成到 Odoo 的 点击游戏。在这款游戏中,目标是积累大量的点击数并实现系统的自动化。有趣的是,我们将使用 Odoo 用户界面作为我们的游乐场。例如,我们会在网页客户端的某些随机部分隐藏奖励。

要开始,您需要一个正在运行的 Odoo 服务器和开发环境。在进入练习之前,请确保您已经完成了此 教程介绍 中描述的所有步骤。

目标

../../../_images/final.png

本章每个练习的解决方案托管在 官方 Odoo 教程仓库 中。

1. 创建系统托盘项

首先,我们希望在系统托盘中显示一个计数器。

  1. 创建一个包含“Hello World”Owl 组件的 clicker_systray_item.js`(和 `xml)文件。

  2. 将其注册到系统托盘注册表,并确保它是可见的。

  3. 更新该项的内容,使其显示以下字符串:点击数:0,并在右侧添加一个按钮以增加数值。

../../../_images/systray.png

瞧,我们有了一个完全可用的点击游戏!

2. 计数外部点击

老实说,这还不太有趣。因此,让我们添加一个新功能:我们希望用户界面上的所有点击都计入总点击数,这样可以激励用户尽可能多地使用 Odoo!但显然,对主计数器的有意点击应该仍然贡献更多。

  1. 使用 useExternalListener 监听 document.body 上的所有点击。

  2. 每次点击都应该使计数器值增加 1。

  3. 修改代码,使得每次点击计数器时,值增加 10。

  4. 确保点击计数器不会使值增加 11!

  5. 额外挑战:确保外部监听器捕获事件,以免错过任何点击。

3. 创建客户端操作

目前,当前用户界面非常小:它只是一个系统托盘项。我们当然需要更多空间来展示更多的游戏内容。为此,让我们创建一个客户端操作。客户端操作是由网页客户端管理的主要操作,用于显示组件。

  1. 创建一个包含“Hello World”组件的 client_action.js`(和 `xml)文件。

  2. 将该客户端操作注册到操作注册表中,名称为 awesome_clicker.client_action

  3. 在系统托盘项上添加一个文本为 打开 的按钮。点击它应该会打开客户端操作 awesome_clicker.client_action (使用操作服务来完成)。

  4. 为了避免干扰员工的工作流程,我们更希望客户端操作在一个弹出窗口中打开,而不是全屏模式。修改 doAction 调用,使其在弹出窗口中打开。

    小技巧

    您可以在 doAction 中使用 target: "new" 来在弹出窗口中打开操作:

    {
       type: "ir.actions.client",
       tag: "awesome_clicker.client_action",
       target: "new",
       name: "Clicker"
    }
    
../../../_images/client_action.png

4. 将状态移动到服务中

目前,我们的客户端操作只是一个“Hello World”组件。我们希望它能够显示游戏状态,但该状态目前仅在系统托盘项中可用。因此,我们需要更改状态的位置,使其对所有组件都可用。这是服务的一个完美用例。

  1. 创建一个带有相应服务的 clicker_service.js 文件。

  2. 此服务应导出一个响应式值(点击次数)以及一些用于更新它的函数:

    const state = reactive({ clicks: 0 });
    ...
    return {
       state,
       increment(inc) {
          state.clicks += inc
       }
    };
    
  3. 在系统托盘项和客户端操作中访问状态(别忘了使用 useState)。修改系统托盘项以移除其自身的本地状态并使用它。此外,您可以移除 +10 点击 按钮。

  4. 在客户端操作中显示状态,并添加一个 +10 点击按钮。

../../../_images/increment_button.png

5. 使用自定义钩子

目前,代码中每个需要使用点击服务的部分都必须导入 useServiceuseState。由于这种情况很常见,让我们使用一个自定义钩子。这也有助于更强调 clicker 部分,而减少对 service 部分的关注。

  1. 导出一个 useClicker 钩子。

  2. 将所有当前对点击服务的使用更新为新钩子:

    this.clicker = useClicker();
    

6. 人性化显示值

未来我们将显示大数字,因此让我们为此做好准备。有一个 humanNumber 函数可以将数字格式化为更易于理解的方式:例如,1234 可以格式化为 1.2k

  1. 使用它来显示我们的计数器(在系统托盘项和客户端操作中)。

  2. 创建一个显示值的 ClickValue 组件。

    注解

    Owl 允许仅包含文本节点的组件!

../../../_images/humanized_number.png

7. 在 ClickValue 组件中添加工具提示

使用 humanNumber 函数后,我们在界面上失去了一些精度。让我们将实际数字作为工具提示显示出来。

  1. 工具提示需要一个 HTML 元素。将 ClickValue 修改为用 <span/> 元素包裹值。

  2. 添加一个动态的 data-tooltip 属性以显示精确值。

../../../_images/humanized_tooltip.png

8. 购买点击机器人

让我们让游戏变得更有趣:当玩家第一次达到 1000 次点击时,游戏应该解锁一个新功能:玩家可以用 1000 次点击购买机器人。这些机器人将每 10 秒生成 10 次点击。

  1. 在我们的状态中添加一个 level 数字。这是一个在某些里程碑时会递增的数字,并解锁新功能。

  2. 在我们的状态中添加一个 clickBots 数字。它表示已购买的机器人数量。

  3. 修改客户端操作以显示点击机器人的数量(仅当 level >= 1 时),并添加一个“购买”按钮,该按钮在 clicks >= 1000 时启用。“购买”按钮应将点击机器人数量增加 1。

  4. 在服务中设置一个 10 秒的间隔,该间隔将点击次数增加 10*clickBots

  5. 如果玩家没有足够的点击次数,请确保“购买”按钮被禁用。

../../../_images/clickbot.png

9. 重构为类模型

当前代码是以某种函数式风格编写的。但为此,我们必须在点击器对象中导出状态及其所有更新函数。随着项目的发展,这可能会变得越来越复杂。为了简化,让我们将业务逻辑从业务服务中分离出来,并放入一个类中。

  1. 创建一个 clicker_model 文件,导出一个响应式类。将所有状态和更新函数从服务移动到模型中。

    小技巧

    您可以使用 @web/core/utils/reactive 中的 Reactive 类扩展 ClickerModel。Reactive 类将模型包装为响应式代理。

  2. 重写点击器服务以实例化并导出点击器模型类。

10. 在达到里程碑时通知

当我们达到 1k 点击时,几乎没有反馈表明发生了变化。让我们使用 effect 服务来清楚地传达这一信息。问题是我们的点击模型无法访问服务。此外,我们希望尽可能将 UI 关注点排除在模型之外。因此,我们可以探索一种新的通信策略:事件总线。

  1. 更新点击器模型以实例化一个总线,并在我们首次达到 1000 次点击时触发 MILESTONE_1k 事件。

  2. 更改点击器服务以监听模型总线上的相同事件。

  3. 当这种情况发生时,使用 effect 服务显示彩虹人。

  4. 添加一些文字说明用户现在可以购买点击机器人。

../../../_images/milestone1.png

11. 添加 BigBots

显然,我们需要一种方法为玩家提供更多选择。让我们添加一种新型的点击机器人:BigBots,它们更强大:每 10 秒提供 100 次点击,但需要 5000 次点击才能购买。

  1. 当分数达到 5k 时增加 level (因此应该是 2)。

  2. 更新状态以跟踪 BigBots。

  3. BigBots 应在 level >= 2 时可用。

  4. 在客户端操作中显示相应的信息。

小技巧

如果需要在模板中使用 <> 作为 JavaScript 表达式,请小心,因为这可能会与 XML 解析器冲突。为了解决这个问题,您可以使用以下特殊别名之一:gt、gte、lt lte。请参阅 Owl 文档中的模板表达式页面

../../../_images/bigbot.png

12. 添加一种新资源:能量

现在,为了增加另一个扩展点,让我们添加一种新资源:能量倍增器。这是一个可以在 level >= 3 时提升的数字,并且会将机器人操作的结果乘以该倍数(因此,点击机器人不再提供一次点击,而是提供 multiplier 次点击)。

  1. 当分数达到 100k 时递增 level (因此应该是 3)。

  2. 更新状态以跟踪能量值(初始值为 1)。

  3. 更改机器人以使用该数字作为倍增器。

  4. 更新用户界面以显示并允许用户购买新的能量等级(费用:50k)。

../../../_images/bigbot.png

13. 定义一些随机奖励

我们希望用户有时可以获得奖励,以鼓励使用 Odoo。

  1. 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,
       },
    ];
    

    您可以向该列表中添加任何您想要的内容!

  2. 定义一个函数 getReward,它将从与当前解锁等级匹配的奖励列表中随机选择一个奖励。

  3. 提取在数组中随机选择的代码到一个名为 choose 的函数中,并将其移动到另一个 utils.js 文件中。

14. 在打开表单视图时提供奖励

  1. 修补表单控制器。每次创建表单控制器时,它应随机决定(1% 的几率)是否应给予奖励。

  2. 如果答案是肯定的,在模型上调用方法 getReward

  3. 该方法应选择一个奖励,发送一个带有 收集 按钮的粘性通知,然后应用奖励,最后打开 clicker 客户端操作。

../../../_images/reward.png

15. 在命令面板中添加命令

  1. 在命令面板中添加一个命令 打开点击游戏

  2. 添加另一个命令:购买 1 个点击机器人

../../../_images/command_palette.png

16. 添加另一种资源:树木

现在是时候引入一种全新的资源类型了。这里有一个不太具争议的选择:树木。我们将允许用户种植(或收集?)果树。一棵树需要花费 100 万次点击,但它会为我们提供水果(梨或樱桃)。

  1. 更新状态以跟踪各种类型的树木:梨树/樱桃树,以及它们的果实。

  2. 添加一个计算树木和果实总数的函数。

  3. 点击数 >= 1 000 000 时定义一个新的解锁等级。

  4. 更新客户端用户界面以显示树木和果实的数量,并允许购买树木。

  5. 每 30 秒为每棵树增加 1 个果实。

../../../_images/trees.png

17. 为系统托盘项使用下拉菜单

我们的游戏开始变得有趣了。但目前,系统托盘只显示点击总数。我们希望看到更多信息:树木和果实的总数。此外,能够快速访问一些命令和其他信息也很有用。让我们使用下拉菜单吧!

  1. 将系统托盘项替换为下拉菜单。

  2. 它应该显示点击数、树木数和果实数,每个项目都配有精美的图标。

  3. 点击它应打开一个下拉菜单,显示更详细的信息:每种树木和果实的详细信息。

  4. 此外,一些带有命令的下拉菜单项:打开点击游戏、购买点击机器人等。

../../../_images/dropdown.png

18. 使用笔记本组件

我们现在跟踪了更多的信息。让我们通过使用 Notebook 组件,将信息和功能组织到不同的标签页中来改进客户端界面:

  1. 使用 Notebook 组件。

  2. 所有 点击 内容应显示在一个标签页中。

  3. 所有 树木/果实 内容应显示在另一个标签页中。

../../../_images/notebook.png

19. 持久化游戏状态

您一定注意到了我们游戏中的一个重大缺陷:它是短暂的。每次用户关闭浏览器标签页时,游戏状态都会丢失。让我们修复这个问题。我们将使用本地存储来持久化状态。

  1. @web/core/browser/browser 导入 browser 以访问本地存储。

  2. 每 10 秒序列化一次状态(在同一间隔代码中),并将其存储在本地存储中。

  3. clicker 服务启动时,它应该从本地存储加载状态(如果有的话),否则初始化自身。

20. 引入状态迁移系统

一旦您在某处持久化了状态,就会出现一个新问题:当您更新代码时,状态的结构发生了变化,而用户使用旧版本创建的状态打开了浏览器,会发生什么?欢迎来到迁移问题的世界!

尽早解决这个问题可能是明智的。我们在这里要做的就是为状态添加一个版本号,并引入一个系统,如果状态不是最新的,则自动更新状态。

  1. 为状态添加一个版本号。

  2. 定义一个(空的)迁移列表。迁移是一个对象,包含一个 fromVersion 数字、一个 toVersion 数字和一个 apply 函数。

  3. 每当代码从本地存储加载状态时,它应该检查版本号。如果状态不是最新的,则应应用所有必要的迁移。

21. 添加另一种类型的树木

为了测试我们的迁移系统,让我们添加一种新的树木类型:桃树。

  1. 添加 peach 树木。

  2. 增加状态版本号。

  3. 定义一个迁移。

../../../_images/peach_tree.png