@@ -513,78 +513,78 @@ class Fly { |
| 513 | 513 | } |
| 514 | 514 | } |
| 515 | 515 | |
| 516 | | - checkWebCollisions() { |
| 517 | | - let currentlyTouching = new Set(); |
| 516 | + checkWebCollisions () { |
| 517 | + let currentlyTouching = new Set() |
| 518 | 518 | |
| 519 | 519 | for (let strand of webStrands) { |
| 520 | | - let touching = false; |
| 520 | + let touching = false |
| 521 | 521 | |
| 522 | 522 | // Check collision with strand path |
| 523 | 523 | if (strand.path && strand.path.length > 1) { |
| 524 | 524 | // OPTIMIZATION: Skip every other point for collision detection |
| 525 | 525 | for (let i = 0; i < strand.path.length - 1; i += 2) { |
| 526 | | - let p1 = strand.path[i]; |
| 527 | | - let p2 = strand.path[Math.min(i + 1, strand.path.length - 1)]; |
| 528 | | - let d = this.pointToLineDistance(this.pos, p1, p2); |
| 526 | + let p1 = strand.path[i] |
| 527 | + let p2 = strand.path[Math.min(i + 1, strand.path.length - 1)] |
| 528 | + let d = this.pointToLineDistance(this.pos, p1, p2) |
| 529 | 529 | if (d < this.radius + 3) { |
| 530 | | - touching = true; |
| 531 | | - break; |
| 530 | + touching = true |
| 531 | + break |
| 532 | 532 | } |
| 533 | 533 | } |
| 534 | 534 | } else if (strand.start && strand.end) { |
| 535 | 535 | // Fallback for strands without path |
| 536 | | - let d = this.pointToLineDistance(this.pos, strand.start, strand.end); |
| 536 | + let d = this.pointToLineDistance(this.pos, strand.start, strand.end) |
| 537 | 537 | if (d < this.radius + 3) { |
| 538 | | - touching = true; |
| 538 | + touching = true |
| 539 | 539 | } |
| 540 | 540 | } |
| 541 | 541 | |
| 542 | 542 | if (touching) { |
| 543 | | - currentlyTouching.add(strand); |
| 543 | + currentlyTouching.add(strand) |
| 544 | 544 | |
| 545 | 545 | // If this is a new strand we're touching |
| 546 | 546 | if (!this.touchedStrands.has(strand)) { |
| 547 | | - this.touchedStrands.add(strand); |
| 547 | + this.touchedStrands.add(strand) |
| 548 | 548 | |
| 549 | 549 | // Vibrate the web when first touching |
| 550 | | - strand.vibrate(3); |
| 550 | + strand.vibrate(3) |
| 551 | 551 | |
| 552 | 552 | // First strand slows us down |
| 553 | 553 | if (this.touchedStrands.size === 1) { |
| 554 | | - this.currentSpeed = this.baseSpeed * 0.4; // Slow to 40% speed |
| 555 | | - this.slowedBy.add(strand); |
| 554 | + this.currentSpeed = this.baseSpeed * 0.4 // Slow to 40% speed |
| 555 | + this.slowedBy.add(strand) |
| 556 | 556 | |
| 557 | 557 | // Visual feedback - yellow particles for slowing |
| 558 | 558 | // LIMIT PARTICLES TO PREVENT FREEZE |
| 559 | | - let particleCount = Math.min(3, 100 - particles.length); |
| 559 | + let particleCount = Math.min(3, 100 - particles.length) |
| 560 | 560 | for (let j = 0; j < particleCount; j++) { |
| 561 | | - let p = new Particle(this.pos.x, this.pos.y); |
| 562 | | - p.color = color(255, 255, 0, 150); |
| 563 | | - p.vel = createVector(random(-1, 1), random(-1, 1)); |
| 564 | | - p.size = 3; |
| 565 | | - particles.push(p); |
| 561 | + let p = new Particle(this.pos.x, this.pos.y) |
| 562 | + p.color = color(255, 255, 0, 150) |
| 563 | + p.vel = createVector(random(-1, 1), random(-1, 1)) |
| 564 | + p.size = 3 |
| 565 | + particles.push(p) |
| 566 | 566 | } |
| 567 | 567 | } |
| 568 | 568 | // Second strand catches us |
| 569 | 569 | else if (this.touchedStrands.size >= 2 && !this.caught) { |
| 570 | | - this.caught = true; |
| 571 | | - this.currentSpeed = 0; |
| 570 | + this.caught = true |
| 571 | + this.currentSpeed = 0 |
| 572 | 572 | |
| 573 | 573 | // Stronger vibration when caught |
| 574 | | - strand.vibrate(8); |
| 574 | + strand.vibrate(8) |
| 575 | 575 | |
| 576 | 576 | // FIX: OPTIMIZE NEARBY STRAND VIBRATION |
| 577 | 577 | // This is likely the main cause of the freeze - checking distances between all strands |
| 578 | 578 | // Use a more efficient method |
| 579 | | - propagateVibration(strand, 2); |
| 579 | + propagateVibration(strand, 2) |
| 580 | 580 | |
| 581 | 581 | // Create caught particles - LIMIT TO PREVENT FREEZE |
| 582 | | - let particleCount = Math.min(6, 100 - particles.length); |
| 582 | + let particleCount = Math.min(6, 100 - particles.length) |
| 583 | 583 | for (let j = 0; j < particleCount; j++) { |
| 584 | | - let p = new Particle(this.pos.x, this.pos.y); |
| 585 | | - p.color = color(255, 200, 0, 200); |
| 586 | | - p.vel = createVector(random(-2, 2), random(-2, 2)); |
| 587 | | - particles.push(p); |
| 584 | + let p = new Particle(this.pos.x, this.pos.y) |
| 585 | + p.color = color(255, 200, 0, 200) |
| 586 | + p.vel = createVector(random(-2, 2), random(-2, 2)) |
| 587 | + particles.push(p) |
| 588 | 588 | } |
| 589 | 589 | } |
| 590 | 590 | } |
@@ -593,32 +593,34 @@ class Fly { |
| 593 | 593 | |
| 594 | 594 | // If we're no longer touching strands we were slowed by, speed back up |
| 595 | 595 | if (this.slowedBy.size > 0 && currentlyTouching.size === 0) { |
| 596 | | - this.currentSpeed = this.baseSpeed; |
| 597 | | - this.slowedBy.clear(); |
| 596 | + this.currentSpeed = this.baseSpeed |
| 597 | + this.slowedBy.clear() |
| 598 | 598 | } |
| 599 | 599 | } |
| 600 | 600 | |
| 601 | | - pointToLineDistance(point, lineStart, lineEnd) { |
| 601 | + pointToLineDistance (point, lineStart, lineEnd) { |
| 602 | 602 | // Add null checks |
| 603 | | - if (!point || !lineStart || !lineEnd) return Infinity; |
| 604 | | - |
| 605 | | - let dx = lineEnd.x - lineStart.x; |
| 606 | | - let dy = lineEnd.y - lineStart.y; |
| 607 | | - let lineLength = sqrt(dx * dx + dy * dy); |
| 608 | | - |
| 603 | + if (!point || !lineStart || !lineEnd) return Infinity |
| 604 | + |
| 605 | + let dx = lineEnd.x - lineStart.x |
| 606 | + let dy = lineEnd.y - lineStart.y |
| 607 | + let lineLength = sqrt(dx * dx + dy * dy) |
| 608 | + |
| 609 | 609 | // If line has no length, return distance to point |
| 610 | 610 | if (lineLength < 0.01) { |
| 611 | | - return dist(point.x, point.y, lineStart.x, lineStart.y); |
| 611 | + return dist(point.x, point.y, lineStart.x, lineStart.y) |
| 612 | 612 | } |
| 613 | | - |
| 613 | + |
| 614 | 614 | // Use optimized calculation without creating new vectors |
| 615 | | - let t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / (lineLength * lineLength); |
| 616 | | - t = constrain(t, 0, 1); |
| 617 | | - |
| 618 | | - let closestX = lineStart.x + t * dx; |
| 619 | | - let closestY = lineStart.y + t * dy; |
| 620 | | - |
| 621 | | - return dist(point.x, point.y, closestX, closestY); |
| 615 | + let t = |
| 616 | + ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / |
| 617 | + (lineLength * lineLength) |
| 618 | + t = constrain(t, 0, 1) |
| 619 | + |
| 620 | + let closestX = lineStart.x + t * dx |
| 621 | + let closestY = lineStart.y + t * dy |
| 622 | + |
| 623 | + return dist(point.x, point.y, closestX, closestY) |
| 622 | 624 | } |
| 623 | 625 | |
| 624 | 626 | display () { |
@@ -1374,379 +1376,399 @@ class FoodBox { |
| 1374 | 1376 | } |
| 1375 | 1377 | |
| 1376 | 1378 | class Bird { |
| 1377 | | - constructor(pattern, isThief = false) { |
| 1378 | | - this.pattern = pattern; // 'dive', 'swoop', 'glide', 'circle' |
| 1379 | | - this.isThief = isThief; |
| 1380 | | - this.active = false; |
| 1381 | | - this.attacking = false; |
| 1382 | | - this.attackDelay = isThief ? 120 : random(30, 90); // MUCH shorter initial delay |
| 1383 | | - |
| 1384 | | - // Position and movement |
| 1385 | | - this.x = random(width); |
| 1386 | | - this.y = -50; // Start above screen |
| 1387 | | - this.vx = 0; |
| 1388 | | - this.vy = 0; |
| 1389 | | - this.targetX = 0; |
| 1390 | | - this.targetY = 0; |
| 1391 | | - this.speed = 5; // Increased from 3 |
| 1392 | | - this.angle = 0; |
| 1393 | | - this.wingPhase = random(TWO_PI); |
| 1394 | | - |
| 1395 | | - // Visual properties |
| 1396 | | - this.size = isThief ? 25 : 20; |
| 1397 | | - this.color = isThief ? color(100, 50, 150) : color(50, 50, 50); |
| 1398 | | - |
| 1399 | | - // Pattern-specific properties |
| 1400 | | - if (pattern === 'circle') { |
| 1401 | | - this.circleRadius = 150; |
| 1402 | | - this.circleAngle = 0; |
| 1403 | | - this.circleCenter = createVector(width/2, height/2); |
| 1404 | | - } |
| 1405 | | - |
| 1406 | | - // Attack properties - MUCH MORE AGGRESSIVE |
| 1407 | | - this.diveSpeed = 12; // Increased from 8 |
| 1408 | | - this.retreatSpeed = 6; // Increased from 4 |
| 1409 | | - this.state = 'waiting'; // 'waiting', 'approaching', 'attacking', 'retreating' |
| 1410 | | - this.consecutiveAttacks = 0; // Track multiple attacks |
| 1411 | | - this.maxConsecutiveAttacks = random(2, 4); // Each bird does 2-4 attacks before retreating |
| 1379 | + constructor (pattern, isThief = false) { |
| 1380 | + this.pattern = pattern // 'dive', 'swoop', 'glide', 'circle' |
| 1381 | + this.isThief = isThief |
| 1382 | + this.active = false |
| 1383 | + this.attacking = false |
| 1384 | + this.attackDelay = isThief ? 120 : random(30, 90) // MUCH shorter initial delay |
| 1385 | + |
| 1386 | + // Position and movement |
| 1387 | + this.x = random(width) |
| 1388 | + this.y = -50 // Start above screen |
| 1389 | + this.vx = 0 |
| 1390 | + this.vy = 0 |
| 1391 | + this.targetX = 0 |
| 1392 | + this.targetY = 0 |
| 1393 | + this.speed = 5 // Increased from 3 |
| 1394 | + this.angle = 0 |
| 1395 | + this.wingPhase = random(TWO_PI) |
| 1396 | + |
| 1397 | + // Visual properties |
| 1398 | + this.size = isThief ? 25 : 20 |
| 1399 | + this.color = isThief ? color(100, 50, 150) : color(50, 50, 50) |
| 1400 | + |
| 1401 | + // Pattern-specific properties |
| 1402 | + if (pattern === 'circle') { |
| 1403 | + this.circleRadius = 150 |
| 1404 | + this.circleAngle = 0 |
| 1405 | + this.circleCenter = createVector(width / 2, height / 2) |
| 1412 | 1406 | } |
| 1413 | | - |
| 1414 | | - update() { |
| 1415 | | - // Update wing animation |
| 1416 | | - this.wingPhase += 0.3; // Faster wing flapping |
| 1417 | | - |
| 1418 | | - // Countdown to attack - MUCH FASTER |
| 1419 | | - if (this.attackDelay > 0) { |
| 1420 | | - this.attackDelay--; |
| 1421 | | - // Hover while waiting - more aggressive hovering |
| 1422 | | - this.y = -30 + sin(frameCount * 0.08) * 15; |
| 1423 | | - this.x += sin(frameCount * 0.05) * 3; |
| 1424 | | - |
| 1425 | | - // Show warning when about to attack |
| 1426 | | - if (this.attackDelay < 30) { |
| 1427 | | - this.y = lerp(this.y, 50, 0.1); // Start moving into view |
| 1428 | | - } |
| 1429 | | - return; |
| 1430 | | - } |
| 1431 | | - |
| 1432 | | - // Activate after delay |
| 1433 | | - if (!this.active) { |
| 1434 | | - this.active = true; |
| 1435 | | - this.state = 'approaching'; |
| 1436 | | - this.updateTarget(); // Set initial target |
| 1437 | | - } |
| 1438 | | - |
| 1439 | | - // Execute movement pattern |
| 1440 | | - switch(this.pattern) { |
| 1441 | | - case 'dive': |
| 1442 | | - this.executeDivePattern(); |
| 1443 | | - break; |
| 1444 | | - case 'swoop': |
| 1445 | | - this.executeSwoopPattern(); |
| 1446 | | - break; |
| 1447 | | - case 'glide': |
| 1448 | | - this.executeGlidePattern(); |
| 1449 | | - break; |
| 1450 | | - case 'circle': |
| 1451 | | - this.executeCirclePattern(); |
| 1452 | | - break; |
| 1453 | | - } |
| 1454 | | - |
| 1455 | | - // Check collisions |
| 1456 | | - this.checkCollisions(); |
| 1457 | | - |
| 1458 | | - // Keep on screen during approach |
| 1459 | | - if (this.state === 'approaching') { |
| 1460 | | - this.x = constrain(this.x, 20, width - 20); |
| 1407 | + |
| 1408 | + // Attack properties - MUCH MORE AGGRESSIVE |
| 1409 | + this.diveSpeed = 12 // Increased from 8 |
| 1410 | + this.retreatSpeed = 6 // Increased from 4 |
| 1411 | + this.state = 'waiting' // 'waiting', 'approaching', 'attacking', 'retreating' |
| 1412 | + this.consecutiveAttacks = 0 // Track multiple attacks |
| 1413 | + this.maxConsecutiveAttacks = random(2, 4) // Each bird does 2-4 attacks before retreating |
| 1414 | + } |
| 1415 | + |
| 1416 | + update () { |
| 1417 | + // Update wing animation |
| 1418 | + this.wingPhase += 0.3 // Faster wing flapping |
| 1419 | + |
| 1420 | + // Countdown to attack - MUCH FASTER |
| 1421 | + if (this.attackDelay > 0) { |
| 1422 | + this.attackDelay-- |
| 1423 | + // Hover while waiting - more aggressive hovering |
| 1424 | + this.y = -30 + sin(frameCount * 0.08) * 15 |
| 1425 | + this.x += sin(frameCount * 0.05) * 3 |
| 1426 | + |
| 1427 | + // Show warning when about to attack |
| 1428 | + if (this.attackDelay < 30) { |
| 1429 | + this.y = lerp(this.y, 50, 0.1) // Start moving into view |
| 1430 | + } |
| 1431 | + return |
| 1432 | + } |
| 1433 | + |
| 1434 | + // Activate after delay |
| 1435 | + if (!this.active) { |
| 1436 | + this.active = true |
| 1437 | + this.state = 'approaching' |
| 1438 | + this.updateTarget() // Set initial target |
| 1439 | + } |
| 1440 | + |
| 1441 | + // Execute movement pattern |
| 1442 | + switch (this.pattern) { |
| 1443 | + case 'dive': |
| 1444 | + this.executeDivePattern() |
| 1445 | + break |
| 1446 | + case 'swoop': |
| 1447 | + this.executeSwoopPattern() |
| 1448 | + break |
| 1449 | + case 'glide': |
| 1450 | + this.executeGlidePattern() |
| 1451 | + break |
| 1452 | + case 'circle': |
| 1453 | + this.executeCirclePattern() |
| 1454 | + break |
| 1455 | + } |
| 1456 | + |
| 1457 | + // Check collisions |
| 1458 | + this.checkCollisions() |
| 1459 | + |
| 1460 | + // Keep on screen during approach |
| 1461 | + if (this.state === 'approaching') { |
| 1462 | + this.x = constrain(this.x, 20, width - 20) |
| 1463 | + } |
| 1464 | + } |
| 1465 | + |
| 1466 | + updateTarget () { |
| 1467 | + if (this.isThief) { |
| 1468 | + // Target caught flies |
| 1469 | + let caughtFlies = flies.filter(f => f.stuck || f.caught) |
| 1470 | + if (caughtFlies.length > 0) { |
| 1471 | + let target = random(caughtFlies) |
| 1472 | + this.targetX = target.pos.x |
| 1473 | + this.targetY = target.pos.y |
| 1474 | + } else { |
| 1475 | + this.active = false // No targets, deactivate |
| 1476 | + return |
| 1477 | + } |
| 1478 | + } else { |
| 1479 | + // Heavily favor targeting spider (90% chance) |
| 1480 | + if (random() < 0.9) { |
| 1481 | + // Target spider with prediction |
| 1482 | + this.targetX = spider.pos.x + spider.vel.x * 10 // Predict where spider will be |
| 1483 | + this.targetY = spider.pos.y + spider.vel.y * 10 |
| 1484 | + } else { |
| 1485 | + // Occasionally target a web strand |
| 1486 | + if (webStrands.length > 0) { |
| 1487 | + let strand = random(webStrands.filter(s => !s.broken)) |
| 1488 | + if (strand && strand.path && strand.path.length > 0) { |
| 1489 | + let point = random(strand.path) |
| 1490 | + this.targetX = point.x |
| 1491 | + this.targetY = point.y |
| 1492 | + } |
| 1461 | 1493 | } |
| 1494 | + } |
| 1462 | 1495 | } |
| 1463 | | - |
| 1464 | | - updateTarget() { |
| 1465 | | - if (this.isThief) { |
| 1466 | | - // Target caught flies |
| 1467 | | - let caughtFlies = flies.filter(f => f.stuck || f.caught); |
| 1468 | | - if (caughtFlies.length > 0) { |
| 1469 | | - let target = random(caughtFlies); |
| 1470 | | - this.targetX = target.pos.x; |
| 1471 | | - this.targetY = target.pos.y; |
| 1472 | | - } else { |
| 1473 | | - this.active = false; // No targets, deactivate |
| 1474 | | - return; |
| 1475 | | - } |
| 1496 | + } |
| 1497 | + |
| 1498 | + executeDivePattern () { |
| 1499 | + if (this.state === 'approaching') { |
| 1500 | + // Move into position above target MUCH FASTER |
| 1501 | + let dx = this.targetX - this.x |
| 1502 | + let dy = 50 - this.y // Lower starting position for faster attack |
| 1503 | + |
| 1504 | + this.x += dx * 0.15 // Much faster positioning |
| 1505 | + this.y += dy * 0.15 |
| 1506 | + |
| 1507 | + // When in position, start diving immediately |
| 1508 | + if (abs(dx) < 50 && abs(dy) < 30) { |
| 1509 | + this.state = 'attacking' |
| 1510 | + this.attacking = true |
| 1511 | + this.updateTarget() // Update target position for more accurate dive |
| 1512 | + } |
| 1513 | + } else if (this.state === 'attacking') { |
| 1514 | + // IMPROVED: Better tracking dive |
| 1515 | + let dx = this.targetX - this.x |
| 1516 | + let dy = this.targetY - this.y |
| 1517 | + |
| 1518 | + // Accelerate toward target with better tracking |
| 1519 | + this.vx = dx * 0.08 // Increased horizontal tracking |
| 1520 | + this.vy = min(this.diveSpeed, this.vy + 1) // Accelerating dive |
| 1521 | + |
| 1522 | + this.x += this.vx |
| 1523 | + this.y += this.vy |
| 1524 | + |
| 1525 | + // Update target position while diving for better accuracy |
| 1526 | + if (frameCount % 10 === 0) { |
| 1527 | + this.targetX = spider.pos.x |
| 1528 | + this.targetY = spider.pos.y |
| 1529 | + } |
| 1530 | + |
| 1531 | + // FIX: Check if we've reached or passed the target |
| 1532 | + // More generous hit detection |
| 1533 | + if (this.y > this.targetY - 10 || this.y > height - 50) { |
| 1534 | + this.consecutiveAttacks++ |
| 1535 | + |
| 1536 | + // Do multiple attacks before retreating |
| 1537 | + if (this.consecutiveAttacks < this.maxConsecutiveAttacks) { |
| 1538 | + // Quick pull up and attack again |
| 1539 | + this.state = 'approaching' |
| 1540 | + this.attacking = false |
| 1541 | + this.y = min(this.y, height - 100) // Don't go too low |
| 1542 | + this.updateTarget() // Get new target position |
| 1476 | 1543 | } else { |
| 1477 | | - // Heavily favor targeting spider (90% chance) |
| 1478 | | - if (random() < 0.9) { |
| 1479 | | - // Target spider with prediction |
| 1480 | | - this.targetX = spider.pos.x + spider.vel.x * 10; // Predict where spider will be |
| 1481 | | - this.targetY = spider.pos.y + spider.vel.y * 10; |
| 1482 | | - } else { |
| 1483 | | - // Occasionally target a web strand |
| 1484 | | - if (webStrands.length > 0) { |
| 1485 | | - let strand = random(webStrands.filter(s => !s.broken)); |
| 1486 | | - if (strand && strand.path && strand.path.length > 0) { |
| 1487 | | - let point = random(strand.path); |
| 1488 | | - this.targetX = point.x; |
| 1489 | | - this.targetY = point.y; |
| 1490 | | - } |
| 1491 | | - } |
| 1492 | | - } |
| 1544 | + // Finally retreat after multiple attacks |
| 1545 | + this.state = 'retreating' |
| 1546 | + this.attacking = false |
| 1493 | 1547 | } |
| 1548 | + } |
| 1549 | + } else if (this.state === 'retreating') { |
| 1550 | + // Fly back up faster |
| 1551 | + this.vy = -this.retreatSpeed |
| 1552 | + this.y += this.vy |
| 1553 | + this.x += sin(frameCount * 0.1) * 2 // Weave while retreating |
| 1554 | + |
| 1555 | + // Reset when off screen |
| 1556 | + if (this.y < -50) { |
| 1557 | + this.state = 'approaching' |
| 1558 | + this.attackDelay = random(60, 120) // Shorter delay between attack runs |
| 1559 | + this.x = random(width) |
| 1560 | + this.consecutiveAttacks = 0 // Reset attack counter |
| 1561 | + this.maxConsecutiveAttacks = random(2, 4) // Randomize next attack count |
| 1562 | + } |
| 1494 | 1563 | } |
| 1495 | | - |
| 1496 | | - executeDivePattern() { |
| 1497 | | - if (this.state === 'approaching') { |
| 1498 | | - // Move into position above target MUCH FASTER |
| 1499 | | - let dx = this.targetX - this.x; |
| 1500 | | - let dy = 50 - this.y; // Lower starting position for faster attack |
| 1501 | | - |
| 1502 | | - this.x += dx * 0.15; // Much faster positioning |
| 1503 | | - this.y += dy * 0.15; |
| 1504 | | - |
| 1505 | | - // When in position, start diving immediately |
| 1506 | | - if (abs(dx) < 50 && abs(dy) < 30) { |
| 1507 | | - this.state = 'attacking'; |
| 1508 | | - this.attacking = true; |
| 1509 | | - this.updateTarget(); // Update target position for more accurate dive |
| 1510 | | - } |
| 1511 | | - } else if (this.state === 'attacking') { |
| 1512 | | - // FAST dive toward target |
| 1513 | | - let dx = this.targetX - this.x; |
| 1514 | | - this.vy = this.diveSpeed; |
| 1515 | | - this.vx = dx * 0.05; // Track horizontally too |
| 1516 | | - this.x += this.vx; |
| 1517 | | - this.y += this.vy; |
| 1518 | | - |
| 1519 | | - // Hit ground or passed target |
| 1520 | | - if (this.y > this.targetY + 20 || this.y > height - 50) { |
| 1521 | | - this.consecutiveAttacks++; |
| 1522 | | - |
| 1523 | | - // Do multiple attacks before retreating |
| 1524 | | - if (this.consecutiveAttacks < this.maxConsecutiveAttacks) { |
| 1525 | | - // Quick pull up and attack again |
| 1526 | | - this.state = 'approaching'; |
| 1527 | | - this.attacking = false; |
| 1528 | | - this.y = min(this.y, height - 100); // Don't go too low |
| 1529 | | - this.updateTarget(); // Get new target position |
| 1530 | | - } else { |
| 1531 | | - // Finally retreat after multiple attacks |
| 1532 | | - this.state = 'retreating'; |
| 1533 | | - this.attacking = false; |
| 1534 | | - } |
| 1535 | | - } |
| 1536 | | - } else if (this.state === 'retreating') { |
| 1537 | | - // Fly back up faster |
| 1538 | | - this.vy = -this.retreatSpeed; |
| 1539 | | - this.y += this.vy; |
| 1540 | | - this.x += sin(frameCount * 0.1) * 2; // Weave while retreating |
| 1541 | | - |
| 1542 | | - // Reset when off screen |
| 1543 | | - if (this.y < -50) { |
| 1544 | | - this.state = 'approaching'; |
| 1545 | | - this.attackDelay = random(60, 120); // Shorter delay between attack runs |
| 1546 | | - this.x = random(width); |
| 1547 | | - this.consecutiveAttacks = 0; // Reset attack counter |
| 1548 | | - this.maxConsecutiveAttacks = random(2, 4); // Randomize next attack count |
| 1549 | | - } |
| 1564 | + } |
| 1565 | + |
| 1566 | + executeSwoopPattern () { |
| 1567 | + if (this.state === 'approaching') { |
| 1568 | + // Come from the side FAST |
| 1569 | + if (this.x < 0) { |
| 1570 | + this.x += 8 // Faster approach |
| 1571 | + this.y = height * 0.3 + sin(this.x * 0.03) * 50 |
| 1572 | + } else { |
| 1573 | + this.state = 'attacking' |
| 1574 | + this.attacking = true |
| 1575 | + this.updateTarget() |
| 1576 | + } |
| 1577 | + } else if (this.state === 'attacking') { |
| 1578 | + // Swoop across screen following sine wave but faster |
| 1579 | + this.x += 9 // Faster swoop |
| 1580 | + this.y = height * 0.3 + sin(this.x * 0.03) * 120 |
| 1581 | + |
| 1582 | + // Track toward target when close |
| 1583 | + if (abs(this.x - this.targetX) < 100) { |
| 1584 | + // Aggressively dive toward target |
| 1585 | + let dy = this.targetY - this.y |
| 1586 | + this.y += dy * 0.2 |
| 1587 | + } |
| 1588 | + |
| 1589 | + // Exit screen |
| 1590 | + if (this.x > width + 50) { |
| 1591 | + this.consecutiveAttacks++ |
| 1592 | + |
| 1593 | + if (this.consecutiveAttacks < this.maxConsecutiveAttacks) { |
| 1594 | + // Come back from other side |
| 1595 | + this.x = -50 |
| 1596 | + this.state = 'approaching' |
| 1597 | + this.updateTarget() |
| 1598 | + } else { |
| 1599 | + this.state = 'retreating' |
| 1600 | + this.attacking = false |
| 1550 | 1601 | } |
| 1602 | + } |
| 1603 | + } else if (this.state === 'retreating') { |
| 1604 | + // Reset |
| 1605 | + this.state = 'approaching' |
| 1606 | + this.attackDelay = random(90, 150) |
| 1607 | + this.x = -50 |
| 1608 | + this.consecutiveAttacks = 0 |
| 1609 | + this.maxConsecutiveAttacks = random(2, 4) |
| 1551 | 1610 | } |
| 1552 | | - |
| 1553 | | - executeSwoopPattern() { |
| 1554 | | - if (this.state === 'approaching') { |
| 1555 | | - // Come from the side FAST |
| 1556 | | - if (this.x < 0) { |
| 1557 | | - this.x += 8; // Faster approach |
| 1558 | | - this.y = height * 0.3 + sin(this.x * 0.03) * 50; |
| 1559 | | - } else { |
| 1560 | | - this.state = 'attacking'; |
| 1561 | | - this.attacking = true; |
| 1562 | | - this.updateTarget(); |
| 1563 | | - } |
| 1564 | | - } else if (this.state === 'attacking') { |
| 1565 | | - // Swoop across screen following sine wave but faster |
| 1566 | | - this.x += 9; // Faster swoop |
| 1567 | | - this.y = height * 0.3 + sin(this.x * 0.03) * 120; |
| 1568 | | - |
| 1569 | | - // Track toward target when close |
| 1570 | | - if (abs(this.x - this.targetX) < 100) { |
| 1571 | | - // Aggressively dive toward target |
| 1572 | | - let dy = this.targetY - this.y; |
| 1573 | | - this.y += dy * 0.2; |
| 1574 | | - } |
| 1575 | | - |
| 1576 | | - // Exit screen |
| 1577 | | - if (this.x > width + 50) { |
| 1578 | | - this.consecutiveAttacks++; |
| 1579 | | - |
| 1580 | | - if (this.consecutiveAttacks < this.maxConsecutiveAttacks) { |
| 1581 | | - // Come back from other side |
| 1582 | | - this.x = -50; |
| 1583 | | - this.state = 'approaching'; |
| 1584 | | - this.updateTarget(); |
| 1585 | | - } else { |
| 1586 | | - this.state = 'retreating'; |
| 1587 | | - this.attacking = false; |
| 1588 | | - } |
| 1589 | | - } |
| 1590 | | - } else if (this.state === 'retreating') { |
| 1591 | | - // Reset |
| 1592 | | - this.state = 'approaching'; |
| 1593 | | - this.attackDelay = random(90, 150); |
| 1594 | | - this.x = -50; |
| 1595 | | - this.consecutiveAttacks = 0; |
| 1596 | | - this.maxConsecutiveAttacks = random(2, 4); |
| 1611 | + } |
| 1612 | + |
| 1613 | + executeGlidePattern () { |
| 1614 | + if (this.state === 'approaching') { |
| 1615 | + // Glide in from top corner faster |
| 1616 | + this.x += 5 |
| 1617 | + this.y += 2.5 |
| 1618 | + |
| 1619 | + if (this.y > height * 0.15) { |
| 1620 | + this.state = 'attacking' |
| 1621 | + this.attacking = true |
| 1622 | + this.updateTarget() |
| 1623 | + } |
| 1624 | + } else if (this.state === 'attacking') { |
| 1625 | + // Glide toward target aggressively |
| 1626 | + let dx = this.targetX - this.x |
| 1627 | + let dy = this.targetY - this.y |
| 1628 | + let dist = sqrt(dx * dx + dy * dy) |
| 1629 | + |
| 1630 | + if (dist > 10) { |
| 1631 | + this.x += (dx / dist) * 7 // Much faster glide |
| 1632 | + this.y += (dy / dist) * 7 |
| 1633 | + } |
| 1634 | + |
| 1635 | + // Pass through and maybe attack again |
| 1636 | + if (this.y > height - 100 || this.x < -50 || this.x > width + 50) { |
| 1637 | + this.consecutiveAttacks++ |
| 1638 | + |
| 1639 | + if (this.consecutiveAttacks < this.maxConsecutiveAttacks) { |
| 1640 | + // Reset for another pass |
| 1641 | + this.state = 'approaching' |
| 1642 | + this.x = random() < 0.5 ? -50 : width + 50 |
| 1643 | + this.y = random(50, 150) |
| 1644 | + this.updateTarget() |
| 1645 | + } else { |
| 1646 | + this.state = 'retreating' |
| 1647 | + this.attacking = false |
| 1597 | 1648 | } |
| 1649 | + } |
| 1650 | + } else if (this.state === 'retreating') { |
| 1651 | + // Continue off screen |
| 1652 | + this.x += this.vx |
| 1653 | + this.y += this.vy |
| 1654 | + |
| 1655 | + // Reset |
| 1656 | + if (this.y > height + 50 || this.x < -100 || this.x > width + 100) { |
| 1657 | + this.state = 'approaching' |
| 1658 | + this.attackDelay = random(120, 180) |
| 1659 | + this.x = random() < 0.5 ? -50 : width + 50 |
| 1660 | + this.y = random(50, 150) |
| 1661 | + this.vx = this.x < width / 2 ? 5 : -5 |
| 1662 | + this.vy = 2.5 |
| 1663 | + this.consecutiveAttacks = 0 |
| 1664 | + this.maxConsecutiveAttacks = random(2, 4) |
| 1665 | + } |
| 1598 | 1666 | } |
| 1599 | | - |
| 1600 | | - executeGlidePattern() { |
| 1601 | | - if (this.state === 'approaching') { |
| 1602 | | - // Glide in from top corner faster |
| 1603 | | - this.x += 5; |
| 1604 | | - this.y += 2.5; |
| 1605 | | - |
| 1606 | | - if (this.y > height * 0.15) { |
| 1607 | | - this.state = 'attacking'; |
| 1608 | | - this.attacking = true; |
| 1609 | | - this.updateTarget(); |
| 1610 | | - } |
| 1611 | | - } else if (this.state === 'attacking') { |
| 1612 | | - // Glide toward target aggressively |
| 1613 | | - let dx = this.targetX - this.x; |
| 1614 | | - let dy = this.targetY - this.y; |
| 1615 | | - let dist = sqrt(dx * dx + dy * dy); |
| 1616 | | - |
| 1617 | | - if (dist > 10) { |
| 1618 | | - this.x += (dx / dist) * 7; // Much faster glide |
| 1619 | | - this.y += (dy / dist) * 7; |
| 1620 | | - } |
| 1621 | | - |
| 1622 | | - // Pass through and maybe attack again |
| 1623 | | - if (this.y > height - 100 || this.x < -50 || this.x > width + 50) { |
| 1624 | | - this.consecutiveAttacks++; |
| 1625 | | - |
| 1626 | | - if (this.consecutiveAttacks < this.maxConsecutiveAttacks) { |
| 1627 | | - // Reset for another pass |
| 1628 | | - this.state = 'approaching'; |
| 1629 | | - this.x = random() < 0.5 ? -50 : width + 50; |
| 1630 | | - this.y = random(50, 150); |
| 1631 | | - this.updateTarget(); |
| 1632 | | - } else { |
| 1633 | | - this.state = 'retreating'; |
| 1634 | | - this.attacking = false; |
| 1635 | | - } |
| 1636 | | - } |
| 1637 | | - } else if (this.state === 'retreating') { |
| 1638 | | - // Continue off screen |
| 1639 | | - this.x += this.vx; |
| 1640 | | - this.y += this.vy; |
| 1641 | | - |
| 1642 | | - // Reset |
| 1643 | | - if (this.y > height + 50 || this.x < -100 || this.x > width + 100) { |
| 1644 | | - this.state = 'approaching'; |
| 1645 | | - this.attackDelay = random(120, 180); |
| 1646 | | - this.x = random() < 0.5 ? -50 : width + 50; |
| 1647 | | - this.y = random(50, 150); |
| 1648 | | - this.vx = this.x < width/2 ? 5 : -5; |
| 1649 | | - this.vy = 2.5; |
| 1650 | | - this.consecutiveAttacks = 0; |
| 1651 | | - this.maxConsecutiveAttacks = random(2, 4); |
| 1652 | | - } |
| 1653 | | - } |
| 1667 | + } |
| 1668 | + |
| 1669 | + executeCirclePattern () { |
| 1670 | + if (this.state === 'approaching') { |
| 1671 | + // Move to circle start position |
| 1672 | + let startX = this.circleCenter.x + cos(0) * this.circleRadius |
| 1673 | + let startY = this.circleCenter.y + sin(0) * this.circleRadius |
| 1674 | + |
| 1675 | + let dx = startX - this.x |
| 1676 | + let dy = startY - this.y |
| 1677 | + |
| 1678 | + this.x += dx * 0.1 |
| 1679 | + this.y += dy * 0.1 |
| 1680 | + |
| 1681 | + if (abs(dx) < 20 && abs(dy) < 20) { |
| 1682 | + this.state = 'attacking' |
| 1683 | + this.attacking = true |
| 1684 | + this.circleAngle = 0 |
| 1685 | + } |
| 1686 | + } else if (this.state === 'attacking') { |
| 1687 | + // Circle around center FASTER |
| 1688 | + this.circleAngle += 0.08 // Faster circling |
| 1689 | + this.x = this.circleCenter.x + cos(this.circleAngle) * this.circleRadius |
| 1690 | + this.y = this.circleCenter.y + sin(this.circleAngle) * this.circleRadius |
| 1691 | + |
| 1692 | + // More frequent dives toward center |
| 1693 | + if (frameCount % 60 === 0) { |
| 1694 | + // Every second instead of every 2 seconds |
| 1695 | + this.circleRadius = max(30, this.circleRadius - 50) |
| 1696 | + } else { |
| 1697 | + this.circleRadius = min(150, this.circleRadius + 2) |
| 1698 | + } |
| 1699 | + |
| 1700 | + // Complete circle faster |
| 1701 | + if (this.circleAngle > TWO_PI * 1.5) { |
| 1702 | + // 1.5 circles instead of 2 |
| 1703 | + this.state = 'retreating' |
| 1704 | + this.attacking = false |
| 1705 | + } |
| 1706 | + } else if (this.state === 'retreating') { |
| 1707 | + // Fly away |
| 1708 | + this.y -= 7 |
| 1709 | + |
| 1710 | + if (this.y < -50) { |
| 1711 | + this.state = 'approaching' |
| 1712 | + this.attackDelay = random(150, 240) |
| 1713 | + this.x = random(width) |
| 1714 | + } |
| 1654 | 1715 | } |
| 1716 | + } |
| 1717 | + |
| 1718 | +checkCollisions() { |
| 1719 | + // FIX: Increased collision radius for more generous hit detection |
| 1720 | + let collisionDistance = this.size + spider.radius + 5; // Added 5 pixel buffer |
| 1655 | 1721 | |
| 1656 | | - executeCirclePattern() { |
| 1657 | | - if (this.state === 'approaching') { |
| 1658 | | - // Move to circle start position |
| 1659 | | - let startX = this.circleCenter.x + cos(0) * this.circleRadius; |
| 1660 | | - let startY = this.circleCenter.y + sin(0) * this.circleRadius; |
| 1722 | + // Check collision with spider |
| 1723 | + if (this.attacking && dist(this.x, this.y, spider.pos.x, spider.pos.y) < collisionDistance) { |
| 1724 | + // Hit spider! |
| 1725 | + if (gamePhase === 'DAWN') { |
| 1726 | + // Calculate damage |
| 1727 | + let damage = 20; // Base damage |
| 1661 | 1728 | |
| 1662 | | - let dx = startX - this.x; |
| 1663 | | - let dy = startY - this.y; |
| 1729 | + // If spider has no stamina, GAME OVER! |
| 1730 | + if (jumpStamina <= 0) { |
| 1731 | + triggerGameOver('Exhausted spider caught by bird!'); |
| 1732 | + return; |
| 1733 | + } |
| 1664 | 1734 | |
| 1665 | | - this.x += dx * 0.1; |
| 1666 | | - this.y += dy * 0.1; |
| 1735 | + // Otherwise, reduce stamina |
| 1736 | + jumpStamina = max(0, jumpStamina - damage); |
| 1737 | + stats.birdHitsTaken++; |
| 1667 | 1738 | |
| 1668 | | - if (abs(dx) < 20 && abs(dy) < 20) { |
| 1669 | | - this.state = 'attacking'; |
| 1670 | | - this.attacking = true; |
| 1671 | | - this.circleAngle = 0; |
| 1672 | | - } |
| 1673 | | - } else if (this.state === 'attacking') { |
| 1674 | | - // Circle around center FASTER |
| 1675 | | - this.circleAngle += 0.08; // Faster circling |
| 1676 | | - this.x = this.circleCenter.x + cos(this.circleAngle) * this.circleRadius; |
| 1677 | | - this.y = this.circleCenter.y + sin(this.circleAngle) * this.circleRadius; |
| 1739 | + // Knockback effect |
| 1740 | + spider.vel.x = (spider.pos.x - this.x) * 0.3; |
| 1741 | + spider.vel.y = -3; |
| 1742 | + spider.isAirborne = true; |
| 1678 | 1743 | |
| 1679 | | - // More frequent dives toward center |
| 1680 | | - if (frameCount % 60 === 0) { // Every second instead of every 2 seconds |
| 1681 | | - this.circleRadius = max(30, this.circleRadius - 50); |
| 1682 | | - } else { |
| 1683 | | - this.circleRadius = min(150, this.circleRadius + 2); |
| 1744 | + // Red damage particles |
| 1745 | + for (let i = 0; i < 12; i++) { |
| 1746 | + let p = new Particle(spider.pos.x, spider.pos.y); |
| 1747 | + p.color = color(255, 50, 50); |
| 1748 | + p.vel = createVector(random(-4, 4), random(-4, 1)); |
| 1749 | + p.size = random(4, 8); |
| 1750 | + particles.push(p); |
| 1684 | 1751 | } |
| 1685 | 1752 | |
| 1686 | | - // Complete circle faster |
| 1687 | | - if (this.circleAngle > TWO_PI * 1.5) { // 1.5 circles instead of 2 |
| 1688 | | - this.state = 'retreating'; |
| 1689 | | - this.attacking = false; |
| 1753 | + // Screen shake effect |
| 1754 | + if (typeof screenShake !== 'undefined') { |
| 1755 | + screenShake = 10; |
| 1690 | 1756 | } |
| 1691 | | - } else if (this.state === 'retreating') { |
| 1692 | | - // Fly away |
| 1693 | | - this.y -= 7; |
| 1694 | 1757 | |
| 1695 | | - if (this.y < -50) { |
| 1696 | | - this.state = 'approaching'; |
| 1697 | | - this.attackDelay = random(150, 240); |
| 1698 | | - this.x = random(width); |
| 1699 | | - } |
| 1700 | | - } |
| 1701 | | - } |
| 1702 | | - |
| 1703 | | - checkCollisions() { |
| 1704 | | - // Check collision with spider |
| 1705 | | - if (this.attacking && dist(this.x, this.y, spider.pos.x, spider.pos.y) < this.size + spider.radius) { |
| 1706 | | - // Hit spider! |
| 1707 | | - if (gamePhase === 'DAWN') { |
| 1708 | | - // Calculate damage based on remaining stamina |
| 1709 | | - let damage = 20; // Base damage |
| 1710 | | - |
| 1711 | | - // If spider has no stamina, GAME OVER! |
| 1712 | | - if (jumpStamina <= 0) { |
| 1713 | | - triggerGameOver('Exhausted spider caught by bird!'); |
| 1714 | | - return; |
| 1715 | | - } |
| 1716 | | - |
| 1717 | | - // Otherwise, reduce stamina |
| 1718 | | - jumpStamina = max(0, jumpStamina - damage); |
| 1719 | | - stats.birdHitsTaken++; |
| 1720 | | - |
| 1721 | | - // Knockback effect |
| 1722 | | - spider.vel.x = (spider.pos.x - this.x) * 0.3; |
| 1723 | | - spider.vel.y = -3; |
| 1724 | | - spider.isAirborne = true; |
| 1725 | | - |
| 1726 | | - // Red damage particles |
| 1727 | | - for (let i = 0; i < 12; i++) { |
| 1728 | | - let p = new Particle(spider.pos.x, spider.pos.y); |
| 1729 | | - p.color = color(255, 50, 50); |
| 1730 | | - p.vel = createVector(random(-4, 4), random(-4, 1)); |
| 1731 | | - p.size = random(4, 8); |
| 1732 | | - particles.push(p); |
| 1733 | | - } |
| 1734 | | - |
| 1735 | | - // Screen shake effect |
| 1736 | | - screenShake = 10; |
| 1737 | | - |
| 1738 | | - // Warning notification if stamina is low |
| 1758 | + // Warning notifications - but limited to prevent spam |
| 1759 | + if (notifications.length < 3) { // Limit notifications |
| 1739 | 1760 | if (jumpStamina <= 20) { |
| 1740 | 1761 | notifications.push(new Notification("CRITICAL STAMINA!", color(255, 50, 50))); |
| 1741 | 1762 | } else if (jumpStamina <= 40) { |
| 1742 | 1763 | notifications.push(new Notification("Low stamina - find cover!", color(255, 150, 50))); |
| 1743 | 1764 | } |
| 1744 | 1765 | } |
| 1745 | | - |
| 1746 | | - // Bird bounces off |
| 1747 | | - this.state = 'retreating'; |
| 1748 | | - this.attacking = false; |
| 1749 | 1766 | } |
| 1767 | + |
| 1768 | + // Bird bounces off |
| 1769 | + this.state = 'retreating'; |
| 1770 | + this.attacking = false; |
| 1771 | + } |
| 1750 | 1772 | |
| 1751 | 1773 | // Check collision with web strands |
| 1752 | 1774 | if (this.attacking) { |