MOODUL 08b · TASE 3 — KESKMINE

🎨 UI/UX Mängu Disain

HUD disain, menüüsüsteemid, juurdepääsetavus, mängukogemuse disain — kuidas luua intuitiivset ja nauditavat mängukogemust.

⏱️ ~4-5 tundi 📝 5 harjutust 🟠 Keskmine

📊 Samm 1 — HUD (Heads-Up Display)

HUD näitab olulist infot ilma mängu katkestamata: tervis, skoor, relv, minikaart.

hud.ts — HUD süsteem
class HUD {
  private elements: HUDElement[] = [];

  add(element: HUDElement): void {
    this.elements.push(element);
  }

  draw(ctx: CanvasRenderingContext2D): void {
    ctx.save();
    ctx.setTransform(1, 0, 0, 1, 0, 0); // Ignoreeri kaamerat
    for (const el of this.elements) {
      el.draw(ctx);
    }
    ctx.restore();
  }
}

// Tervisriba
class HealthBar implements HUDElement {
  constructor(
    private x: number, private y: number,
    private width: number, private height: number,
    private player: { health: number; maxHealth: number }
  ) {}

  draw(ctx: CanvasRenderingContext2D): void {
    const pct = this.player.health / this.player.maxHealth;

    // Taust
    ctx.fillStyle = '#1f2937';
    ctx.fillRect(this.x, this.y, this.width, this.height);

    // Tervis (värvivahetus)
    const color = pct > 0.6 ? '#22c55e' : pct > 0.3 ? '#eab308' : '#ef4444';
    ctx.fillStyle = color;
    ctx.fillRect(this.x, this.y, this.width * pct, this.height);

    // Ääris
    ctx.strokeStyle = '#374151';
    ctx.lineWidth = 2;
    ctx.strokeRect(this.x, this.y, this.width, this.height);

    // Tekst
    ctx.fillStyle = '#ffffff';
    ctx.font = '12px Inter';
    ctx.textAlign = 'center';
    ctx.fillText(
      `${this.player.health}/${this.player.maxHealth}`,
      this.x + this.width / 2,
      this.y + this.height / 2 + 4
    );
  }
}

// Skoor animatsiooniga
class ScoreDisplay implements HUDElement {
  private displayScore = 0;  // Animeeritud väärtus
  private targetScore = 0;

  constructor(private x: number, private y: number) {}

  setScore(score: number): void {
    this.targetScore = score;
  }

  draw(ctx: CanvasRenderingContext2D): void {
    // Sujuv skoori tõus
    if (this.displayScore < this.targetScore) {
      this.displayScore += Math.ceil((this.targetScore - this.displayScore) * 0.1);
    }

    ctx.fillStyle = '#ffffff';
    ctx.font = 'bold 24px Inter';
    ctx.textAlign = 'right';
    ctx.fillText(`⭐ ${this.displayScore}`, this.x, this.y);
  }
}

// Minikaart
class Minimap implements HUDElement {
  constructor(
    private x: number, private y: number,
    private size: number,
    private world: { width: number; height: number },
    private entities: { x: number; y: number; type: string }[]
  ) {}

  draw(ctx: CanvasRenderingContext2D): void {
    const scale = this.size / Math.max(this.world.width, this.world.height);

    // Taust
    ctx.fillStyle = 'rgba(15, 23, 42, 0.8)';
    ctx.fillRect(this.x, this.y, this.size, this.size);
    ctx.strokeStyle = '#475569';
    ctx.strokeRect(this.x, this.y, this.size, this.size);

    // Entiteedid
    for (const e of this.entities) {
      const mx = this.x + e.x * scale;
      const my = this.y + e.y * scale;
      ctx.fillStyle = e.type === 'player' ? '#22c55e' :
                      e.type === 'enemy' ? '#ef4444' : '#3b82f6';
      ctx.fillRect(mx - 2, my - 2, 4, 4);
    }
  }
}

📋 Samm 2 — Menüüsüsteem

menu.ts — Navigeeritav menüü
interface MenuItem {
  label: string;
  action: () => void;
  disabled?: boolean;
}

