From 02a3118756cace7aaf7324aafcd4edad621ea61b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Ta=C5=84czyk?= Date: Tue, 15 Oct 2024 00:58:51 +0200 Subject: [PATCH] mix soldier shading with terrain shading --- .../screens/battle/game/game-render-queue.ts | 7 + .../src/screens/battle/game/game-render.ts | 2 +- .../screens/battle/game/game-world-render.ts | 18 +- .../game/objects/object-soldier-render.ts | 33 +++- .../battle/game/terrain/terrain-generator.ts | 4 +- .../battle/game/terrain/terrain-renderer.ts | 47 ++--- .../screens/battle/game/terrain/terrain.ts | 56 +++--- .../screens/battle/model/simulate.cache.json | 160 ++++++++++++++++++ 8 files changed, 275 insertions(+), 52 deletions(-) diff --git a/games/masterplan/src/screens/battle/game/game-render-queue.ts b/games/masterplan/src/screens/battle/game/game-render-queue.ts index b20e43ea..38e0fd41 100644 --- a/games/masterplan/src/screens/battle/game/game-render-queue.ts +++ b/games/masterplan/src/screens/battle/game/game-render-queue.ts @@ -1,4 +1,5 @@ import { Canvas } from '../util/canvas'; +import { GameWorldRender } from './game-world-render'; // Define types for render commands type RenderCommand = { @@ -20,8 +21,14 @@ const layerOrder: { [key: string]: number } = { }; export class RenderQueue { + constructor(private worldRender: GameWorldRender) {} + private commands: RenderCommand[] = []; + getShadingAt(x: number, y: number): number { + return this.worldRender.getShadingAt(x, y); + } + renderShape(layer: RenderCommand['layer'], x: number, y: number, z: number, points: number[][], fillStyle: string) { this.commands.push({ layer, diff --git a/games/masterplan/src/screens/battle/game/game-render.ts b/games/masterplan/src/screens/battle/game/game-render.ts index 9dad1927..97539fd4 100644 --- a/games/masterplan/src/screens/battle/game/game-render.ts +++ b/games/masterplan/src/screens/battle/game/game-render.ts @@ -6,7 +6,7 @@ import { GameWorldRender } from './game-world-render'; export function renderGame(world: GameWorld, worldRender: GameWorldRender) { const canvas = getCanvas(LAYER_DEFAULT); - const renderQueue = new RenderQueue(); + const renderQueue = new RenderQueue(worldRender); // clear canvas.clear(); diff --git a/games/masterplan/src/screens/battle/game/game-world-render.ts b/games/masterplan/src/screens/battle/game/game-world-render.ts index ac5c2431..8b9a3aa9 100644 --- a/games/masterplan/src/screens/battle/game/game-world-render.ts +++ b/games/masterplan/src/screens/battle/game/game-world-render.ts @@ -1,15 +1,29 @@ import { GameWorld } from './game-world'; +import { interpolateGridValue } from './terrain/terrain'; import { createTerrainTexture } from './terrain/terrain-renderer'; export class GameWorldRender { terrainCanvas: HTMLCanvasElement; + shadingMap: number[][]; - constructor(gameWorld: GameWorld) { - this.terrainCanvas = createTerrainTexture( + constructor(private gameWorld: GameWorld) { + const { terrainCanvas, shadingMap } = createTerrainTexture( gameWorld.terrain.width, gameWorld.terrain.height, gameWorld.terrain.heightMap, gameWorld.terrain.tileSize, ); + this.terrainCanvas = terrainCanvas; + this.shadingMap = shadingMap; + } + + getShadingAt(x: number, y: number): number { + return interpolateGridValue( + [x, y], + this.shadingMap, + this.gameWorld.terrain.tileSize, + this.gameWorld.terrain.offsetX, + this.gameWorld.terrain.offsetY, + ); } } diff --git a/games/masterplan/src/screens/battle/game/objects/object-soldier-render.ts b/games/masterplan/src/screens/battle/game/objects/object-soldier-render.ts index cad1a5b5..556bc9a2 100644 --- a/games/masterplan/src/screens/battle/game/objects/object-soldier-render.ts +++ b/games/masterplan/src/screens/battle/game/objects/object-soldier-render.ts @@ -55,11 +55,19 @@ export class SoldierRender { // Sort shapes by z-index for proper layering const shapes = isAlive ? SHAPES[this.soldier.type] : FALLEN_SHAPES[this.soldier.type]; + // Get terrain shading factor + const terrainShadingFactor = renderQueue.getShadingAt(...this.soldier.vec); + // Render each part of the soldier for (const [, shape] of shapes) { const rotatedPoints = rotate3D(shape.points, rotationAngle + Math.PI / 2); const perspectivePoints = rotatedPoints.map((p) => applyPerspective(p)); - const shadedColor = applyShading(baseColor, rotationAngle, this.soldier.state.isAlive() ? 1 : 0.5); + + // Apply shading to both live and fallen soldiers + const shadedColor = applyShading(baseColor, rotationAngle, isAlive ? 1 : 0.5); + + // Combine soldier shading with terrain shading + const finalColor = this.combineColors(shadedColor, terrainShadingFactor); renderQueue.addObjectCommand( this.soldier.getX(), @@ -68,9 +76,30 @@ export class SoldierRender { this.soldier.world.terrain.getHeightAt(this.soldier.vec), this.soldier.getZ(), isAlive, - shadedColor, + finalColor, perspectivePoints.map((p) => [p.x, p.y]), ); } } + + private combineColors(soldierColor: string, terrainShadingFactor: number): string { + // Parse the soldier color + const rgbaMatch = soldierColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*([\d.]+)?\)/); + if (!rgbaMatch) { + return soldierColor; + } + + const r = parseInt(rgbaMatch[1], 10); + const g = parseInt(rgbaMatch[2], 10); + const b = parseInt(rgbaMatch[3], 10); + const a = rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1; + + // Apply terrain shading factor + const newR = Math.floor(r * terrainShadingFactor); + const newG = Math.floor(g * terrainShadingFactor); + const newB = Math.floor(b * terrainShadingFactor); + + // Return the combined color + return `rgba(${newR},${newG},${newB},${a})`; + } } diff --git a/games/masterplan/src/screens/battle/game/terrain/terrain-generator.ts b/games/masterplan/src/screens/battle/game/terrain/terrain-generator.ts index 4c225cab..575fa38b 100644 --- a/games/masterplan/src/screens/battle/game/terrain/terrain-generator.ts +++ b/games/masterplan/src/screens/battle/game/terrain/terrain-generator.ts @@ -1,6 +1,6 @@ import { EDGE_RADIUS } from '../../consts'; -export type TerrainData = { width: number; height: number; heightMap: number[][] }; +export type TerrainData = { width: number; height: number; tileSize: number; heightMap: number[][] }; // Helper function to get the number of active neighbors for a cell function getActiveNeighbors(grid: number[][], x: number, y: number): number { @@ -107,5 +107,5 @@ export function generateTerrain(tileSize: number): TerrainData { const height = EDGE_RADIUS * 2; const heightMap = generateTerrainHeightMap(width, height, tileSize); - return { width, height, heightMap }; + return { width, height, tileSize, heightMap }; } diff --git a/games/masterplan/src/screens/battle/game/terrain/terrain-renderer.ts b/games/masterplan/src/screens/battle/game/terrain/terrain-renderer.ts index 91633e46..eeb39ade 100644 --- a/games/masterplan/src/screens/battle/game/terrain/terrain-renderer.ts +++ b/games/masterplan/src/screens/battle/game/terrain/terrain-renderer.ts @@ -37,17 +37,25 @@ function calculateNormal(heightMap: number[][], x: number, y: number, tileSize: return [normal[0] / length, normal[1] / length, normal[2] / length]; } -// Function to render terrain based on heightmap +// Function to render terrain based on heightmap and generate shading map export function renderTerrain( - ctx: CanvasRenderingContext2D, heightMap: number[][], width: number, height: number, tileSize: number, -): void { +): { terrainCanvas: HTMLCanvasElement; shadingMap: number[][] } { const gridWidth = Math.ceil(width / tileSize); const gridHeight = Math.ceil(height / tileSize); + const terrainCanvas = document.createElement('canvas'); + terrainCanvas.width = width; + terrainCanvas.height = height; + const terrainCtx = terrainCanvas.getContext('2d')!; + + const shadingMap: number[][] = Array(gridHeight) + .fill(0) + .map(() => Array(gridWidth).fill(0)); + // Draw the terrain based on the heightmap for (let y = 0; y < gridHeight; y++) { for (let x = 0; x < gridWidth; x++) { @@ -76,18 +84,24 @@ export function renderTerrain( const shadedColor = applyShading(baseColor, rotationAngle, averageHeight); // Draw the tile using a path to represent height variations - ctx.beginPath(); - ctx.moveTo(x * tileSize, topLeftY); - ctx.lineTo((x + 1) * tileSize, topRightY); - ctx.lineTo((x + 1) * tileSize, bottomRightY); - ctx.lineTo(x * tileSize, bottomLeftY); - ctx.closePath(); + terrainCtx.beginPath(); + terrainCtx.moveTo(x * tileSize, topLeftY); + terrainCtx.lineTo((x + 1) * tileSize, topRightY); + terrainCtx.lineTo((x + 1) * tileSize, bottomRightY); + terrainCtx.lineTo(x * tileSize, bottomLeftY); + terrainCtx.closePath(); // Set the fill color with shading - ctx.fillStyle = shadedColor; - ctx.fill(); + terrainCtx.fillStyle = shadedColor; + terrainCtx.fill(); + + // Calculate shading factor for the shading map + const shadingFactor = (Math.cos(rotationAngle) * 0.3 + 0.7) * (0.8 + factor * 0.4); + shadingMap[y][x] = shadingFactor; } } + + return { terrainCanvas, shadingMap }; } // Function to create a terrain texture @@ -96,15 +110,8 @@ export function createTerrainTexture( height: number, heightMap: number[][], tileSize: number, -): HTMLCanvasElement { - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d')!; - - renderTerrain(ctx, heightMap, width, height, tileSize); - - return canvas; +): { terrainCanvas: HTMLCanvasElement; shadingMap: number[][] } { + return renderTerrain(heightMap, width, height, tileSize); } const interpolateColor = (color1: string, color2: string, factor: number): string => { diff --git a/games/masterplan/src/screens/battle/game/terrain/terrain.ts b/games/masterplan/src/screens/battle/game/terrain/terrain.ts index 91ee0f22..c48d97e9 100644 --- a/games/masterplan/src/screens/battle/game/terrain/terrain.ts +++ b/games/masterplan/src/screens/battle/game/terrain/terrain.ts @@ -17,30 +17,36 @@ export class Terrain { } getHeightAt(pos: Vec): number { - // Calculate tile indices and positions within the tile - const x = pos[0] / this.tileSize + this.offsetX; - const y = pos[1] / this.tileSize + this.offsetY; - - const x0 = Math.min(Math.max(Math.floor(x), 0), this.heightMap.length - 1); - const x1 = Math.min(x0 + 1, this.heightMap[0].length - 1); - const y0 = Math.min(Math.max(Math.floor(y), 0), this.heightMap.length - 1); - const y1 = Math.max(Math.min(y0 + 1, this.heightMap.length - 1), 0); - - // Get heights at the four corners - const h00 = this.heightMap[y0][x0]; - const h10 = this.heightMap[y0][x1]; - const h01 = this.heightMap[y1][x0]; - const h11 = this.heightMap[y1][x1]; - - // Calculate the fractional part of x and y within the tile - const dx = x - x0; - const dy = y - y0; - - // Bilinear interpolation - const h0 = h00 * (1 - dx) + h10 * dx; - const h1 = h01 * (1 - dx) + h11 * dx; - const height = h0 * (1 - dy) + h1 * dy; - - return height; + return interpolateGridValue(pos, this.heightMap, this.tileSize, this.offsetX, this.offsetY); } } + +export function interpolateGridValue( + pos: Vec, + grid: number[][], + tileSize: number, + offsetX: number, + offsetY: number, +): number { + // Calculate tile indices and positions within the tile + const x = pos[0] / tileSize + offsetX; + const y = pos[1] / tileSize + offsetY; + + const x0 = Math.min(Math.max(Math.floor(x), 0), grid[0].length - 1); + const x1 = Math.min(x0 + 1, grid[0].length - 1); + const y0 = Math.min(Math.max(Math.floor(y), 0), grid.length - 1); + const y1 = Math.max(Math.min(y0 + 1, grid.length - 1), 0); + + const h00 = grid[y0][x0]; + const h10 = grid[y0][x1]; + const h01 = grid[y1][x0]; + const h11 = grid[y1][x1]; + + const dx = x - x0; + const dy = y - y0; + + const h0 = h00 * (1 - dx) + h10 * dx; + const h1 = h01 * (1 - dx) + h11 * dx; + + return h0 * (1 - dy) + h1 * dy; +} diff --git a/games/masterplan/src/screens/battle/model/simulate.cache.json b/games/masterplan/src/screens/battle/model/simulate.cache.json index 778f8dcf..733f557d 100644 --- a/games/masterplan/src/screens/battle/model/simulate.cache.json +++ b/games/masterplan/src/screens/battle/model/simulate.cache.json @@ -4710,5 +4710,165 @@ "27c40c274d5e7defca8bf38024fbeb33_dbd700ac90369ce4b1e275065accba64_b618b611d2f83d9442a84633a4b0bba7": { "balance": 1, "result": "plan" + }, + "d49730723bbc0bda800c4e8e3675dba3_a407a44fc4b54d6586f7d9387aafd250_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "f6e59d5559ca8edf0f6a97293ba4f2fa_9f0ced5d80b1ce894d70baf7f0639b83_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "d49730723bbc0bda800c4e8e3675dba3_f2f146f40f7a5c86fbbec93ada6b71c9_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0.7391304347826086, + "result": "draw" + }, + "7c2c4882293d456513bb8185dabfa158_9f0ced5d80b1ce894d70baf7f0639b83_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0.45454545454545453, + "result": "draw" + }, + "d49730723bbc0bda800c4e8e3675dba3_2029396885ff7403cdf76486e71a017b_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "f16a523afa8658ca9c97df7949ea22a8_9f0ced5d80b1ce894d70baf7f0639b83_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "d49730723bbc0bda800c4e8e3675dba3_c2b59c54d5cb9c52b8b3707052fe64f2_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0.1111111111111111, + "result": "counterPlan" + }, + "18802e411257c2dd83ab61482d20aad4_9f0ced5d80b1ce894d70baf7f0639b83_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "d49730723bbc0bda800c4e8e3675dba3_04599a930c23d16d61d955676b7af30d_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0.21428571428571427, + "result": "counterPlan" + }, + "85e777672a92423afebfb6c6052ba1bf_9f0ced5d80b1ce894d70baf7f0639b83_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0.2, + "result": "counterPlan" + }, + "d49730723bbc0bda800c4e8e3675dba3_e14fa1b677c62f2418d82fe2713bd941_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "80a30ebd2e618e5733b7a7e93dd34ebc_9f0ced5d80b1ce894d70baf7f0639b83_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "d49730723bbc0bda800c4e8e3675dba3_2e62f3ddc3a99f27f192d7e5509f7cc2_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0.88, + "result": "plan" + }, + "3145129cf6879a4669578f0f81e20809_9f0ced5d80b1ce894d70baf7f0639b83_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0.9615384615384616, + "result": "plan" + }, + "d49730723bbc0bda800c4e8e3675dba3_ce8c253ea00e055953bb094a49614118_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0.9444444444444444, + "result": "plan" + }, + "c2d564876a7b0773a623aba97b2e67df_9f0ced5d80b1ce894d70baf7f0639b83_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "d49730723bbc0bda800c4e8e3675dba3_327f9638873c2a29bbcecb7bee8793d0_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0.9583333333333334, + "result": "plan" + }, + "27c40c274d5e7defca8bf38024fbeb33_9f0ced5d80b1ce894d70baf7f0639b83_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "f6e59d5559ca8edf0f6a97293ba4f2fa_f2f146f40f7a5c86fbbec93ada6b71c9_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "7c2c4882293d456513bb8185dabfa158_a407a44fc4b54d6586f7d9387aafd250_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "f6e59d5559ca8edf0f6a97293ba4f2fa_2029396885ff7403cdf76486e71a017b_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "f16a523afa8658ca9c97df7949ea22a8_a407a44fc4b54d6586f7d9387aafd250_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "f6e59d5559ca8edf0f6a97293ba4f2fa_c2b59c54d5cb9c52b8b3707052fe64f2_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "18802e411257c2dd83ab61482d20aad4_a407a44fc4b54d6586f7d9387aafd250_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "f6e59d5559ca8edf0f6a97293ba4f2fa_04599a930c23d16d61d955676b7af30d_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "85e777672a92423afebfb6c6052ba1bf_a407a44fc4b54d6586f7d9387aafd250_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "f6e59d5559ca8edf0f6a97293ba4f2fa_e14fa1b677c62f2418d82fe2713bd941_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "80a30ebd2e618e5733b7a7e93dd34ebc_a407a44fc4b54d6586f7d9387aafd250_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "f6e59d5559ca8edf0f6a97293ba4f2fa_2e62f3ddc3a99f27f192d7e5509f7cc2_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "3145129cf6879a4669578f0f81e20809_a407a44fc4b54d6586f7d9387aafd250_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "f6e59d5559ca8edf0f6a97293ba4f2fa_ce8c253ea00e055953bb094a49614118_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "c2d564876a7b0773a623aba97b2e67df_a407a44fc4b54d6586f7d9387aafd250_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "f6e59d5559ca8edf0f6a97293ba4f2fa_327f9638873c2a29bbcecb7bee8793d0_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "27c40c274d5e7defca8bf38024fbeb33_a407a44fc4b54d6586f7d9387aafd250_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "7c2c4882293d456513bb8185dabfa158_2029396885ff7403cdf76486e71a017b_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0.4, + "result": "draw" + }, + "f16a523afa8658ca9c97df7949ea22a8_f2f146f40f7a5c86fbbec93ada6b71c9_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "7c2c4882293d456513bb8185dabfa158_c2b59c54d5cb9c52b8b3707052fe64f2_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" + }, + "18802e411257c2dd83ab61482d20aad4_f2f146f40f7a5c86fbbec93ada6b71c9_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 1, + "result": "plan" + }, + "7c2c4882293d456513bb8185dabfa158_04599a930c23d16d61d955676b7af30d_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0.9, + "result": "plan" + }, + "85e777672a92423afebfb6c6052ba1bf_f2f146f40f7a5c86fbbec93ada6b71c9_fb8194af3f34d3a12c2b2e95068ae12f": { + "balance": 0, + "result": "counterPlan" } } \ No newline at end of file