/*
--------------------------------------------------------------------------------
x         x
 x       x
  x x x x  
 x       x
x         x
--------------------------------------------------------------------------------
*/
crystal_blast_size = 30; // was 32

SOWER_NOISE_DRAW_RATE = 4;


function Hunter(_x, _y) {
	this.remove = false;
	this.score = 1;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = this.sizeY = 3;
	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 0.5;
	
	this.eatsFood = false;
	this.acceleration = 0.025;
	this.explodeTimer = 40;
	this.exploding = false;
	this.blastRadius = 16;

	this.targetTimer = randomInt(60);
	this.targetX = width / 2;
	this.targetY = height / 2;
	this.avoidPoint = [];
	this.headingAllowed = new Array(17);
	for (var i = 0; i < headingDivisions; i ++) {
		this.headingAllowed[i] = true;
	}
	
	this.obstructedTimer = 0;
	this.wallCircleDirection = Math.random() < 0.5 ? -1 : 1;

	this.r = 128;
	this.g = 255;
	this.b = 0;
	this.sprite = 8;
	
	this.timer = 120;
	this.damageTimer = 0;
	this.integrity = 2;

	this.update = updateHunter;
	this.render = renderEntitySprite;// renderHunterTest; 
	this.damage = damageEntitySimple;
	this.jiggly = false;
}


function Mother(_x, _y) {
	this.remove = false;
	this.score = 5;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 3;
	this.sizeY = 3;
	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 0.5;
	
	this.targetTimer = randomInt(60);
	this.targetX = width / 2;
	this.targetY = height / 2;
	
	this.sprite = 32;
	
	this.timer = 120;
	this.integrity = 5;
	this.energy = 40;
	this.managed_entity = [];
	
	this.update = updateMother;
	this.render = renderEntitySprite;
	this.damage = damageEntitySimple;
	this.jiggly = false;
}



function Shy(_x, _y) {
	this.remove = false;
	this.score = 5;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 3;
	this.sizeY = 3;
	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 0.5;
	
	this.targetTimer = randomInt(60);
	this.targetX = width / 2;
	this.targetY = height / 2;
	
	this.sprite = 16;
	
	this.timer = 120;
	this.integrity = 4;
	this.energy = 40;
	
	this.update = updateShy;
	this.render = renderEntitySprite;
	this.damage = damageEntitySimple;
	this.jiggly = false;
}


function ChewChew(_x, _y) {
	this.remove = false;
	this.score = 4;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 6;
	this.sizeY = 6;

	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 0.75;

	this.targetTimer = randomInt(60);
	this.targetX = this.x;
	this.targetY = this.y;

	if ((this.x < 0) || (this.x > width))
		this.targetX = width / 2 + (width / 2 - this.x);
	if ((this.y < 0) || (this.y > height))
		this.targetY = height / 2 + (height / 2 - this.y); 	
	
	this.sprite = 44;
	
	this.integrity = 4;
	
	this.delay = 60;

	this.update = updateChewChew;
	this.render = renderEntitySprite;
	this.damage = damageEntitySimple;
	this.jiggly = false;
}


function Sower(_x, _y) {
	this.remove = false;
	this.score = 4;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 3;
	this.sizeY = 3;

	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 1;

	this.targetTimer = randomInt(60);
	this.targetX = this.x;
	this.targetY = this.y;

	if ((this.x < 0) || (this.x > width))
		this.targetX = width / 2 + (width / 2 - this.x);
	if ((this.y < 0) || (this.y > height))
		this.targetY = height / 2 + (height / 2 - this.y); 	
	
	this.sprite = 42;
	
	this.integrity = 2;
	
	this.delay = 60;

	this.update = updateSower;
	this.render = renderEntitySprite;
	this.damage = damageEntitySimple;
	this.jiggly = false;
}


// for two?

function Seeker(_x, _y) {
	this.remove = false;
	this.score = 1;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 2;
	this.sizeY = 2;
	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 0.25;
	
	this.acceleration = 0.025;
	this.explodeTimer = 40;
	this.exploding = false;
	this.blastRadius = 16;

	this.targetTimer = randomInt(60);
	this.targetX = width / 2;
	this.targetY = height / 2;
	this.avoidPoint = [];
	this.headingAllowed = new Array(17);
	for (var i = 0; i < headingDivisions; i ++) {
		this.headingAllowed[i] = true;
	}
	
	this.r = 128;
	this.g = 255;
	this.b = 0;
	this.sprite = 38;
	
	this.timer = 120;
	this.damageTimer = 0;
	this.integrity = 1;
	
	this.update = updateSeeker;
	this.render = renderEntitySprite;
	this.damage = damageEntityExplodingProjectile;
	this.jiggly = false;
}



function Crystal(_x, _y) {
	this.remove = false;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 2;
	this.sizeY = 2;
	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 0.0;
	
	this.targetTimer = randomInt(60);
	this.targetX = width / 2;
	this.targetY = height / 2;
	
	this.sprite = 24 + 2 * Math.floor(Math.random() * 2);
	
	this.timer = 120;
	this.integrity = 1;
	this.energy = 0;
	this.exploding = false;
	this.explodeTimer = 80;
	this.blastRadius = crystal_blast_size;
	
	this.update = updateCrystal;
	this.render = renderEntitySpriteIndexed;
	this.damage = damageEntityExploding;

	var clearX = Math.floor(this.x);
	var clearY = Math.floor(this.y);
	var clearRange = 4;
	clearRegion(clearX, clearY, clearRange);
	this.jiggly = false;
}


function updateCrystal() {
	var limit = 8;
	var player = getPlayerCharacters();
	this.asleep = true;
	for (var i in player) {
		if ((player[i] != null)
		&& (player[i].remove == false)
		&& ((Math.abs(player[i].x - this.x) < limit) && (Math.abs(player[i].y - this.y) < limit))) {

			this.remove = true;
			increaseScore(1, i);
			// if we had per-player score, the fork would be inside this

			playSound("crystal");
			effect.push(new Puff(this.x, this.y, 8, 255, 0, 255));
			clearRegion(this.x, this.y, 8);
			if (this.warningRectangle != null)
				this.warningRectangle.needed = false;
		}
	}

	if (this.exploding == true) {
		//this.x = jitter(this.x, 1);
		//this.y = jitter(this.y, 1);
		this.explodeTimer --;
		if (explosionCount < 1) {
			explosionCount ++;
			if (this.explodeTimer <= 0) {
				this.remove = true;
				this.warningRectangle.needed = false;
				explode(this.x, this.y, this.blastRadius);
				playSound("impact")
			}
		}
	}
}


function Eater(_x, _y) {
	this.remove = false;
	this.score = 1;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = this.sizeY = 3;
	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 0.875;
	
	this.eatsFood = false;
	this.acceleration = 0.005;
	this.explodeTimer = 40;
	this.exploding = false;
	this.blastRadius = 16;

	this.targetTimer = randomInt(60);
	this.targetX = width / 2;
	this.targetY = height / 2;
	this.target = undefined;
	
	this.r = 128;
	this.g = 255;
	this.b = 0;
	this.sprite = 18;
	
	this.timer = 0;
	this.damageTimer = 0;
	this.integrity = 4;

	this.update = updateEater;
	this.render = renderEntitySprite; //renderHunterTest; 
	this.damage = damageEntitySimple;
	this.jiggly = false;
}


function updateEater() {
	enemyCount ++;
	var index = 0;
		
	var offsetX = 0;
	var offsetY = 0;
	var scale = 0.01;
		

	if (this.target != undefined) {
		if (this.target.remove == false) {
			this.targetX = this.target.x;
			this.targetY = this.target.y;
		}
		// wobble if target has been removed?
		if ((this.target.remove)
		&& ((clock % 16) == 0)) {
			var range = 8;
			this.targetX += randomIntIn(-range, range);
			this.targetY += randomIntIn(-range, range);
		}
	}


	// move toward target
	offsetX = this.targetX - this.x;
	offsetY = this.targetY - this.y;

	// eat target
	var threshold = 4; 
	if ((this.target != undefined)
	&& (this.target.remove == false)) {
		if ((Math.abs(this.target.x - this.x) < threshold) 
		&& (Math.abs(this.target.y - this.y) < threshold)) {
			playSound("eating");
			if (this.target.type == "crystal") {
				this.target.remove = true; // just remove?
				// but manually add a destruction effect
				var newEffect = new Explosion(this.target.x, this.target.y);
				newEffect.baseImage = 12;
				newEffect.imageSize = 16;
				effect.push(newEffect);
				//message(entity.length + " entities");
				playSound("die");
			}
			else
				this.target.damage(1);

			this.timer = 60;
		}
	}

	scale = this.acceleration;
	this.velocityX += scale * offsetX;
	this.velocityY += scale * offsetY;

	
	// velocity limit
	var velocity = Math.sqrt(Math.pow(this.velocityX, 2) + Math.pow(this.velocityY, 2));
	if (velocity > this.velocityMax) {
		this.velocityX = this.velocityMax * this.velocityX / velocity;
		this.velocityY = this.velocityMax * this.velocityY / velocity;
	}	

	// eat player
	damageTouchingPlayers(this);
	
	// don't damage other things unless they're targeted?
	
	this.x += this.velocityX;
	this.y += this.velocityY;
	var friction = 0.85;
	this.velocityX *= friction;
	this.velocityY *= friction;

	// every so often, select a new target
	this.timer --;
	if (this.timer <= 0) {
		var searching = true;
		var tries = 0;
		var limit = 64;
		var distance = 32;
		// we are making a duplicate array, so we can remove entries
		var candidate = [];
		for (var i = 0; i < entity.length; i ++) {
			candidate.push(entity[i]);
		}
		while (searching) {
			// this removes the entity from the array
			// this way it will pick entities randomly, but won't inefficiently evaluate them multiple times
			this.target = draw(candidate); 

			// add some criteria other than "is an entity"
			searching = false;
			if (this.target == this) {
				console.log("error: eater selecting self");
				searching = true;
			}
			// check radius
			if ((Math.abs(this.target.x - this.x) + Math.abs(this.target.y - this.y)) > distance)
				searching = true;
			distance += 16;
			// exclude projectiles
			// exclude crystals?
			// exclude extra lives
			if (this.target.type == "extra life") {
				searching = true;
			}

			tries ++;
			if (tries > limit) {
				this.target = undefined;
				searching = false;
			}
		}

		if (this.target != undefined) {
			this.timer = randomIntIn(300, 400);
		}
	}	

	// damage animation
	if (this.damageTimer > 0) {
		this.damageTimer --;
		if (this.damageTimer > 0) {
			this.jiggly = true;
		}
		if (this.damageTimer <= 0) {
			this.jiggly = false;
		}
	}
}