class Menu {
  private items: MenuItem[] = [];
  private selectedIndex = 0;

  constructor(private x: number, private y: number) {}

  addItem(item: MenuItem): void {
    this.items.push(item);
  }

  up(): void {
    do {
      this.selectedIndex = (this.selectedIndex - 1 + this.items.length) % this.items.length;
    } while (this.items[this.selectedIndex].disabled);
  }

  down(): void {
    do {
      this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
    } while (this.items[this.selectedIndex].disabled);
  }

  select(): void {
    const item = this.items[this.selectedIndex];
    if (item && !item.disabled) item.action();
  }

  draw(ctx: CanvasRenderingContext2D): void {
    const lineHeight = 50;
    const padding = 20;

    for (let i = 0; i < this.items.length; i++) {
      const item = this.items[i];
      const y = this.y + i * lineHeight;
      const isSelected = i === this.selectedIndex;

      // Valitud elemendi taust
      if (isSelected) {
        ctx.fillStyle = 'rgba(34, 197, 94, 0.15)';
        ctx.fillRect(this.x - padding, y - 15, 300, lineHeight - 5);
        ctx.fillStyle = '#22c55e';
        ctx.fillText('▶', this.x - padding + 5, y + 8);
      }

      // Tekst
      ctx.font = isSelected ? 'bold 22px Inter' : '20px Inter';
      ctx.fillStyle = item.disabled ? '#4b5563'
                    : isSelected ? '#22c55e' : '#d1d5db';
      ctx.textAlign = 'left';
      ctx.fillText(item.label, this.x, y + 8);
    }
  }
}

// const mainMenu = new Menu(300, 250);
// mainMenu.addItem({ label: 'Alusta Mängu', action: () => startGame() });
// mainMenu.addItem({ label: 'Seaded', action: () => openSettings() });
// mainMenu.addItem({ label: 'Paremusjärjestus', action: () => showLeaderboard() });
// mainMenu.addItem({ label: 'Välju', action: () => quitGame() });

💬 Samm 3 — Dialoogisüsteem

dialog.ts — Typewriter efektiga dialoog
interface DialogLine {
  speaker: string;
  text: string;
  portrait?: string;
}

class DialogSystem {
  private lines: DialogLine[] = [];
  private currentLine = 0;
  private displayedText = '';
  private charIndex = 0;
  private timer = 0;
  private charSpeed = 30; // ms tähemärgi kohta
  public active = false;

  show(lines: DialogLine[]): void {
    this.lines = lines;
    this.currentLine = 0;
    this.displayedText = '';
    this.charIndex = 0;
    this.active = true;
  }

  advance(): void {
    const line = this.lines[this.currentLine];
    if (this.charIndex < line.text.length) {
      // Näita kogu tekst kohe
      this.displayedText = line.text;
      this.charIndex = line.text.length;
    } else {
      // Järgmine rida
      this.currentLine++;
      if (this.currentLine >= this.lines.length) {
        this.active = false;
        return;
      }
      this.displayedText = '';
      this.charIndex = 0;
    }
  }

  update(dt: number): void {
    if (!this.active) return;
    const line = this.lines[this.currentLine];
    if (this.charIndex < line.text.length) {
      this.timer += dt * 1000;
      while (this.timer >= this.charSpeed && this.charIndex < line.text.length) {
        this.displayedText += line.text[this.charIndex];
        this.charIndex++;
        this.timer -= this.charSpeed;
      }
    }
  }

