diff --git a/awesome_clicker/static/src/click_value.js b/awesome_clicker/static/src/click_value.js new file mode 100644 index 00000000000..32a4b101aa9 --- /dev/null +++ b/awesome_clicker/static/src/click_value.js @@ -0,0 +1,25 @@ +import { Component, xml } from "@odoo/owl"; +import { humanNumber } from "@web/core/utils/numbers"; + +export class ClickValue extends Component { + static props = { + label: { type: String, optional: true }, + icon: { type: String, optional: true }, + value: Number, + }; + static template = xml` + + + + + + `; + + format(value) { + if (value < 1000) { + return value; + } else { + return humanNumber(value, { minDigits: 0, decimals: 1 }); + } + } +} diff --git a/awesome_clicker/static/src/clicker_action/clicker_action.js b/awesome_clicker/static/src/clicker_action/clicker_action.js new file mode 100644 index 00000000000..4ec38f2d57c --- /dev/null +++ b/awesome_clicker/static/src/clicker_action/clicker_action.js @@ -0,0 +1,16 @@ +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useClicker } from "../utils"; +import { ClickValue } from "../click_value"; +import { Notebook } from "@web/core/notebook/notebook"; + +export class ClickerAction extends Component { + static template = "awesome_clicker.clicker_action"; + static components = { ClickValue, Notebook }; + + setup() { + this.clicker = useClicker(); + } +} + +registry.category("actions").add("awesome_clicker.clicker_action", ClickerAction); diff --git a/awesome_clicker/static/src/clicker_action/clicker_action.xml b/awesome_clicker/static/src/clicker_action/clicker_action.xml new file mode 100644 index 00000000000..d27f30304d6 --- /dev/null +++ b/awesome_clicker/static/src/clicker_action/clicker_action.xml @@ -0,0 +1,52 @@ + + + + +
+ + + + +
+ +
+
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + +
+
+ + + +
+
+ +
+
+
+
+
+
+
+ +
diff --git a/awesome_clicker/static/src/clicker_commands.js b/awesome_clicker/static/src/clicker_commands.js new file mode 100644 index 00000000000..cc560c1ed85 --- /dev/null +++ b/awesome_clicker/static/src/clicker_commands.js @@ -0,0 +1,19 @@ +import { registry } from "@web/core/registry"; +import { doClickerAction } from "./clicker_service"; + +const commandRegistry = registry.category("command_provider"); + +commandRegistry.add("clicker", { + provide: (env, options) => [ + { + name: "Open Clicker Game", + action: () => doClickerAction(env.services.action), + }, + { + name: "Buy 1 click bot", + action: () => { + env.services["awesome_clicker.game_service"].purchase("clickbot"); + }, + }, + ], +}); diff --git a/awesome_clicker/static/src/clicker_data.js b/awesome_clicker/static/src/clicker_data.js new file mode 100644 index 00000000000..23053ad8537 --- /dev/null +++ b/awesome_clicker/static/src/clicker_data.js @@ -0,0 +1,152 @@ +export const CLICKBOT_CLICKS = 10; +export const BIGBOT_CLICKS = 100; +export const BOT_FREQUENCY = 10000; // In milliseconds +export const TREE_FREQUENCY = 30000; // In milliseconds + +export const PURCHASABLE_REWARDS = [ + { + id: "clickbot", + name: "ClickBot", + icon: "fa-android", + category: "Bots", + clicks: CLICKBOT_CLICKS, + price: 1000, + minLevel: 1, + currentNumber: (clicker) => clicker.clickBots, + buy(clicker) { + if (clicker.verifyPurchase(1, 1000)) { + clicker.clickBots++; + } + }, + }, + { + id: "bigbot", + name: "BigBot", + icon: "fa-android", + category: "Bots", + clicks: BIGBOT_CLICKS, + price: 5000, + minLevel: 2, + currentNumber: (clicker) => clicker.bigBots, + buy(clicker) { + if (clicker.verifyPurchase(2, 5000)) { + clicker.bigBots++; + } + }, + }, + { + id: "power-multiplier", + name: "Power Multiplier", + icon: "fa-bolt", + category: "Bots", + price: 50000, + minLevel: 3, + currentNumber: (clicker) => clicker.multiplier, + buy(clicker) { + if (clicker.verifyPurchase(3, 50000)) { + clicker.multiplier++; + } + }, + }, + { + id: "pear-tree", + name: "Pear Tree", + icon: "fa-tree", + category: "Trees", + price: 1000000, + minLevel: 4, + currentNumber: (clicker) => clicker.trees.pear ?? 0, + buy(clicker) { + if (clicker.verifyPurchase(4, 1000000)) { + if (!clicker.trees.pear) { + clicker.trees.pear = 0; + } + clicker.trees.pear++; + } + }, + }, + { + id: "cherry-tree", + name: "Cherry Tree", + icon: "fa-tree", + category: "Trees", + price: 1000000, + minLevel: 4, + currentNumber: (clicker) => clicker.trees.cherry ?? 0, + buy(clicker) { + if (clicker.verifyPurchase(4, 1000000)) { + if (!clicker.trees.cherry) { + clicker.trees.cherry = 0; + } + clicker.trees.cherry++; + } + }, + }, +]; + +// Ordered milestones +export const MILESTONES = [ + { + level: 1, + clicks: 1000, + event: "MILESTONE_1k", + description: "You can now buy clickbots.", + }, + { + level: 2, + clicks: 5000, + event: "MILESTONE_5k", + description: "You can now buy bigbots.", + }, + { + level: 3, + clicks: 100000, + event: "MILESTONE_100k", + description: "You can now increase your bots' power.", + }, + { + level: 4, + clicks: 1000000, + event: "MILESTONE_1m", + description: "You can now plant trees.", + }, +]; + +export const RANDOM_REWARDS = [ + { + description: "Get 1 click bot", + apply(clicker) { + clicker.clickBots += 1; + }, + maxLevel: 1, + }, + { + description: "Get 3 click bots", + apply(clicker) { + clicker.clickBots += 3; + }, + minLevel: 1, + maxLevel: 2, + }, + { + description: "Get 3 big bots", + apply(clicker) { + clicker.bigBots += 3; + }, + minLevel: 2, + }, + { + description: "Get 10 big bots", + apply(clicker) { + clicker.bigBots += 10; + }, + minLevel: 3, + }, + { + description: "Increase bot power!", + apply(clicker) { + clicker.multipler += 1; + }, + minLevel: 3, + }, +]; diff --git a/awesome_clicker/static/src/clicker_menu/clicker_menu.js b/awesome_clicker/static/src/clicker_menu/clicker_menu.js new file mode 100644 index 00000000000..5b43527af7f --- /dev/null +++ b/awesome_clicker/static/src/clicker_menu/clicker_menu.js @@ -0,0 +1,28 @@ +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { useClicker } from "../utils"; +import { ClickValue } from "../click_value"; +import { doClickerAction } from "../clicker_service"; +import { Dropdown } from "@web/core/dropdown/dropdown"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; + +export class ClickerMenu extends Component { + static template = "awesome_clicker.clicker_menu"; + static components = { ClickValue, Dropdown, DropdownItem }; + + setup() { + this.clicker = useClicker(); + this.action = useService("action"); + } + + doAction() { + doClickerAction(this.action); + } +} + +export const systrayItem = { + Component: ClickerMenu, +}; + +registry.category("systray").add("awesome_clicker.clicker_menu", systrayItem); diff --git a/awesome_clicker/static/src/clicker_menu/clicker_menu.xml b/awesome_clicker/static/src/clicker_menu/clicker_menu.xml new file mode 100644 index 00000000000..3897af4311d --- /dev/null +++ b/awesome_clicker/static/src/clicker_menu/clicker_menu.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/awesome_clicker/static/src/clicker_model.js b/awesome_clicker/static/src/clicker_model.js new file mode 100644 index 00000000000..45cf6348f6c --- /dev/null +++ b/awesome_clicker/static/src/clicker_model.js @@ -0,0 +1,119 @@ +import { EventBus } from "@odoo/owl"; +import { Reactive } from "@web/core/utils/reactive"; +import { + BIGBOT_CLICKS, + BOT_FREQUENCY, + CLICKBOT_CLICKS, + MILESTONES, + PURCHASABLE_REWARDS, + RANDOM_REWARDS, + TREE_FREQUENCY, +} from "./clicker_data"; +import { choose, randomBoolean } from "./utils"; + +export class ClickerModel extends Reactive { + constructor() { + super(...arguments); + this.clicks = 0; + this.level = 0; + this.clickBots = 0; + this.bigBots = 0; + this.multiplier = 1; + this.trees = {}; + this.fruits = {}; + this.shopItems = PURCHASABLE_REWARDS; + this.botFrequency = BOT_FREQUENCY; + this.treeFrequency = TREE_FREQUENCY; + this.bus = new EventBus(); + } + + get totalFruits() { + return Object.values(this.fruits).reduce((a, b) => a + b, 0); + } + + get totalTrees() { + return Object.values(this.trees).reduce((a, b) => a + b, 0); + } + + get persistentState() { + return { + clicks: this.clicks, + level: this.level, + clickBots: this.clickBots, + bigBots: this.bigBots, + multiplier: this.multiplier, + trees: this.trees, + fruits: this.fruits, + }; + } + + set persistentState(state) { + Object.assign(this, state); + } + + getItemsByCategory(category) { + return this.shopItems.filter((item) => item.category === category); + } + + _getApplicableRewards() { + return RANDOM_REWARDS.filter( + (r) => + (r.minLevel == null || this.level >= r.minLevel) && + (r.maxLevel == null || this.level <= r.maxLevel) + ); + } + + increment(val) { + this.clicks += val; + + for (const milestone of MILESTONES) { + if (this.level < milestone.level && this.clicks >= milestone.clicks) { + this.bus.trigger(milestone.event); + this.level = milestone.level; + break; + } + } + } + + verifyPurchase(minLevel, price) { + if (this.level < minLevel || this.clicks < price) { + return false; + } + + this.clicks -= price; + return true; + } + + purchase(id) { + const index = this.shopItems.findIndex((elem) => elem.id === id); + if (index >= 0) { + this.shopItems[index].buy(this); + } + } + + giveRandomReward(chance = 1) { + if (!randomBoolean(chance)) { + return; + } + + const reward = choose(this._getApplicableRewards()); + // reward can be undefined if no applicable reward + if (reward) { + this.bus.trigger("REWARD", reward); + } + } + + botsDoClicks() { + this.clicks += + (this.clickBots * CLICKBOT_CLICKS + this.bigBots * BIGBOT_CLICKS) * this.multiplier; + } + + treesProduceFruit() { + for (const [type, nb] of Object.entries(this.trees)) { + if (!this.fruits[type]) { + this.fruits[type] = 0; + } + this.fruits[type] += nb; + } + } +} diff --git a/awesome_clicker/static/src/clicker_service.js b/awesome_clicker/static/src/clicker_service.js new file mode 100644 index 00000000000..a5bfa6ecbca --- /dev/null +++ b/awesome_clicker/static/src/clicker_service.js @@ -0,0 +1,66 @@ +import { registry } from "@web/core/registry"; +import { ClickerModel } from "./clicker_model"; +import { BOT_FREQUENCY, MILESTONES, TREE_FREQUENCY } from "./clicker_data"; +import { browser } from "@web/core/browser/browser"; + +export function doClickerAction(actionService) { + actionService.doAction({ + type: "ir.actions.client", + tag: "awesome_clicker.clicker_action", + target: "new", + name: "Clicker Game", + }); +} + +export const clickerService = { + dependencies: ["effect", "action", "notification"], + start(env, { effect, action, notification }) { + const model = new ClickerModel(); + const persistentState = browser.localStorage.getItem("clicker_state"); + if (persistentState) { + model.persistentState = JSON.parse(persistentState); + } + + for (const milestone of MILESTONES) { + model.bus.addEventListener(milestone.event, () => + effect.add({ message: `Milestone reached! ${milestone.description}` }) + ); + } + + model.bus.addEventListener("REWARD", (ev) => { + const closeNotif = notification.add( + `You've earned a reward: ${ev.detail.description}`, + { + title: "Clicker Reward", + type: "success", + sticky: true, + buttons: [ + { + name: "Collect", + onClick: () => { + closeNotif(); + ev.detail.apply(model); + doClickerAction(action); + }, + }, + ], + } + ); + }); + + document.addEventListener("click", () => model.increment(1), true); + setInterval(() => model.botsDoClicks(), BOT_FREQUENCY); + setInterval(() => model.treesProduceFruit(), TREE_FREQUENCY); + setInterval( + () => + browser.localStorage.setItem( + "clicker_state", + JSON.stringify(model.persistentState) + ), + 10000 + ); + return model; + }, +}; + +registry.category("services").add("awesome_clicker.game_service", clickerService); diff --git a/awesome_clicker/static/src/patches/form_controller_patch.js b/awesome_clicker/static/src/patches/form_controller_patch.js new file mode 100644 index 00000000000..3474c8aa3bb --- /dev/null +++ b/awesome_clicker/static/src/patches/form_controller_patch.js @@ -0,0 +1,12 @@ +import { patch } from "@web/core/utils/patch"; +import { FormController } from "@web/views/form/form_controller"; +import { useClicker } from "../utils"; + +const FormControllerPatch = { + setup() { + super.setup(...arguments); + useClicker().giveRandomReward(0.01); + }, +}; + +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..4ce3667e2e8 --- /dev/null +++ b/awesome_clicker/static/src/utils.js @@ -0,0 +1,16 @@ +import { useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +export function useClicker() { + const service = useService("awesome_clicker.game_service"); + + return useState(service); +} + +export function choose(choices) { + return choices[Math.floor(Math.random() * choices.length)]; +} + +export function randomBoolean(trueChance) { + return Math.random() < trueChance; +}