/*
----------------------------------------------------------------
pickups
----------------------------------------------------------------
various items
the base Pickup class can be modified to make various types of things- just replace the collect function.
*/
function Pickup(_x, _y) {
	this.remove = false;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 2;
	this.sizeY = 2;
	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 0.0;
	
	this.targetTimer = randomInt(60);
	this.targetX = width / 2;
	this.targetY = height / 2;
	
	this.sprite = 6;
	
	this.timer = 120;
	this.integrity = 8;
	this.energy = 0;
	this.exploding = false;
	this.explodeTimer = 40;
	this.blastRadius = 32;
	
	this.update = updatePickup;
	this.render = renderEntitySprite;
	this.damage = damageEntitySimple;
	this.collect = function() { 
		console.log("pickup collected");
	}

	var clearX = Math.floor(this.x);
	var clearY = Math.floor(this.y);
	var clearRange = 4;
	clearRegion(clearX, clearY, clearRange);
	this.jiggly = false;
}


function updatePickup() {
	var limit = 8;
	var player = getPlayerCharacters();
	for (var i = 0; i < player.length; i ++) {
		if ((player[i] != undefined)
		&& (player[i].remove == false)) {
			offsetX = player[i].x - this.x;
			offsetY = player[i].y - this.y;
			if ((Math.abs(offsetX) < limit) 
			&& (Math.abs(offsetY) < limit)
			&& (this.remove == false)) {
				this.remove = true;
				playSound("crystal");
				effect.push(new Puff(this.x, this.y, 8, 255, 0, 255));
				clearRegion(this.x, this.y, 8);
				if (this.warningRectangle != null)
					this.warningRectangle.needed = false;
				this.collect();
			}
		}
	}
}


// this extra life item uses Pickup as a base
function makeExtraLife(x, y) {
	var entity = new Pickup(x, y);
	entity.sprite = 6;
	entity.type = "extra life";
	entity.collect = function() {
		messageCenter("extra life");
		scrollMessage = true;
		messageOpaque = true;
		messageHeight = 8;
		messageDuration = 32;
		lives ++;
	}
	entity.render = renderEntitySpriteIndexed;
	return entity;
}


function updateEntitySimple() {
	if (this.spawnTimer < 120) {
		this.spawnTimer ++;
		if (this.spawnTimer == 120)
			this.damage = damageEntityPlayer;
	}


	this.x += this.velocityX;
	this.y += this.velocityY;

	var testX = (Math.floor(this.x) + width) % width;
	var testY = (Math.floor(this.y) + height) % height;
	var index = 0;
	var testState = state[index][testX][testY];

	if (testState == damageState) {
		this.damage(1);
	}
}


function renderEntitySimple() {
	if ((this.x >= 0) && (this.x <= width) && (this.y >= 0) && (this.y <= height))
		drawDot(this.x, this.y, this.r, this.g, this.b);
}


function damageEntitySimple(amount) {
	//addNoiseToRegion(this.x, this.y, 5);
	this.damageTimer = 4;
	this.integrity -= amount;
	this.energy += 12 * amount;
	effect.push(new Puff(this.x, this.y, 4, 255, 0, 255));
	if (this.integrity <= 0) {
		if (this.remove == false) {
			if (this.notifyRemove != undefined) // notification function, if any
				this.notifyRemove();
		}

		//addNoiseToRegion(this.x, this.y, 5);
		this.remove = true;
		if ((isNaN(this.score) == false)
		&& (this.score != undefined)) {
			crystalsEarned += this.score;
			crystalSpawnTimer = 16;
		}

		// this displays the enemy sprite for a little while under the explosion
		// subtle but important for physical believability
		var newEffect = new NonAnimatedEffectTinted(this.sprite, this.x, this.y);
		effect.push(newEffect);
		
		var newEffect = new Explosion(this.x, this.y);
		newEffect.baseImage = 12;
		newEffect.imageSize = 16;
		effect.push(newEffect);
		//message(entity.length + " entities");
		playSound("die");
	}
	else {
		//playSound("impact"); // take pitch from projectile?
	}
}


function damageEntityExploding(amount) {
	this.integrity -= amount;
	if ((this.remove == false)
	&& (this.exploding == false)
	&& (this.integrity <= 0)) {
		this.sprite += 2;
		this.exploding = true;
		this.velocityX = 0;
		this.velocityY = 0;
		this.jiggly = true;
		effect.push(new Puff(this.x, this.y, 12, 255, 0, 255));
		var warningRectangle = new WarningRectangle(this.x - this.blastRadius, this.y - this.blastRadius, this.x + this.blastRadius, this.y + this.blastRadius, 40);
		warningRectangle.waitWhileNeeded = true;
		effect.push(warningRectangle);
		this.warningRectangle = warningRectangle;
	}
}


function damageEntityExplodingProjectile(amount) {
	//addNoiseToRegion(this.x, this.y, 5);
	this.damageTimer = 4;
	this.integrity -= amount;
	this.energy += 12 * amount;
	effect.push(new Puff(this.x, this.y, 4, 255, 0, 255));
	if (this.integrity <= 0) {
		//addNoiseToRegion(this.x, this.y, 5);
		this.remove = true;
		crystalsEarned += this.score;
		crystalSpawnTimer = 16;
		var newEffect = new Explosion(this.x, this.y);
		newEffect.baseImage = 12;
		newEffect.imageSize = 16;
		effect.push(newEffect);
		

		// this will draw the sprite of the monster for a few frames under the explosion, which is important for continuity
		var newEffect = new NonAnimatedEffectTinted(this.sprite, this.x, this.y);
		effect.push(newEffect);

		var newEffect = new Explosion(this.x, this.y);
		newEffect.baseImage = 12;
		newEffect.imageSize = 16;
		effect.push(newEffect);

		paranoid_second_noise_function(this.x, this.y, 6, 1);		//message(entity.length + " entities");
		playSound("die");

		// star of projectiles
		var startAngle = 0;
		var count = 8;
		var velocity = 2.5;
		for (var i = 0; i < count; i ++) {
			var currentAngle = i * 2 * Math.PI / count;
			var error = 0.0;
			var newProjectile = new MonsterBolt(this.x, this.y, jitter(this.x + Math.cos(currentAngle), error), jitter(this.y + Math.sin(currentAngle), error));
			entity.push(newProjectile);
			
			//newProjectile.velocityX += 0.5 * this.velocityX;
			//newProjectile.velocityY += 0.5 * this.velocityY;
			
			/*
			newProjectile.velocityX *= velocity;
			newProjectile.velocityY *= velocity;
			newProjectile.sprite = 3;
			newProjectile.delay = 0;
			newProjectile.render = renderMonsterBoltAngular;
			entity.push(newProjectile);
			*/
		}
	}
	else {
		//playSound("impact"); // take pitch from projectile?
	}
}


function damageEntityPlayer(amount) {
	//addNoiseToRegion(this.x, this.y, 5);
	this.damageTimer = 4;
	this.integrity -= amount;
	this.energy += 32 * amount;
	effect.push(new Puff(this.x, this.y, 4, 255, 0, 255));
	if ((this.remove == false)
	&& (this.integrity <= 0)) {
		//addNoiseToRegion(this.x, this.y, 5);
		this.remove = true;
		var newEffect = new Explosion(this.x, this.y);
		newEffect.baseImage = 12;
		newEffect.imageSize = 16;
		effect.push(newEffect);
		if (lives == 0) { // psi guy's tears permit no bullets
			// once per player
			for (var p = 0; p< 2; p ++) {
				if ((playerArray[p] != undefined)
				&& (player_active[p])) {
					for (var ring = 0; ring < 17; ring ++) { // 17
						for (var i = 0.0; i < Math.PI * 2; i += Math.PI / 16) {
							var newProjectile = new Bolt(playerArray[p].x, playerArray[p].y);
							newProjectile.velocityX = 4.0 * Math.cos(i + Math.PI / 64 * ring);
							newProjectile.velocityY = 4.0 * Math.sin(i + Math.PI / 64 * ring);
							newProjectile.delay = ring * 2;
							entity.push(newProjectile);
							// do multiple waves, with delays?
						}
					}
				}
			}
		}
	}
}


function ignoreDamage(amount) {
	// possibly do effect
}


function renderEntitySprite() {
	var baseImage = this.sprite + Math.floor(clock / 7) % 2;
	var spriteSize = 8;
	var left = (baseImage % spritesAcross) * spriteSize;
	var top = Math.floor(baseImage / spritesAcross) * spriteSize;
	var right = left + spriteSize - 1;
	var bottom = top + spriteSize - 1;
	//offscreenImageData.putImage(spriteImage, this.x, this.y);
	var centerX = this.x;
	var centerY = this.y;
	if (this.jiggly) {
		centerX = jitter(centerX, 1);
		centerY = jitter(centerY, 1);
	}

	if (this.damageTimer > 0)
		drawTintedSprite(baseImage, Math.floor(centerX - spriteSize / 2), Math.floor(centerY - spriteSize / 2));
	else
		copyImageDataRegion(spriteImage, offscreenImageData, 
			Math.floor(centerX - spriteSize / 2), Math.floor(centerY - spriteSize / 2), left, top, right, bottom);
	//if ((this.x >= 0) && (this.x <= width) && (this.y >= 0) && (this.y <= height))
		//drawDot(this.x, this.y, this.r, this.g, this.b);
	//setPixel(this.targetX, this.targetY, 255, 0, 0);
	
	/*
	var r = 128;
	var g = 0;
	var b = 128;
	if (this.hasEntered)
		g = 64;
	drawLine(this.x, this.y, this.targetX, this.targetY, r, g, b);
	*/
}


function renderEntitySpriteSimple() {
	var baseImage = this.sprite;
	var spriteSize = 8;
	var left = (baseImage % spritesAcross) * spriteSize;
	var top = Math.floor(baseImage / spritesAcross) * spriteSize;
	var right = left + spriteSize - 1;
	var bottom = top + spriteSize - 1;
	//offscreenImageData.putImage(spriteImage, this.x, this.y);
	var centerX = this.x;
	var centerY = this.y;
	if (this.jiggly) {
		centerX = jitter(centerX, 1);
		centerY = jitter(centerY, 1);
	}
	copyImageDataRegion(spriteImage, offscreenImageData, Math.floor(centerX - spriteSize / 2), Math.floor(centerY - spriteSize / 2), left, top, right, bottom);
}


function drawSprite(index, x, y) {
	var spriteSize = 8;
	var left = (index % spritesAcross) * spriteSize;
	var top = Math.floor(index / spritesAcross) * spriteSize;
	var right = left + spriteSize - 1;
	var bottom = top + spriteSize - 1;
	//offscreenImageData.putImage(spriteImage, this.x, this.y);
	var centerX = this.x;
	var centerY = this.y;
	if (this.jiggly) {
		centerX = jitter(centerX, 1);
		centerY = jitter(centerY, 1);
	}
	copyImageDataRegion(spriteImage, offscreenImageData, Math.floor(centerX - spriteSize / 2), Math.floor(centerY - spriteSize / 2), left, top, right, bottom);
}


