Skip to content
Closed
82 changes: 82 additions & 0 deletions games/tribe/public/assets/sprites/emojis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Emoji Sprites

This directory contains 32x32 pixel PNG sprites that replace Unicode emoji rendering throughout the tribe game.

## Naming Convention

Sprite files use descriptive snake_case names rather than Unicode characters:
- `crown.png` - 👑 (tribe badge)
- `skull.png` - 💀 (tribe badge)
- `handshake.png` - 🤝 (friendly diplomacy)
- `crossed_swords.png` - ⚔️ (hostile diplomacy or attack action)

## Sprite Categories

### Tribe Badge Sprites
Used for identifying different tribes in the game:
- `crown.png` - 👑 Royal/leadership tribe
- `skull.png` - 💀 Death/warrior tribe
- `fire.png` - 🔥 Fire tribe
- `water_drop.png` - 💧 Water tribe
- `shamrock.png` - ☘️ Nature/luck tribe
- `sun.png` - ☀️ Solar tribe
- `crescent_moon.png` - 🌙 Lunar tribe
- `star.png` - ⭐ Star tribe
- `lightning.png` - ⚡ Storm tribe
- `fleur_de_lis.png` - ⚜️ Noble tribe

### Diplomacy Status Sprites
Used in tribe list UI to show relationships:
- `handshake.png` - 🤝 Friendly status
- `crossed_swords.png` - ⚔️ Hostile status

### Player Action Sprites
Used for player commands and autopilot actions:
- `raised_hand.png` - ✋ Gather action
- `meat.png` - 🍖 Eat action
- `heart.png` - ❤️ Procreate action
- `crossed_swords.png` - ⚔️ Attack action (reused)
- `seedling.png` - 🌱 Plant action
- `megaphone.png` - 📢 Call to attack
- `trident.png` - 🔱 Split tribe
- `right_arrow.png` - ➡️ Follow me
- `man_feeding_child.png` - 👨‍👧 Feed child
- `bullseye.png` - 🎯 Autopilot move

### Status Indicator Sprites
Used in UI status bars and displays:
- `calendar.png` - 🗓️ Time indicator
- `meat.png` - 🍖 Hunger indicator (reused)
- `heart.png` - ❤️ Hitpoints indicator (reused)
- `strawberry.png` - 🍓 Food indicator
- `robot.png` - 🤖 Autopilot indicator

### Visual Effect Sprites
Used for floating visual effects during gameplay:
- `baby_bottle.png` - 🍼 Child fed effect
- `shield.png` - 🛡️ Attack deflected effect
- `muscle.png` - 💪 Attack resisted effect
- `explosion.png` - 💥 Hit effect

## Usage in Code

### Main Files Using Sprites:
- `src/game/render/render-effects.ts` - Visual effects rendering
- `src/game/render/ui/render-tribe-list.ts` - Tribe badges and diplomacy
- `src/game/render/ui/render-buttons.ts` - Player action buttons
- `src/game/render/ui/render-top-left-panel.ts` - Status indicators
- `src/game/ui/ui-types.ts` - Emoji mappings configuration

### Sprite Loading:
Sprites are loaded through `src/game/sprites/sprite-loader.ts` which preloads all images and provides a sprite cache for rendering functions.

### Rendering:
The `drawSprite()` function replaces `ctx.fillText()` calls for emoji rendering, supporting the same positioning and scaling used by the original emoji system.

## Technical Notes

- All sprites are 32x32 pixels with transparent backgrounds
- PNG format for best quality and transparency support
- Sprites use distinctive colors and shapes for clear recognition
- Fallback system in place for missing sprites (shows colored circle with letter)
- Compatible with existing floating effect animations (fade, rise up, etc.)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added games/tribe/public/assets/sprites/emojis/crown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added games/tribe/public/assets/sprites/emojis/family.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added games/tribe/public/assets/sprites/emojis/fire.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added games/tribe/public/assets/sprites/emojis/heart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added games/tribe/public/assets/sprites/emojis/meat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added games/tribe/public/assets/sprites/emojis/muscle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added games/tribe/public/assets/sprites/emojis/robot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added games/tribe/public/assets/sprites/emojis/shield.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added games/tribe/public/assets/sprites/emojis/skull.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added games/tribe/public/assets/sprites/emojis/star.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added games/tribe/public/assets/sprites/emojis/sun.png
Binary file added games/tribe/public/assets/sprites/emojis/trident.png
6 changes: 6 additions & 0 deletions games/tribe/src/components/game-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GameWorldState } from '../game/world-types';
import { renderGame } from '../game/render';
import { Vector2D } from '../game/utils/math-types';
import { PlayerActionHint } from '../game/ui/ui-types';
import { preloadSprites } from '../game/sprites/sprite-loader';

