/* Hurry Curry! - a game about cooking Copyright (C) 2025 Hurry Curry! Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ type Component = (ctx: CanvasRenderingContext2D) => void function base(fill: string, stroke?: string, stroke_width?: number): Component { return c => { c.fillStyle = fill; c.strokeStyle = stroke ?? "black"; c.lineWidth = stroke_width ?? 0.05 c.lineJoin = "miter" c.lineCap = "square" c.fillRect( -0.5, -0.5, 1, 1 ) if (stroke) c.strokeRect( -0.5 + c.lineWidth / 2, -0.5 + c.lineWidth / 2, 1 - c.lineWidth, 1 - c.lineWidth ) } } function rect(inset: number, fill: string, stroke?: string, stroke_width?: number): Component { return c => { c.fillStyle = fill; c.strokeStyle = stroke ?? "black"; c.lineWidth = stroke_width ?? 0.05 c.lineJoin = "round" c.lineCap = "round" c.fillRect( -0.5 + inset, -0.5 + inset, 1 - inset * 2, 1 - inset * 2 ) if (stroke) c.strokeRect( -0.5 + inset, -0.5 + inset, 1 - inset * 2, 1 - inset * 2 ) } } function circle(radius: number, fill: string, stroke?: string, stroke_width?: number): Component { return c => { c.fillStyle = fill; c.strokeStyle = stroke ?? "black"; c.lineWidth = stroke_width ?? 0.05 c.beginPath() c.arc(0.0, 0.0, radius, 0, Math.PI * 2) if (stroke) c.stroke() c.fill() } } function cross(size: number, stroke: string, stroke_width = 0.05): Component { return c => { c.strokeStyle = stroke c.lineWidth = stroke_width c.lineCap = "round" c.beginPath() c.moveTo(-size, -size) c.lineTo(size, size) c.moveTo(size, -size) c.lineTo(-size, size) c.stroke() } } function text(s: string): Component { return c => { c.font = "0.8px sans-serif" c.strokeStyle = "#e38242" c.fillStyle = "white" c.lineWidth = 0.05 c.textAlign = "center" c.textBaseline = "middle" c.lineJoin = "round" c.lineCap = "round" c.strokeText(s, 0, 0) c.fillText(s, 0, 0) } } function arrange_items(...items: ItemName[]): Component { return c => { for (let index = 0; index < items.length; index++) { const item = items[index]; const t = index / items.length * Math.PI * 2. const radius = items.length == 1 ? 0 : (0.4 / items.length) const off_x = Math.sin(t) * radius const off_y = Math.cos(t) * radius const scale = 1. / Math.sqrt(items.length) c.save() c.translate(off_x, off_y) c.scale(scale, scale) iref(item)(c) c.restore() } } } const door: Component = c => { c.fillStyle = "#ff9843" c.fillRect(-0.5, -0.1, 1, 0.2) } const iref = (name: ItemName): Component => c => draw_item_sprite(c, name) const tref = (name: ItemName): Component => c => draw_tile_sprite(c, name) export type ItemName = string export type TileName = string const ITEMS: { [key in ItemName]: (c: string[]) => Component } = { "bun": () => circle(0.3, "#853e20"), "burned": () => c => (circle(0.3, "rgb(61, 18, 0)")(c), cross(0.2, "red", 0.02)(c)), "coconut": () => circle(0.3, "rgb(75, 49, 25)"), "cooked-rice": () => circle(0.3, "rgb(233, 233, 233)"), "curry": () => circle(0.3, "rgb(185, 67, 37)"), "dough": () => circle(0.3, "#b38d7d"), "fish": () => circle(0.3, "rgb(62, 66, 104)"), "flour": () => circle(0.3, "#d8c9c2"), "leek": () => circle(0.3, "rgb(50, 133, 17)"), "milk": () => circle(0.3, "rgb(252, 243, 208)"), "nigiri": () => circle(0.25, "rgb(233, 233, 233)", "salmon"), "rice": () => circle(0.3, "rgb(163, 163, 163)"), "seared-steak": () => circle(0.3, "#702200"), "sliced-bun": () => circle(0.3, "#853e20"), "sliced-fish": () => circle(0.3, "salmon", "rgb(62, 66, 104)"), "steak": () => circle(0.3, "#ca3510"), "patty": () => circle(0.3, "#c26149"), "seared-patty": () => circle(0.3, "#502c23"), "strawberry-icecream": () => circle(0.2, "rgb(250, 148, 236)"), "strawberry-mochi": () => circle(0.2, "rgb(161, 111, 132)"), "strawberry-shake": () => circle(0.3, "rgb(255, 180, 180)"), "strawberry": () => circle(0.3, "rgb(228, 79, 111)"), "tomato-juice": () => circle(0.3, "#b80000"), "tomato-soup": () => circle(0.3, "#ff2600"), "water": () => circle(0.3, "rgb(86, 92, 206)"), "tomato": () => circle(0.3, "#d63838"), "sliced-tomato": () => circle(0.3, "#d16363", "#d63838", 0.08), "lettuce": () => circle(0.3, "#64a30b"), "sliced-lettuce": () => circle(0.3, "#a0da4f", "#64a30b", 0.08), "cheese": () => circle(0.3, "#b3b615"), "sliced-cheese": () => rect(0.25, "#dcdf29", "#b3b615", 0.05), "dirty-plate": () => circle(0.4, "#947a6f", "#d3a187", 0.02), "mochi-dough": () => circle(0.3, "rgb(172, 162, 151)"), "rice-flour": () => iref("rice"), "unknown-order": () => text("!"), "pan": i => c => (circle(0.35, "rgb(29, 29, 29)", "rgb(39, 39, 39)", 0.04)(c), arrange_items(...i)(c)), "pot": i => c => (circle(0.27, "rgb(29, 29, 29)", "rgb(56, 56, 56)", 0.2)(c), arrange_items(...i)(c)), "foodprocessor": i => c => (circle(0.35, "rgb(86, 168, 189)", "rgb(88, 222, 255)", 0.04)(c), arrange_items(...i)(c)), "plate": i => c => (circle(0.4, "#b6b6b6", "#f7f7f7", 0.02)(c), arrange_items(...i)(c)), "glass": i => c => (circle(0.35, "rgb(150, 255, 237)", "rgb(52, 129, 155)", 0.02), arrange_items(...i)(c)), } const counter = tref("counter"); const TILES: { [key in TileName]: (param: string) => Component } = { "floor": () => base("#333", "#222", 0.05), "street": () => base("rgb(19, 19, 19)"), "table": () => base("rgb(133, 76, 38)"), "door": () => door, "chair": () => circle(0.45, "rgb(136, 83, 41)"), "wall": () => base("rgb(0, 14, 56)"), "wall-window": () => base("rgb(19, 40, 102)"), "counter": () => base("rgb(182, 172, 164)"), "counter-window": () => base("rgb(233, 233, 233)"), "grass": () => base("rgb(0, 107, 4)"), "path": () => base("rgb(100, 80, 55)"), "conveyor": () => base("rgb(107, 62, 128)"), "tree": () => base("rgb(1, 82, 4)"), "cutting-board": () => rect(0.3, "#9eec44ff", "#9eec44ff", 0.2), "rolling-board": () => rect(0.3, "#ece644ff", "#ece644ff", 0.2), "trash": () => c => (circle(0.4, "rgb(20, 20, 20)")(c), cross(0.3, "rgb(90, 36, 36)")(c)), "sink": () => base("rgb(131, 129, 161)", "rgb(177, 174, 226)", 0.2), "oven": () => base("rgb(241, 97, 61)", "rgb(109, 84, 84)", 0.3), "freezer": () => base("rgb(61, 97, 241)", "rgb(84, 88, 109)", 0.3), "stove": () => c => (counter(c), circle(0.4, "#444", "#999")(c)), "book": () => c => (counter(c), rect(0.2, "rgb(88, 44, 7)")(c)), "lamp": () => rect(0.3, "rgb(255, 217, 127)", "rgb(32, 32, 32)", 0.1), "crate": name => c => (base("#60701e", "#b9da37", 0.05)(c), iref(name)(c)) } function debug_label(ctx: CanvasRenderingContext2D, name: string) { ctx.save() ctx.font = "10px sans-serif" ctx.fillStyle = "white" ctx.strokeStyle = "black" ctx.lineWidth = 1 ctx.textAlign = "center" ctx.textBaseline = "middle" ctx.scale(0.03, 0.03) ctx.strokeText(name, 0, 0) ctx.fillText(name, 0, 0) ctx.restore() } // TODO performance export function draw_item_sprite(ctx: CanvasRenderingContext2D, name: ItemName) { const [base, cont] = name.split(":") if (ITEMS[base]) { ITEMS[base](cont?.split(",") ?? [])(ctx) } else { circle(0.4, "#f0f")(ctx) debug_label(ctx, name) } } export function draw_tile_sprite(ctx: CanvasRenderingContext2D, name: TileName) { if (!name) return const [kind, param] = name.split(":", 2) if (TILES[kind]) { TILES[kind](param)(ctx) } else { base("#f0f")(ctx) debug_label(ctx, name) } }