function renderEntityPlayer() {
	var baseImage = this.sprite + Math.floor(clock / 7) % 2;
	var spriteSize = 8;
	var left = (baseImage % spritesAcross) * spriteSize;
	var top = Math.floor(baseImage / spritesAcross) * spriteSize;
	var right = left + spriteSize - 1;
	var bottom = top + spriteSize - 1;
	//offscreenImageData.putImage(spriteImage, this.x, this.y);
	var centerX = this.x;
	var centerY = this.y;
	if (this.jiggly) {
		centerX = jitter(centerX, 1);
		centerY = jitter(centerY, 1);
	}
	
	var drawSilhouette = false;
	if (this.spawnTimer < 120) {
		if ((Math.floor(clock / 3) % 2) == 0)
			drawSilhouette = true;

		// for reduced flicker mode, don't blink
		if ((antiflicker) || (!cycle_colors)) {
			drawSilhouette = true;
			this.damageTimer = 32;
		}
	}

	if (drawSilhouette == false) {
		copyImageDataRegion(spriteImage, offscreenImageData, Math.floor(centerX - spriteSize / 2), Math.floor(centerY - spriteSize / 2), left, top, right, bottom);
	}
	else { 
		if (cycle_colors)
			drawIndexedSpriteToRGB(spriteImageIndexed, baseImage, newPalette, offscreenImageData, Math.floor(this.x - 4), Math.floor(this.y - 4));
		else
			drawTintedSprite(baseImage, Math.floor(centerX - spriteSize / 2), Math.floor(centerY - spriteSize / 2));
	}
}


// copies a region of one imagedata into another imagedata
// is there a built in way to do this? putImage doesn't seem to work with an imageData as destination
function copyImageDataRegion(source, destination, destinationX, destinationY, left, top, right, bottom) {
	var drawWidth = right - left + 1;
	var drawHeight = bottom - top + 1;
	var destinationIndex;
	var sourceR, sourceG, sourceB;
	var sourceWidth = source.width;
	var sourceI;
	
	/*for (var y = 0; y < drawHeight; y ++) {
		for (var x = 0; x < drawWidth; x ++) {
			sourceI = (left + x + (top + y) * sourceWidth) * 4;
			destinationIndex = (destinationX + x + (destinationY + y) * width) * 4;

			sourceR = source.data[sourceI];
			sourceG = source.data[sourceI + 1];
			sourceB = source.data[sourceI + 2];
			
			if (sourceR != 0) {
				if ((((destinationX + x) > 0) && ((destinationX + x) < width))
				&& (((destinationY + y) > 0) && ((destinationY + y) < height))) {
					destination.data[destinationIndex] = sourceR;
					destination.data[destinationIndex + 1] = sourceG;
					destination.data[destinationIndex + 2] = sourceB;
				}
			}
		}
	}*/
	 
	
	var spriteWidth = right - left;
	var spriteHeight = bottom - top;
	offscreenContext.drawImage(source, left, top, spriteWidth + 1, spriteHeight + 1, destinationX, destinationY, spriteWidth + 1, spriteHeight + 1);
	
}


// copies a region of one imagedata into another imagedata
// is there a built in way to do this? putImage doesn't seem to work with an imageData as destination
function copyImageDataRegionAsSilhouette(source, destination, destinationX, destinationY, left, top, right, bottom) {
	var drawWidth = right - left + 1;
	var drawHeight = bottom - top + 1;
	var destinationIndex;
	var sourceR, sourceG, sourceB;
	var sourceWidth = 64;
	var sourceI;
	
	for (var y = 0; y < drawHeight; y ++) {
		for (var x = 0; x < drawWidth; x ++) {
			sourceI = (left + x + (top + y) * sourceWidth) * 4;
			destinationIndex = (destinationX + x + (destinationY + y) * width) * 4;

			sourceR = source.data[sourceI];
			sourceG = source.data[sourceI + 1];
			sourceB = source.data[sourceI + 2];
			
			if (sourceR != 0) {
				if ((((destinationX + x) > 0) && ((destinationX + x) < width))
				&& (((destinationY + y) > 0) && ((destinationY + y) < height))) {
					destination.data[destinationIndex] = 255;
					destination.data[destinationIndex + 1] = 0;
					destination.data[destinationIndex + 2] = 255;
				}
			}
		}
	}
	
	/*
	var spriteWidth = right - left;
	var spriteHeight = bottom - top;
	offscreenContext.drawImage(spriteCanvas, left, top, spriteWidth, spriteHeight, destinationX, destinationY, spriteWidth, spriteHeight);
	*/
}


function drawTintedSprite(index, destinationX, destinationY) {
	var spriteSize = 8;
	var left = (index % 8) * spriteSize;
	var top = Math.floor(index / 8) * spriteSize;
	var right = left + spriteSize;
	var bottom = top + spriteSize;

	var drawWidth = right - left + 1;
	var drawHeight = bottom - top + 1;
	var destinationIndex;
	var sourceR, sourceG, sourceB;
	var sourceWidth = 64;
	var sourceI;

	var tintR = 255;
	var tintG = 128;
	var tintB = 255;

	var source = spriteImageData;
	
	for (var y = 0; y < drawHeight; y ++) {
		for (var x = 0; x < drawWidth; x ++) {
			sourceI = (left + x + (top + y) * sourceWidth) * 4;
			destinationIndex = (destinationX + x + (destinationY + y) * width) * 4;

			sourceR = source.data[sourceI];
			sourceG = source.data[sourceI + 1];
			sourceB = source.data[sourceI + 2];
			
			if (sourceR != 0) {
				if ((((destinationX + x) > 0) && ((destinationX + x) < width))
				&& (((destinationY + y) > 0) && ((destinationY + y) < height))) {
					offscreenImageData.data[destinationIndex] = Math.min(sourceR + tintR, 255);
					offscreenImageData.data[destinationIndex + 1] = Math.min(sourceG + tintG, 255);
					offscreenImageData.data[destinationIndex + 2] = Math.min(sourceB + tintB, 255);
				}
			}
		}
	}
	
	/*
	var spriteWidth = right - left;
	var spriteHeight = bottom - top;
	offscreenContext.drawImage(spriteCanvas, left, top, spriteWidth, spriteHeight, destinationX, destinationY, spriteWidth, spriteHeight);
	*/
}


function renderEntitySpriteIndexed() {
	var centerX = this.x;
	var centerY = this.y;
	if (this.jiggly) {
		centerX = jitter(centerX, 1);
		centerY = jitter(centerY, 1);
	}

	if (cycle_colors)
		drawIndexedSpriteToRGB(spriteImageIndexed, this.sprite, newPalette, offscreenImageData, 
				Math.floor(centerX - 4), Math.floor(centerY - 4));

	// otherwise do normal rendering
	else {
		var baseImage = this.sprite;
		var spriteSize = 8;
		var left = (baseImage % spritesAcross) * spriteSize;
		var top = Math.floor(baseImage / spritesAcross) * spriteSize;
		var right = left + spriteSize - 1;
		var bottom = top + spriteSize - 1;
		//offscreenImageData.putImage(spriteImage, this.x, this.y);
		var centerX = this.x;
		var centerY = this.y;
		if (this.jiggly) {
			centerX = jitter(centerX, 1);
			centerY = jitter(centerY, 1);
		}
		copyImageDataRegion(spriteImage, offscreenImageData, Math.floor(centerX - spriteSize / 2), Math.floor(centerY - spriteSize / 2), left, top, right, bottom);
	}
}


function renderBolt() {
	if (this.delay == 0) {
		var scale = 2.0;
		//drawLine(this.x, this.y, this.x + this.velocityX * scale, this.y + this.velocityY * scale, this.r, this.g, this.b);

		var lengthMultiplier = 2.0;
		var endX = this.x + lengthMultiplier * this.velocityX;
		var endY = this.y + lengthMultiplier * this.velocityY;

		// outline
		for (var i = 0.0; i <= 1.0; i += 0.2) {
			drawSprite(this.sprite - 1, 8, lerp(this.x, endX, i), lerp(this.y, endY, i));
		}
		// top layer
		for (var i = 0.0; i <= 1.0; i += 0.2) {
			drawSprite(this.sprite, 8, lerp(this.x, endX, i), lerp(this.y, endY, i));
		}
	}
}


function renderMonsterBoltAngular() {
	if (this.delay == 0) {
		var scale = 2.0;
		//drawLine(this.x, this.y, this.x + this.velocityX * scale, this.y + this.velocityY * scale, this.r, this.g, this.b);

		var lengthMultiplier = 1.5;
		var endX = this.x + lengthMultiplier * this.velocityX;
		var endY = this.y + lengthMultiplier * this.velocityY;

		// top layer
		// outline
		for (var i = 0.0; i < 1.0; i += 0.2) {
			drawSprite(31, 8, lerp(this.x, endX, i), lerp(this.y, endY, i));
		}
		// top layer
		for (var i = 0.0; i < 1.0; i += 0.2) {
			drawSprite(3, 8, lerp(this.x, endX, i), lerp(this.y, endY, i));
		}
	}
}


function updateBolt() {
	if (this.delay > 0)
		this.delay --;
	else {
		this.x += this.velocityX;
		this.y += this.velocityY;

		if (this.remove == false) {

			if (playerBoltBlockedByLiveCells) {
				var hitCell = false;
				// explode on contact with live cell
				var testX = (Math.floor(this.x) + width) % width;
				var testY = (Math.floor(this.y) + height) % height;

				clean_area(testX, testY, 4);

				var index = 0;
				for (var oY = -this.sizeY; oY <= this.sizeY; oY ++) {
					for (var oX = -this.sizeX; oX <= this.sizeX; oX ++) {
						var testX = (Math.floor(this.x + oX) + width) % width;
						var testY = (Math.floor(this.y + oY) + height) % height;
						if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
							if (state[index][testX][testY] == damageState) {
								this.remove = true;
								hitCell = true;
							}
						}
					}
				}
				if (hitCell)
					clearRegion(this.x, this.y, playerBoltBlastRadius);
			}

			// remove when out of bounds (maybe flash side)
			if ((this.x > width) || (this.x < 0) || (this.y > height) || (this.y < 0)) {
				// add: flash side?
				this.remove = true;
			}

			// against all check
			// we could speed this up by putting projectiles in effects
			var offsetX, offsetY;
			var limit = 2;
			for (var i = 0; i < entity.length; i ++) {
				if ((entity[i] === this)
				|| (entity[i] === playerArray[0])
				|| (entity[i] === playerArray[1])
				|| (entity[i].projectile == true)
				|| (entity[i].remove == true)) {
				// don't collide
				}
				else {
					offsetX = entity[i].x - this.x;
					offsetY = entity[i].y - this.y;
					var limitX = (6 + entity[i].sizeX) * 0.5;
					var limitY = (6 + entity[i].sizeY) * 0.5;
					if ((Math.abs(offsetX) < limitX) && (Math.abs(offsetY) < limitY)) {
						this.remove = true;
						entity[i].damage(1);
					}
				}
			}

			if (this.remove == true) {
				//explode(this.x, this.y, 4);
				effect.push(new Puff(this.x, this.y, 8, 255, 0, 255));
				//playSound("impact");
				paranoid_second_noise_function(this.x, this.y, playerBoltBlastEffectRadius, 1);
			}
		}
	}
}


