/* Hurry Curry! - a game about cooking Copyright 2024 metamuffin 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 . */ import { ItemData, MessageData, PlayerData, TileData, camera, camera_scale, canvas, ctx, data, demands_completed, demands_failed, get_interact_target, global_message, interact_active_anim, interact_possible_anim, interact_target_anim, items_removed, keys_down, my_id, nametag_scale_anim, players, points, server_hints, tiles, time_remaining } from "./main.ts"; import { PLAYER_SIZE } from "./movement.ts"; import { draw_item_sprite, draw_tile_sprite, ItemName, TileName } from "./tiles.ts"; import { V2, ceil_v2, floor_v2 } from "./util.ts"; export function draw_wait(text: string) { ctx.fillStyle = "#444" ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.fillStyle = "#555" ctx.font = "50px sans-serif" ctx.strokeStyle = "black" ctx.fillStyle = "white" ctx.lineWidth = 10 ctx.textAlign = "center" ctx.textBaseline = "middle" ctx.lineJoin = "round" ctx.lineCap = "round" ctx.strokeText(text, canvas.width / 2, canvas.height / 2) ctx.fillText(text, canvas.width / 2, canvas.height / 2) } export function draw_ingame() { ctx.fillStyle = "#111" ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.save() ctx.translate(canvas.width / 2, canvas.height / 2) ctx.scale(camera_scale, camera_scale) ctx.translate(-camera.x, -camera.y) draw_grid() // Draw tiles for (const [_, tile] of tiles) { draw_tile(tile) } // Draw players for (const [_, player] of players) draw_player(player) // Draw removed items for (const item of items_removed) draw_item(item) // Draw items on tiles for (const [_, tile] of tiles) if (tile.item) draw_item(tile.item) // Draw player messages for (const [_, player] of players) { if (player.message) draw_message(player.message) if (player.message_persist) draw_message(player.message_persist) } // Draw nametags for (const [_, player] of players) draw_player_nametag(player) // Draw server hints for (const [_, message] of server_hints) draw_message(message) // Draw interact target draw_interact_target() ctx.restore() draw_global_message() ctx.fillStyle = "white" ctx.textAlign = "left" ctx.textBaseline = "bottom" ctx.font = "20px sans-serif" if (time_remaining != undefined) ctx.fillText(`Time remaining: ${time_remaining?.toFixed(2)}`, 10, canvas.height - 90) ctx.font = "30px sans-serif" ctx.fillText(`Points: ${points}`, 10, canvas.height - 60) ctx.font = "20px sans-serif" ctx.fillText(`Completed: ${demands_completed}`, 10, canvas.height - 30) ctx.fillText(`Failed: ${demands_failed}`, 10, canvas.height - 10) if (keys_down.has("KeyP")) { draw_debug() } } function draw_debug() { ctx.fillStyle = "white" ctx.textAlign = "left" ctx.textBaseline = "bottom" ctx.font = "20px sans-serif" ctx.fillText(`position = ${JSON.stringify(players.get(my_id)?.anim_position)}`, 10, 30) ctx.fillText(`velocity = ${JSON.stringify(players.get(my_id)?.vel)}`, 10, 50) ctx.fillText(`interact = ${JSON.stringify(get_interact_target())}`, 10, 70) } function draw_tile(tile: TileData) { ctx.save() ctx.translate(tile.x + 0.5, tile.y + 0.5) draw_tile_sprite(ctx, data.tile_names[tile.kind] as TileName) ctx.restore() } function draw_item(item: ItemData) { ctx.save() ctx.translate(item.x, item.y) if (item.remove_anim) ctx.scale(1 - item.remove_anim, 1 - item.remove_anim) draw_item_sprite(ctx, data.item_names[item.kind] as ItemName) if (item.active) { ctx.fillStyle = item.active.warn ? "rgba(230, 58, 58, 0.66)" : "rgba(115, 230, 58, 0.66)" ctx.fillRect(-0.5, -0.5, 1, item.active.position) } ctx.restore() } function draw_player(player: PlayerData) { ctx.save() ctx.translate(player.anim_position.x, player.anim_position.y) ctx.rotate(-player.rot) if (player.boosting) ctx.scale(1.3, 1.3) draw_character(player.character) ctx.restore() if (player.item) draw_item(player.item) } function draw_player_nametag(player: PlayerData) { if (nametag_scale_anim > 0.01) { ctx.save() ctx.translate(player.anim_position.x, player.anim_position.y) ctx.translate(0, -1) ctx.textAlign = "center" ctx.font = "15px sans-serif" ctx.scale(nametag_scale_anim / camera_scale, nametag_scale_anim / camera_scale) const w = ctx.measureText(player.name).width + 20 ctx.fillStyle = "#fffa" ctx.beginPath() ctx.roundRect(-w / 2, -10, w, 20, 5) ctx.fill() ctx.fillStyle = "black" ctx.textBaseline = "middle" ctx.fillText(player.name, 0, 0) ctx.restore() } } function draw_interact_target() { ctx.save() ctx.translate(interact_target_anim.x, interact_target_anim.y) ctx.lineCap = "round" ctx.lineJoin = "round" ctx.lineWidth = 0.06 + 0.03 * Math.sin(Date.now() / 100) * interact_possible_anim ctx.strokeStyle = `hsla(${(1 - interact_active_anim) * 225 + interact_active_anim * 125}deg, ${interact_possible_anim * 100}%, 62.70%, ${interact_possible_anim * 0.7 + 0.3})` ctx.strokeRect(0, 0, 1, 1) ctx.restore() } function draw_grid() { ctx.strokeStyle = "#333" ctx.lineWidth = 0.01 ctx.beginPath() const min = floor_v2(map_screen_to_world({ x: 0, y: 0 })) const max = ceil_v2(map_screen_to_world({ x: canvas.width, y: canvas.height })) for (let x = min.x; x < max.x; x++) { ctx.moveTo(x, min.y) ctx.lineTo(x, max.y) } for (let y = min.y; y < max.y; y++) { ctx.moveTo(min.x, y) ctx.lineTo(max.x, y) } ctx.stroke() } function draw_character(character: number) { ctx.fillStyle = `hsl(${character}rad, 50%, 50%)` ctx.beginPath() ctx.arc(0, 0, PLAYER_SIZE, 0, Math.PI * 2) ctx.fill() if (character >= 0) { ctx.fillStyle = `hsl(${character}rad, 80%, 10%)` ctx.beginPath() ctx.arc(0, -0.2, PLAYER_SIZE, 0, Math.PI * 2) ctx.fill() } ctx.fillStyle = `hsl(${character}rad, 80%, 70%)` ctx.beginPath() ctx.moveTo(-0.04, 0.25) ctx.lineTo(0.04, 0.25) ctx.lineTo(0, 0.4) ctx.fill() } function draw_message(m: MessageData) { ctx.save() ctx.translate(m.anim_position.x, m.anim_position.y) const scale = Math.min(m.anim_size, 1 - nametag_scale_anim); ctx.scale(scale, scale) if ("item" in m.inner) { ctx.fillStyle = "#fffa" ctx.beginPath() ctx.moveTo(0, -0.3) ctx.arc(0, -1, 0.5, Math.PI / 4, Math.PI - Math.PI / 4, true) ctx.closePath() ctx.fill() if (m.timeout) { const t = m.timeout.remaining / m.timeout.initial; ctx.beginPath() ctx.strokeStyle = `hsl(${Math.sqrt(t) * 0.3}turn, 100%, 50%)` ctx.lineWidth = 0.1 ctx.arc(0, -1, 0.45, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * (1 - t)) ctx.stroke() } ctx.translate(0, -1) draw_item_sprite(ctx, data.item_names[m.inner.item] as ItemName) ctx.translate(0, 1) } if ("text" in m.inner) { ctx.translate(0, -1) ctx.textAlign = "center" ctx.font = "15px sans-serif" ctx.scale(2 / camera_scale, 2 / camera_scale) const w = ctx.measureText(m.inner.text).width + 30 ctx.fillStyle = "#fffa" ctx.beginPath() ctx.roundRect(-w / 2, -15, w, 30, 5) ctx.fill() ctx.fillStyle = "black" ctx.textBaseline = "middle" ctx.fillText(m.inner.text, 0, 0) ctx.translate(0, 1) } ctx.restore() } function draw_global_message() { if (!global_message) return ctx.save() ctx.translate(canvas.width / 2, canvas.height / 6) if ("text" in global_message.inner) { ctx.font = "20px monospace" const lines = global_message.inner.text.split("\n") const w = lines.reduce((a, v) => Math.max(a, ctx.measureText(v).width), 0) + 20 ctx.fillStyle = "#fffa" ctx.beginPath() ctx.roundRect(-w / 2, -20, w, lines.length * 25 + 20, 5) ctx.fill() ctx.fillStyle = "black" ctx.textAlign = "left" ctx.textBaseline = "middle" for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], -w / 2 + 10, i * 25) } ctx.restore() } function map_screen_to_world(screen: V2): V2 { return { x: ((screen.x - canvas.width / 2) / camera_scale) + camera.x, y: ((screen.y - canvas.height / 2) / camera_scale) + camera.y, } }