|
After Width: | Height: | Size: 846 KiB |
|
After Width: | Height: | Size: 860 KiB |
|
After Width: | Height: | Size: 907 KiB |
|
After Width: | Height: | Size: 656 KiB |
|
After Width: | Height: | Size: 1023 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 652 KiB |
@ -0,0 +1,389 @@
@@ -0,0 +1,389 @@
|
||||
/** |
||||
* Enum representing possible weather conditions in the game. |
||||
* @readonly |
||||
* @enum {string} |
||||
*/ |
||||
const Weather = Object.freeze({ |
||||
COLD: 'cold', |
||||
CLOUDY: 'cloudy', |
||||
SUNNY: 'sunny', |
||||
HOT: 'hot' |
||||
}); |
||||
|
||||
/** |
||||
* Enum representing game duration options. |
||||
* Provides both numeric values and reverse lookups. |
||||
* @readonly |
||||
* @enum {number|string} |
||||
*/ |
||||
const Days = Object.freeze({ |
||||
7: 7, |
||||
14: 14, |
||||
30: 30 |
||||
}); |
||||
|
||||
/** |
||||
* Multiplier applied to base demand based on weather conditions. |
||||
* Higher values mean more customers. |
||||
* @type {Object.<string, number>} |
||||
*/ |
||||
const WeatherFactor = { |
||||
COLD: 0.5, |
||||
CLOUDY: 0.8, |
||||
SUNNY: 1.0, |
||||
HOT: 1.4 |
||||
} |
||||
|
||||
/** |
||||
* The ideal price per cup for each weather condition. |
||||
* Pricing closer to these values yields better sales. |
||||
* @type {Object.<string, number>} |
||||
*/ |
||||
const IdealPrice = { |
||||
COLD: 0.20, |
||||
CLOUDY: 0.30, |
||||
SUNNY: 0.35, |
||||
HOT: 0.50 |
||||
} |
||||
|
||||
/** |
||||
* The ideal recipe (ingredient ratios) for each weather condition. |
||||
* Using these ratios produces the best taste score. |
||||
* @type {Object.<string, {lemons: number, sugar: number, ice: number}>} |
||||
*/ |
||||
const IdealRecipe = { |
||||
COLD: { |
||||
lemons: 1, |
||||
sugar: 2, |
||||
ice: 1 |
||||
}, |
||||
CLOUDY: { |
||||
lemons: 1, |
||||
sugar: 1, |
||||
ice: 2 |
||||
}, |
||||
SUNNY: { |
||||
lemons: 1, |
||||
sugar: 1, |
||||
ice: 3 |
||||
}, |
||||
HOT: { |
||||
lemons: 1, |
||||
sugar: 1, |
||||
ice: 4 |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Probability weights for each weather type. |
||||
* Used to randomly determine the day's weather. |
||||
* @type {Array.<{type: string, weight: number}> |
||||
*/ |
||||
const WeatherChance = [ |
||||
{ type: Weather.CLOUDY, weight: 40 }, |
||||
{ type: Weather.SUNNY, weight: 35 }, |
||||
{ type: Weather.HOT, weight: 15 }, |
||||
{ type: Weather.COLD, weight: 10 } |
||||
] |
||||
|
||||
/** |
||||
* @typedef {Object} Recipe |
||||
* @property {number} lemons |
||||
* @property {number} sugar |
||||
* @property {number} ice |
||||
*/ |
||||
|
||||
/** |
||||
* @typedef {Object} Supplies |
||||
* @property {number} lemons |
||||
* @property {number} sugar |
||||
* @property {number} ice |
||||
* @property {number} cups |
||||
*/ |
||||
|
||||
/** |
||||
* @typedef {Object.<number, number>} PriceTable |
||||
*/ |
||||
|
||||
/** |
||||
* @typedef {Object} GameState |
||||
* @property {number} player_money |
||||
* @property {Recipe} recipe |
||||
* @property {Supplies} supplies |
||||
* @property {string} weather |
||||
* @property {number} price_per_cup |
||||
* @property {number} days |
||||
* @property {{ |
||||
* lemons: PriceTable, |
||||
* sugar: PriceTable, |
||||
* ice: PriceTable, |
||||
* cups: PriceTable |
||||
* }} supplies_prices |
||||
* @property {number} cost_per_cup |
||||
* @property {number} cups_sold |
||||
*/ |
||||
|
||||
/** |
||||
* Initialize a new game state with default values. |
||||
* Sets up empty recipe, zero supplies, default weather, and pricing tables. |
||||
* |
||||
* @returns {GameState} A fresh game state ready to begin. |
||||
*/ |
||||
function init_game() { |
||||
return { |
||||
player_money: 0, |
||||
recipe: { |
||||
lemons: 0, |
||||
sugar: 0, |
||||
ice: 0 |
||||
}, |
||||
supplies: { |
||||
lemons: 0, |
||||
sugar: 0, |
||||
ice: 0, |
||||
cups: 0 |
||||
}, |
||||
weather: Weather.SUNNY, |
||||
price_per_cup: 0, |
||||
days: Days[7], |
||||
supplies_prices: { |
||||
lemons: { |
||||
12: 4.80, |
||||
24: 7.20, |
||||
48: 9.60 |
||||
}, |
||||
sugar: { |
||||
12: 4.80, |
||||
20: 7.00, |
||||
50: 15.00 |
||||
}, |
||||
ice: { |
||||
50: 1.00, |
||||
200: 3.00, |
||||
500: 5.00 |
||||
}, |
||||
cups: { |
||||
75: 1.00, |
||||
225: 2.35, |
||||
400: 3.75 |
||||
} |
||||
}, |
||||
cost_per_cup: 1.00, |
||||
cups_sold: 0 |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Set a new recipe for making lemonade. |
||||
* Defines the ratio of ingredients per cup. |
||||
* |
||||
* @param {GameState} game_state - The current game state. |
||||
* @param {number} lemons - The number of lemons per cup. |
||||
* @param {number} sugar - The amount of sugar per cup. |
||||
* @param {number} ice - The amount of ice per cup. |
||||
* @returns {GameState} A new game state with the updated recipe. |
||||
*/ |
||||
function set_recipe(game_state, lemons, sugar, ice) { |
||||
console.assert(game_state, 'game_state must be defined'); |
||||
console.assert(typeof lemons == 'number', 'lemons must be a number'); |
||||
console.assert(typeof sugar == 'number', 'sugar must be a number'); |
||||
console.assert(typeof ice == 'number', 'ice must be a number'); |
||||
|
||||
return { |
||||
...game_state, |
||||
recipe: { |
||||
...game_state.recipe, |
||||
lemons, |
||||
sugar, |
||||
ice |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Determine the day's weather randomly based on weighted probabilities. |
||||
* Uses WeatherChance to select a weather type. |
||||
* |
||||
* @param {GameState} game_state - The current state of the game. |
||||
* @returns {GameState} A new game state with the updated weather. |
||||
*/ |
||||
function set_weather(game_state) { |
||||
const totalWeight = WeatherChance.reduce((sum, w) => sum + w.weight, 0); |
||||
let roll = Math.random() * totalWeight; |
||||
|
||||
for (const w of WeatherChance) { |
||||
if (roll < w.weight) { |
||||
return { |
||||
...game_state, |
||||
weather: w.type |
||||
} |
||||
} |
||||
|
||||
roll -= w.weight |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Set the number of days the game will play through. |
||||
* Valid options are 7 (week), 14 (two weeks), or 30 (month). |
||||
* |
||||
* @param {GameState} game_state - The current state of the game. |
||||
* @param {number} days - The number of days (7, 14, or 30). |
||||
* @returns {GameState} A new game state with the updated number of days. |
||||
*/ |
||||
function set_days(game_state, days) { |
||||
console.assert(game_state, 'game_state must be defined'); |
||||
console.assert(typeof days === 'number', 'days must be a number'); |
||||
console.assert(Object.values(Days).includes(days), 'invalid days value'); |
||||
|
||||
return { |
||||
...game_state, |
||||
days: days |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Set the price per cup of lemonade. |
||||
* Rounds the cost to two decimal places. |
||||
* |
||||
* @param {GameState} game_state - The current game state. |
||||
* @param {number} cost - The price to charge per cup. |
||||
* @returns {GameState} A new game state with the updated cost per cup. |
||||
*/ |
||||
function set_cost_per_cup(game_state, cost) { |
||||
console.assert(typeof cost === 'number', 'cost must be a number'); |
||||
console.assert(game_state, 'game_state must be defined'); |
||||
return { |
||||
...game_state, |
||||
cost_per_cup: Math.round(parseFloat(cost) * 100) / 100 |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Calculate the number of cups sold based on pricing, weather, and taste. |
||||
* Demand is influenced by weather conditions, price sensitivity, and a random factor. |
||||
* |
||||
* @param {number} price_per_cup - The price charged per cup. |
||||
* @param {number} cups_in_supplies - The number of cups available to sell. |
||||
* @param {string} weather - The current weather condition (from Weather enum). |
||||
* @param {number} [tasteScore=1] - The taste quality score (0.5 to 1.2). |
||||
* @returns {number} The number of cups sold (capped by available supplies). |
||||
*/ |
||||
function calculate_cups_sold(price_per_cup, cups_in_supplies, weather, tasteScore = 1) { |
||||
const base_demand = 30; |
||||
const weather_factor = WeatherFactor[weather] || 1.0; |
||||
const ideal_price = IdealPrice[weather] || 0.35 |
||||
|
||||
const sensitivity = 1.5; |
||||
let price_effect = 1 - (price_per_cup - ideal_price) * sensitivity; |
||||
if (price_effect < 0) { |
||||
price_effect = 0; |
||||
} |
||||
|
||||
let demand = base_demand * weather_factor * price_effect * tasteScore; |
||||
demand *= 0.9 + Math.random() * 0.2; |
||||
|
||||
const cupsSold = Math.min(Math.floor(demand), cups_in_supplies); |
||||
|
||||
return cupsSold; |
||||
} |
||||
|
||||
/** |
||||
* Calculate a taste score based on how close the recipe is to ideal values. |
||||
* Score ranges from 0.5 (poor) to 1.2 (excellent). |
||||
* |
||||
* @param {number} lemons_per_cup - The number of lemons used per cup. |
||||
* @param {number} sugar_per_cup - The amount of sugar used per cup. |
||||
* @param {number} [ideal_lemons=1] - The ideal number of lemons per cup. |
||||
* @param {number} [ideal_sugar=1] - The ideal amount of sugar per cup. |
||||
* @returns {number} A taste score between 0.5 and 1.2. |
||||
*/ |
||||
function calculate_taste_score(lemons_per_cup, sugar_per_cup, ideal_lemons = 1, ideal_sugar = 1) { |
||||
const lemon_diff = Math.abs(lemons_per_cup - ideal_lemons); |
||||
const sugar_diff = Math.abs(sugar_per_cup - ideal_sugar); |
||||
|
||||
let score = 1.0; |
||||
|
||||
score -= (lemon_diff * 0.3 + sugar_diff * 0.2); |
||||
|
||||
if (score < 0.5) score = 0.5; |
||||
if (score > 1.2) score = 1.2; |
||||
|
||||
return score; |
||||
} |
||||
|
||||
/** |
||||
* Simulate a single day of lemonade sales. |
||||
* Calculates cups sold based on recipe quality, weather, and pricing. |
||||
* |
||||
* @param {GameState} game_state - The current game state. |
||||
* @returns {number} The number of cups sold during the day. |
||||
*/ |
||||
function simulate_day(game_state) { |
||||
console.assert(game_state, 'game_state must not be undefined'); |
||||
|
||||
const recipe = game_state.recipe; |
||||
const weather = game_state.weather; |
||||
const supplies = game_state.supplies; |
||||
|
||||
const taste_score = calculate_taste_score( |
||||
recipe.lemons, |
||||
recipe.sugar, |
||||
IdealRecipe[weather].lemons, |
||||
IdealRecipe[weather].sugar, |
||||
); |
||||
|
||||
const cups_sold = calculate_cups_sold(game_state.cost_per_cup, supplies.cups, weather, taste_score); |
||||
|
||||
return cups_sold; |
||||
} |
||||
|
||||
/** |
||||
* Execute lemonade production and sales for the day. |
||||
* Calculates cups that can be made from available supplies, |
||||
* determines sales, updates inventory, and calculates profit. |
||||
* |
||||
* @param {GameState} game_state - The current game state. |
||||
* @returns {GameState} A new game state with updated money, supplies, and cups sold. |
||||
*/ |
||||
function make_lemonade(game_state) { |
||||
console.assert(game_state, 'game_state must be defined'); |
||||
|
||||
const recipe = game_state.recipe; |
||||
const weather = game_state.weather; |
||||
const price = game_state.price_per_cup; |
||||
|
||||
const ideal = IdealRecipe[weather]; |
||||
const tasteScore = calculate_taste_score( |
||||
recipe.lemons, |
||||
recipe.sugar, |
||||
ideal.lemons, |
||||
ideal.sugar |
||||
); |
||||
|
||||
const cups_available = Math.min( |
||||
game_state.supplies.lemons / recipe.lemons || 0, |
||||
game_state.supplies.sugar / recipe.sugar || 0, |
||||
game_state.supplies.ice / recipe.ice || 0, |
||||
game_state.supplies.cups |
||||
); |
||||
|
||||
const cups_sold = calculate_cups_sold(price, cups_available, weather, tasteScore); |
||||
|
||||
const remaining_supplies = { |
||||
lemons: game_state.supplies.lemons - recipe.lemons * cups_sold, |
||||
sugar: game_state.supplies.sugar - recipe.sugar * cups_sold, |
||||
ice: game_state.supplies.ice - recipe.ice * cups_sold, |
||||
cups: game_state.supplies.cups - cups_sold |
||||
} |
||||
|
||||
const cost_per_cup = game_state.cost_per_cup; |
||||
const profit = (price - cost_per_cup) * cups_sold; |
||||
|
||||
return { |
||||
...game_state, |
||||
player_money: game_state.player_money + profit, |
||||
supplies: remaining_supplies, |
||||
cups_sold |
||||
} |
||||
} |
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 920 KiB |
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html> |
||||
<html> |
||||
|
||||
<head> |
||||
<title>Lemonade Stand</title> |
||||
<link rel="stylesheet" type="text/css" href="style.css" /> |
||||
|
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
</head> |
||||
|
||||
<body> |
||||
<header class="game_header"> |
||||
<img src="juice.png" class="game_header_icon"> |
||||
<h1 class="game_header_title">Lemonade Stand</h1> |
||||
</header> |
||||
|
||||
<div class="dashboard"> |
||||
<section class="game_section"> |
||||
<h2 class="section_title">Supplies</h2> |
||||
|
||||
<ul class="game_list"> |
||||
<li>Lemons: </li> |
||||
<li>Sugar: </li> |
||||
<li>Ice: </li> |
||||
<li>Cups: </li> |
||||
</ul> |
||||
</section> |
||||
|
||||
<section class="game_section"> |
||||
<h2 class="section_title">Total Earnings</h2> |
||||
|
||||
<p>$0.00</p> |
||||
</section> |
||||
|
||||
|
||||
<section class="game_section"> |
||||
<h2 class="section_title">Current Price</h2> |
||||
|
||||
<p>$0.00</p> |
||||
</section> |
||||
|
||||
<section class="game_section"> |
||||
<h2 class="section_title">Weather</h2> |
||||
|
||||
<p>Today's weather is: </p> |
||||
</section> |
||||
</div> |
||||
|
||||
<canvas id="scene" width="1200" height="500"></canvas> |
||||
<script src="index.js"></script> |
||||
</body> |
||||
|
||||
</html> |
||||
@ -0,0 +1,272 @@
@@ -0,0 +1,272 @@
|
||||
/** |
||||
* @fileoverview Lemonade stand game rendering module. |
||||
* Handles sprite loading and canvas rendering. |
||||
*/ |
||||
|
||||
/** @type {HTMLCanvasElement} */ |
||||
const canvas = document.getElementById('scene'); |
||||
|
||||
/** @type {CanvasRenderingContext2D} */ |
||||
const ctx = canvas.getContext('2d'); |
||||
|
||||
/** @type {number} Global scale factor for rendering */ |
||||
const scale = 0.4; |
||||
|
||||
/** |
||||
* @typedef {Object} SpriteOptions |
||||
* @property {string} source - Path to the sprite image file |
||||
* @property {number} [frameWidth] - Width of each frame for animated sprites |
||||
* @property {number} [frameHeight] - Height of each frame for animated sprites |
||||
* @property {number} [frameCount=1] - Number of frames in the sprite sheet |
||||
* @property {number} [frameIndex] - Current frame index for animation |
||||
*/ |
||||
|
||||
/** |
||||
* @typedef {Object} Sprite |
||||
* @property {string} source - Path to the sprite image file |
||||
* @property {HTMLImageElement} image - The loaded image element |
||||
* @property {boolean} ready - Whether the image has finished loading |
||||
* @property {number|null} frameWidth - Width of each frame (null for single-frame sprites) |
||||
* @property {number|null} frameHeight - Height of each frame (null for single-frame sprites) |
||||
* @property {number} frameCount - Total number of frames in the sprite |
||||
* @property {number} [frameIndex] - Current frame index for animation |
||||
*/ |
||||
|
||||
/** |
||||
* @typedef {Object} Cup |
||||
* @property {Sprite} sprite - Reference to the cup sprite template |
||||
* @property {number} x - X position on canvas |
||||
* @property {number} y - Y position on canvas |
||||
* @property {number} scale - Render scale |
||||
* @property {number} frameIndex - Current frame (0 = empty, 1 = filled) |
||||
* @property {boolean} filled - Whether the cup is filled |
||||
* @property {Function} fill - Fills the cup |
||||
* @property {Function} empty - Empties the cup |
||||
* @property {Function} isFilled - Returns whether cup is filled |
||||
*/ |
||||
|
||||
/** |
||||
* Collection of sprite templates (shared image/frame data) |
||||
* @type {Object.<string, Sprite>} |
||||
*/ |
||||
const sprites = { |
||||
stand: createSprite({ source: 'stand.png' }), |
||||
cup: createSprite({ |
||||
source: 'cups.png', |
||||
frameWidth: 251, |
||||
frameHeight: 330, |
||||
frameCount: 2 |
||||
}), |
||||
maker: createSprite({ |
||||
source: 'maker.png', |
||||
frameWidth: 562 / 2, |
||||
frameHeight: 432, |
||||
frameCount: 2 |
||||
}), |
||||
ice: createSprite({ |
||||
source: 'ice.png' |
||||
}), |
||||
lemons: createSprite({ |
||||
source: 'lemons.png' |
||||
}), |
||||
grass: createSprite({ |
||||
source: 'grass.png' |
||||
}) |
||||
}; |
||||
|
||||
/** |
||||
* Creates a cup instance. |
||||
* |
||||
* @param {number} x - X position |
||||
* @param {number} y - Y position |
||||
* @param {number} [scale=0.1] - Render scale |
||||
* @returns {Cup} |
||||
*/ |
||||
function createCup(x, y, scale = 0.1) { |
||||
return { |
||||
sprite: sprites.cup, |
||||
x, |
||||
y, |
||||
scale, |
||||
frameIndex: 0, |
||||
filled: false, |
||||
|
||||
/** Fills the cup */ |
||||
fill() { |
||||
this.filled = true; |
||||
this.frameIndex = 1; |
||||
}, |
||||
|
||||
/** Empties the cup */ |
||||
empty() { |
||||
this.filled = false; |
||||
this.frameIndex = 0; |
||||
}, |
||||
|
||||
/** @returns {boolean} Whether the cup is filled */ |
||||
isFilled() { |
||||
return this.filled; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
// Game objects
|
||||
const cups = [ |
||||
createCup(155, 250), |
||||
createCup(185, 250), |
||||
createCup(215, 250), |
||||
createCup(245, 250), |
||||
|
||||
] |
||||
|
||||
/** |
||||
* Creates a new sprite object from the given options. |
||||
* Automatically triggers a render when the image loads. |
||||
* |
||||
* @param {SpriteOptions} options - Configuration options for the sprite |
||||
* @returns {Sprite} The created sprite object |
||||
*/ |
||||
function createSprite(options) { |
||||
const image = new Image(); |
||||
|
||||
const sprite = { |
||||
source: options.source, |
||||
image, |
||||
ready: false, |
||||
|
||||
// optionals
|
||||
frameWidth: options.frameWidth || null, |
||||
frameHeight: options.frameHeight || null, |
||||
frameCount: options.frameCount ?? 1, |
||||
frameIndex: options.frameIndex || 0 |
||||
}; |
||||
|
||||
image.onload = () => { |
||||
sprite.ready = true; |
||||
render(); |
||||
} |
||||
|
||||
image.src = options.source; |
||||
return sprite; |
||||
} |
||||
|
||||
/** |
||||
* Draws a sprite onto the canvas at the specified position. |
||||
* |
||||
* @param {Sprite} spriteObject - The sprite to draw |
||||
* @param {number} x - The x-coordinate on the canvas |
||||
* @param {number} y - The y-coordinate on the canvas |
||||
* @param {number} scale - Scale factor for rendering |
||||
* @returns {void} |
||||
*/ |
||||
function drawSprite(spriteObject, x, y, scale) { |
||||
if (!spriteObject.ready) return; |
||||
|
||||
const frameIndex = spriteObject.frameIndex; |
||||
|
||||
const isFramedSprite = spriteObject.frameWidth !== null |
||||
|| spriteObject.frameHeight !== null; |
||||
|
||||
if (!isFramedSprite) { |
||||
const drawWidth = spriteObject.image.naturalWidth * scale; |
||||
const drawHeight = spriteObject.image.naturalHeight * scale; |
||||
|
||||
ctx.drawImage(spriteObject.image, x, y, drawWidth, drawHeight); |
||||
return; |
||||
} |
||||
|
||||
// framed horizontal sprite
|
||||
const sourceX = frameIndex * spriteObject.frameWidth; |
||||
const sourceY = 0; |
||||
|
||||
const drawWidth = spriteObject.frameWidth * scale; |
||||
const drawHeight = spriteObject.frameHeight * scale; |
||||
|
||||
ctx.drawImage( |
||||
spriteObject.image, |
||||
sourceX, |
||||
sourceY, |
||||
spriteObject.frameWidth, |
||||
spriteObject.frameHeight, |
||||
x, |
||||
y, |
||||
drawWidth, |
||||
drawHeight |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Draws a cup (or any object with sprite, x, y, scale, frameIndex). |
||||
* |
||||
* @param {Cup} instance - The cup to draw |
||||
* @returns {void} |
||||
*/ |
||||
function drawInstance(instance) { |
||||
const { sprite, x, y, scale, frameIndex } = instance; |
||||
|
||||
if (!sprite.ready) return; |
||||
|
||||
const isFramedSprite = sprite.frameWidth !== null |
||||
|| sprite.frameHeight !== null; |
||||
|
||||
if (!isFramedSprite) { |
||||
const drawWidth = sprite.image.naturalWidth * scale; |
||||
const drawHeight = sprite.image.naturalHeight * scale; |
||||
ctx.drawImage(sprite.image, x, y, drawWidth, drawHeight); |
||||
return; |
||||
} |
||||
|
||||
// framed horizontal sprite
|
||||
const sourceX = frameIndex * sprite.frameWidth; |
||||
const drawWidth = sprite.frameWidth * scale; |
||||
const drawHeight = sprite.frameHeight * scale; |
||||
|
||||
ctx.drawImage( |
||||
sprite.image, |
||||
sourceX, |
||||
0, |
||||
sprite.frameWidth, |
||||
sprite.frameHeight, |
||||
x, |
||||
y, |
||||
drawWidth, |
||||
drawHeight |
||||
); |
||||
} |
||||
|
||||
function drawGround() { |
||||
if (!sprites.grass.ready) return; |
||||
|
||||
const pattern = ctx.createPattern(sprites.grass.image, 'repeat') |
||||
ctx.fillStyle = pattern; |
||||
|
||||
const groundHeight = 250; |
||||
ctx.fillRect(0, canvas.height - groundHeight, canvas.width, groundHeight) |
||||
} |
||||
|
||||
function render() { |
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
||||
|
||||
drawGround(); |
||||
|
||||
drawSprite(sprites.maker, 130, 140, 0.4); |
||||
drawSprite(sprites.stand, 50, 50, 0.5); |
||||
drawSprite(sprites.ice, 250, 190, 0.125); |
||||
|
||||
|
||||
// Draw all cup instances
|
||||
cups.forEach(cup => drawInstance(cup)); |
||||
|
||||
drawSprite(sprites.lemons, 60, 220, 0.1); |
||||
|
||||
} |
||||
|
||||
render(); |
||||
|
||||
// Example: fill the first cup after 1 second
|
||||
setTimeout(() => { |
||||
cups[0].fill(); |
||||
sprites.maker.frameIndex = 1; |
||||
render(); |
||||
|
||||
}, 1000); |
||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 555 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 945 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 411 KiB |
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap'); |
||||
|
||||
body { |
||||
background: linear-gradient(180deg, #cfefff 0%, #f7ffe5 100%); |
||||
min-height: 100vh; |
||||
font-family: 'Inter', sans-serif; |
||||
} |
||||
|
||||
.game_header { |
||||
display: flex; |
||||
margin: 0 auto; |
||||
width: 500px; |
||||
align-items: center; |
||||
} |
||||
|
||||
.dashboard { |
||||
max-width: 1200px; |
||||
margin: 0 auto; |
||||
display: grid; |
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); |
||||
gap: 20px; |
||||
} |
||||
|
||||
.game_header_title { |
||||
font-family: 'Fredoka', cursive; |
||||
font-size: 54px; |
||||
color: #FDB813; |
||||
text-shadow: 3px 3px 0px #3f7a33; |
||||
letter-spacing: 1px; |
||||
} |
||||
|
||||
.game_header_icon { |
||||
height: 64px; |
||||
} |
||||
|
||||
.game_section { |
||||
background-color: #FFF9E6; |
||||
box-sizing: border-box; |
||||
padding: 12px; |
||||
border: 4px solid #8fd16a; |
||||
border-radius: 20px; |
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); |
||||
color: #7A6146; |
||||
} |
||||
|
||||
.game_list { |
||||
list-style: none; |
||||
margin: 0; |
||||
padding: 0; |
||||
} |
||||
|
||||
.section_title { |
||||
font-family: 'Fredoka', sans-serif; |
||||
margin: 0; |
||||
padding: 0; |
||||
color: #5C4632; |
||||
letter-spacing: 1px; |
||||
} |
||||
|
||||
canvas { |
||||
width: 1200px; |
||||
height: 500px; |
||||
margin: 0 auto; |
||||
display: block; |
||||
pointer-events: none; |
||||
} |
||||
|
After Width: | Height: | Size: 842 KiB |
|
After Width: | Height: | Size: 822 KiB |
|
After Width: | Height: | Size: 429 KiB |