function updateMonsterBolt() {
	enemyProjectileCount ++;
	this.x += this.velocityX;
	this.y += this.velocityY;
	
	if (this.remove == false) {
		if (monsterBoltBlockedByLiveCells) {
			var hitCell = false;
			// explode on contact with live cell
			var testX = (Math.floor(this.x) + width) % width;
			var testY = (Math.floor(this.y) + height) % height;
			var index = 0;
			for (var oY = -this.sizeY; oY <= this.sizeY; oY ++) {
				for (var oX = -this.sizeX; oX <= this.sizeX; oX ++) {
					var testX = (Math.floor(this.x + oX) + width) % width;
					var testY = (Math.floor(this.y + oY) + height) % height;
					if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
						if (state[index][testX][testY] != emptyState) {
							this.remove = true;
							hitCell = true;
						}
					}
				}
			}
			if (hitCell)
				clearRegion(this.x, this.y, playerBoltBlastRadius);
		}

		// explode on contact with live cell
		var testX = (Math.floor(this.x) + width) % width;
		var testY = (Math.floor(this.y) + height) % height;
		var index = 0;
		var testState = state[index][testX][testY];
		if (testState != emptyState) {
			//this.remove = true;
		}

		// remove when out of bounds (maybe flash side)
		if ((this.x > width) || (this.x < 0) || (this.y > height) || (this.y < 0)) {
			// add: flash side?
			this.remove = true;
		}
		
		// against all check
		// we could speed this up by putting projectiles in effects
		var offsetX, offsetY;
		var limit = 2;
		var player = getPlayerCharacters();
		for (var i = 0; i < player.length; i ++) {
			if ((player[i] != undefined)
			&& (player[i].remove == false)) {
				offsetX = player[i].x - this.x;
				offsetY = player[i].y - this.y;
				var limitX = (4 + player[i].sizeX) * 0.5;
				var limitY = (4 + player[i].sizeY) * 0.5;
				if ((Math.abs(offsetX) < limitX) && (Math.abs(offsetY) < limitY)) {
					this.remove = true;
					//playerCharacter.damage(1);
					player[i].damage(1);
				}
			}
		}	

		if (this.remove == true) {
			//explode(this.x, this.y, 4);
			effect.push(new Puff(this.x, this.y, 8, 255, 0, 255));
		}
	}
}


function updateHunter() {
	enemyCount ++;
	var index = 0;
		
	var offsetX = 0;
	var offsetY = 0;
	var scale = 0.01;
	
	// randomly alter wall circle direction
	// this is the direction skeletoids will walk around obstructions
	if (skeletoidsCircleWalls) {
		if ((clock % 256) == 0) {
			this.wallCircleDirection = Math.random() < 0.5 ? -1 : 1;
		}
	}
	
	// move away from avoid point
	var i2 = 0;
	var avoidCount = 0;
	var nudgeX = 0;
	var nudgeY = 0;
	var nearest = 1000;
	if (this.avoidPoint.length > 0) {
		while (i2 < this.avoidPoint.length) {
			offsetX = this.avoidPoint[i2].x - this.x;
			offsetY = this.avoidPoint[i2].y - this.y;
			var distance = Math.max(Math.floor(Math.abs(offsetX) + width) % width, Math.floor(Math.abs(offsetY) + height) % height);
			var limit = 6;
			if (distance < limit) {
				//this.x += (limit - distance) * scale * offsetX;
				//this.y += (limit - distance) * scale * offsetY;
				nudgeX += offsetX;
				nudgeY += offsetY;
				avoidCount ++;
			}
			if ((distance > 10)
			|| (state[index][this.avoidPoint[i2].x][this.avoidPoint[i2].y] == emptyState)) {
				// remove this avoid point
				this.avoidPoint.splice(i2, 1);
			}
			else
				i2 ++;
		}	
	}
	
	if (avoidCount > 0) {
		this.obstructedTimer ++;
		var length = Math.sqrt(Math.pow(nudgeX, 2) + Math.pow(nudgeY, 2));
		// normalize
		if (length != 0) {
			nudgeX /= length;
			nudgeY /= length;
		};
		// now push away
		var force = -0.1;
		this.velocityX += force * nudgeX;
		this.velocityY += force * nudgeY;
		if (skeletoidsCircleWalls) {
			force = 0.1;
			var perpendicularX = this.wallCircleDirection * nudgeY;
			var perpendicularY = this.wallCircleDirection * -nudgeX;
			this.velocityX += force * perpendicularX;
			this.velocityY += force * perpendicularY;
		}
	}
	else {
		this.obstructedTimer = 0;
	}
	
	if (this.obstructedTimer > 128) {
		this.damage(1);
		explode_wall_only(this.x, this.y, 6);
	}

	// move toward target
	if ((avoidCount == 0)
	&& (this.exploding == false)) {
		offsetX = this.targetX - this.x;
		offsetY = this.targetY - this.y;
		var distance = Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2));
		this.velocityX += this.acceleration * offsetX / distance;
		this.velocityY += this.acceleration * offsetY / distance;
	}
	
	// velocity limit
	var velocity = Math.sqrt(Math.pow(this.velocityX, 2) + Math.pow(this.velocityY, 2));
	if (velocity > this.velocityMax) {
		this.velocityX = this.velocityMax * this.velocityX / velocity;
		this.velocityY = this.velocityMax * this.velocityY / velocity;
	}	
	// eat player
	damageTouchingPlayers(this);
	
	var newX = this.x + this.velocityX;
	var newY = this.y + this.velocityY;
	// test solidity
	
	var obstructedX = isRectangleSolid(newX, this.y, this.sizeX, this.sizeY);
	if (obstructedX) {
		this.velocityX = 0;
		newX = this.x;
	}
	var obstructedY = isRectangleSolid(newX, newY, this.sizeX, this.sizeY);
	if (obstructedY) {
		this.velocityY = 0;
		newY = this.y;
	}
	
	this.x = newX;
	this.y = newY;
	
	var friction = 0.95;
	this.velocityX *= friction;
	this.velocityY *= friction;
	// add: max velocity limit
		
	// edge behavior
	// if we haven't entered yet and we're within bounds, mark us as having entered
	if ((this.hasEntered == false)
	&& ((this.x > 0)
	&& (this.x < width)
	&& (this.y > 0)
	&& (this.y < height)))
		this.hasEntered = true;
	// if we've already entered, exit at edges
	var buffer = 4;
	if (this.hasEntered == true) {
		var exit = false;
		if (this.x > width + buffer)
			exit = true;
		if (this.x < 0 - buffer)
			exit = true;
		if (this.y < 0 - buffer)
			exit = true;
		if (this.y > height + buffer)
			exit = true;
		// exit
		if (exit)
			this.remove = true;
	}

	if (this.exploding == true) {
		//this.x = jitter(this.x, 1);
		//this.y = jitter(this.y, 1);
		this.explodeTimer --;
		if (Math.random() < 0.1) {
			var radius = 2;
			effect.push(new Puff(jitter(this.x, radius), jitter(this.y, radius), 8, 255, 0, 255));
		}
		if (this.explodeTimer <= 0) {
			this.remove = true;
			this.warningRectangle.needed = false;
			addNoiseToRegion(this.x, this.y, this.blastRadius);
		}
	}
	
	if ((this.remove == false)
	&& (this.hasEntered == true)) {
		//this.x = jitter(this.x, 0.5);
		//this.y = jitter(this.y, 0.5);
			
		if (this.eatsFood) {
			var testX, testY;
			for (var oY = -this.sizeY; oY <= this.sizeY; oY ++) {
				for (var oX = -this.sizeX; oX <= this.sizeX; oX ++) {
					var testX = (Math.floor(this.x + oX) + width) % width;
					var testY = (Math.floor(this.y + oY) + height) % height;
					if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
						var testState = state[index][testX][testY];
						/*
						if (testState == 2) {
							// possibly check over wider area
							this.remove = true;
						}
						*/
						if (testState == damageState) {
							// eat
							state[index][testX][testY] = emptyState;
							this.velocityX *= 0.9;
							this.velocityY *= 0.9;
							// add energy or something
						}
					}
				}
			}
		}

		// sense surroundings
		// obstacle avoidance
		// test ahead
		//if (this.targetTimer % 2 == 0) {
		if (true) {
			// check at this angle
			var checkRadius = randomIn(1, 8);
			var checkAngle = randomIn(0, 2 * Math.PI);
			// use a point between us and our target (n away from us)
			offsetX = this.targetX - this.x;
			offsetY = this.targetY - this.y;
			var distance = Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2));
			var testRange = 6;
			if (distance > 0) {
				offsetX /= distance;
				offsetY /= distance;
			}
			this.testX = this.x + testRange * offsetX;
			this.testY = this.y + testRange * offsetY;

			this.testX += checkRadius * Math.cos(checkAngle);
			this.testY += checkRadius * Math.sin(checkAngle);
			this.testX = Math.floor(this.testX);
			this.testY = Math.floor(this.testY);
			if ((this.testX >= 0) && (this.testX < width) && (this.testY >= 0) && (this.testY < height)) {
				var testState = state[index][this.testX][this.testY];
				if (testState != emptyState) { // avoid walls
					var newPoint = {x:this.testX, y:this.testY};
					this.avoidPoint.push(newPoint);
				}				
			}
		}
			
		// if we've arrived at the target
		offsetX = this.targetX - this.x;
		offsetY = this.targetY - this.y;
		var distance = Math.max(Math.abs(offsetX), Math.abs(offsetY));
		limit = 1;
		if ((this.hasEntered)
		&& (this.targetTimer > 60)
		|| (distance < limit)) {
			this.targetTimer = 0;
			//effect.push(new ShrinkingRectangle(this.x, this.y, 4, 128, 255, 0));
			this.targetX = jitter(this.x, 32);
			this.targetY = jitter(this.y, 32);
			var error = 16;
			if ((playerCharacter != null)
			&& (playerCharacter.remove == false)
			&& ((Math.abs(playerCharacter.x - this.x) < 80) && (Math.abs(playerCharacter.y - this.y) < 80))) {
				this.targetX = jitter(playerCharacter.x, error);
				this.targetY = jitter(playerCharacter.y, error);
			}
			// maybe only check target position every so often, to add a perception delay?
		}
		this.targetTimer ++;
	}
	
	// damage animation
	damageAnimationUpdate(this);
}


function Hen(_x, _y) {
	this.remove = false;
	this.score = 5;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 3;
	this.sizeY = 3;
	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 1.0;
	
	this.targetTimer = randomInt(60);
	this.targetX = width / 2;
	this.targetY = height / 2;
	
	this.sprite = 20;
	
	this.timer = 120;
	this.integrity = 5;
	this.energy = 40;
	
	this.update = updateHen;
	this.render = renderEntitySprite;
	this.damage = damageEntitySimple;
	this.jiggly = false;
}