interface GameRenderProps {
canvasRef: React.RefObject<HTMLCanvasElement>;
Expand All @@ -26,6 +27,11 @@ export const GameRender: React.FC<GameRenderProps> = ({
if (!canvas) return;
ctxRef.current = canvas.getContext('2d');

// Preload sprites when component mounts
preloadSprites().catch(error => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot this wont' work for render.test.ts

also could you run the test and commit the rendered output?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the sprite loading issue for Node.js test environment. The preloadSprites() function now gracefully skips sprite loading when running in Node.js (like in render.test.ts) and falls back to Unicode text rendering. I've run the test and committed the updated render output. The tests all pass (31/31) and use the expected fallback behavior. See commit 2c37b98.

console.warn('Failed to preload some sprites, fallback rendering will be used:', error);
});

const handleResize = () => {
if (canvas && ctxRef.current && gameStateRef.current) {
canvas.width = window.innerWidth;
Expand Down
23 changes: 17 additions & 6 deletions games/tribe/src/game/render/render-effects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { VisualEffect, VisualEffectType } from '../visual-effects/visual-effect-types';
import { Vector2D } from '../utils/math-types';
import { drawSpriteWithEffect } from '../sprites/sprite-loader';

const EFFECT_BASE_RADIUS = 15;

Expand Down Expand Up @@ -87,12 +88,22 @@ function drawEmoji(ctx: CanvasRenderingContext2D, effect: VisualEffect, currentT
const opacity = 1 - progress;
const yOffset = -20 * progress; // Rise up

ctx.save();
ctx.globalAlpha = opacity;
ctx.font = '20px Arial';
ctx.textAlign = 'center';
ctx.fillText(emoji, effect.position.x, effect.position.y + yOffset);
ctx.restore();
// Use sprite-based rendering with fallback to text
const spriteRendered = drawSpriteWithEffect(
ctx,
emoji,
effect.position.x,
effect.position.y,
opacity,
yOffset,
20 // Size of the sprite for effects
);

// If sprite rendering failed, log for debugging but don't fallback
// The drawSpriteWithEffect function already handles fallback internally
if (!spriteRendered) {
console.debug(`Sprite rendering fallback used for emoji: ${emoji}`);
}
}

export function renderVisualEffect(ctx: CanvasRenderingContext2D, effect: VisualEffect, currentTime: number): void {
Expand Down
Binary file modified games/tribe/src/game/render/render-test-output.png
8 changes: 6 additions & 2 deletions games/tribe/src/game/render/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import * as path from 'path';
import { initGame } from '../index';
import { renderGame } from '../render';
import { GameWorldState } from '../world-types';
import { preloadSprites } from '../sprites/sprite-loader';

describe('Game Rendering', () => {
it('should render the game world to a canvas and save it', () => {
it('should render the game world to a canvas and save it', async () => {
// Preload sprites first (works in both browser and Node.js now)
await preloadSprites();

// Initialize the game state
const gameState: GameWorldState = initGame();

Expand Down Expand Up @@ -35,7 +39,7 @@ describe('Game Rendering', () => {
// Save the canvas buffer to a file
const buffer = canvas.toBuffer('image/png');
const outputPath = path.resolve(
'/Users/gtanczyk/src/www.gamedev.pl/games/tribe/src/game/render',
__dirname,
'render-test-output.png',
);
fs.writeFileSync(outputPath, new Uint8Array(buffer));
Expand Down
21 changes: 17 additions & 4 deletions games/tribe/src/game/render/ui/render-autopilot-indicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '../../world-consts';
import { GameWorldState } from '../../world-types';
import { findPlayerEntity } from '../../utils/world-utils';
import { drawSprite } from '../../sprites/sprite-loader';

function drawIndicator(
ctx: CanvasRenderingContext2D,
Expand Down Expand Up @@ -37,12 +38,24 @@ function drawIndicator(
const emoji = PLAYER_ACTION_EMOJIS[action];
const name = PLAYER_ACTION_NAMES[action];
if (emoji) {
ctx.font = `${PLAYER_ACTION_HINT_FONT_SIZE * 1.5}px "Press Start 2P", Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const textY = position.y - circleRadius - 25;
ctx.fillText(emoji, position.x, textY);
const emojiSize = PLAYER_ACTION_HINT_FONT_SIZE * 1.5;

// Try to render emoji as sprite
const spriteRendered = drawSprite(ctx, emoji, position.x, textY, emojiSize);

if (!spriteRendered) {
// Fallback to text rendering
ctx.font = `${emojiSize}px "Press Start 2P", Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(emoji, position.x, textY);
}

// Draw action name
ctx.font = `${PLAYER_ACTION_HINT_FONT_SIZE * 0.8}px "Press Start 2P", Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(name, position.x, textY + 25);
}

Expand Down
20 changes: 15 additions & 5 deletions games/tribe/src/game/render/ui/render-buttons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { AutopilotControls, GameWorldState } from '../../world-types.js';
import { ClickableUIButton, PlayerActionType, PLAYER_ACTION_EMOJIS, UIButtonActionType } from '../../ui/ui-types';
import { Rect2D, Vector2D } from '../../utils/math-types';
import { findPlayerEntity, getAvailablePlayerActions } from '../../utils/world-utils';
import { drawSprite } from '../../sprites/sprite-loader';

function drawButton(ctx: CanvasRenderingContext2D, button: ClickableUIButton, isHovered: boolean): void {
ctx.save();
Expand Down Expand Up @@ -85,11 +86,20 @@ function drawButton(ctx: CanvasRenderingContext2D, button: ClickableUIButton, is
ctx.fillStyle = button.isDisabled ? UI_BUTTON_DISABLED_TEXT_COLOR : button.textColor;

if (button.icon) {
// Render large icon in the center
ctx.font = `${height * 0.55}px "Press Start 2P", Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(button.icon, x + width / 2, y + height / 2);
// Try to render icon as sprite first
const iconSize = height * 0.55;
const iconX = x + width / 2;
const iconY = y + height / 2;

const spriteRendered = drawSprite(ctx, button.icon, iconX, iconY, iconSize);

if (!spriteRendered) {
// Fallback to text rendering
ctx.font = `${iconSize}px "Press Start 2P", Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(button.icon, iconX, iconY);
}

// Render small text in the bottom right corner
if (button.text) {
Expand Down
28 changes: 23 additions & 5 deletions games/tribe/src/game/render/ui/render-top-left-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ import { drawFoodBar, drawProgressBar } from './render-bars';
import { drawFamilyMemberBar, renderMiniatureCharacter } from './render-characters-ui';
import { EntityId } from '../../entities/entities-types';
import { Rect2D } from '../../utils/math-types';
import { drawSprite } from '../../sprites/sprite-loader';

/**
* Helper function to render status icons using sprites with fallback to text
*/
function renderStatusIcon(ctx: CanvasRenderingContext2D, emoji: string, x: number, y: number, size: number = UI_FONT_SIZE): boolean {
const spriteRendered = drawSprite(ctx, emoji, x + size / 2, y, size);
if (!spriteRendered) {
// Fallback to text rendering
ctx.save();
ctx.font = `${size}px Arial`;
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(emoji, x, y);
ctx.restore();
}
return spriteRendered;
}

export function renderTopLeftPanel(
ctx: CanvasRenderingContext2D,
Expand All @@ -47,7 +65,7 @@ export function renderTopLeftPanel(
const iconTextPadding = 25;
const barX = UI_PADDING + iconTextPadding;

ctx.fillText(timeEmoji, UI_PADDING, uiLineY + UI_BAR_HEIGHT / 2 + UI_FONT_SIZE / 3);
renderStatusIcon(ctx, timeEmoji, UI_PADDING, uiLineY + UI_BAR_HEIGHT / 2 + UI_FONT_SIZE / 3);
drawProgressBar(
ctx,
barX,
Expand Down Expand Up @@ -184,7 +202,7 @@ export function renderTopLeftPanel(
const emojiY = uiLineY + UI_FAMILY_MEMBER_ICON_SIZE / 2;
ctx.font = `${UI_FONT_SIZE}px "Press Start 2P", Arial`;
ctx.textBaseline = 'middle';
ctx.fillText(familyEmoji, UI_PADDING, emojiY);
renderStatusIcon(ctx, familyEmoji, UI_PADDING, emojiY);

const familyBarY = uiLineY + UI_FAMILY_MEMBER_ICON_SIZE / 2; // Center the single row

Expand All @@ -202,7 +220,7 @@ export function renderTopLeftPanel(

// Hitpoints Bar
const hpEmoji = UI_STATUS_EMOJIS[UIStatusType.Hitpoints];
ctx.fillText(hpEmoji, UI_PADDING, uiLineY + UI_BAR_HEIGHT / 2 + UI_FONT_SIZE / 3);
renderStatusIcon(ctx, hpEmoji, UI_PADDING, uiLineY + UI_BAR_HEIGHT / 2 + UI_FONT_SIZE / 3);
drawProgressBar(
ctx,
barX,
Expand All @@ -217,7 +235,7 @@ export function renderTopLeftPanel(

// Hunger Bar
const hungerEmoji = UI_STATUS_EMOJIS[UIStatusType.Hunger];
ctx.fillText(hungerEmoji, UI_PADDING, uiLineY + UI_BAR_HEIGHT / 2 + UI_FONT_SIZE / 3);
renderStatusIcon(ctx, hungerEmoji, UI_PADDING, uiLineY + UI_BAR_HEIGHT / 2 + UI_FONT_SIZE / 3);
drawProgressBar(
ctx,
barX,
Expand All @@ -239,7 +257,7 @@ export function renderTopLeftPanel(
// Food Bar
ctx.textBaseline = 'middle';
const foodEmoji = UI_STATUS_EMOJIS[UIStatusType.Food];
ctx.fillText(foodEmoji, UI_PADDING, uiLineY + UI_BERRY_ICON_SIZE / 2);
renderStatusIcon(ctx, foodEmoji, UI_PADDING, uiLineY + UI_BERRY_ICON_SIZE / 2);
drawFoodBar(
ctx,
barX, // Use the same X as other bars for alignment
Expand Down
36 changes: 27 additions & 9 deletions games/tribe/src/game/render/ui/render-tribe-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { ClickableUIButton, TribeInfo, UIButtonActionType } from '../../ui/ui-types';
import { renderMiniatureCharacter } from './render-characters-ui';
import { DiplomacyStatus, GameWorldState } from '../../world-types';
import { drawSprite } from '../../sprites/sprite-loader';

export function renderTribeList(
ctx: CanvasRenderingContext2D,
Expand Down Expand Up @@ -68,10 +69,20 @@ export function renderTribeList(
let currentX = startX + UI_TRIBE_LIST_PADDING;

// --- Badge --
ctx.font = `${UI_TRIBE_LIST_BADGE_SIZE}px "Press Start 2P", Arial`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'left';
ctx.fillText(tribe.tribeBadge, currentX, centerY - UI_TRIBE_LIST_BADGE_SIZE / 4);
const badgeX = currentX + UI_TRIBE_LIST_BADGE_SIZE / 2;
const badgeY = centerY;

// Use sprite-based rendering for tribe badge
const badgeRendered = drawSprite(ctx, tribe.tribeBadge, badgeX, badgeY, UI_TRIBE_LIST_BADGE_SIZE);

if (!badgeRendered) {
// Fallback: original text rendering
ctx.font = `${UI_TRIBE_LIST_BADGE_SIZE}px "Press Start 2P", Arial`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'left';
ctx.fillText(tribe.tribeBadge, currentX, centerY - UI_TRIBE_LIST_BADGE_SIZE / 4);
}

currentX += UI_TRIBE_LIST_BADGE_SIZE + UI_TRIBE_LIST_PADDING;

// --- Diplomacy Status & Button ---
Expand Down Expand Up @@ -108,11 +119,18 @@ export function renderTribeList(
ctx.globalAlpha = 1;
}

// Draw the icon
ctx.font = `${UI_TRIBE_LIST_BADGE_SIZE * 0.8}px "Press Start 2P", Arial`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(diplomacyIcon, currentX + UI_TRIBE_LIST_BADGE_SIZE / 2, centerY);
// Draw the icon using sprite
const iconX = currentX + UI_TRIBE_LIST_BADGE_SIZE / 2;
const iconY = centerY;
const iconRendered = drawSprite(ctx, diplomacyIcon, iconX, iconY, UI_TRIBE_LIST_BADGE_SIZE * 0.8);

if (!iconRendered) {
// Fallback: original text rendering
ctx.font = `${UI_TRIBE_LIST_BADGE_SIZE * 0.8}px "Press Start 2P", Arial`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(diplomacyIcon, currentX + UI_TRIBE_LIST_BADGE_SIZE / 2, centerY);
}
}
currentX += UI_TRIBE_LIST_BADGE_SIZE + UI_TRIBE_LIST_PADDING;

Expand Down
Loading
Loading