source-code/
snakey-extension
Public
codeCodeinfoIssues 0call_splitPull Requestsplay_circleActions
snakey-extension/src/game/core/Snake.ts
typescript169 lines6.8 KB
import Phaser from 'phaser';
import { GRID_SIZE, CANVAS_WIDTH, CANVAS_HEIGHT } from '../constants';

/**
 * Snake represents the core player controller model and animation controller.
 * It manages segment positioning, logical coordinate arrays, input-driven directional shifts, 
 * self-collision checks, tween animations for fluid movement, and wall collision rules.
 */
export class Snake {
  private scene: Phaser.Scene;
  private segments: Phaser.GameObjects.Image[] = [];
  private logicalPositions: Phaser.Math.Vector2[] = [];
  public direction: Phaser.Math.Vector2 = new Phaser.Math.Vector2(1, 0);
  public nextDirection: Phaser.Math.Vector2 = new Phaser.Math.Vector2(1, 0);
  public stepSize: number = GRID_SIZE;
  private readonly TEXTURE_SCALE = 0.2;

  constructor(scene: Phaser.Scene) {
    this.scene = scene;
  }

  /**
   * Spawns the initial 3-segment snake. Defaults to center of Phaser canvas.
   */
  public create(startX?: number, startY?: number) {
    const offset = GRID_SIZE / 2;
    const finalStartX = startX !== undefined ? startX : (Math.floor(CANVAS_WIDTH / 2 / GRID_SIZE) * GRID_SIZE + offset);
    const finalStartY = startY !== undefined ? startY : (Math.floor(CANVAS_HEIGHT / 2 / GRID_SIZE) * GRID_SIZE + offset);

    this.segments = [];
    this.logicalPositions = [];

    // Initialize 3-segment logical position vectors (head, body, tail)
    this.logicalPositions.push(new Phaser.Math.Vector2(finalStartX, finalStartY));
    this.logicalPositions.push(new Phaser.Math.Vector2(finalStartX - GRID_SIZE, finalStartY));
    this.logicalPositions.push(new Phaser.Math.Vector2(finalStartX - GRID_SIZE * 2, finalStartY));

    // Instantiate game textures for the corresponding segments
    this.segments.push(this.scene.add.image(finalStartX, finalStartY, 'snake-head').setOrigin(0.5).setScale(this.TEXTURE_SCALE));
    this.segments.push(this.scene.add.image(finalStartX - GRID_SIZE, finalStartY, 'snake-body').setOrigin(0.5).setScale(this.TEXTURE_SCALE));
    this.segments.push(this.scene.add.image(finalStartX - GRID_SIZE * 2, finalStartY, 'snake-body').setOrigin(0.5).setScale(this.TEXTURE_SCALE));
    
    this.direction.set(1, 0);
    this.nextDirection.set(1, 0);
    this.stepSize = GRID_SIZE;
  }

  /**
   * Returns list of segments graphics.
   */
  public getSegments() {
    return this.segments;
  }
  
  /**
   * Returns the head segment graphics.
   */
  public getHead() {
    return this.segments[0];
  }