function updateHen() {
	enemyCount ++;
	var index = 0;
		
	var offsetX = 0;
	var offsetY = 0;
	var scale = 0.01;
		
	// move toward target
	offsetX = this.targetX - this.x;
	offsetY = this.targetY - this.y;
	scale = 0.005;
	this.velocityX += scale * offsetX;
	this.velocityY += scale * offsetY;
	
	// velocity limit
	var velocity = Math.sqrt(Math.pow(this.velocityX, 2) + Math.pow(this.velocityY, 2));
	if (velocity > this.velocityMax) {
		this.velocityX = this.velocityMax * this.velocityX / velocity;
		this.velocityY = this.velocityMax * this.velocityY / velocity;
	}	

	/* 
	note on modular behavior:
	we could put all the every frame functions in an array
	and iterate through all of them (this would be order dependent)
	they could even be objects which have some data of their own 
	(even initialization + draw functionality)
	*/
	
	// eat player
	damageTouchingPlayers(this);
	
	
	this.x += this.velocityX;
	this.y += this.velocityY;
	var friction = 0.85;
	this.velocityX *= friction;
	this.velocityY *= friction;
	// add: max velocity limit

		
	// edge behavior
	edgeBehavior(this);
	
	if (this.remove == false) {
		this.timer ++;
		//this.x = jitter(this.x, 0.5);
		//this.y = jitter(this.y, 0.5);
			
		if ((this.timer % 4 == 0)
		&& (this.hasEntered)) {
			var testX, testY;
			for (var oY = -this.sizeY; oY <= this.sizeY; oY ++) {
				for (var oX = -this.sizeX; oX <= this.sizeX; oX ++) {
					var testX = (Math.floor(this.x + oX) + width) % width;
					var testY = (Math.floor(this.y + oY) + height) % height;
					var testState = state[index][testX][testY];
					/*
					if (testState == 2) {
						// possibly check over wider area
						this.remove = true;
					}
					*/
					if (testState != emptyState) {
						// eat
						state[index][testX][testY] = emptyState;
						// add energy or something
						this.energy ++;
					}
				}
			}

			// eggs
			if ((this.timer > 60)
			&& (this.energy > 20)
			&& (previousEnemyCount < desiredEnemyCount + 5)) {
				this.timer = 0;
				var count = 0;
				if (eggCount < eggLimit)
					count = 1;
				for (var i = 0; i < count; i ++) {
					// note that this is outside of the spawn management system-
					// so one can't exploit these to reach the end of a wave sooner
					// there's incentive to dispatch these quickly
					var newMonster = spawn("egg", jitter(this.x, 6), jitter(this.y, 6));
					this.energy -= 4;
					effect.push(new ShrinkingRectangle(newMonster, 64, 128, 255, 0));
				}
				//console.log(eggCount + " / " + eggLimit + " eggs");
			}

			// sense surroundings
			// obstacle avoidance
			// test ahead
			if (true) {
				var lookAhead = 4;
				var checkRange = randomIn(1, 12);
				var checkAngle = randomIn(0, 2 * Math.PI);
				
				testX = Math.floor(this.x + checkRange * Math.cos(checkAngle));
				testY = Math.floor(this.y + checkRange * Math.sin(checkAngle));
				//testX = (Math.floor(testX) + width) % width;
				//testY = (Math.floor(testY) + height) % height;
				if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
					var testState = state[index][testX][testY];
					if (testState != emptyState) { // go toward food
						this.targetX = testX;
						this.targetY = testY;
					}				
				}
				/*
				else if (testState == 2) { // avoid obstacles
					var newPoint = {x:testX, y:testY};
					this.avoidPoint.push(newPoint);
				}
				*/
			}
		}			
		// if we've arrived at the target
		offsetX = this.targetX - this.x;
		offsetY = this.targetY - this.y;
		var distance = Math.max(Math.abs(offsetX), Math.abs(offsetY));
		limit = 1;
		if ((this.hasEntered)
		&& (this.targetTimer > 60)
		|| (distance < limit)) {
			this.targetTimer = 0;
			var margin = 4;
			this.targetX = jitter(this.x, 32);
			this.targetY = jitter(this.y, 32);
			this.targetX = Math.max(0 + margin, Math.min(width - margin, this.targetX));
			this.targetY = Math.max(0 + margin, Math.min(height - margin, this.targetY));
			if ((playerCharacter != null)
			&& (playerCharacter.remove == false)
			&& ((Math.abs(playerCharacter.x - this.x) < 64) && (Math.abs(playerCharacter.y - this.y) < 64))) {
				this.targetX = playerCharacter.x;
				this.targetY = playerCharacter.y;
			}
			// maybe only check target position every so often, to add a perception delay?
		}
		this.targetTimer ++;
	}

	// damage animation
	if (this.damageTimer > 0) {
		this.damageTimer --;
		if (this.damageTimer > 0) {
			this.sprite = 22;
			this.jiggly = true;
		}
		if (this.damageTimer <= 0) {
			this.sprite = 20;
			this.jiggly = false;
		}
	}
}


// this extra life item uses Pickup as a base
var eggCount = 0;
var eggLimit = 16;
function Egg(_x, _y) {
	eggCount ++;
	
	this.remove = false;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 2;
	this.sizeY = 2;
	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 0.0;
	
	this.targetTimer = randomInt(60);
	this.targetX = width / 2;
	this.targetY = height / 2;
	
	this.sprite = 22;
	
	this.timer = 1024;
	this.integrity = 1;
	this.energy = 0;
	this.exploding = false;
	this.explodeTimer = 40;
	this.blastRadius = 12;
	
	this.update = updateEgg;
	this.render = renderEntitySpriteSimple;
	this.damage = damageEntitySimple;// damageEntityExploding; // track egg count here?
	this.notifyRemove = function() {
		if (this.remove == false) {
			// when removed, update egg count
			eggCount --;
			//alert("egg removed. egg count = " + eggCount);
		}
	}
	this.hatching = false;
	this.hatchingTimer = 0;
	this.hatch = function() {
		if (this.hatching == false) {
			this.hatching = true;
			this.hatchingTimer = 0;
			eggCount --;
			//hunter.sprite = 4;
			return true;
		}
		return false;
	}
	this.open = function() {
		if (this.remove == false) {
			this.remove = true;
			var hunter = spawn("hunter", this.x, this.y);
			hunter.score = 0;
		}
	}

	var clearX = Math.floor(this.x);
	var clearY = Math.floor(this.y);
	var clearRange = 4;
	clearRegion(clearX, clearY, clearRange);
	this.jiggly = false;
}


function updateEgg() {
	damageTouchingPlayers(this);
	
	// set sprite independently, not based on central clock
	this.sprite = 22 + Math.floor(this.timer / 7) % 2; 

	if (this.hatching) {
		this.hatchingTimer ++; // this can be externally set to stagger timing
		this.jiggly = true;
		if (this.hatchingTimer > 64) {
			this.open();
		}
	}

	if (this.exploding == true) {
		//this.x = jitter(this.x, 1);
		//this.y = jitter(this.y, 1);
		this.explodeTimer --;
		if (Math.random() < 0.1) {
			var radius = 2;
			effect.push(new Puff(jitter(this.x, radius), jitter(this.y, radius), 8, 255, 0, 255));
		}
		if (this.explodeTimer <= 0) {
			this.remove = true;
			this.warningRectangle.needed = false;
			explode_wall_only(this.x, this.y, this.blastRadius);
			//playSound("impact")
		}
	}

	if (this.remove == false) {
		this.timer --;
		// for now these don't auto-hatch
		if (eggsHatchIndividually) {
			if (this.timer <= 0) {
				this.hatch();
			}	
		}
	}
}


function hatchNEggs(n) {
	var current;
	var count = 0;
	for (var i in entity) {
		current = entity[i];
		if ((count < n)
		&& ((current.type != undefined) && (current.type == "egg"))) {
			if (current.hatch()) {
				current.hatchingTimer = -2 * count;
				count ++;
			}
		}
	}
}


function testEggHatch() {
	hatchNEggs(16);
}


// takes any entity (not in manager, but in memory)
// draws an effect at the object's position and then adds the object to the world
function SpawnPoint(object) {
	this.object = object;
	this.remove = false;
	this.x = object.x;
	this.y = object.y;
	this.sizeX = 2;
	this.sizeY = 2;
	
	this.sprite = 68;
	
	this.delay = 0;
	this.step = 0;
	this.subframe = 0;
	this.stepCount = 4;
	
	this.update = function() {
		if (this.delay > 0) {
			this.delay --;
		}
		else {
			this.subframe ++;
			if (this.subframe > 1) {
				this.subframe = 0;
				this.sprite ++;
				this.step ++;
				if (this.step >= this.stepCount) {
					this.remove = true;
					entity.push(object);
					// effect
				}
			}
		}
	};
	this.render = renderEntitySpriteSimple;
	this.damage = damageEntitySimple;// damageEntityExploding; // track egg count here?

	var clearX = Math.floor(this.x);
	var clearY = Math.floor(this.y);
	var clearRange = 4;
	clearRegion(clearX, clearY, clearRange);
	this.jiggly = false;
}


function Turret(_x, _y) {
	this.remove = false;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 2;
	this.sizeY = 2;
	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 0.0;
	
	this.asleep = true;
	
	this.targetTimer = randomInt(60);
	this.targetX = width / 2;
	this.targetY = height / 2;
	
	this.sprite = 64;
	
	this.integrity = 4;
	
	this.update = updateTurret;
	this.render = renderEntitySprite;
	this.damage = damageEntitySimple;// damageEntityExploding; // track egg count here?

	var clearX = Math.floor(this.x);
	var clearY = Math.floor(this.y);
	var clearRange = 4;
	clearRegion(clearX, clearY, clearRange);
	
	this.delay = 32;
	this.jiggly = true;
}


function updateTurret() {
	
	if (this.delay > 0) {
		this.jiggly = true;
		this.delay --;
		//this.sprite = 72;
		//if (this.delay == 0)
			//this.sprite = 66;
	}
	else {
		
		this.jiggly = false;
		var wasAsleep = this.asleep;
		var player = getPlayerCharacters();
		this.asleep = true;
		for (var i in player) {
			var current =  player[i];
			var limitX = turretRange * 1.25;
			var limitY = turretRange * 1.25;
			var offsetX = current.x - this.x;
			var offsetY = current.y - this.y;
			if ((Math.abs(offsetX) < limitX) && (Math.abs(offsetY) < limitY)) {
				this.asleep = false; // possibly have effect / sound
			}
		}
		
		if (wasAsleep && (this.asleep == false)) {
			// wake effects
			this.fireTimer = 0;
			this.sprite = 66;
		}
		if ((wasAsleep == false) && (this.asleep)) {
			// go to sleep effects
			this.sprite = 64;
		}
		
		if (this.asleep == false) {
			// get all players in list
			var player = getPlayerCharacters();
			for (var i in player) {
				var current =  player[i];
				var limitX = turretRange;
				var limitY = turretRange;
				var offsetX = current.x - this.x;
				var offsetY = current.y - this.y;
				if ((Math.abs(offsetX) < limitX) || (Math.abs(offsetY) < limitY)) {
					var distance = Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2));
					if ((distance < turretRange)
					&& ((this.fireTimer % 16) == 0)) {
					//&& (previousEnemyProjectileCount < desiredEnemyProjectileCount)) {	
						var error = 8;
						var newProjectile = new MonsterBolt(this.x, this.y, jitter(playerCharacter.x, error), jitter(playerCharacter.y, error));
						entity.push(newProjectile);
					}					
				}
			}
			this.fireTimer ++;
		}
	}
	damageAnimationUpdate(this);
}


