From bb7a3ecc7521c985eaf8db277f4b37b175e90369 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Mon, 22 Dec 2025 17:20:41 +0100 Subject: [PATCH 01/11] [IMP] awesome_clicker: add click counter to systray This will add a click counter to the systray. This counter is incremented when clicking the button next to the counter. It will serve as the base of the awesome_clicker module game. --- .../clicker_systray_item.js | 21 +++++++++++++++++++ .../clicker_systray_item.xml | 11 ++++++++++ 2 files changed, 32 insertions(+) create mode 100644 awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js create mode 100644 awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js new file mode 100644 index 00000000000..7bc36576bf3 --- /dev/null +++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js @@ -0,0 +1,21 @@ +import { Component, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; + +class ClickerSystray extends Component { + static template = "awesome_clicker.ClickerSystray"; + static props = {}; + + setup() { + this.state = useState({ counter: 0 }); + } + + increment() { + this.state.counter++; + } +} + +export const systrayItem = { + Component: ClickerSystray, +}; + +registry.category("systray").add("awesome_clicker.ClickerSystray", systrayItem, { sequence: 1000 }); diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml new file mode 100644 index 00000000000..fc509c776b4 --- /dev/null +++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml @@ -0,0 +1,11 @@ + + + +
+ Clicks: + +
+
+
From 25059b8ec409e06e60d62d85f4517c65221c9a8a Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Tue, 23 Dec 2025 09:34:22 +0100 Subject: [PATCH 02/11] [IMP] awesome_clicker: create a client action and move the state to a service. The client action will display a dedicated window that will serve to play the game. In the current state, it only contains an increment button with a total value of 10. The clicks state has been transfer to a service to share it between the systray item and the client action. That way the clicks count will be the same for the entire web client. The service also implement the increment() function to replace the one that was defined in the systray item class. --- awesome_clicker/static/src/clicker_service.js | 22 +++++++++++++++++++ .../clicker_systray_item.js | 13 ++++++++--- .../clicker_systray_item.xml | 7 ++++-- .../static/src/client_action/client_action.js | 14 ++++++++++++ .../src/client_action/client_action.xml | 9 ++++++++ 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 awesome_clicker/static/src/clicker_service.js create mode 100644 awesome_clicker/static/src/client_action/client_action.js create mode 100644 awesome_clicker/static/src/client_action/client_action.xml diff --git a/awesome_clicker/static/src/clicker_service.js b/awesome_clicker/static/src/clicker_service.js new file mode 100644 index 00000000000..03bb09dc893 --- /dev/null +++ b/awesome_clicker/static/src/clicker_service.js @@ -0,0 +1,22 @@ +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; + +const clickerService = { + start(env) { + const state = reactive({ clicks: 0 }); + + function increment(inc) { + state.clicks += inc + } + + document.addEventListener("click", () => increment(1), true); + + return { + state, + increment, + }; + }, +}; + + +registry.category("services").add("awesome_clicker.clicker", clickerService); diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js index 7bc36576bf3..de816073f01 100644 --- a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js +++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js @@ -1,16 +1,23 @@ import { Component, useState } from "@odoo/owl"; import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; class ClickerSystray extends Component { static template = "awesome_clicker.ClickerSystray"; static props = {}; setup() { - this.state = useState({ counter: 0 }); + this.clickerService = useState(useService("awesome_clicker.clicker")); + this.action = useService("action"); } - increment() { - this.state.counter++; + openClientAction() { + this.action.doAction({ + type: "ir.actions.client", + tag: "awesome_clicker.client_action", + target: "new", + name: "Clicker", + }); } } diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml index fc509c776b4..e89c87fb0a1 100644 --- a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml +++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml @@ -2,10 +2,13 @@
- Clicks: - +
diff --git a/awesome_clicker/static/src/client_action/client_action.js b/awesome_clicker/static/src/client_action/client_action.js new file mode 100644 index 00000000000..b926e6a1f96 --- /dev/null +++ b/awesome_clicker/static/src/client_action/client_action.js @@ -0,0 +1,14 @@ +import { Component, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; + +class ClickerClientAction extends Component { + static template = "awesome_clicker.ClientAction"; + static props = ["*"]; + + setup() { + this.clickerService = useState(useService("awesome_clicker.clicker")); + } +} + +registry.category("actions").add("awesome_clicker.client_action", ClickerClientAction); diff --git a/awesome_clicker/static/src/client_action/client_action.xml b/awesome_clicker/static/src/client_action/client_action.xml new file mode 100644 index 00000000000..1f3ee7a21dc --- /dev/null +++ b/awesome_clicker/static/src/client_action/client_action.xml @@ -0,0 +1,9 @@ + + + + Clicks: + + + From f86b382683f8dc6e1a396880c95d775db994db72 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Tue, 23 Dec 2025 10:24:43 +0100 Subject: [PATCH 03/11] [IMP] awesome_clicker: add a custom clicker hook, humanize the clicks counter and add the ClickBots. This will introduce a custom cliker hook that will handle the required import to create and use the clicker service. That way, the classes that use this service will only require to import and use this hook. This will also add the clickValue class and component. It transform the displayed value of the clicks counter to something more readable for human, especially with greater values. To allow the player to see the exact number of clicks, a dynamic tooltip has been added to the counter. Finally, this will add the possibility to buy ClickBots with 1000 clicks. Those ClickBots will be available when reaching level 1 (1000 clicks) and will generate 10 clicks each every 10 seconds. --- awesome_clicker/static/src/clicker_hook.js | 6 +++++ awesome_clicker/static/src/clicker_service.js | 25 +++++++++++++++++-- .../clicker_systray_item.js | 7 ++++-- .../clicker_systray_item.xml | 4 +-- .../static/src/clicker_value/clicker_value.js | 19 ++++++++++++++ .../src/clicker_value/clicker_value.xml | 6 +++++ .../static/src/client_action/client_action.js | 8 +++--- .../src/client_action/client_action.xml | 18 +++++++++++-- 8 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 awesome_clicker/static/src/clicker_hook.js create mode 100644 awesome_clicker/static/src/clicker_value/clicker_value.js create mode 100644 awesome_clicker/static/src/clicker_value/clicker_value.xml diff --git a/awesome_clicker/static/src/clicker_hook.js b/awesome_clicker/static/src/clicker_hook.js new file mode 100644 index 00000000000..6e46efd3468 --- /dev/null +++ b/awesome_clicker/static/src/clicker_hook.js @@ -0,0 +1,6 @@ +import { useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +export function useClicker() { + return useState(useService("awesome_clicker.clicker")); +} diff --git a/awesome_clicker/static/src/clicker_service.js b/awesome_clicker/static/src/clicker_service.js index 03bb09dc893..59d78270e89 100644 --- a/awesome_clicker/static/src/clicker_service.js +++ b/awesome_clicker/static/src/clicker_service.js @@ -3,10 +3,30 @@ import { reactive } from "@odoo/owl"; const clickerService = { start(env) { - const state = reactive({ clicks: 0 }); + const state = reactive({ + clicks: 0, + level: 0, + clickBots: 0, + }); + + setInterval(() => { + state.clicks += state.clickBots * 10; + }, 10000); function increment(inc) { - state.clicks += inc + state.clicks += inc; + if (state.level < 1 && state.clicks >= 1000){ + state.level++; + } + } + + function buyClickBot() { + const clickBotPrice = 1000; + if (state.clicks < clickBotPrice){ + return false; + } + state.clicks -= clickBotPrice; + state.clickBots++; } document.addEventListener("click", () => increment(1), true); @@ -14,6 +34,7 @@ const clickerService = { return { state, increment, + buyClickBot, }; }, }; diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js index de816073f01..6aafe3564a7 100644 --- a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js +++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js @@ -1,13 +1,16 @@ -import { Component, useState } from "@odoo/owl"; +import { Component } from "@odoo/owl"; import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; +import { useClicker } from "../clicker_hook"; +import { ClickerValue } from "../clicker_value/clicker_value"; class ClickerSystray extends Component { static template = "awesome_clicker.ClickerSystray"; static props = {}; + static components = { ClickerValue }; setup() { - this.clickerService = useState(useService("awesome_clicker.clicker")); + this.clicker = useClicker() this.action = useService("action"); } diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml index e89c87fb0a1..74c28f5ea2b 100644 --- a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml +++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml @@ -2,8 +2,8 @@
- Clicks: - +
+

Bots

+
+
+ x ClickBots (10 clicks/10 secons) + +
+
+ +
+
+
From a0d0a870b5e1fb146d4ddb82fa9e761d0ea00d3c Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Tue, 23 Dec 2025 10:43:42 +0100 Subject: [PATCH 04/11] [REF] awesome_clicker: add the clickerModel class split business logic out of the service. --- awesome_clicker/static/src/clicker_model.js | 31 ++++++++++++++++ awesome_clicker/static/src/clicker_service.js | 37 +------------------ .../static/src/clicker_value/clicker_value.js | 3 +- .../src/clicker_value/clicker_value.xml | 3 +- .../src/client_action/client_action.xml | 23 +++++++----- 5 files changed, 48 insertions(+), 49 deletions(-) create mode 100644 awesome_clicker/static/src/clicker_model.js diff --git a/awesome_clicker/static/src/clicker_model.js b/awesome_clicker/static/src/clicker_model.js new file mode 100644 index 00000000000..e6ab70a514a --- /dev/null +++ b/awesome_clicker/static/src/clicker_model.js @@ -0,0 +1,31 @@ +import { Reactive } from "@web/core/utils/reactive"; + +export class ClickerModel extends Reactive { + constructor() { + super(); + this.clicks = 0; + this.level = 0; + this.clickBots = 0; + + document.addEventListener("click", () => this.increment(1), true); + setInterval(() => { + this.clicks += this.clickBots * 10; + }, 10000); + } + + increment(inc) { + this.clicks += inc; + if (this.level < 1 && this.clicks >= 1000) { + this.level++; + } + } + + buyClickBot() { + const clickBotPrice = 1000; + if (this.clicks < clickBotPrice) { + return false; + } + this.clicks -= clickBotPrice; + this.clickBots += 1; + } +} diff --git a/awesome_clicker/static/src/clicker_service.js b/awesome_clicker/static/src/clicker_service.js index 59d78270e89..d04b7ef526b 100644 --- a/awesome_clicker/static/src/clicker_service.js +++ b/awesome_clicker/static/src/clicker_service.js @@ -1,43 +1,10 @@ import { registry } from "@web/core/registry"; -import { reactive } from "@odoo/owl"; +import { ClickerModel } from "./clicker_model"; const clickerService = { start(env) { - const state = reactive({ - clicks: 0, - level: 0, - clickBots: 0, - }); - - setInterval(() => { - state.clicks += state.clickBots * 10; - }, 10000); - - function increment(inc) { - state.clicks += inc; - if (state.level < 1 && state.clicks >= 1000){ - state.level++; - } - } - - function buyClickBot() { - const clickBotPrice = 1000; - if (state.clicks < clickBotPrice){ - return false; - } - state.clicks -= clickBotPrice; - state.clickBots++; - } - - document.addEventListener("click", () => increment(1), true); - - return { - state, - increment, - buyClickBot, - }; + return new ClickerModel(); }, }; - registry.category("services").add("awesome_clicker.clicker", clickerService); diff --git a/awesome_clicker/static/src/clicker_value/clicker_value.js b/awesome_clicker/static/src/clicker_value/clicker_value.js index b3e30bfbc83..a5018bad8ac 100644 --- a/awesome_clicker/static/src/clicker_value/clicker_value.js +++ b/awesome_clicker/static/src/clicker_value/clicker_value.js @@ -11,9 +11,8 @@ export class ClickerValue extends Component { } get humanizedClicks() { - return humanNumber(this.clicker.state.clicks, { + return humanNumber(this.clicker.clicks, { decimals: 1, }); } - } diff --git a/awesome_clicker/static/src/clicker_value/clicker_value.xml b/awesome_clicker/static/src/clicker_value/clicker_value.xml index de4437434e2..24fd29643f2 100644 --- a/awesome_clicker/static/src/clicker_value/clicker_value.xml +++ b/awesome_clicker/static/src/clicker_value/clicker_value.xml @@ -1,6 +1,5 @@ - - + diff --git a/awesome_clicker/static/src/client_action/client_action.xml b/awesome_clicker/static/src/client_action/client_action.xml index 9f8768286d4..e3677b6a968 100644 --- a/awesome_clicker/static/src/client_action/client_action.xml +++ b/awesome_clicker/static/src/client_action/client_action.xml @@ -1,23 +1,26 @@ - - - Clicks: - -
+ + +
+ Clicks: + +
+

Bots

- x ClickBots (10 clicks/10 secons) - + x ClickBots (10 clicks/10seconds) +
-
+ From 1269ecf02ddc1c84ee5c1c97a87e450c25bbdc6d Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Tue, 23 Dec 2025 11:48:55 +0100 Subject: [PATCH 05/11] [IMP] awesome_clicker: add milestone notification and bigbots This will add a new effect when reaching milestones: the rainbow man effect, to give a feedack to the player and let them know that they can buy new bots. This will also add new bigbots: they are more expensive than clickbot but generate 100 clicks every 10 seconds. --- awesome_clicker/static/src/clicker_model.js | 49 +++++++++++++++---- awesome_clicker/static/src/clicker_service.js | 15 +++++- .../src/client_action/client_action.xml | 29 ++++++----- 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/awesome_clicker/static/src/clicker_model.js b/awesome_clicker/static/src/clicker_model.js index e6ab70a514a..5af31fbc9b8 100644 --- a/awesome_clicker/static/src/clicker_model.js +++ b/awesome_clicker/static/src/clicker_model.js @@ -1,31 +1,62 @@ import { Reactive } from "@web/core/utils/reactive"; +import { EventBus } from "@odoo/owl"; export class ClickerModel extends Reactive { constructor() { super(); this.clicks = 0; this.level = 0; - this.clickBots = 0; + this.bus = new EventBus(); + this.bots = { + clickbots: { + price: 1000, + level: 1, + increment: 10, + purchased: 0, + }, + bigbots: { + price: 5000, + level: 2, + increment: 100, + purchased: 0, + }, + }; document.addEventListener("click", () => this.increment(1), true); setInterval(() => { - this.clicks += this.clickBots * 10; + for (const bot in this.bots){ + this.clicks += this.bots[bot].increment * this.bots[bot].purchased; + } }, 10000); } increment(inc) { this.clicks += inc; - if (this.level < 1 && this.clicks >= 1000) { - this.level++; + if( + this.milestones[this.level] && + this.clicks >= this.milestones[this.level].clicks + ) { + this.bus.trigger("MILESTONE", this.milestones[this.level]); + this.level += 1; } } - buyClickBot() { - const clickBotPrice = 1000; - if (this.clicks < clickBotPrice) { + buyBot(name) { + if (!Object.keys(this.bots).includes(name)){ + throw new Error('Invalid bot name ${name}'); + } + if (this.clicks < this.bots[name].price){ return false; } - this.clicks -= clickBotPrice; - this.clickBots += 1; + + this.clicks -= this.bots[name].price; + this.bots[name].purchased++; + } + + get milestones() { + return [ + { clicks: 1000, unlock: "clickBot"}, + { clicks: 5000, unlock: "bigBot"}, + ]; } } diff --git a/awesome_clicker/static/src/clicker_service.js b/awesome_clicker/static/src/clicker_service.js index d04b7ef526b..590c56d7c0b 100644 --- a/awesome_clicker/static/src/clicker_service.js +++ b/awesome_clicker/static/src/clicker_service.js @@ -2,8 +2,19 @@ import { registry } from "@web/core/registry"; import { ClickerModel } from "./clicker_model"; const clickerService = { - start(env) { - return new ClickerModel(); + dependencies: ["effect"], + start(env, services) { + const clickerModel = new ClickerModel(); + const bus = clickerModel.bus; + + bus.addEventListener("MILESTONE", (ev) => { + services.effect.add({ + message: `Milestone reached! You can now buy ${ev.detail.unlock}`, + type: "rainbow_man", + }); + }); + + return clickerModel; }, }; diff --git a/awesome_clicker/static/src/client_action/client_action.xml b/awesome_clicker/static/src/client_action/client_action.xml index e3677b6a968..8a4cdf38996 100644 --- a/awesome_clicker/static/src/client_action/client_action.xml +++ b/awesome_clicker/static/src/client_action/client_action.xml @@ -1,24 +1,31 @@ - +
Clicks:
+
+ Level: +

Bots

-
-
- x ClickBots (10 clicks/10seconds) - -
-
- -
+
+ +
+
+ x ( clicks/10seconds) + +
+
+ +
+
+
From 330aa318551faeeb04083132f50c59a5c0b2b942 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Tue, 23 Dec 2025 14:24:03 +0100 Subject: [PATCH 06/11] [IMP] awesome_clicker: add a new power resource. This will add a new resource to the game: power upgrades. This upgrade will unlock at level 3 (100k clicks) and will cost 50k for each. It multiplies the power of the bots. Thi will also add the total clicks per second of every bot and the grand total on the client action view. --- awesome_clicker/static/src/clicker_model.js | 29 ++++++++++++++++--- .../src/client_action/client_action.xml | 23 +++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/awesome_clicker/static/src/clicker_model.js b/awesome_clicker/static/src/clicker_model.js index 5af31fbc9b8..bbe0fb56660 100644 --- a/awesome_clicker/static/src/clicker_model.js +++ b/awesome_clicker/static/src/clicker_model.js @@ -7,6 +7,7 @@ export class ClickerModel extends Reactive { this.clicks = 0; this.level = 0; this.bus = new EventBus(); + this.power = 1; this.bots = { clickbots: { price: 1000, @@ -21,13 +22,14 @@ export class ClickerModel extends Reactive { purchased: 0, }, }; + this.clicksPerSecond = 0; document.addEventListener("click", () => this.increment(1), true); setInterval(() => { for (const bot in this.bots){ - this.clicks += this.bots[bot].increment * this.bots[bot].purchased; + this.increment(this.bots[bot].increment * this.bots[bot].purchased * this.power); } - }, 10000); + }, 1000); } increment(inc) { @@ -38,6 +40,7 @@ export class ClickerModel extends Reactive { ) { this.bus.trigger("MILESTONE", this.milestones[this.level]); this.level += 1; + console.log("new level: " + this.level) } } @@ -51,12 +54,30 @@ export class ClickerModel extends Reactive { this.clicks -= this.bots[name].price; this.bots[name].purchased++; + this.updateClicksPerSecond(); + } + + updateClicksPerSecond(){ + const totalCPS = Object.values(this.bots).reduce((total, bot) => { + return total + (bot.increment * bot.purchased * this.power); + }, 0); + this.clicksPerSecond = totalCPS; + } + + buyPower(){ + const powerPrice = 50000; + if (this.clicks < powerPrice) { + return false; + } + this.clicks -= powerPrice; + this.power += 1; } get milestones() { return [ - { clicks: 1000, unlock: "clickBot"}, - { clicks: 5000, unlock: "bigBot"}, + { clicks: 1000, unlock: "clickBot" }, + { clicks: 5000, unlock: "bigBot" }, + { clicks: 100000, unlock: "power" }, ]; } } diff --git a/awesome_clicker/static/src/client_action/client_action.xml b/awesome_clicker/static/src/client_action/client_action.xml index 8a4cdf38996..08d4c68aefe 100644 --- a/awesome_clicker/static/src/client_action/client_action.xml +++ b/awesome_clicker/static/src/client_action/client_action.xml @@ -11,14 +11,17 @@ Level:
-

Bots

+

Bots ( clicks/second)

- x ( clicks/10seconds) + x (+ clicks/seconds)
+
+ Total clicks/seconds: +
+
+

Power Upgrades

+
+
+
+ x + +
+
+ +
+
+
+
From aba8527554731f488ee6d33829836cda689060ae Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Tue, 23 Dec 2025 15:02:09 +0100 Subject: [PATCH 07/11] [IMP] awesome_clicker: add random rewards This will add reward that will randomly be given when opening form views inside the Odoo databse. The rewards are defined in the click_rewards.js file and all have a description and an action (apply). A minimum and maximum level can also be set. --- awesome_clicker/static/src/click_rewards.js | 24 ++++++++++++++++ awesome_clicker/static/src/clicker_model.js | 14 ++++++++++ awesome_clicker/static/src/clicker_service.js | 28 ++++++++++++++++++- .../clicker_systray_item.js | 2 +- .../src/client_action/client_action.xml | 3 ++ .../form_controller/form_controller_patch.js | 18 ++++++++++++ awesome_clicker/static/src/utils.js | 3 ++ 7 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 awesome_clicker/static/src/click_rewards.js create mode 100644 awesome_clicker/static/src/form_controller/form_controller_patch.js create mode 100644 awesome_clicker/static/src/utils.js diff --git a/awesome_clicker/static/src/click_rewards.js b/awesome_clicker/static/src/click_rewards.js new file mode 100644 index 00000000000..b1b60248f36 --- /dev/null +++ b/awesome_clicker/static/src/click_rewards.js @@ -0,0 +1,24 @@ +export const rewards = [ + { + description: "Get 1 free click", + apply(clicker) { + clicker.increment(1); + }, + maxLevel: 3, + }, + { + description: "Get 10 free click", + apply(clicker) { + clicker.increment(10); + }, + minLevel: 3, + maxLevel: 4, + }, + { + description: "Free power upgrade", + apply(clicker) { + clicker.power++; + }, + minLevel: 3, + }, +]; diff --git a/awesome_clicker/static/src/clicker_model.js b/awesome_clicker/static/src/clicker_model.js index bbe0fb56660..6463db5d0d3 100644 --- a/awesome_clicker/static/src/clicker_model.js +++ b/awesome_clicker/static/src/clicker_model.js @@ -1,5 +1,7 @@ import { Reactive } from "@web/core/utils/reactive"; import { EventBus } from "@odoo/owl"; +import { rewards } from "./click_rewards"; +import { choose } from "./utils"; export class ClickerModel extends Reactive { constructor() { @@ -80,4 +82,16 @@ export class ClickerModel extends Reactive { { clicks: 100000, unlock: "power" }, ]; } + + getReward(){ + const availableRewards = rewards.filter(reward => { + const minLevel = reward.minLevel ?? 0; + const maxLevel = reward.maxLevel ?? Infinity; + + return this.level >= minLevel && this.level <= maxLevel; + }); + const reward = choose(availableRewards); + this.bus.trigger("REWARD", reward); + return reward; + } } diff --git a/awesome_clicker/static/src/clicker_service.js b/awesome_clicker/static/src/clicker_service.js index 590c56d7c0b..cbe50eec4c7 100644 --- a/awesome_clicker/static/src/clicker_service.js +++ b/awesome_clicker/static/src/clicker_service.js @@ -2,7 +2,7 @@ import { registry } from "@web/core/registry"; import { ClickerModel } from "./clicker_model"; const clickerService = { - dependencies: ["effect"], + dependencies: ["action", "effect", "notification"], start(env, services) { const clickerModel = new ClickerModel(); const bus = clickerModel.bus; @@ -14,6 +14,32 @@ const clickerService = { }); }); + bus.addEventListener("REWARD", (ev) => { + const reward = ev.detail; + const closeNotification = services.notification.add( + `Congrats you won a reward: "${reward.description}"`, + { + type: "success", + sticky: true, + buttons: [ + { + name: "Collect", + onClick: () => { + reward.apply(clickerModel); + closeNotification(); + services.action.doAction({ + type: "ir.actions.client", + tag: "awesome_clicker.client_action", + target: "new", + name: "Clicker Game", + }); + }, + }, + ], + }, + ); + }); + return clickerModel; }, }; diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js index 6aafe3564a7..7fbb321c4b1 100644 --- a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js +++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js @@ -19,7 +19,7 @@ class ClickerSystray extends Component { type: "ir.actions.client", tag: "awesome_clicker.client_action", target: "new", - name: "Clicker", + name: "Clicker Game", }); } } diff --git a/awesome_clicker/static/src/client_action/client_action.xml b/awesome_clicker/static/src/client_action/client_action.xml index 08d4c68aefe..8efa739e87e 100644 --- a/awesome_clicker/static/src/client_action/client_action.xml +++ b/awesome_clicker/static/src/client_action/client_action.xml @@ -9,6 +9,9 @@
Level: +

Bots ( clicks/second)

diff --git a/awesome_clicker/static/src/form_controller/form_controller_patch.js b/awesome_clicker/static/src/form_controller/form_controller_patch.js new file mode 100644 index 00000000000..1ff97dc398c --- /dev/null +++ b/awesome_clicker/static/src/form_controller/form_controller_patch.js @@ -0,0 +1,18 @@ +import { FormController } from "@web/views/form/form_controller"; +import { patch } from "@web/core/utils/patch"; +import { useClicker } from "../clicker_hook"; + +const FormControllerPatch = { + setup() { + super.setup(...arguments); + const clicker = useClicker(); + console.log("Reward ?") + const random = Math.random; + if (Math.random() < 0.05){ + console.log("Reward granted"); + clicker.getReward(); + } + }, +}; + +patch(FormController.prototype, FormControllerPatch); diff --git a/awesome_clicker/static/src/utils.js b/awesome_clicker/static/src/utils.js new file mode 100644 index 00000000000..f3f7c3977e9 --- /dev/null +++ b/awesome_clicker/static/src/utils.js @@ -0,0 +1,3 @@ +export function choose(list) { + return list[Math.floor(Math.random() * list.length)]; +} From f53ce52f95fa544e16a7aab1c1d0c72a2faa352e Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Wed, 24 Dec 2025 09:15:16 +0100 Subject: [PATCH 08/11] [IMP] awesome_clicker: add tree resource and commands. This will add a new resource: trees. Each tree will cost 1M clicks and produce a fruit each 30 seconds. It represent the actual end-game of the clicker. The systray item has been improve to display the click count, tree count and fruit count and transform it into a dropdown menu. Inside the dropdown, we can access the clicker game panel and use a command to buy a clickbot. It also display the number of purchased trees and produced fruits in more detail. --- awesome_clicker/static/src/clicker_model.js | 41 ++++++++++++++++++- .../static/src/clicker_provider.js | 27 ++++++++++++ .../clicker_systray_item.js | 20 ++++++++- .../clicker_systray_item.xml | 30 ++++++++++---- .../src/client_action/client_action.xml | 29 +++++++++++++ 5 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 awesome_clicker/static/src/clicker_provider.js diff --git a/awesome_clicker/static/src/clicker_model.js b/awesome_clicker/static/src/clicker_model.js index 6463db5d0d3..7a2418e9128 100644 --- a/awesome_clicker/static/src/clicker_model.js +++ b/awesome_clicker/static/src/clicker_model.js @@ -11,27 +11,52 @@ export class ClickerModel extends Reactive { this.bus = new EventBus(); this.power = 1; this.bots = { - clickbots: { + clickbot: { price: 1000, level: 1, increment: 10, purchased: 0, }, - bigbots: { + bigbot: { price: 5000, level: 2, increment: 100, purchased: 0, }, }; + this.trees = { + pearTree:{ + price: 1000000, + level: 4, + produce: 'pear', + purchased: 0, + }, + cherryTree:{ + price: 1000000, + level: 4, + produce: 'cherry', + purchased: 0, + }, + }; + this.fruits = { + pear: 0, + cherry: 0, + }; this.clicksPerSecond = 0; document.addEventListener("click", () => this.increment(1), true); + setInterval(() => { for (const bot in this.bots){ this.increment(this.bots[bot].increment * this.bots[bot].purchased * this.power); } }, 1000); + + setInterval(() => { + for (const tree in this.trees) { + this.fruits[this.trees[tree].produce] += this.trees[tree].purchased; + } + }, 30000); } increment(inc) { @@ -75,11 +100,23 @@ export class ClickerModel extends Reactive { this.power += 1; } + buyTree(name) { + if (!Object.keys(this.trees).includes(name)){ + throw new Error(`Invalid tree name "${name}"`); + } + if (this.clicks < this.trees[name].price){ + return false; + } + this.clicks -= this.trees[name].price; + this.trees[name].purchased += 1; + } + get milestones() { return [ { clicks: 1000, unlock: "clickBot" }, { clicks: 5000, unlock: "bigBot" }, { clicks: 100000, unlock: "power" }, + { clicks: 1000000, unlock: "trees" }, ]; } diff --git a/awesome_clicker/static/src/clicker_provider.js b/awesome_clicker/static/src/clicker_provider.js new file mode 100644 index 00000000000..a59cc673b31 --- /dev/null +++ b/awesome_clicker/static/src/clicker_provider.js @@ -0,0 +1,27 @@ +import { registry } from "@web/core/registry"; + +const commandProviderRegistry = registry.category("command_provider"); + +commandProviderRegistry.add("clicker", { + provide: (env, options) => { + return [ + { + name: "Buy 1 click bot", + action() { + env.services["awesome_clicker.clicker"].buyBot("clickbot"); + }, + }, + { + name: "Open Clicker Game", + action() { + env.services.action.doAction({ + type: "ir.actions.client", + tag: "awesome_clicker.client_action", + target: "new", + name: "Clicker Game", + }); + }, + }, + ]; + }, +}); diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js index 7fbb321c4b1..a3c27d50855 100644 --- a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js +++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js @@ -3,11 +3,13 @@ import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; import { useClicker } from "../clicker_hook"; import { ClickerValue } from "../clicker_value/clicker_value"; +import { Dropdown } from "@web/core/dropdown/dropdown"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; class ClickerSystray extends Component { static template = "awesome_clicker.ClickerSystray"; static props = {}; - static components = { ClickerValue }; + static components = { ClickerValue, Dropdown, DropdownItem }; setup() { this.clicker = useClicker() @@ -22,6 +24,22 @@ class ClickerSystray extends Component { name: "Clicker Game", }); } + + get numberTrees(){ + let sum = 0; + for (const tree in this.clicker.trees){ + sum += this.clicker.trees[tree].purchased; + } + return sum; + } + + get numberFruits(){ + let sum = 0; + for (const fruit in this.clicker.fruits){ + sum += this.clicker.fruits[fruit]; + } + return sum; + } } export const systrayItem = { diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml index 74c28f5ea2b..209ef2b6249 100644 --- a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml +++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml @@ -2,13 +2,29 @@
- Clicks: - - + + + + + + + + + + + x + + + + x + + + +
diff --git a/awesome_clicker/static/src/client_action/client_action.xml b/awesome_clicker/static/src/client_action/client_action.xml index 8efa739e87e..f23e754af57 100644 --- a/awesome_clicker/static/src/client_action/client_action.xml +++ b/awesome_clicker/static/src/client_action/client_action.xml @@ -50,6 +50,35 @@
+
+

Trees

+
+ +
+
+ x (1x /30seconds) + +
+
+ +
+
+
+
+ +

Fruits

+
+ +
+
+ x +
+
+
+
+
From 60320190bbe9bdadbd53504d1e3444cf61c02d50 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Wed, 24 Dec 2025 09:48:11 +0100 Subject: [PATCH 09/11] [IMP] awesome_clicker: improve the client action view This will separate the clicks upgrades from the trees and fruits using a notebook. This will allow to keep thing clearer for the user. --- .../static/src/client_action/client_action.js | 3 +- .../src/client_action/client_action.xml | 153 ++++++++++-------- 2 files changed, 85 insertions(+), 71 deletions(-) diff --git a/awesome_clicker/static/src/client_action/client_action.js b/awesome_clicker/static/src/client_action/client_action.js index 33ab9b59d77..c1f13da18ac 100644 --- a/awesome_clicker/static/src/client_action/client_action.js +++ b/awesome_clicker/static/src/client_action/client_action.js @@ -2,11 +2,12 @@ import { Component } from "@odoo/owl"; import { registry } from "@web/core/registry"; import { useClicker } from "../clicker_hook"; import { ClickerValue } from "../clicker_value/clicker_value"; +import { Notebook } from "@web/core/notebook/notebook"; class ClickerClientAction extends Component { static template = "awesome_clicker.ClientAction"; static props = ["*"]; - static components = { ClickerValue }; + static components = { ClickerValue, Notebook }; setup() { this.clicker = useClicker() diff --git a/awesome_clicker/static/src/client_action/client_action.xml b/awesome_clicker/static/src/client_action/client_action.xml index f23e754af57..e7b0ff1a1d3 100644 --- a/awesome_clicker/static/src/client_action/client_action.xml +++ b/awesome_clicker/static/src/client_action/client_action.xml @@ -1,84 +1,97 @@ -
- Clicks: - -
-
- Level: - -
-
-

Bots ( clicks/second)

-
- -
-
- x (+ clicks/seconds) - -
-
- Total clicks/seconds: -
-
- -
-
-
+
+
+
+ Current Clicks: + +
-
-
-

Power Upgrades

-
-
-
- x - -
-
- -
+ +
+
+ Current Level:
-
-

Trees

-
- -
-
- x (1x /30seconds) - -
-
- -
+ + + +
+

Bots ( clicks/second)

+
+ +
+
+ x (+ clicks/seconds) + +
+
+ Total clicks/seconds: +
+
+ +
+
+
- -
+
-

Fruits

-
- -
-
- x +
+

Power Upgrades

+
+
+
+ x + +
+
+ +
- -
-
+
+
+ + +
+

Trees

+
+ +
+
+ x (1x /30seconds) + +
+
+ +
+
+
+
+ +

Fruits

+
+ +
+
+ x +
+
+
+
+
+
+ From 64a2be2d1bcffd63891263f637182c6753b81445 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Wed, 24 Dec 2025 10:01:57 +0100 Subject: [PATCH 10/11] [IMP] awesome_clicker: add game state persistence. The game will save automatically every 10 seconds inside the browser local storage and this state will be retreive when the database is loaded. If there are no data saved in the browser, a new instance of ClickerModel will be created with the default values. --- awesome_clicker/static/src/clicker_model.js | 17 ++++++++++++++--- awesome_clicker/static/src/clicker_service.js | 8 +++++++- .../static/src/client_action/client_action.xml | 2 +- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/awesome_clicker/static/src/clicker_model.js b/awesome_clicker/static/src/clicker_model.js index 7a2418e9128..f23d71c9071 100644 --- a/awesome_clicker/static/src/clicker_model.js +++ b/awesome_clicker/static/src/clicker_model.js @@ -42,7 +42,6 @@ export class ClickerModel extends Reactive { pear: 0, cherry: 0, }; - this.clicksPerSecond = 0; document.addEventListener("click", () => this.increment(1), true); @@ -81,14 +80,14 @@ export class ClickerModel extends Reactive { this.clicks -= this.bots[name].price; this.bots[name].purchased++; - this.updateClicksPerSecond(); } - updateClicksPerSecond(){ + getClicksPerSecond(){ const totalCPS = Object.values(this.bots).reduce((total, bot) => { return total + (bot.increment * bot.purchased * this.power); }, 0); this.clicksPerSecond = totalCPS; + return totalCPS; } buyPower(){ @@ -111,6 +110,18 @@ export class ClickerModel extends Reactive { this.trees[name].purchased += 1; } + toJSON(){ + const json = Object.assign({}, this); + delete json["bus"]; + return json; + } + + static fromJSON(json){ + const clicker = new ClickerModel(); + const clickerInstance = Object.assign(clicker, json); + return clickerInstance; + } + get milestones() { return [ { clicks: 1000, unlock: "clickBot" }, diff --git a/awesome_clicker/static/src/clicker_service.js b/awesome_clicker/static/src/clicker_service.js index cbe50eec4c7..a3ea6f2f0e1 100644 --- a/awesome_clicker/static/src/clicker_service.js +++ b/awesome_clicker/static/src/clicker_service.js @@ -1,12 +1,18 @@ import { registry } from "@web/core/registry"; import { ClickerModel } from "./clicker_model"; +import { browser } from "@web/core/browser/browser"; const clickerService = { dependencies: ["action", "effect", "notification"], start(env, services) { - const clickerModel = new ClickerModel(); + const localState = JSON.parse(browser.localStorage.getItem("clickerState")); + const clickerModel = localState ? ClickerModel.fromJSON(localState) : new ClickerModel(); const bus = clickerModel.bus; + setInterval(() => { + browser.localStorage.setItem("clickerState", JSON.stringify(clickerModel)); + }, 10000); + bus.addEventListener("MILESTONE", (ev) => { services.effect.add({ message: `Milestone reached! You can now buy ${ev.detail.unlock}`, diff --git a/awesome_clicker/static/src/client_action/client_action.xml b/awesome_clicker/static/src/client_action/client_action.xml index e7b0ff1a1d3..75aac5a537d 100644 --- a/awesome_clicker/static/src/client_action/client_action.xml +++ b/awesome_clicker/static/src/client_action/client_action.xml @@ -21,7 +21,7 @@
-

Bots ( clicks/second)

+

Bots ( clicks/second)

From fb5fbd2f99dee26888fa2d89e24e85b6379a58e1 Mon Sep 17 00:00:00 2001 From: "Gilles (gicha)" Date: Wed, 24 Dec 2025 11:02:10 +0100 Subject: [PATCH 11/11] [IMP] awesome_clicker: add migration mechanism and new tree resource This will add a new migration mechanism, ensuring that when a new version is released, the state save in the local storage is updated correctly. This will also add a new tree resource: peachTree. The game is now in version 2 and will update its save file automatically. --- .../static/src/clicker_migration.js | 36 +++++++++++++++++++ awesome_clicker/static/src/clicker_model.js | 10 ++++++ awesome_clicker/static/src/clicker_service.js | 3 +- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 awesome_clicker/static/src/clicker_migration.js diff --git a/awesome_clicker/static/src/clicker_migration.js b/awesome_clicker/static/src/clicker_migration.js new file mode 100644 index 00000000000..750543aff3e --- /dev/null +++ b/awesome_clicker/static/src/clicker_migration.js @@ -0,0 +1,36 @@ +export const CURRENT_VERSION = 2.0; +export const migrations = [ + { + fromVersion: 1.0, + toVersion: 2.0, + apply: (state) => { + state.trees.peachTree = { + price: 1500000, + level: 5, + produce: 'peach', + purchased: 0, + }; + state.fruits.peach = 0; + }, + }, +]; + +export function migrate(localState) { + console.log("Migration started") + if (localState?.version < CURRENT_VERSION) { + console.log("Current save version: " + localState.version); + for (const migration of migrations) { + console.log("Checking migration from " + migration.fromVersion + " to " + migration.toVersion); + if (localState.version === migration.fromVersion){ + console.log("Applying migration"); + migration.apply(localState); + console.log("Updating save version"); + localState.version = migration.toVersion; + console.log("Update completed"); + } + } + } + console.log("LocalState updated"); + console.log(localState); + return localState; +} diff --git a/awesome_clicker/static/src/clicker_model.js b/awesome_clicker/static/src/clicker_model.js index f23d71c9071..b079a87d2c9 100644 --- a/awesome_clicker/static/src/clicker_model.js +++ b/awesome_clicker/static/src/clicker_model.js @@ -2,10 +2,12 @@ import { Reactive } from "@web/core/utils/reactive"; import { EventBus } from "@odoo/owl"; import { rewards } from "./click_rewards"; import { choose } from "./utils"; +import { CURRENT_VERSION } from "./clicker_migration"; export class ClickerModel extends Reactive { constructor() { super(); + this.version = CURRENT_VERSION; this.clicks = 0; this.level = 0; this.bus = new EventBus(); @@ -37,10 +39,17 @@ export class ClickerModel extends Reactive { produce: 'cherry', purchased: 0, }, + peachTree: { + price: 1500000, + level: 5, + produce: 'peach', + purchased: 0, + }, }; this.fruits = { pear: 0, cherry: 0, + peach: 0, }; document.addEventListener("click", () => this.increment(1), true); @@ -128,6 +137,7 @@ export class ClickerModel extends Reactive { { clicks: 5000, unlock: "bigBot" }, { clicks: 100000, unlock: "power" }, { clicks: 1000000, unlock: "trees" }, + { clicks: 1500000, unlock: "peach trees" } ]; } diff --git a/awesome_clicker/static/src/clicker_service.js b/awesome_clicker/static/src/clicker_service.js index a3ea6f2f0e1..bcc1047627b 100644 --- a/awesome_clicker/static/src/clicker_service.js +++ b/awesome_clicker/static/src/clicker_service.js @@ -1,11 +1,12 @@ import { registry } from "@web/core/registry"; import { ClickerModel } from "./clicker_model"; import { browser } from "@web/core/browser/browser"; +import { migrate } from "./clicker_migration"; const clickerService = { dependencies: ["action", "effect", "notification"], start(env, services) { - const localState = JSON.parse(browser.localStorage.getItem("clickerState")); + const localState = migrate(JSON.parse(browser.localStorage.getItem("clickerState"))); const clickerModel = localState ? ClickerModel.fromJSON(localState) : new ClickerModel(); const bus = clickerModel.bus;