|
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 @@ |
|||||||
|
/** |
||||||
|
* 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 @@ |
|||||||
|
<!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 @@ |
|||||||
|
/** |
||||||
|
* @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 @@ |
|||||||
|
@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 |