function TurretBug(_x, _y) {
	this.remove = false;
	this.score = 4;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 6;
	this.sizeY = 6;

	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 1;

	this.targetX = this.x;
	this.targetY = this.y;
	
	// these walk across toward the side opposite the one they start on
	if ((this.x < 0) || (this.x > width))
		this.targetX = width / 2 + (width / 2 - this.x);
	if ((this.y < 0) || (this.y > height))
		this.targetY = height / 2 + (height / 2 - this.y); 	
	
	this.timer = randomIntIn(32, 64); // and time out, then become a turret

	this.sprite = 72;
	
	this.integrity = 4;
	
	this.delay = 0;
	
	this.deploy = function() { // become stationary turret
		if (this.remove == false) {
			this.remove = true;
			// effects
			spawn("turret", this.x, this.y);
		}
	}

	this.update = updateTurretBug;
	this.render = renderEntitySprite;
	this.damage = damageEntitySimple;
	this.jiggly = false;
}


function updateTurretBug() {
	cellRegulatorCount --; // decrease for chew chews
	var index = 0;

	var testX, testY;
	// eat
	var range = 6;
	for (var oY = -range; oY <= range; oY ++) {
		for (var oX = -range; oX <= range; oX ++) {
			testX = Math.floor(this.x + oX);
			testY = Math.floor(this.y + oY);
			if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
				state[index][testX][testY] = emptyState;
			}
		}
	}
		
	var offsetX = 0;
	var offsetY = 0;
	var scale = 0.01;
		
	// move toward target
	offsetX = this.targetX - this.x;
	offsetY = this.targetY - this.y;
	scale = 0.005;
	this.velocityX += scale * offsetX;
	this.velocityY += scale * offsetY;
	
	// velocity limit
	var velocity = Math.sqrt(Math.pow(this.velocityX, 2) + Math.pow(this.velocityY, 2));
	if (velocity > this.velocityMax) {
		this.velocityX = this.velocityMax * this.velocityX / velocity;
		this.velocityY = this.velocityMax * this.velocityY / velocity;
	}	
	// eat player
	if ((playerCharacter != null)
	&& (playerCharacter.remove == false)) {
		var limitX = (this.sizeX + playerCharacter.sizeX) * 0.5;
		var limitY = (this.sizeY + playerCharacter.sizeY) * 0.5;
		offsetX = playerCharacter.x - this.x;
		offsetY = playerCharacter.y - this.y;
		if ((Math.abs(offsetX) < limitX) && (Math.abs(offsetY) < limitY)) {
			playerCharacter.damage(1);
			// replace with kill function
		}
	}
	
	if (this.delay > 0)
		this.delay --;
	if (this.delay <= 0) {
		this.x += this.velocityX;
		this.y += this.velocityY;
		//var friction = 0.85;
		//this.velocityX *= friction;
		//this.velocityY *= friction;
	}

	
	// deploy
	if (this.timer > 0) {
		this.timer --;
		if (this.timer <= 0)
			this.deploy();
	}
	
	// this won't ever happen because these deploy as stationary turrets
	
	// edge behavior
	// if we haven't entered yet and we're within bounds, mark us as having entered
	if ((this.hasEntered == false)
	&& ((this.x > 0)
	&& (this.x < width)
	&& (this.y > 0)
	&& (this.y < height)))
		this.hasEntered = true;
	// if we've already entered, exit at edges
	var buffer = 8;
	if (this.hasEntered == true) {
		var exit = false;
		if (this.x > width + buffer)
			exit = true;
		if (this.x < 0 - buffer)
			exit = true;
		if (this.y < 0 - buffer)
			exit = true;
		if (this.y > height + buffer)
			exit = true;
		// exit
		if (exit)
			this.remove = true;
	}
	
	// damage animation
	if (this.damageTimer > 0) {
		this.damageTimer --;
		if (this.damageTimer > 0) {
			this.sprite = 46;
			this.jiggly = true;
		}
		if (this.damageTimer <= 0) {
			this.sprite = 44;
			this.jiggly = false;
		}
	}
}


// shuttles are multipurpose things which tow other objects
// they can tow them all the way across the screen, or drop them off
function Shuttle(_x, _y) {
	this.remove = false;
	this.score = 4;
	this.hasEntered = false;
	this.x = _x;
	this.y = _y;
	this.sizeX = 6;
	this.sizeY = 6;

	this.velocityX = 0;
	this.velocityY = 0;
	this.velocityMax = 1;

	this.targetX = this.x;
	this.targetY = this.y;
	
	// these walk across toward the side opposite the one they start on
	if ((this.x < 0) || (this.x > width))
		this.targetX = width / 2 + (width / 2 - this.x);
	if ((this.y < 0) || (this.y > height))
		this.targetY = height / 2 + (height / 2 - this.y); 	
	
	this.followTarget = undefined;

	// for towed objects:
	// if the timer has a value greater than 0, then tick down and exit follow mode
	this.timer = -1;
	// this is not undefined, then we have something to convert to 
	this.transformType = undefined;

	this.sprite = 80;
	if (this.targetY > this.y)
		this.sprite += 2;
	if (this.targetX < this.x)
		this.sprite += 4;
	if (this.targetY < this.y)
		this.sprite += 6;

	
	this.integrity = 4;
	
	this.delay = 0;

	this.update = updateShuttle;
	this.render = renderEntitySprite;
	this.damage = damageEntitySimple;
	this.jiggly = false;
}


function updateShuttle() {
	var index = 0;

	var testX, testY;
	// eat
	var range = 6;
	for (var oY = -range; oY <= range; oY ++) {
		for (var oX = -range; oX <= range; oX ++) {
			testX = Math.floor(this.x + oX);
			testY = Math.floor(this.y + oY);
			if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
				state[index][testX][testY] = emptyState;
			}
		}
	}
		
	var offsetX = 0;
	var offsetY = 0;
	var scale = 0.01;
		
	// if we are following an entity
	if (this.followTarget != undefined) {
		this.targetX = this.followTarget.x;
		this.targetY = this.followTarget.y;
	}

	// move toward target
	offsetX = this.targetX - this.x;
	offsetY = this.targetY - this.y;
	scale = 0.01;
	this.velocityX += scale * offsetX;
	this.velocityY += scale * offsetY;
	
	// velocity limit
	var velocity = Math.sqrt(Math.pow(this.velocityX, 2) + Math.pow(this.velocityY, 2));
	if (velocity > this.velocityMax) {
		this.velocityX = this.velocityMax * this.velocityX / velocity;
		this.velocityY = this.velocityMax * this.velocityY / velocity;
	}	
	
	damageTouchingPlayers(this);
	
	if (this.delay > 0)
		this.delay --;
	if (this.delay <= 0) {
		this.x += this.velocityX;
		this.y += this.velocityY;
		//var friction = 0.85;
		//this.velocityX *= friction;
		//this.velocityY *= friction;
	}

	
	// deploy
	if (this.timer > 0) {
		// maybe have a message chain which sends deploy messages back to towed objects, as desirable?
		this.timer --;
		if (this.timer <= 0) {
			// remove this
			this.remove = true;
			// add our deploy type			
			if (this.transformType != undefined)
				spawn(this.transformType, this.x, this.y);
		}
	}
	
	// edge behavior
	// if we haven't entered yet and we're within bounds, mark us as having entered
	edgeBehavior(this);
	
	// damage animation
	damageAnimationUpdate(this);
}


// if none, return 0 length array
// if only one, return array with length 1 containing player character.
// otherwise return all in an array
function getPlayerCharacters() {
	// for now just works for single hard coded player
	// but this format lets us future proof this for 2+ players!
	var player = [];
	
	for (var i = 0; i < 2; i ++) {
		if ((playerArray[i] != undefined)
		&& (playerArray[i].remove == false))
			player.push(playerArray[i]);
	}

	return player;
}


/*
--------------------------------------------------------------------------------
modular behavior
--------------------------------------------------------------------------------
*/

function damageAnimationUpdate(object) {
	// damage animation
	if (object.damageTimer > 0) {
		object.damageTimer --;
		if (object.damageTimer > 0) {
			object.jiggly = true;
		}
		if (object.damageTimer <= 0) {
			object.jiggly = false;
		}
	}
}
// note: 
// damageEntitySimple sets the damageTimer when an entity is damaged
// renderEntitySprite has a switch in it which uses tinter sprite draw when the timer is > 0


function damageTouchingPlayers(object) {
	//uses sizeX and sizeY
	
	// get all players in list
	var player = getPlayerCharacters();
	for (var i in player) {
		var current =  player[i];
		var limitX = (object.sizeX + current.sizeX);
		var limitY = (object.sizeY + current.sizeY);
		offsetX = current.x - object.x;
		offsetY = current.y - object.y;
		if ((Math.abs(offsetX) < limitX) && (Math.abs(offsetY) < limitY)) {
			current.damage(1);
		}
	}
}


function edgeBehavior(object) {
	// if we haven't entered yet and we're within bounds, mark us as having entered
	if ((object.hasEntered == false)
	&& ((object.x > 0)
	&& (object.x < width)
	&& (object.y > 0)
	&& (object.y < height)))
		object.hasEntered = true;
	// if we've already entered, exit at edges
	var buffer = 4;
	if (object.hasEntered == true) {
		var exit = false;
		if (object.x > width + buffer)
			exit = true;
		if (object.x < 0 - buffer)
			exit = true;
		if (object.y < 0 - buffer)
			exit = true;
		if (object.y > height + buffer)
			exit = true;
		// exit
		if (exit)
			object.remove = true;
	}
}


function isRectangleSolid(x, y, sizeX, sizeY) {
	var solid = false;
	for (var oY = -sizeY; oY <= sizeY; oY ++) {
		for (var oX = -sizeX; oX <= sizeX; oX ++) {
			var testX = Math.floor(x + oX);
			var testY = Math.floor(y + oY);
			if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
				var testState = state[0][testX][testY];
				if (testState != emptyState) {
					solid = true;
				}
			}
		}
	}
	return solid;
}


