diff --git a/css/index.css b/css/index.css new file mode 100644 index 0000000..c97262b --- /dev/null +++ b/css/index.css @@ -0,0 +1,142 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-size: 16px; + min-width: 100%; + min-height: 100%; + overflow-x: hidden; +} + +body { + margin: 0; + padding: 0; + font-family: 'Inter', sans-serif; + background-color: #f5f5f5; + + width: 100vw; + height: 100vh; +} + +/* App */ +.app { + display: flex; + flex-direction: row; + min-height: 100%; +} + +/* Shop */ +.store { + display: flex; + flex-direction: column; + flex: 1; + min-height: 100%; + max-width: 20rem; + + background-color: #eee; + border-right: 1px dotted #ccc; +} + +.store .store__elements { + list-style: none; + margin: 0; + padding: 0; +} + +.store .store__elements .item { + display: block; + margin: 1rem; + position: relative; +} +.store .store__elements .item .item__details { + position: absolute; + z-index: 999; + left: 0; + top: 100%; + margin-top: .25rem; + transform: translateY(-1rem); + padding: .5rem 1rem; + background-color: rgba(0, 0, 0, 0.6); + color: #fff; + user-select: none; + pointer-events: none; + opacity: 0; + transition: opacity .2s ease-in-out, transform .2s ease-in-out; +} +.store .store__elements .item:hover .item__details { + opacity: 1; + transform: translateY(0); +} + +.store .store__elements .item .item__buy { + display: block; + width: 100%; + padding: 1rem 1rem; + border: none; + text-align: left; + background-color: rgb(26, 173, 75); + font-family: 'Inter', sans-serif; + color: white; +} + +/* Main area */ +.content { + display: flex; + flex-direction: column; + flex: 1; + min-height: 100%; + align-items: center; + justify-content: center; +} + +.content .content__button { + display: flex; + flex-direction: column; + align-items: center; +} + +.content .content__button #clicks { + font-size: 2rem; + font-weight: 700; + margin-bottom: 1rem; +} +.content .content__button #clicks::after { + content: '⚡'; +} + +.content .content__button #clicker { + margin-top: 1rem; + background-color: transparent; + border: none; + cursor: pointer; + animation: rotate 2s infinite ease-in-out; +} +.content .content__button #clicker .clicker__emoji { + display: block; + margin: 0 auto; + font-size: 10rem; + line-height: 1; + transition: transform .2s ease-in-out; +} +.content .content__button #clicker:hover .clicker__emoji { + transform: scale(1.1); +} +.content .content__button #clicker:active .clicker__emoji { + transform: scale(1); +} + +/* Animations */ +@keyframes rotate { + 0% { + transform: rotate(-5deg); + } + 50% { + transform: rotate(5deg); + } + 100% { + transform: rotate(-5deg); + } +} \ No newline at end of file diff --git a/index.html b/index.html index 5917d19..73cc5ef 100644 --- a/index.html +++ b/index.html @@ -6,13 +6,24 @@ PC clicker + - 0 - +
+ - +
+
+ 0 + +
+
+
\ No newline at end of file diff --git a/js/core/game.js b/js/core/game.js index e23e7bb..c2e8c90 100644 --- a/js/core/game.js +++ b/js/core/game.js @@ -1,85 +1,133 @@ import Store from "./store.js"; +/** + * Class representing the game. + */ export default class Game { /** - * @param {string} id The id of the button - * - * @property {HTMLElement} button The button - * @property {number} clicks The number of clicks - * @property {number} clicksMultiplier The multiplier of the clicks - * @property {number} clicksPerSecondAdder The number of clicks per second added by the upgrades - * @property {number} cps The number of clicks per second of the user - * @property {number} lastClickTime The time of the last click - * @property {Store} store The store associated with the game + * Creates an instance of Game. + * @param {string} id - The id of the button. */ constructor(id) { /* Properties */ this.button = document.getElementById(id); this.clicks = 0; - this.clicksMultiplier = 1; - this.clicksPerSecondAdder = 0; - this.cps = 0; + this.clicksAdder = 0; this.lastClickTime = 0; this.store = null; + this.clickIntervals = []; + this.maxSameInterval = 8; + this.intervalFunction = null; + + this.regularityCounter = 0; + this.regularityThreshold = 10; // If the player is too regular and gets a regularityCounter > regularityThreshold, he's most likely cheating + /* Binds */ this.button.addEventListener("click", this.clickHandler.bind(this)); - setInterval(this.interval.bind(this), 1000); + this.intervalFunction = setInterval(this.interval.bind(this), 1000); } - /* Methods */ /** - * Interval CPS adder + * Interval handler. */ interval() { - this.clicks += this.clicksPerSecondAdder; this.renderSpan(); } /** - * Displays the amount of clicks on the screen + * Displays the amount of clicks on the screen. */ renderSpan() { document.getElementById("clicks").textContent = this.clicks; } /** - * Handles the click event + * Handles the click event. + * @param {Event} e - The click event. */ - clickHandler() { + clickHandler(e) { const currentTime = Date.now(); const timeSinceLastClick = currentTime - this.lastClickTime; - if (timeSinceLastClick > 0) { - this.cps = 1000 / timeSinceLastClick; + + this.clickIntervals.push(timeSinceLastClick); + + if (this.clickIntervals.length > this.maxSameInterval) { + this.clickIntervals.shift(); } - this.clicks += 1 * this.clicksMultiplier; + this.clicks += 1 + this.clicksAdder; this.lastClickTime = currentTime; this.renderSpan(); - this.cheatDetector(); + this.cheatDetector(e); } /** - * Detects if the user is cheating and clicking in a humanly impossible way + * Detects if the user is cheating and clicking in a humanly impossible way. + * @param {Event} e - The click event. */ - cheatDetector() { - const maxCPS = 22; - if (this.cps > maxCPS) { - this.clicks = 0; - this.clicksPerSecondAdder = 0; - this.cps = 0; - this.lastClickTime = 0; - - this.button.removeEventListener("click", this.clickHandler); - alert("You're cheating!"); + cheatDetector(e) { + const irregularityTolerance = 10; // ms + + if (this.checkRegularIntervals(irregularityTolerance)) { + this.regularityCounter++; + } + + if (!e.isTrusted) return this.cheating(); // Instant ban + if (this.regularityCounter > this.regularityThreshold) { + console.warn("You're clicking too regularly, you're most likely cheating."); + this.cheating(); } } /** - * Sets a store to the game + * A cheating behavior is detected, the game is stopped. + */ + cheating() { + clearInterval(this.intervalFunction); + this.button.remove(); + this.resetGameProperties(); + document.body.innerHTML = "You're cheating, shame on you!"; + } + + /** + * Sets a store to the game. + * @param {string} storeID - The id of the store. */ setStore(storeID) { this.store = new Store(storeID, this); } + + /** + * Verifies if clicks are legit by checking their regularity. + * @param {number} tolerance - The tolerance of irregularity. + * @returns {boolean} - True if clicks are too regular, false otherwise. + */ + checkRegularIntervals(tolerance) { + const intervals = this.clickIntervals; + const averageInterval = intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length; + + for (let i = 0; i < this.clickIntervals.length; i++) { + const difference = Math.abs(this.clickIntervals[i] - averageInterval); + if (difference > tolerance) { + return false; + } + } + + return true; + } + + /** + * Resets game properties to their initial values. + */ + resetGameProperties() { + this.clicks = 0; + this.clicksAdder = 0; + this.lastClickTime = 0; + this.store = null; + this.clickIntervals = []; + this.maxSameInterval = 8; + this.intervalFunction = null; + } } diff --git a/js/core/items.js b/js/core/items.js index 11c8fa3..a476a60 100644 --- a/js/core/items.js +++ b/js/core/items.js @@ -1,13 +1,25 @@ +/** + * Note: all numbers must be integers, not floats. + * Available properties: + * - name: name of the item + * - text: text displayed in the button + * - price: price of the item + * - maxQuantity: max quantity of the item (default: Infinity) + * - cpsAdder: CPS added by the item (default: 0) + * - cpsAdderMultiplier: multiplier of the CPS added by the item (default: 2) + * - clicksAdder: clicks added by the item (default: 0) + */ export default [ { name: "Server", - text: "+{{cpsAdder}} cps", - + text: "+ {{clicksAdder}} to each click", price: 10, + clicksAdder: 1, + }, + { + name: "Developer", + text: "+ {{cpsAdder}}/s", + price: 100, cpsAdder: 1, - quantity: 0, - // maxQuantity: 1, - - cpsAdderMultiplier: 2, } ]; \ No newline at end of file diff --git a/js/core/store.js b/js/core/store.js index 7a10d09..64d33f1 100644 --- a/js/core/store.js +++ b/js/core/store.js @@ -1,13 +1,13 @@ import StoreItem from "./store__item.js"; +/** + * Class representing a store. + */ export default class Store { /** - * @param {string} id The id of the store - * @param {Game} game The game - * - * @property {HTMLElement} store The store - * @property {Game} game The game - * @property {StoreItem[]} items The items of the store + * Creates an instance of Store. + * @param {string} id - The id of the store. + * @param {Game} game - The game instance. */ constructor(id, game) { /* Properties */ @@ -16,9 +16,8 @@ export default class Store { this.items = []; } - /* Methods */ /** - * Displays the store on the screen + * Displays the store on the screen. */ render() { this.store.innerHTML = ""; @@ -26,12 +25,19 @@ export default class Store { } /** - * Adds an item to the store - * - * @param {Object} item The item to add + * Adds an item to the store. + * @param {Object} item - The item to add. + * @param {string} item.name - The name of the item. + * @param {string} [item.text=""] - The text of the item. + * @param {number} [item.price=5] - The price of the item. + * @param {number} [item.cpsAdder=0] - The number of clicks per second added by the item. + * @param {number} [item.maxQuantity=null] - The max quantity of the item. + * @param {number} [item.cpsAdderMultiplier=2] - The multiplier of the cps adder. + * @param {number} [item.clicksAdder=0] - The multiplier of the clicks. */ addItem(item) { - this.items.push(new StoreItem(item, this.game)); - this.render(); + const storeItem = new StoreItem(item, this.game); + this.items.push(storeItem); + this.store.appendChild(storeItem.render()); } } diff --git a/js/core/store__item.js b/js/core/store__item.js index e474374..763f952 100644 --- a/js/core/store__item.js +++ b/js/core/store__item.js @@ -1,58 +1,59 @@ export default class StoreItem { /** - * @param {Object} item The item - * @param {Game} game The game - * - * @property {string} name The name of the item - * @property {string} text The text of the item - * @property {number} price The price of the item - * @property {number} cpsAdder The number of clicks per second added by the item - * @property {number} quantity The quantity of the item - * @property {number} maxQuantity The max quantity of the item - * @property {number} cpsAdderMultiplier The multiplier of the cps adder - * @property {number} clickMultiplier The multiplier of the clicks - * @property {Game} game The game - * @property {HTMLElement} button The button of the item - * @property {HTMLElement} details The details of the item + * Represents an item in the store. + * @param {Object} item - The item properties. + * @param {string} item.name - The name of the item. + * @param {string} [item.text=""] - The text of the item. + * @param {number} [item.price=5] - The price of the item. + * @param {number} [item.cpsAdder=0] - The number of clicks per second added by the item. + * @param {number} [item.maxQuantity=null] - The max quantity of the item. + * @param {number} [item.cpsAdderMultiplier=2] - The multiplier of the cps adder. + * @param {number} [item.clicksAdder=0] - The multiplier of the clicks. + * @param {Game} game - The game instance. */ - constructor({ name, text, price, cpsAdder, quantity = 0, maxQuantity = null, cpsAdderMultiplier = 1, clickMultiplier = 1 }, game) { + constructor({ name = "No name 😔", text = "", price = 5, cpsAdder = 0, maxQuantity = null, cpsAdderMultiplier = 2, clicksAdder = 0 }, game) { /* Properties */ this.name = name; this.text = text; this.price = price; this.cpsAdder = cpsAdder; - this.quantity = quantity; this.maxQuantity = maxQuantity; this.cpsAdderMultiplier = cpsAdderMultiplier; - this.clickMultiplier = clickMultiplier; + this.clicksAdder = clicksAdder; this.game = game; + this.quantity = 0; this.button = null; this.details = null; } - /* Methods */ /** - * Parses the text of the item to replace {{variable}} by the value of the variable + * Parses the text of the item to replace {{variable}} by the value of the variable. + * @param {string} text - The text to parse. + * @returns {string} - The parsed text. */ parse(text) { return text.replace(/{{(.*?)}}/g, (_, g) => this[g]); } /** - * Displays the item on the screen + * Displays the item on the screen. + * @returns {HTMLElement} - The HTML element representing the item. */ render() { // Initiates the buy button this.button = document.createElement("button"); - this.button.textContent = `${this.name} - ${this.price}€ (${this.parse(this.text)})`; + this.button.textContent = `${this.name} - ${this.price} (${this.parse(this.text)})`; + this.button.classList.add("item__buy"); // Initiates the details this.details = document.createElement("span"); this.details.innerHTML = `Quantity: ${this.quantity}`; + this.details.classList.add("item__details"); // Initiates the li const li = document.createElement("li"); + li.classList.add("item"); li.appendChild(this.details); li.appendChild(this.button); @@ -63,15 +64,15 @@ export default class StoreItem { } /** - * Refreshes the item on the screen + * Refreshes the item on the screen. */ update() { - this.button.textContent = `${this.name} - ${this.price}€ (${this.parse(this.text)})`; + this.button.textContent = `${this.name} - ${this.price} (${this.parse(this.text)})`; this.details.innerHTML = `Quantity: ${this.quantity}`; } /** - * Max quantity reached + * Max quantity reached. */ max() { this.button.textContent = "Max quantity reached"; @@ -79,7 +80,7 @@ export default class StoreItem { } /** - * Buy the item + * Buy the item. */ buy() { const canAfford = this.game.clicks >= this.price; @@ -88,11 +89,13 @@ export default class StoreItem { if (canAfford && belowMaxQuantity) { this.game.clicks -= this.price; this.game.clicksPerSecondAdder += this.cpsAdder; - this.game.clickMultiplier += this.clickMultiplier; + this.game.clicksAdder += this.clicksAdder; this.quantity++; this.cpsAdder *= this.cpsAdderMultiplier; + if (this.clicksAdder > 0) this.clicksAdder++; + this.price *= this.quantity + 1; this.update();