  /**
   * Logic routine executed on each game tick to slide the snake forward.
   * Calculates new coordinates, evaluates wall boundaries, checks self-collision, 
   * shifts logical position vectors, runs linear tweens, and rotates the head texture.
   * 
   * @param duration Tick interval duration in ms (used to synchronize tween speeds)
   * @param isEscaped Set to true if the snake has crawled out of the canvas onto the browser viewport
   */
  public move(duration: number, isEscaped: boolean = false): { dead: boolean, hitWall: boolean, newX: number, newY: number, tailOldX: number, tailOldY: number } {
    this.direction.copy(this.nextDirection);
    
    const headPos = this.logicalPositions[0];
    let newX = headPos.x + this.direction.x * this.stepSize;
    let newY = headPos.y + this.direction.y * this.stepSize;

    let tailOldPos = this.logicalPositions[this.logicalPositions.length - 1];
    let tailOldX = tailOldPos.x;
    let tailOldY = tailOldPos.y;

    // Define coordinate boundary limits
    let minX = 0;
    let maxX = CANVAS_WIDTH;
    let minY = 0;
    let maxY = CANVAS_HEIGHT;

    if (isEscaped) {
      // Use document scroll dimensions rather than window viewport boundaries during the escape phase.
      // This is crucial: checking viewport boundaries would kill the snake immediately if the player 
      // scrolled the page (shifting viewport limits relative to coordinates).
      // Checking document bounds lets the player scroll freely to follow the snake, 
      // and boundaries only trigger a collision when the snake leaves the actual page borders.
      minX = 0;
      maxX = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth, window.innerWidth);
      minY = 0;
      maxY = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, window.innerHeight);
    }

    if (newX >= maxX || newX < minX || newY >= maxY || newY < minY) {
      return { dead: true, hitWall: true, newX, newY, tailOldX, tailOldY };
    }

    // Self collision check.
    // Uses a slightly lenient distance threshold (0.4 * stepSize) to account for visual overlap 
    // during direction shift animations, making control feel responsive.
    for (let i = 1; i < this.logicalPositions.length - 1; i++) {
      const pos = this.logicalPositions[i];
      const dist = Phaser.Math.Distance.Between(newX, newY, pos.x, pos.y);
      if (dist < this.stepSize * 0.4) {
        return { dead: true, hitWall: false, newX, newY, tailOldX, tailOldY };
      }
    }

    // Update logical position vector sequence backwards (shift values down the queue)
    for (let i = this.logicalPositions.length - 1; i > 0; i--) {
      this.logicalPositions[i].copy(this.logicalPositions[i - 1]);
    }
    
    // Update head logical coordinates
    this.logicalPositions[0].set(newX, newY);

    // Interpolate sprite graphics to match the updated logical coordinates.
    // Adds a smooth linear tween to make movement appear continuous rather than cell-jumping.
    for (let i = 0; i < this.segments.length; i++) {
      const target = this.logicalPositions[i];
      const sprite = this.segments[i];
      
      const dx = Math.abs(sprite.x - target.x);
      const dy = Math.abs(sprite.y - target.y);

      // If the delta is too large (like during escape coordinate shifts), snap coordinates instantly
      if (dx > this.stepSize * 1.5 || dy > this.stepSize * 1.5) {
        sprite.setPosition(target.x, target.y);
      } else {
        this.scene.tweens.add({
          targets: sprite,
          x: target.x,
          y: target.y,
          duration: duration,
          ease: 'Linear'
        });
      }
    }
    
    // Rotate head texture to face the current heading direction
    const head = this.segments[0];
    if (this.direction.x === 1) head.setAngle(0);
    else if (this.direction.x === -1) head.setAngle(180);
    else if (this.direction.y === 1) head.setAngle(90);
    else if (this.direction.y === -1) head.setAngle(-90);

    return { dead: false, newX, newY, tailOldX, tailOldY };
  }

  /**
   * Appends a new segment to the end of the snake tail.
   */
  public grow(tailOldX: number, tailOldY: number) {
    this.logicalPositions.push(new Phaser.Math.Vector2(tailOldX, tailOldY));
    const newSegment = this.scene.add.image(tailOldX, tailOldY, 'snake-body').setOrigin(0.5);
    newSegment.setScale(this.TEXTURE_SCALE);
    this.segments.push(newSegment);
  }

  public fatten() {
    // Disabled as requested
  }
}

About

Snakey Browser Extension is a cross-browser extension built using Manifest V3 that injects a playable Phaser 3 game onto any active tab. It parses the page DOM, turns HTML elements into target coordinates, and features custom chomp/collapse animations. It supports both Chromium (background service worker) and Firefox (background scripts), implements a Canvas-based rendering fallback to bypass strict WebGL CORS limitations, and applies fully container-scoped vanilla CSS overrides to prevent style bleeding on host pages.

Browser ExtensionChrome MV3Firefox MV3PhaserReactTypeScriptVite

Contributors

1