function updateSeeker() {
	enemyCount ++;
	var index = 0;
		
	var offsetX = 0;
	var offsetY = 0;
	var scale = 0.01;
	
	/*
	// move away from avoid point
	var i2 = 0;
	var avoidCount = 0;
	if (this.avoidPoint.length > 0) {
		while (i2 < this.avoidPoint.length) {
			offsetX = this.avoidPoint[i2].x - this.x;
			offsetY = this.avoidPoint[i2].y - this.y;
			var distance = Math.max(Math.floor(Math.abs(offsetX) + width) % width, Math.floor(Math.abs(offsetY) + height) % height);
			var limit = 6;
			if (distance < limit) {
				scale = -1.0;
				this.velocityX += (limit - distance) * scale * offsetX;
				this.velocityY += (limit - distance) * scale * offsetY;
				avoidCount ++;
			}
			if ((distance > 10)
			|| (state[index][this.avoidPoint[i2].x][this.avoidPoint[i2].y] == emptyState)) {
				// remove this avoid point
				this.avoidPoint.splice(i2, 1);
			}
			else
				i2 ++;
		}	
	}
	*/

	// move toward target
	if (this.exploding == false) {
		offsetX = this.targetX - this.x;
		offsetY = this.targetY - this.y;
		var distance = Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2));
		this.velocityX += this.acceleration * offsetX / distance;
		this.velocityY += this.acceleration * offsetY / distance;
	}
	
	// velocity limit
	var velocity = Math.sqrt(Math.pow(this.velocityX, 2) + Math.pow(this.velocityY, 2));
	if (velocity > this.velocityMax) {
		this.velocityX = this.velocityMax * this.velocityX / velocity;
		this.velocityY = this.velocityMax * this.velocityY / velocity;
	}	
	// eat player
	if ((playerCharacter != null)
	&& (playerCharacter.remove == false)) {
		var limitX = (this.sizeX + playerCharacter.sizeX);
		var limitY = (this.sizeY + playerCharacter.sizeY);
		offsetX = playerCharacter.x - this.x;
		offsetY = playerCharacter.y - this.y;
		if ((Math.abs(offsetX) < limitX) && (Math.abs(offsetY) < limitY)) {
			// replace with kill function
			playerCharacter.damage(1);
		}
	}
	
	this.x += this.velocityX;
	this.y += this.velocityY;
	var friction = 0.95;
	this.velocityX *= friction;
	this.velocityY *= friction;
	// add: max velocity limit
		
	// edge behavior
	// if we haven't entered yet and we're within bounds, mark us as having entered
	if ((this.hasEntered == false)
	&& ((this.x > 0)
	&& (this.x < width)
	&& (this.y > 0)
	&& (this.y < height)))
		this.hasEntered = true;
	// if we've already entered, exit at edges
	var buffer = 4;
	if (this.hasEntered == true) {
		var exit = false;
		if (this.x > width + buffer)
			exit = true;
		if (this.x < 0 - buffer)
			exit = true;
		if (this.y < 0 - buffer)
			exit = true;
		if (this.y > height + buffer)
			exit = true;
		// exit
		if (exit)
			this.remove = true;
	}

	if (this.exploding == true) {
		//this.x = jitter(this.x, 1);
		//this.y = jitter(this.y, 1);
		this.explodeTimer --;
		if (Math.random() < 0.1) {
			var radius = 2;
			effect.push(new Puff(jitter(this.x, radius), jitter(this.y, radius), 8, 255, 0, 255));
		}
		if (this.explodeTimer <= 0) {
			this.remove = true;
			this.warningRectangle.needed = false;
			addNoiseToRegion(this.x, this.y, this.blastRadius);
		}
	}
	
	if ((this.remove == false)
	&& (this.hasEntered == true)) {
		//this.x = jitter(this.x, 0.5);
		//this.y = jitter(this.y, 0.5);

		// sense surroundings
		// obstacle avoidance
		// test ahead
		if ((this.eatsFood)
		&& (this.targetTimer % 2 == 0)) {
			var checkRange = randomIn(1, 12);
			var checkAngle = randomIn(0, 2 * Math.PI);
			testX = Math.floor(this.x + checkRange * Math.cos(checkAngle));
			testY = Math.floor(this.y + checkRange * Math.sin(checkAngle));
			//testX = (Math.floor(testX) + width) % width;
			//testY = (Math.floor(testY) + height) % height;
			if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
				var testState = state[index][testX][testY];
				if (testState == damageState) { // go toward food
					this.targetX = testX;
					this.targetY = testY;
				}				
			}
			/*
			else if (testState == 2) { // avoid obstacles
				var newPoint = {x:testX, y:testY};
				this.avoidPoint.push(newPoint);
			}
			*/
		}
			
		// if we've arrived at the target
		offsetX = this.targetX - this.x;
		offsetY = this.targetY - this.y;
		var distance = Math.max(Math.abs(offsetX), Math.abs(offsetY));
		limit = 1;
		if ((this.hasEntered)
		&& (this.targetTimer > 60)
		|| (distance < limit)) {
			this.targetTimer = 0;
			//effect.push(new ShrinkingRectangle(this.x, this.y, 4, 128, 255, 0));
			this.targetX = jitter(this.x, 32);
			this.targetY = jitter(this.y, 32);
			var error = 16;
			if ((playerCharacter != null)
			&& (playerCharacter.remove == false)
			&& ((Math.abs(playerCharacter.x - this.x) < 80) && (Math.abs(playerCharacter.y - this.y) < 80))) {
				this.targetX = jitter(playerCharacter.x, error);
				this.targetY = jitter(playerCharacter.y, error);
			}
			// maybe only check target position every so often, to add a perception delay?
		}
		this.targetTimer ++;
	}
	
	// damage animation
	if (this.damageTimer > 0) {
		this.damageTimer --;
		if (this.damageTimer > 0) {
			this.sprite = 38;
			this.jiggly = true;
		}
		if (this.damageTimer <= 0) {
			this.sprite = 38;
			this.jiggly = false;
		}
	}
}


// if an entity has a list of other entities it manages (for a creature pump, for instance) run this every so often to clean up removed entities, etc.
// then any counting attached to the list will be accurate.
function update_managed_list(object) {
	if (object.managed_entity != undefined) {
		var i = 0;
		while (i < object.managed_entity.length) {
			if (object.managed_entity[i].remove == true) {
				object.managed_entity.splice(i, 1);
			}
			else
				i ++;
		}
	}
}


function updateMother() {
	update_managed_list(this);

	enemyCount ++;
	var index = 0;
		
	var offsetX = 0;
	var offsetY = 0;
	var scale = 0.01;
		
	// move toward target
	offsetX = this.targetX - this.x;
	offsetY = this.targetY - this.y;
	scale = 0.0025;
	this.velocityX += scale * offsetX;
	this.velocityY += scale * offsetY;
	
	// velocity limit
	var velocity = Math.sqrt(Math.pow(this.velocityX, 2) + Math.pow(this.velocityY, 2));
	if (velocity > this.velocityMax) {
		this.velocityX = this.velocityMax * this.velocityX / velocity;
		this.velocityY = this.velocityMax * this.velocityY / velocity;
	}	
	// eat player
	if ((playerCharacter != null)
	&& (playerCharacter.remove == false)) {
		var limitX = (this.sizeX + playerCharacter.sizeX) * 0.5;
		var limitY = (this.sizeY + playerCharacter.sizeY) * 0.5;
		offsetX = playerCharacter.x - this.x;
		offsetY = playerCharacter.y - this.y;
		if ((Math.abs(offsetX) < limitX) && (Math.abs(offsetY) < limitY)) {
			playerCharacter.damage(1);
		}
	}
	
	
	this.x += this.velocityX;
	this.y += this.velocityY;
	var friction = 0.85;
	this.velocityX *= friction;
	this.velocityY *= friction;
	// add: max velocity limit

		
	// edge behavior
	// if we haven't entered yet and we're within bounds, mark us as having entered
	if ((this.hasEntered == false)
	&& ((this.x > 0)
	&& (this.x < width)
	&& (this.y > 0)
	&& (this.y < height)))
		this.hasEntered = true;
	// if we've already entered, exit at edges
	var buffer = 4;
	if (this.hasEntered == true) {
		var exit = false;
		if (this.x > width + buffer)
			exit = true;
		if (this.x < 0 - buffer)
			exit = true;
		if (this.y < 0 - buffer)
			exit = true;
		if (this.y > height + buffer)
			exit = true;
		// exit
		if (exit)
			this.remove = true;
	}
	
	if (this.remove == false) {
		this.timer ++;
		//this.x = jitter(this.x, 0.5);
		//this.y = jitter(this.y, 0.5);
			
		if ((this.timer % 4 == 0)
		&& (this.hasEntered)) {
			var testX, testY;
			for (var oY = -this.sizeY; oY <= this.sizeY; oY ++) {
				for (var oX = -this.sizeX; oX <= this.sizeX; oX ++) {
					var testX = (Math.floor(this.x + oX) + width) % width;
					var testY = (Math.floor(this.y + oY) + height) % height;
					var testState = state[index][testX][testY];
					/*
					if (testState == 2) {
						// possibly check over wider area
						this.remove = true;
					}
					*/
					if (testState != emptyState) {
						// eat
						state[index][testX][testY] = emptyState;
						// add energy or something
						this.energy ++;
					}
				}
			}

			// starfish babies
			if ((this.timer > 60)
			&& (this.energy > 20)
			&& (this.managed_entity.length < starfish_brood_limit)
			&& (previousEnemyCount < desiredEnemyCount + 5)) {
				this.timer = 0;
				for (var i = 0; i < 2; i ++) {
					// note that this is outside of the spawn management system-
					// so one can't exploit these to reach the end of a wave sooner
					// there's incentive to dispatch these quickly
					var newMonster = new Hunter(jitter(this.x, 6), jitter(this.y, 6));
					newMonster.sprite = 36;
					newMonster.integrity = 1;
					newMonster.velocityX = jitter(0, 5);
					newMonster.velocityY = jitter(0, 5);
					newMonster.velocityMax = 1.0;
					newMonster.score = 0;
					newMonster.eatsFood = false;

					// add to our managed list
					this.managed_entity.push(newMonster);
					// add to manager
					entity.push(newMonster);
					this.energy -= 4;
					effect.push(new ShrinkingRectangle(newMonster, 64, 128, 255, 0));
				}
			}

			// sense surroundings
			// obstacle avoidance
			// test ahead
			if (true) {
				var lookAhead = 4;
				var checkRange = randomIn(1, 12);
				var checkAngle = randomIn(0, 2 * Math.PI);
				
				testX = Math.floor(this.x + checkRange * Math.cos(checkAngle));
				testY = Math.floor(this.y + checkRange * Math.sin(checkAngle));
				//testX = (Math.floor(testX) + width) % width;
				//testY = (Math.floor(testY) + height) % height;
				if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
					var testState = state[index][testX][testY];
					if (testState != emptyState) { // go toward food
						this.targetX = testX;
						this.targetY = testY;
					}				
				}
				/*
				else if (testState == 2) { // avoid obstacles
					var newPoint = {x:testX, y:testY};
					this.avoidPoint.push(newPoint);
				}
				*/
			}
		}			
		// if we've arrived at the target
		offsetX = this.targetX - this.x;
		offsetY = this.targetY - this.y;
		var distance = Math.max(Math.abs(offsetX), Math.abs(offsetY));
		limit = 1;
		if ((this.hasEntered)
		&& (this.targetTimer > 60)
		|| (distance < limit)) {
			this.targetTimer = 0;
			var margin = 4;
			this.targetX = jitter(this.x, 32);
			this.targetY = jitter(this.y, 32);
			this.targetX = Math.max(0 + margin, Math.min(width - margin, this.targetX));
			this.targetY = Math.max(0 + margin, Math.min(height - margin, this.targetY));
			if ((playerCharacter != null)
			&& (playerCharacter.remove == false)
			&& ((Math.abs(playerCharacter.x - this.x) < 64) && (Math.abs(playerCharacter.y - this.y) < 64))) {
				this.targetX = playerCharacter.x;
				this.targetY = playerCharacter.y;
			}
			// maybe only check target position every so often, to add a perception delay?
		}
		this.targetTimer ++;
	}

	// damage animation
	damageAnimationUpdate(this);
}