  draw(ctx: CanvasRenderingContext2D): void {
    if (!this.active) return;
    const line = this.lines[this.currentLine];

    // Dialoogikast
    const y = ctx.canvas.height - 150;
    ctx.fillStyle = 'rgba(15, 23, 42, 0.95)';
    ctx.fillRect(20, y, ctx.canvas.width - 40, 130);
    ctx.strokeStyle = '#3b82f6';
    ctx.lineWidth = 2;
    ctx.strokeRect(20, y, ctx.canvas.width - 40, 130);

    // Kõneleja nimi
    ctx.fillStyle = '#3b82f6';
    ctx.font = 'bold 16px Inter';
    ctx.fillText(line.speaker, 40, y + 25);

    // Tekst
    ctx.fillStyle = '#e2e8f0';
    ctx.font = '14px Inter';
    this.wrapText(ctx, this.displayedText, 40, y + 50, ctx.canvas.width - 80, 20);

    // "Jätka" viide
    if (this.charIndex >= line.text.length) {
      ctx.fillStyle = '#64748b';
      ctx.font = '12px Inter';
      ctx.fillText('▼ Vajuta SPACE', ctx.canvas.width - 140, y + 115);
    }
  }

  private wrapText(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number, lineHeight: number): void {
    const words = text.split(' ');
    let line = '';
    let lineY = y;
    for (const word of words) {
      const test = line + word + ' ';
      if (ctx.measureText(test).width > maxWidth) {
        ctx.fillText(line, x, lineY);
        line = word + ' ';
        lineY += lineHeight;
      } else {
        line = test;
      }
    }
    ctx.fillText(line, x, lineY);
  }
}

♿ Samm 4 — Juurdepääsetavus

ℹ️

Mängude juurdepääsetavuse põhimõtted:

  • Värvid: Ära kasuta ainult värvi info edastamiseks — lisa kujundid/ikoonid
  • Tekst: Minimaalselt 16px, hea kontrastisuhe (4.5:1)
  • Sisend: Toeta nii klaviatuuri kui hiirt, konfigureeritavad klahvid
  • Tempo: Pause vajaduse korral, raskusastme valik
  • Subtiitrid: Igale olulisele helile teksti alternatiiv
  • Värvipimeda režiim: Alternatiivne värviskeem
settings.ts — Juurdepääsetavuse seaded
interface AccessibilitySettings {
  screenShake: boolean;
  flashEffects: boolean;
  subtitles: boolean;
  colorblindMode: 'none' | 'protanopia' | 'deuteranopia' | 'tritanopia';
  textSize: 'small' | 'medium' | 'large';
  autoAim: boolean;
  difficulty: 'easy' | 'normal' | 'hard';
}

const defaultSettings: AccessibilitySettings = {
  screenShake: true,
  flashEffects: true,
  subtitles: true,
  colorblindMode: 'none',
  textSize: 'medium',
  autoAim: false,
  difficulty: 'normal',
};

// Värvipimeda palettid
const colorSchemes = {
  none: { player: '#22c55e', enemy: '#ef4444', item: '#3b82f6' },
  protanopia: { player: '#2563eb', enemy: '#f59e0b', item: '#8b5cf6' },
  deuteranopia: { player: '#06b6d4', enemy: '#f97316', item: '#a855f7' },
  tritanopia: { player: '#ec4899', enemy: '#14b8a6', item: '#6366f1' },
};

📝 Harjutused

  • HUD süsteemi ehitamine

    Loo HUD: tervisriba (animeeritud), skoor (sujuv tõus), relvaindikaator, minikaart. Kogu HUD peab ignoreerima kaamerat ja olema alati nähtav.

  • Menüüsüsteem

    Loo navigeeritav menüü: Main Menu, Settings, Controls. Toeta klaviatuuri (üles/alla/enter) JA hiirekliki. Lisa animeeritud valik ja hover efektid.

  • Dialoogisüsteem

    Ehita typewriter-efektiga dialoogisüsteem. Lisa kõneleja portree, erinevad kõnetempod, teksti murdmine. SPACE jätkamiseks, kogu teksti vahele jätmine.

  • Juurdepääsetavuse seaded

    Lisa mängu juurdepääsetavuse menüü: värvipimeda režiim (3 varianti), teksti suurus, ekraani raputuse toggle, subtiitrid, raskusaste. Salvesta localStorage'i.

  • UI animatsioonid ja juice

    Lisa "game juice": skoori lisandumisel popup tekst (+10!), tervisriba raputus tabamuse korral, menüüelemendid libisevad sisse, nupu hover skaleerimine, ekraani freeze-frame.

← Moodul 08 Moodul 09: Multiplayer →