From f194886ed93e53a0917388a70f2596876e97466f Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sat, 21 Feb 2026 10:24:03 -0700 Subject: [PATCH] Add alien variants, homing missiles, asteroid speed scaling, CRT pipeline, and custom reticle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New alien types (basic, spread, missile) with distinct speed, fire‑rate, explosion colors and textures. - Implement spread‑shot pattern and homing‑missile behavior with steering logic. - Asteroid constructor now accepts a speed multiplier; level speed increases each level. - Extend Bullet to support homing missiles (new texture, lifetime, turn rate, steering). - Introduce CRTPipeline post‑FX (barrel distortion, scanlines, vignette) and register it in the game config. - Apply CRT pipeline to GameScene and MenuScene. - Add custom reticle cursor graphics (rotating outer/inner layers). - Update BootScene to generate textures for new alien types and reticle. --- js/entities/AlienShip.js | 91 ++++++++++++++++++++++++++++--------- js/entities/Asteroid.js | 6 +-- js/entities/Bullet.js | 73 +++++++++++++++++++++++------ js/main.js | 10 ++-- js/pipelines/CRTPipeline.js | 55 ++++++++++++++++++++++ js/scenes/BootScene.js | 66 +++++++++++++++++++++++++++ js/scenes/GameScene.js | 67 +++++++++++++++++++++++---- js/scenes/MenuScene.js | 5 ++ 8 files changed, 323 insertions(+), 50 deletions(-) create mode 100644 js/pipelines/CRTPipeline.js diff --git a/js/entities/AlienShip.js b/js/entities/AlienShip.js index 6c74afd..e56d20b 100644 --- a/js/entities/AlienShip.js +++ b/js/entities/AlienShip.js @@ -1,18 +1,52 @@ -const ALIEN_SPEED = 130; // pixels/sec -const ALIEN_FIRE_RATE = 2500; // ms between shots +// Speed and fire-rate constants per alien type +const SPEED = { + basic: 130, + spread: 155, + missile: 110 +}; +const FIRE_RATE = { + basic: 2500, + spread: 3200, + missile: 4000 +}; +// Explosion ring/flash color per type +const EXPLODE_COLOR = { + basic: 0x00ffff, + spread: 0xff4400, + missile: 0xcc00ff +}; export default class AlienShip { - constructor(scene, x, y, group) { + /** + * @param {Phaser.Scene} scene + * @param {number} x + * @param {number} y + * @param {Phaser.Physics.Arcade.Group} group + * @param {'basic'|'spread'|'missile'} type + */ + constructor(scene, x, y, group, type = 'basic') { this.scene = scene; this.alive = true; + this.type = type; - this.sprite = scene.physics.add.sprite(x, y, 'alien'); + // Select texture by type + const texKey = type === 'spread' ? 'alien_spread' + : type === 'missile' ? 'alien_missile' + : 'alien'; + + this.sprite = scene.physics.add.sprite(x, y, texKey); group.add(this.sprite); this.sprite.gameEntity = this; this.sprite.body.setAllowGravity(false); - // Circular hitbox: texture is 64x48, use radius 20 centered at (32,24) - this.sprite.body.setCircle(20, 12, 4); + // Circular hitbox — missile alien is slightly larger + if (type === 'missile') { + this.sprite.body.setCircle(22, 18, 8); + } else if (type === 'spread') { + this.sprite.body.setCircle(20, 16, 7); + } else { + this.sprite.body.setCircle(20, 12, 4); + } this.lastFired = 0; } @@ -25,25 +59,44 @@ export default class AlienShip { const dx = player.sprite.x - this.sprite.x; const dy = player.sprite.y - this.sprite.y; const dist = Math.sqrt(dx * dx + dy * dy); + const speed = SPEED[this.type]; if (dist > 5) { this.sprite.setVelocity( - (dx / dist) * ALIEN_SPEED, - (dy / dist) * ALIEN_SPEED + (dx / dist) * speed, + (dy / dist) * speed ); } - // Fire at player periodically - if (time - this.lastFired > ALIEN_FIRE_RATE) { + // Fire periodically + if (time - this.lastFired > FIRE_RATE[this.type]) { this.lastFired = time; const angle = Phaser.Math.RadToDeg(Math.atan2(dy, dx)); - this.scene.spawnAlienBullet(this.sprite.x, this.sprite.y, angle, this); + + if (this.type === 'spread') { + this._fireSpread(angle); + } else if (this.type === 'missile') { + this.scene.spawnHomingMissile(this.sprite.x, this.sprite.y, this); + } else { + this.scene.spawnAlienBullet(this.sprite.x, this.sprite.y, angle, this); + } } // Wrap around screen this.scene.physics.world.wrap(this.sprite, 40); } + _fireSpread(centerAngle) { + // 5 bullets fanning out ±40° around the target direction + [-40, -20, 0, 20, 40].forEach(offset => { + this.scene.spawnAlienBullet( + this.sprite.x, this.sprite.y, + centerAngle + offset, + this + ); + }); + } + warpOut() { if (!this.alive) return; this.alive = false; @@ -76,22 +129,17 @@ export default class AlienShip { onUpdate: () => { const t = counter.t; gfx.clear(); - // Ring expands 0→38px over first 30%, then collapses 38→0px const r = t < 0.3 ? (t / 0.3) * 38 : ((1 - t) / 0.7) * 38; if (r < 1) return; const a = Math.max(0, 1 - t * 0.7); - // Dark singularity core gfx.fillStyle(0x000000, a); gfx.fillCircle(x, y, r * 0.55); - // Outer cyan ring gfx.lineStyle(3, 0x00ffff, a); gfx.strokeCircle(x, y, r); - // Inner white ring gfx.lineStyle(1, 0xffffff, a * 0.6); gfx.strokeCircle(x, y, r * 0.55); - // Rotating distortion spokes const off = t * Math.PI * 6; for (let i = 0; i < 6; i++) { const ang = (i / 6) * Math.PI * 2 + off; @@ -114,14 +162,15 @@ export default class AlienShip { const scene = this.scene; const x = this.sprite.x; const y = this.sprite.y; + const color = EXPLODE_COLOR[this.type]; - // Brief cyan flash before implosion - this.sprite.setTint(0x00ffff); + // Brief flash before implosion + this.sprite.setTint(color); scene.time.delayedCall(60, () => { if (this.sprite && this.sprite.active) this.sprite.clearTint(); }); - // Phase 1 (0–400ms): sprite collapses and spins 360° + // Phase 1: sprite collapses and spins scene.tweens.add({ targets: this.sprite, scaleX: 0, @@ -134,7 +183,7 @@ export default class AlienShip { } }); - // Phase 2 (200–700ms): cyan shockwave ring expands and fades + // Phase 2: expanding shockwave ring scene.time.delayedCall(200, () => { const gfx = scene.add.graphics(); gfx.setDepth(5); @@ -148,7 +197,7 @@ export default class AlienShip { gfx.clear(); const radius = t * 120; if (radius < 1) return; - gfx.lineStyle(3, 0x00ffff, Math.max(0, 1 - t)); + gfx.lineStyle(3, color, Math.max(0, 1 - t)); gfx.strokeCircle(x, y, radius); }, onComplete: () => gfx.destroy() diff --git a/js/entities/Asteroid.js b/js/entities/Asteroid.js index 25acd5f..278d253 100644 --- a/js/entities/Asteroid.js +++ b/js/entities/Asteroid.js @@ -6,7 +6,7 @@ export const ASTEROID_CONFIG = { }; export default class Asteroid { - constructor(scene, x, y, size, group) { + constructor(scene, x, y, size, group, speedMultiplier = 1) { this.scene = scene; this.size = size; this.alive = true; @@ -21,8 +21,8 @@ export default class Asteroid { const offset = cfg.texSize / 2 - cfg.radius; this.sprite.body.setCircle(cfg.radius, offset, offset); - // Random velocity and slow spin - const speed = cfg.minSpeed + Math.random() * (cfg.maxSpeed - cfg.minSpeed); + // Random velocity and slow spin; speedMultiplier scales up per level + const speed = (cfg.minSpeed + Math.random() * (cfg.maxSpeed - cfg.minSpeed)) * speedMultiplier; const angle = Math.random() * Math.PI * 2; this.sprite.setVelocity(Math.cos(angle) * speed, Math.sin(angle) * speed); this.sprite.setAngularVelocity((Math.random() - 0.5) * 60); diff --git a/js/entities/Bullet.js b/js/entities/Bullet.js index 0082fbd..5ac168c 100644 --- a/js/entities/Bullet.js +++ b/js/entities/Bullet.js @@ -1,49 +1,94 @@ const PLAYER_BULLET_SPEED = 700; // pixels/sec -const ALIEN_BULLET_SPEED = 350; // pixels/sec – noticeably slower than player shots -const BULLET_LIFETIME = 1800; // ms +const ALIEN_BULLET_SPEED = 350; // pixels/sec +const MISSILE_SPEED = 180; // pixels/sec — slow enough for the player to outrun +const MISSILE_TURN_RATE = 120; // degrees/sec max steering + +const BULLET_LIFETIME = 1800; // ms +const MISSILE_LIFETIME = 5000; // ms export default class Bullet { constructor(scene, x, y, angle, owner, group, ownerEntity = null) { - this.scene = scene; - this.owner = owner; // 'player' or 'alien' - this.ownerEntity = ownerEntity; // AlienShip instance that fired this bullet, or null - this.alive = true; + this.scene = scene; + this.owner = owner; // 'player', 'alien', or 'missile' + this.ownerEntity = ownerEntity; + this.alive = true; + this.isHoming = (owner === 'missile'); + + const textureKey = owner === 'player' ? 'bullet' + : owner === 'missile' ? 'homing_missile' + : 'alien_bullet'; - const textureKey = owner === 'player' ? 'bullet' : 'alien_bullet'; this.sprite = scene.physics.add.sprite(x, y, textureKey); group.add(this.sprite); this.sprite.gameEntity = this; this.sprite.body.setAllowGravity(false); - const speed = owner === 'player' ? PLAYER_BULLET_SPEED : ALIEN_BULLET_SPEED; + const speed = owner === 'player' ? PLAYER_BULLET_SPEED + : owner === 'missile' ? MISSILE_SPEED + : ALIEN_BULLET_SPEED; + const rad = Phaser.Math.DegToRad(angle); - this.sprite.setVelocity( - Math.cos(rad) * speed, - Math.sin(rad) * speed - ); + this.sprite.setVelocity(Math.cos(rad) * speed, Math.sin(rad) * speed); + + // Track current heading for homing missiles; rotate sprite to face direction of travel + this.currentAngleDeg = angle; + if (this.isHoming) { + this.sprite.setAngle(angle); + } this.createdAt = scene.time.now; + this.lifetime = this.isHoming ? MISSILE_LIFETIME : BULLET_LIFETIME; } update(time) { if (!this.alive) return false; // Expire by lifetime - if (time - this.createdAt > BULLET_LIFETIME) { + if (time - this.createdAt > this.lifetime) { this.destroy(); return false; } // Expire if far off-screen const { x, y } = this.sprite; - if (x < -60 || x > 1660 || y < -60 || y > 960) { + if (x < -80 || x > 1680 || y < -80 || y > 980) { this.destroy(); return false; } + // Homing steering — updates velocity and rotation every frame + if (this.isHoming) { + this._steerTowardPlayer(); + } + return true; } + _steerTowardPlayer() { + const player = this.scene.player; + if (!player || !player.alive || !player.sprite.active) return; + + // Cap dt to avoid huge first-frame jumps + const dt = Math.min(this.scene.game.loop.delta / 1000, 0.1); + + const targetAngle = Phaser.Math.RadToDeg( + Math.atan2( + player.sprite.y - this.sprite.y, + player.sprite.x - this.sprite.x + ) + ); + + // Rotate toward target at limited turn rate + const maxTurn = MISSILE_TURN_RATE * dt; + const diff = Phaser.Math.Angle.ShortestBetween(this.currentAngleDeg, targetAngle); + this.currentAngleDeg += Phaser.Math.Clamp(diff, -maxTurn, maxTurn); + + // Apply updated velocity and rotate sprite to face direction of travel + const rad = Phaser.Math.DegToRad(this.currentAngleDeg); + this.sprite.setVelocity(Math.cos(rad) * MISSILE_SPEED, Math.sin(rad) * MISSILE_SPEED); + this.sprite.setAngle(this.currentAngleDeg); + } + destroy() { if (!this.alive) return; this.alive = false; diff --git a/js/main.js b/js/main.js index 72c3418..e95de8a 100644 --- a/js/main.js +++ b/js/main.js @@ -1,12 +1,14 @@ -import BootScene from './scenes/BootScene.js'; -import MenuScene from './scenes/MenuScene.js'; -import GameScene from './scenes/GameScene.js'; +import BootScene from './scenes/BootScene.js'; +import MenuScene from './scenes/MenuScene.js'; +import GameScene from './scenes/GameScene.js'; +import CRTPipeline from './pipelines/CRTPipeline.js'; const config = { - type: Phaser.AUTO, + type: Phaser.WEBGL, // Required for PostFXPipeline (CRT shader) width: 1600, height: 900, backgroundColor: '#000011', + pipeline: { CRTPipeline }, // Registers the pipeline by class name physics: { default: 'arcade', arcade: { diff --git a/js/pipelines/CRTPipeline.js b/js/pipelines/CRTPipeline.js new file mode 100644 index 0000000..7d42330 --- /dev/null +++ b/js/pipelines/CRTPipeline.js @@ -0,0 +1,55 @@ +// CRTPipeline – PostFX pipeline that simulates a classic CRT monitor: +// - Barrel distortion (curved screen edges) +// - Scanlines (every other row darkened) +// - Vignette (edge darkening) + +const fragShader = ` + precision mediump float; + + uniform sampler2D uMainSampler; + uniform vec2 uResolution; + + varying vec2 outTexCoord; + + void main (void) { + // --- Screen curvature (barrel distortion) --- + // Remap UV from [0,1] to centred [-0.5,0.5], distort, remap back + vec2 uv = outTexCoord; + vec2 dc = uv - 0.5; + float strength = 0.18; + uv = uv + dc * dot(dc, dc) * strength; + + // Pixels pushed outside [0,1] become black (the CRT bezel) + if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { + gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + + // --- Base colour --- + vec4 color = texture2D(uMainSampler, uv); + + // --- Scanlines --- + // Every other screen-pixel row is darkened by ~28% + float lineY = uv.y * uResolution.y; + float scanline = mod(floor(lineY), 2.0); + color.rgb *= mix(1.0, 0.72, scanline); + + // --- Vignette --- + float vig = 1.0 - dot(dc * 1.8, dc * 1.8); + vig = clamp(vig, 0.0, 1.0); + vig = pow(vig, 0.45); + color.rgb *= vig; + + gl_FragColor = color; + } +`; + +export default class CRTPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline { + constructor(game) { + super({ game, name: 'CRTPipeline', fragShader }); + } + + onPreRender() { + this.set2f('uResolution', this.game.scale.width, this.game.scale.height); + } +} diff --git a/js/scenes/BootScene.js b/js/scenes/BootScene.js index 4b5b4dd..154584d 100644 --- a/js/scenes/BootScene.js +++ b/js/scenes/BootScene.js @@ -79,6 +79,72 @@ export default class BootScene extends Phaser.Scene { g.fillCircle(4, 4, 4); g.generateTexture('alien_bullet', 8, 8); g.destroy(); + + // Spread-shot alien – 72×54, orange/red menacing saucer + g = this.make.graphics({ add: false }); + g.lineStyle(2, 0xff4400, 1); + g.strokeEllipse(36, 34, 62, 18); // wide flat hull + g.strokeEllipse(36, 24, 38, 22); // taller upper dome + g.fillStyle(0xff8800, 1); + g.fillCircle(36, 26, 5); // central cannon port + g.lineStyle(2, 0xff2200, 1); + g.beginPath(); g.moveTo(5, 34); g.lineTo(0, 27); g.strokePath(); // left spike + g.beginPath(); g.moveTo(67, 34); g.lineTo(72, 27); g.strokePath(); // right spike + g.generateTexture('alien_spread', 72, 54); + g.destroy(); + + // Homing-missile alien – 80×60, purple/magenta angular diamond + g = this.make.graphics({ add: false }); + g.lineStyle(2, 0xcc00ff, 1); + g.strokePoints([ + { x: 40, y: 5 }, + { x: 72, y: 30 }, + { x: 40, y: 55 }, + { x: 8, y: 30 } + ], true); + g.lineStyle(1, 0x9900cc, 1); + g.strokePoints([ + { x: 40, y: 18 }, + { x: 58, y: 30 }, + { x: 40, y: 42 }, + { x: 22, y: 30 } + ], true); + g.fillStyle(0xff00ff, 1); + g.fillCircle(40, 30, 5); + g.generateTexture('alien_missile', 80, 60); + g.destroy(); + + // Homing missile projectile – 14×6, magenta body + orange nose + g = this.make.graphics({ add: false }); + g.fillStyle(0xff00ff, 1); + g.fillRect(0, 1, 10, 4); + g.fillStyle(0xff8800, 1); + g.fillRect(10, 0, 4, 6); + g.generateTexture('homing_missile', 14, 6); + g.destroy(); + + // Reticle outer layer – outer ring + diagonal ticks (rotates one way) + g = this.make.graphics({ add: false }); + g.lineStyle(1.5, 0x00ff44, 1); + g.strokeCircle(32, 32, 28); + g.lineStyle(1, 0x00ff44, 0.5); + g.beginPath(); g.moveTo(12, 12); g.lineTo(18, 18); g.strokePath(); + g.beginPath(); g.moveTo(52, 12); g.lineTo(46, 18); g.strokePath(); + g.beginPath(); g.moveTo(12, 52); g.lineTo(18, 46); g.strokePath(); + g.beginPath(); g.moveTo(52, 52); g.lineTo(46, 46); g.strokePath(); + g.generateTexture('reticle_outer', 64, 64); + g.destroy(); + + // Reticle inner layer – inner ring + cross lines (rotates opposite way) + g = this.make.graphics({ add: false }); + g.lineStyle(1.5, 0x00ff44, 1); + g.strokeCircle(32, 32, 6); + g.beginPath(); g.moveTo(32, 2); g.lineTo(32, 24); g.strokePath(); // top + g.beginPath(); g.moveTo(32, 40); g.lineTo(32, 62); g.strokePath(); // bottom + g.beginPath(); g.moveTo( 2, 32); g.lineTo(24, 32); g.strokePath(); // left + g.beginPath(); g.moveTo(40, 32); g.lineTo(62, 32); g.strokePath(); // right + g.generateTexture('reticle_inner', 64, 64); + g.destroy(); } _asteroidPoints(cx, cy, radius, numPoints, offsets) { diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js index 33252fe..e7b8742 100644 --- a/js/scenes/GameScene.js +++ b/js/scenes/GameScene.js @@ -17,6 +17,7 @@ export default class GameScene extends Phaser.Scene { this.lives = STARTING_LIVES; this.gameOver = false; this.levelComplete = false; + this.levelSpeedMult = 1; // Physics groups (used for overlap detection) this.asteroidsGroup = this.physics.add.group(); @@ -42,6 +43,23 @@ export default class GameScene extends Phaser.Scene { callbackScope: this, loop: true }); + + // Apply CRT post-processing (scanlines + screen curvature) + if (this.renderer.type === Phaser.WEBGL) { + this.cameras.main.setPostPipeline('CRTPipeline'); + } + + // Custom reticle cursor – hide the OS cursor and draw our own + this.game.canvas.style.cursor = 'none'; + this.reticleOuter = this.add.image(800, 450, 'reticle_outer').setDepth(20); + this.reticleInner = this.add.image(800, 450, 'reticle_inner').setDepth(20); + this.tweens.add({ targets: this.reticleOuter, angle: 360, duration: 8000, repeat: -1, ease: 'Linear' }); + this.tweens.add({ targets: this.reticleInner, angle: -360, duration: 12000, repeat: -1, ease: 'Linear' }); + this.events.once('shutdown', () => { + this.game.canvas.style.cursor = 'default'; + this.reticleOuter.destroy(); + this.reticleInner.destroy(); + }); } // ─── Collision Setup ───────────────────────────────────────────────────── @@ -109,12 +127,20 @@ export default class GameScene extends Phaser.Scene { // Respawn player at center this.player.respawn(800, 450); - // Spawn large asteroids – more per level - const count = 3 + this.level; - for (let i = 0; i < count; i++) { + // Progressive asteroid difficulty: more rocks, mixed sizes, faster per level + this.levelSpeedMult = Math.min(1 + (this.level - 1) * 0.12, 2.0); + + const largeCount = Math.max(3, 2 + this.level); + const mediumCount = Math.floor(this.level / 2); + const smallCount = Math.floor(this.level / 3); + + const spawnOne = (size) => { const { x, y } = this.edgePosition(); - this.asteroids.push(new Asteroid(this, x, y, 'large', this.asteroidsGroup)); - } + this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup, this.levelSpeedMult)); + }; + for (let i = 0; i < largeCount; i++) spawnOne('large'); + for (let i = 0; i < mediumCount; i++) spawnOne('medium'); + for (let i = 0; i < smallCount; i++) spawnOne('small'); this.updateUI(); this.showMessage(`LEVEL ${this.level}`, 2000); @@ -146,7 +172,17 @@ export default class GameScene extends Phaser.Scene { if (this.gameOver || this.levelComplete) return; if (this.aliens.length >= 2) return; const { x, y } = this.edgePosition(); - this.aliens.push(new AlienShip(this, x, y, this.aliensGroup)); + + // Select alien type by level: missile aliens at 5+, spread at 3+ + let type = 'basic'; + if (this.level >= 5) { + const r = Math.random(); + type = r < 0.40 ? 'missile' : r < 0.75 ? 'spread' : 'basic'; + } else if (this.level >= 3) { + type = Math.random() < 0.5 ? 'spread' : 'basic'; + } + + this.aliens.push(new AlienShip(this, x, y, this.aliensGroup, type)); } spawnPlayerBullet(x, y, angle) { @@ -161,6 +197,16 @@ export default class GameScene extends Phaser.Scene { ); } + spawnHomingMissile(x, y, ownerShip) { + const player = this.player; + const angle = Phaser.Math.RadToDeg( + Math.atan2(player.sprite.y - y, player.sprite.x - x) + ); + this.alienBullets.push( + new Bullet(this, x, y, angle, 'missile', this.alienBulletsGroup, ownerShip) + ); + } + // ─── Collision Callbacks ───────────────────────────────────────────────── onPlayerBulletHitAsteroid(bulletSprite, asteroidSprite) { @@ -255,11 +301,11 @@ export default class GameScene extends Phaser.Scene { const count = Phaser.Math.Between(1, 3); for (let i = 0; i < count; i++) { const size = Math.random() < 0.5 ? 'medium' : 'small'; - this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup)); + this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup, this.levelSpeedMult)); } } else if (asteroid.size === 'medium') { for (let i = 0; i < 2; i++) { - this.asteroids.push(new Asteroid(this, x, y, 'small', this.asteroidsGroup)); + this.asteroids.push(new Asteroid(this, x, y, 'small', this.asteroidsGroup, this.levelSpeedMult)); } } // small: fully destroyed – no fragments @@ -347,6 +393,11 @@ export default class GameScene extends Phaser.Scene { // ─── Main Update Loop ──────────────────────────────────────────────────── update(time, delta) { + // Track reticle to mouse pointer (always, even on game over) + const ptr = this.input.activePointer; + this.reticleOuter.setPosition(ptr.x, ptr.y); + this.reticleInner.setPosition(ptr.x, ptr.y); + if (this.gameOver) return; this.player.update(time, delta); diff --git a/js/scenes/MenuScene.js b/js/scenes/MenuScene.js index 173bb4c..d760bd7 100644 --- a/js/scenes/MenuScene.js +++ b/js/scenes/MenuScene.js @@ -41,6 +41,11 @@ export default class MenuScene extends Phaser.Scene { this._drawTitle(); this._drawControlsPanel(); this._drawStartButton(); + + // Apply CRT post-processing (scanlines + screen curvature) + if (this.renderer.type === Phaser.WEBGL) { + this.cameras.main.setPostPipeline('CRTPipeline'); + } } update() {