function updateShy() {
	enemyCount ++;
	var index = 0;
		
	var offsetX = 0;
	var offsetY = 0;
	var scale = 0.01;
		
	// move toward target
	offsetX = this.targetX - this.x;
	offsetY = this.targetY - this.y;
	scale = 0.025;
	this.velocityX += scale * offsetX;
	this.velocityY += scale * offsetY;
	
	// velocity limit
	var velocity = Math.sqrt(Math.pow(this.velocityX, 2) + Math.pow(this.velocityY, 2));
	if (velocity > this.velocityMax) {
		this.velocityX = this.velocityMax * this.velocityX / velocity;
		this.velocityY = this.velocityMax * this.velocityY / velocity;
	}	
	// eat player
	damageTouchingPlayers(this);
	
	
	this.x += this.velocityX;
	this.y += this.velocityY;
	var friction = 0.5;
	this.velocityX *= friction;
	this.velocityY *= friction;
	// add: max velocity limit

		
	// edge behavior
	edgeBehavior(this);
	
	if (this.remove == false) {
		this.timer ++;
		//this.x = jitter(this.x, 0.5);
		//this.y = jitter(this.y, 0.5);
			
		if (this.hasEntered) {
			var testX, testY;
			for (var oY = -this.sizeY; oY <= this.sizeY; oY ++) {
				for (var oX = -this.sizeX; oX <= this.sizeX; oX ++) {
					var testX = (Math.floor(this.x + oX) + width) % width;
					var testY = (Math.floor(this.y + oY) + height) % height;
					if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
						var testState = state[index][testX][testY];
						if (testState != emptyState) {
							// eat
							state[index][testX][testY] = emptyState;
							// add energy or something
							this.energy ++;
						}
					}
				}
			}

			// fire
			if ((noBulletTimer == 0)
			&& (previousEnemyProjectileCount < desiredEnemyProjectileCount)
			&& (this.timer > 40)
			&& (this.energy > 12)) {
				this.timer = 0;
				this.energy -= 12;
				// fire
				if ((playerCharacter != null)
				&& (playerCharacter.remove == false)) {
					var error = 8;
					var newProjectile = new MonsterBolt(this.x, this.y, jitter(playerCharacter.x, error), jitter(playerCharacter.y, error));
					entity.push(newProjectile);
				}
				else {
					var newProjectile = new MonsterBolt(this.x, this.y, randomIn(0, width), randomIn(0, height));
					entity.push(newProjectile);
				}
				previousEnemyProjectileCount ++; // this bumps up our bookkeeping so we can't create too many projectiles in one frame
				// add firing effect
				playSound("shy fire");
			}
			// sense surroundings
			// obstacle avoidance
			// test ahead
			if (true) {
				var lookAhead = 4;
				var checkRange = randomIn(1, 12);
				var checkAngle = randomIn(0, 2 * Math.PI);
				
				testX = Math.floor(this.x + checkRange * Math.cos(checkAngle));
				testY = Math.floor(this.y + checkRange * Math.sin(checkAngle));
				//testX = (Math.floor(testX) + width) % width;
				//testY = (Math.floor(testY) + height) % height;
				if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
					var testState = state[index][testX][testY];
					if (testState != emptyState) { // go toward food
						this.targetX = testX;
						this.targetY = testY;
					}				
				}
				/*
				else if (testState == 2) { // avoid obstacles
					var newPoint = {x:testX, y:testY};
					this.avoidPoint.push(newPoint);
				}
				*/
			}
		}			
		// if we've arrived at the target
		offsetX = this.targetX - this.x;
		offsetY = this.targetY - this.y;
		var distance = Math.max(Math.abs(offsetX), Math.abs(offsetY));
		limit = 1;
		if ((this.hasEntered)
		&& (this.targetTimer > 60)
		|| (distance < limit)) {
			this.targetTimer = 0;
			var margin = 4;
			this.targetX = jitter(this.x, 32);
			this.targetY = jitter(this.y, 32);
			// add conditional for choosing to leave? time out and then leave sometimes?
			this.targetX = Math.max(0 + margin, Math.min(width - margin, this.targetX));
			this.targetY = Math.max(0 + margin, Math.min(height - margin, this.targetY));
			// add avoidant behavior
			// maybe only check target position every so often, to add a perception delay?
		}
		this.targetTimer ++;
	}

	// damage animation
	damageAnimationUpdate(this);
}


function updateSower() {
	cellRegulatorCount ++; // increase for sowers
	var index = 0;

	if ((clock % SOWER_NOISE_DRAW_RATE) == 0) { // every fourth frame
		var testX, testY;
		// fill area around sower with random cell values
		var range = sowerRange;
		if (liveCellCount < minimumLiveCellCount + 2048) {
		// if (true) {
			for (var oY = -range; oY <= range; oY ++) {
				for (var oX = -range; oX <= range; oX ++) {
					testX = Math.floor(this.x + oX);
					testY = Math.floor(this.y + oY);
					if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
						state[index][testX][testY] = randomInt(3);
					}
				}
			}
		}
	}
	
	var offsetX = 0;
	var offsetY = 0;
	var scale = 0.01;
		
	// move toward target
	offsetX = this.targetX - this.x;
	offsetY = this.targetY - this.y;
	scale = 0.0025;
	this.velocityX += scale * offsetX;
	this.velocityY += scale * offsetY;
	
	// velocity limit
	var velocity = Math.sqrt(Math.pow(this.velocityX, 2) + Math.pow(this.velocityY, 2));
	if (velocity > this.velocityMax) {
		this.velocityX = this.velocityMax * this.velocityX / velocity;
		this.velocityY = this.velocityMax * this.velocityY / velocity;
	}	
	// eat player
	if ((playerCharacter != null)
	&& (playerCharacter.remove == false)) {
		var limitX = (this.sizeX + playerCharacter.sizeX) * 0.5;
		var limitY = (this.sizeY + playerCharacter.sizeY) * 0.5;
		offsetX = playerCharacter.x - this.x;
		offsetY = playerCharacter.y - this.y;
		if ((Math.abs(offsetX) < limitX) && (Math.abs(offsetY) < limitY)) {
			playerCharacter.remove = true;
			// replace with kill function
		}
	}
	
	if (this.delay > 0)
		this.delay --;
	if (this.delay <= 0) {
		this.x += this.velocityX;
		this.y += this.velocityY;
		//var friction = 0.85;
		//this.velocityX *= friction;
		//this.velocityY *= friction;
	}
	// add: max velocity limit

		
	// edge behavior
	// if we haven't entered yet and we're within bounds, mark us as having entered
	if ((this.hasEntered == false)
	&& ((this.x > 0)
	&& (this.x < width)
	&& (this.y > 0)
	&& (this.y < height)))
		this.hasEntered = true;
	// if we've already entered, exit at edges
	var buffer = 8;
	if (this.hasEntered == true) {
		var exit = false;
		if (this.x > width + buffer)
			exit = true;
		if (this.x < 0 - buffer)
			exit = true;
		if (this.y < 0 - buffer)
			exit = true;
		if (this.y > height + buffer)
			exit = true;
		// exit
		if (exit)
			this.remove = true;
	}
}


function updateChewChew() {
	cellRegulatorCount --; // decrease for chew chews
	var index = 0;

	var testX, testY;
	// eat
	var range = 6;
	for (var oY = -range; oY <= range; oY ++) {
		for (var oX = -range; oX <= range; oX ++) {
			testX = Math.floor(this.x + oX);
			testY = Math.floor(this.y + oY);
			if ((testX >= 0) && (testX < width) && (testY >= 0) && (testY < height)) {
				state[index][testX][testY] = emptyState;
			}
		}
	}
		
	var offsetX = 0;
	var offsetY = 0;
	var scale = 0.01;
		
	// move toward target
	offsetX = this.targetX - this.x;
	offsetY = this.targetY - this.y;
	scale = 0.0025;
	this.velocityX += scale * offsetX;
	this.velocityY += scale * offsetY;
	
	// velocity limit
	var velocity = Math.sqrt(Math.pow(this.velocityX, 2) + Math.pow(this.velocityY, 2));
	if (velocity > this.velocityMax) {
		this.velocityX = this.velocityMax * this.velocityX / velocity;
		this.velocityY = this.velocityMax * this.velocityY / velocity;
	}	
	// eat player
	if ((playerCharacter != null)
	&& (playerCharacter.remove == false)) {
		var limitX = (this.sizeX + playerCharacter.sizeX) * 0.5;
		var limitY = (this.sizeY + playerCharacter.sizeY) * 0.5;
		offsetX = playerCharacter.x - this.x;
		offsetY = playerCharacter.y - this.y;
		if ((Math.abs(offsetX) < limitX) && (Math.abs(offsetY) < limitY)) {
			playerCharacter.damage(1);
			// replace with kill function
		}
	}
	
	if (this.delay > 0)
		this.delay --;
	if (this.delay <= 0) {
		this.x += this.velocityX;
		this.y += this.velocityY;
		//var friction = 0.85;
		//this.velocityX *= friction;
		//this.velocityY *= friction;
	}
	// add: max velocity limit

		
	// edge behavior
	// if we haven't entered yet and we're within bounds, mark us as having entered
	if ((this.hasEntered == false)
	&& ((this.x > 0)
	&& (this.x < width)
	&& (this.y > 0)
	&& (this.y < height)))
		this.hasEntered = true;
	// if we've already entered, exit at edges
	var buffer = 8;
	if (this.hasEntered == true) {
		var exit = false;
		if (this.x > width + buffer)
			exit = true;
		if (this.x < 0 - buffer)
			exit = true;
		if (this.y < 0 - buffer)
			exit = true;
		if (this.y > height + buffer)
			exit = true;
		// exit
		if (exit)
			this.remove = true;
	}
	
	// damage animation
	damageAnimationUpdate(this);
}




function renderHunterTest() {
	if ((this.x >= 0) && (this.x <= width) && (this.y >= 0) && (this.y <= height))
	drawDot(this.x, this.y, this.r, this.g, this.b);

	// test point
	if (this.testX && this.testY)
		drawDot(this.testX, this.testY, 255, 64, 0);

	drawLine(this.x, this.y, this.targetX, this.targetY, 0, 16, 0);


	for (var i2 = 0; i2 < this.avoidPoint.length; i2 ++) {
		offsetX = this.avoidPoint[i2].x - this.x;
		offsetY = this.avoidPoint[i2].y - this.y;
		var distance = Math.max(Math.abs(offsetX) , Math.abs(offsetY));
		var limit = 16;
		if (distance < limit) {
			drawLine(this.x, this.y, this.avoidPoint[i2].x, this.avoidPoint[i2].y, (16 - distance) * 16, 0, 0);
		}
	}
	
	var spriteSize = 8;
	// if offscreen
	if ((this.x > width) || (this.x < 0) || (this.y > height) || (this.y < 0)) {
		var markerX = clamp(this.x, 0, width);
		var markerY = clamp(this.y, 0, height);
		baseImage = 40;
		var left = (baseImage % spritesAcross) * spriteSize;
		var top = Math.floor(baseImage / spritesAcross) * spriteSize;
		var right = left + spriteSize - 1;
		var bottom = top + spriteSize - 1;
		copyImageDataRegion(spriteImage, offscreenImageData, Math.floor(markerX - spriteSize / 2), Math.floor(markerY - spriteSize / 2), left, top, right, bottom);
	}

}
