@@ -1417,6 +1417,8 @@ class Bird { |
| 1417 | 1417 | // Update wing animation |
| 1418 | 1418 | this.wingPhase += 0.3 // Faster wing flapping |
| 1419 | 1419 | |
| 1420 | + this.avoidObstacles() |
| 1421 | + |
| 1420 | 1422 | // Countdown to attack - MUCH FASTER |
| 1421 | 1423 | if (this.attackDelay > 0) { |
| 1422 | 1424 | this.attackDelay-- |
@@ -1497,77 +1499,112 @@ class Bird { |
| 1497 | 1499 | |
| 1498 | 1500 | executeDivePattern () { |
| 1499 | 1501 | if (this.state === 'approaching') { |
| 1500 | | - // Move into position above target MUCH FASTER |
| 1502 | + // Move into position above target |
| 1501 | 1503 | let dx = this.targetX - this.x |
| 1502 | | - let dy = 50 - this.y // Lower starting position for faster attack |
| 1504 | + let dy = 50 - this.y |
| 1503 | 1505 | |
| 1504 | | - this.x += dx * 0.15 // Much faster positioning |
| 1506 | + this.x += dx * 0.15 |
| 1505 | 1507 | this.y += dy * 0.15 |
| 1506 | 1508 | |
| 1507 | | - // When in position, start diving immediately |
| 1509 | + // When in position, start diving |
| 1508 | 1510 | if (abs(dx) < 50 && abs(dy) < 30) { |
| 1509 | 1511 | this.state = 'attacking' |
| 1510 | 1512 | this.attacking = true |
| 1511 | | - this.updateTarget() // Update target position for more accurate dive |
| 1513 | + this.updateTarget() |
| 1512 | 1514 | } |
| 1513 | 1515 | } else if (this.state === 'attacking') { |
| 1514 | | - // IMPROVED: Better tracking dive |
| 1516 | + // FIX: Better tracking dive that reaches bottom |
| 1515 | 1517 | let dx = this.targetX - this.x |
| 1516 | 1518 | let dy = this.targetY - this.y |
| 1517 | 1519 | |
| 1518 | 1520 | // 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 | + this.vx = dx * 0.1 // Better horizontal tracking |
| 1522 | + this.vy = min(this.diveSpeed * 1.5, this.vy + 1.5) // Faster acceleration |
| 1521 | 1523 | |
| 1522 | 1524 | this.x += this.vx |
| 1523 | 1525 | this.y += this.vy |
| 1524 | 1526 | |
| 1525 | | - // Update target position while diving for better accuracy |
| 1526 | | - if (frameCount % 10 === 0) { |
| 1527 | + // Update target position while diving |
| 1528 | + if (frameCount % 8 === 0) { |
| 1527 | 1529 | this.targetX = spider.pos.x |
| 1528 | 1530 | this.targetY = spider.pos.y |
| 1529 | 1531 | } |
| 1530 | 1532 | |
| 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++ |
| 1533 | + // FIX: Extend dive range to reach bottom spiders |
| 1534 | + // Check if we've reached the target OR the absolute bottom |
| 1535 | + let reachedTarget = this.y > this.targetY - 10 |
| 1536 | + let reachedBottom = this.y > height - 20 // Go almost to canvas bottom |
| 1537 | + |
| 1538 | + // FIX: Also check if we're very close horizontally for bottom edge spiders |
| 1539 | + let closeToSpider = dist(this.x, this.y, spider.pos.x, spider.pos.y) < 50 |
| 1540 | + |
| 1541 | + if (reachedTarget || reachedBottom || closeToSpider) { |
| 1542 | + // FIX: If spider is at bottom and we haven't hit it yet, do a horizontal sweep |
| 1543 | + if (spider.pos.y > height - 30 && !closeToSpider && !this.sweeping) { |
| 1544 | + this.sweeping = true |
| 1545 | + this.y = spider.pos.y // Match spider height |
| 1546 | + this.vy = 0 // Stop vertical movement |
| 1547 | + |
| 1548 | + // Sweep horizontally toward spider |
| 1549 | + let sweepDirection = spider.pos.x > this.x ? 1 : -1 |
| 1550 | + this.vx = sweepDirection * 8 |
| 1551 | + |
| 1552 | + // Continue sweep for a bit |
| 1553 | + setTimeout(() => { |
| 1554 | + this.sweeping = false |
| 1555 | + this.state = 'retreating' |
| 1556 | + this.attacking = false |
| 1557 | + }, 500) // Sweep for 0.5 seconds |
| 1558 | + } else if (!this.sweeping) { |
| 1559 | + // Normal attack completion |
| 1560 | + this.consecutiveAttacks++ |
| 1561 | + |
| 1562 | + if (this.consecutiveAttacks < this.maxConsecutiveAttacks) { |
| 1563 | + // Quick pull up and attack again |
| 1564 | + this.state = 'approaching' |
| 1565 | + this.attacking = false |
| 1566 | + this.y = min(this.y, height - 50) |
| 1567 | + this.updateTarget() |
| 1568 | + } else { |
| 1569 | + // Finally retreat |
| 1570 | + this.state = 'retreating' |
| 1571 | + this.attacking = false |
| 1572 | + } |
| 1573 | + } |
| 1574 | + } |
| 1535 | 1575 | |
| 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 |
| 1543 | | - } else { |
| 1544 | | - // Finally retreat after multiple attacks |
| 1576 | + // Safety: Don't go below canvas |
| 1577 | + if (this.y > height - 10) { |
| 1578 | + this.y = height - 10 |
| 1579 | + if (!this.sweeping) { |
| 1545 | 1580 | this.state = 'retreating' |
| 1546 | 1581 | this.attacking = false |
| 1547 | 1582 | } |
| 1548 | 1583 | } |
| 1549 | 1584 | } else if (this.state === 'retreating') { |
| 1550 | | - // Fly back up faster |
| 1585 | + // Clear sweep flag |
| 1586 | + this.sweeping = false |
| 1587 | + |
| 1588 | + // Fly back up |
| 1551 | 1589 | this.vy = -this.retreatSpeed |
| 1552 | 1590 | this.y += this.vy |
| 1553 | | - this.x += sin(frameCount * 0.1) * 2 // Weave while retreating |
| 1591 | + this.x += sin(frameCount * 0.1) * 2 |
| 1554 | 1592 | |
| 1555 | 1593 | // Reset when off screen |
| 1556 | 1594 | if (this.y < -50) { |
| 1557 | 1595 | this.state = 'approaching' |
| 1558 | | - this.attackDelay = random(60, 120) // Shorter delay between attack runs |
| 1596 | + this.attackDelay = random(60, 120) |
| 1559 | 1597 | this.x = random(width) |
| 1560 | | - this.consecutiveAttacks = 0 // Reset attack counter |
| 1561 | | - this.maxConsecutiveAttacks = random(2, 4) // Randomize next attack count |
| 1598 | + this.consecutiveAttacks = 0 |
| 1599 | + this.maxConsecutiveAttacks = random(2, 4) |
| 1562 | 1600 | } |
| 1563 | 1601 | } |
| 1564 | 1602 | } |
| 1565 | 1603 | |
| 1566 | 1604 | executeSwoopPattern () { |
| 1567 | 1605 | if (this.state === 'approaching') { |
| 1568 | | - // Come from the side FAST |
| 1569 | 1606 | if (this.x < 0) { |
| 1570 | | - this.x += 8 // Faster approach |
| 1607 | + this.x += 8 |
| 1571 | 1608 | this.y = height * 0.3 + sin(this.x * 0.03) * 50 |
| 1572 | 1609 | } else { |
| 1573 | 1610 | this.state = 'attacking' |
@@ -1575,23 +1612,31 @@ class Bird { |
| 1575 | 1612 | this.updateTarget() |
| 1576 | 1613 | } |
| 1577 | 1614 | } 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 |
| 1615 | + this.x += 9 |
| 1616 | + |
| 1617 | + // FIX: Adjust swoop pattern if spider is at bottom |
| 1618 | + if (spider.pos.y > height - 50) { |
| 1619 | + // Lower swoop pattern for bottom spiders |
| 1620 | + this.y = height * 0.7 + sin(this.x * 0.03) * 50 |
| 1621 | + } else { |
| 1622 | + // Normal swoop |
| 1623 | + this.y = height * 0.3 + sin(this.x * 0.03) * 120 |
| 1624 | + } |
| 1581 | 1625 | |
| 1582 | 1626 | // Track toward target when close |
| 1583 | 1627 | if (abs(this.x - this.targetX) < 100) { |
| 1584 | | - // Aggressively dive toward target |
| 1585 | 1628 | let dy = this.targetY - this.y |
| 1586 | 1629 | this.y += dy * 0.2 |
| 1587 | 1630 | } |
| 1588 | 1631 | |
| 1632 | + // Avoid going below canvas |
| 1633 | + this.y = min(this.y, height - 15) |
| 1634 | + |
| 1589 | 1635 | // Exit screen |
| 1590 | 1636 | if (this.x > width + 50) { |
| 1591 | 1637 | this.consecutiveAttacks++ |
| 1592 | 1638 | |
| 1593 | 1639 | if (this.consecutiveAttacks < this.maxConsecutiveAttacks) { |
| 1594 | | - // Come back from other side |
| 1595 | 1640 | this.x = -50 |
| 1596 | 1641 | this.state = 'approaching' |
| 1597 | 1642 | this.updateTarget() |
@@ -1601,7 +1646,6 @@ class Bird { |
| 1601 | 1646 | } |
| 1602 | 1647 | } |
| 1603 | 1648 | } else if (this.state === 'retreating') { |
| 1604 | | - // Reset |
| 1605 | 1649 | this.state = 'approaching' |
| 1606 | 1650 | this.attackDelay = random(90, 150) |
| 1607 | 1651 | this.x = -50 |
@@ -1610,6 +1654,67 @@ class Bird { |
| 1610 | 1654 | } |
| 1611 | 1655 | } |
| 1612 | 1656 | |
| 1657 | + avoidObstacles () { |
| 1658 | + // Check collision with all obstacles |
| 1659 | + for (let obstacle of obstacles) { |
| 1660 | + let d = dist(this.x, this.y, obstacle.x, obstacle.y) |
| 1661 | + |
| 1662 | + // If too close to an obstacle, push away |
| 1663 | + if (d < obstacle.radius + this.size + 10) { |
| 1664 | + // Calculate push direction (away from obstacle) |
| 1665 | + let pushX = (this.x - obstacle.x) / d |
| 1666 | + let pushY = (this.y - obstacle.y) / d |
| 1667 | + |
| 1668 | + // Apply push force |
| 1669 | + this.x += pushX * 5 |
| 1670 | + this.y += pushY * 5 |
| 1671 | + |
| 1672 | + // If stuck for too long, teleport away |
| 1673 | + if (this.stuckCounter === undefined) { |
| 1674 | + this.stuckCounter = 0 |
| 1675 | + } |
| 1676 | + this.stuckCounter++ |
| 1677 | + |
| 1678 | + if (this.stuckCounter > 30) { |
| 1679 | + // Stuck for 0.5 seconds |
| 1680 | + // Teleport to a safe position |
| 1681 | + this.y = obstacle.y - obstacle.radius - 30 |
| 1682 | + this.x = obstacle.x + random(-50, 50) |
| 1683 | + this.stuckCounter = 0 |
| 1684 | + |
| 1685 | + // If attacking, abort and retry |
| 1686 | + if (this.state === 'attacking') { |
| 1687 | + this.state = 'approaching' |
| 1688 | + this.attacking = false |
| 1689 | + } |
| 1690 | + } |
| 1691 | + } else { |
| 1692 | + this.stuckCounter = 0 // Reset counter when not stuck |
| 1693 | + } |
| 1694 | + } |
| 1695 | + |
| 1696 | + // Also check home branch collision |
| 1697 | + if (window.homeBranch && this.y > window.homeBranch.y - 40) { |
| 1698 | + // Check if bird is in branch X range |
| 1699 | + let branchStart = Math.min( |
| 1700 | + window.homeBranch.startX, |
| 1701 | + window.homeBranch.endX |
| 1702 | + ) |
| 1703 | + let branchEnd = Math.max(window.homeBranch.startX, window.homeBranch.endX) |
| 1704 | + |
| 1705 | + if (this.x >= branchStart - 20 && this.x <= branchEnd + 20) { |
| 1706 | + // Bird is too close to branch, push up |
| 1707 | + this.y = window.homeBranch.y - 40 |
| 1708 | + |
| 1709 | + // If diving, abort dive |
| 1710 | + if (this.state === 'attacking' && this.pattern === 'dive') { |
| 1711 | + this.state = 'retreating' |
| 1712 | + this.attacking = false |
| 1713 | + } |
| 1714 | + } |
| 1715 | + } |
| 1716 | + } |
| 1717 | + |
| 1613 | 1718 | executeGlidePattern () { |
| 1614 | 1719 | if (this.state === 'approaching') { |
| 1615 | 1720 | // Glide in from top corner faster |
@@ -1715,59 +1820,67 @@ class Bird { |
| 1715 | 1820 | } |
| 1716 | 1821 | } |
| 1717 | 1822 | |
| 1718 | | -checkCollisions() { |
| 1823 | + checkCollisions () { |
| 1719 | 1824 | // FIX: Increased collision radius for more generous hit detection |
| 1720 | | - let collisionDistance = this.size + spider.radius + 5; // Added 5 pixel buffer |
| 1721 | | - |
| 1825 | + let collisionDistance = this.size + spider.radius + 5 // Added 5 pixel buffer |
| 1826 | + |
| 1722 | 1827 | // 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 |
| 1728 | | - |
| 1729 | | - // If spider has no stamina, GAME OVER! |
| 1730 | | - if (jumpStamina <= 0) { |
| 1731 | | - triggerGameOver('Exhausted spider caught by bird!'); |
| 1732 | | - return; |
| 1733 | | - } |
| 1734 | | - |
| 1735 | | - // Otherwise, reduce stamina |
| 1736 | | - jumpStamina = max(0, jumpStamina - damage); |
| 1737 | | - stats.birdHitsTaken++; |
| 1738 | | - |
| 1739 | | - // Knockback effect |
| 1740 | | - spider.vel.x = (spider.pos.x - this.x) * 0.3; |
| 1741 | | - spider.vel.y = -3; |
| 1742 | | - spider.isAirborne = true; |
| 1743 | | - |
| 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); |
| 1751 | | - } |
| 1752 | | - |
| 1753 | | - // Screen shake effect |
| 1754 | | - if (typeof screenShake !== 'undefined') { |
| 1755 | | - screenShake = 10; |
| 1756 | | - } |
| 1757 | | - |
| 1758 | | - // Warning notifications - but limited to prevent spam |
| 1759 | | - if (notifications.length < 3) { // Limit notifications |
| 1760 | | - if (jumpStamina <= 20) { |
| 1761 | | - notifications.push(new Notification("CRITICAL STAMINA!", color(255, 50, 50))); |
| 1762 | | - } else if (jumpStamina <= 40) { |
| 1763 | | - notifications.push(new Notification("Low stamina - find cover!", color(255, 150, 50))); |
| 1764 | | - } |
| 1765 | | - } |
| 1828 | + if ( |
| 1829 | + this.attacking && |
| 1830 | + dist(this.x, this.y, spider.pos.x, spider.pos.y) < collisionDistance |
| 1831 | + ) { |
| 1832 | + // Hit spider! |
| 1833 | + if (gamePhase === 'DAWN') { |
| 1834 | + // Calculate damage |
| 1835 | + let damage = 20 // Base damage |
| 1836 | + |
| 1837 | + // If spider has no stamina, GAME OVER! |
| 1838 | + if (jumpStamina <= 0) { |
| 1839 | + triggerGameOver('Exhausted spider caught by bird!') |
| 1840 | + return |
| 1841 | + } |
| 1842 | + |
| 1843 | + // Otherwise, reduce stamina |
| 1844 | + jumpStamina = max(0, jumpStamina - damage) |
| 1845 | + stats.birdHitsTaken++ |
| 1846 | + |
| 1847 | + // Knockback effect |
| 1848 | + spider.vel.x = (spider.pos.x - this.x) * 0.3 |
| 1849 | + spider.vel.y = -3 |
| 1850 | + spider.isAirborne = true |
| 1851 | + |
| 1852 | + // Red damage particles |
| 1853 | + for (let i = 0; i < 12; i++) { |
| 1854 | + let p = new Particle(spider.pos.x, spider.pos.y) |
| 1855 | + p.color = color(255, 50, 50) |
| 1856 | + p.vel = createVector(random(-4, 4), random(-4, 1)) |
| 1857 | + p.size = random(4, 8) |
| 1858 | + particles.push(p) |
| 1859 | + } |
| 1860 | + |
| 1861 | + // Screen shake effect |
| 1862 | + if (typeof screenShake !== 'undefined') { |
| 1863 | + screenShake = 10 |
| 1864 | + } |
| 1865 | + |
| 1866 | + // Warning notifications - but limited to prevent spam |
| 1867 | + if (notifications.length < 3) { |
| 1868 | + // Limit notifications |
| 1869 | + if (jumpStamina <= 20) { |
| 1870 | + notifications.push( |
| 1871 | + new Notification('CRITICAL STAMINA!', color(255, 50, 50)) |
| 1872 | + ) |
| 1873 | + } else if (jumpStamina <= 40) { |
| 1874 | + notifications.push( |
| 1875 | + new Notification('Low stamina - find cover!', color(255, 150, 50)) |
| 1876 | + ) |
| 1877 | + } |
| 1766 | 1878 | } |
| 1767 | | - |
| 1768 | | - // Bird bounces off |
| 1769 | | - this.state = 'retreating'; |
| 1770 | | - this.attacking = false; |
| 1879 | + } |
| 1880 | + |
| 1881 | + // Bird bounces off |
| 1882 | + this.state = 'retreating' |
| 1883 | + this.attacking = false |
| 1771 | 1884 | } |
| 1772 | 1885 | |
| 1773 | 1886 | // Check collision with web strands |
@@ -1827,6 +1940,17 @@ checkCollisions() { |
| 1827 | 1940 | push() |
| 1828 | 1941 | translate(this.x, this.y) |
| 1829 | 1942 | |
| 1943 | + // Show if bird is stuck (for debugging) |
| 1944 | + if (this.stuckCounter > 15) { |
| 1945 | + // Flash red when stuck |
| 1946 | + push() |
| 1947 | + noFill() |
| 1948 | + stroke(255, 0, 0, 100) |
| 1949 | + strokeWeight(2) |
| 1950 | + ellipse(0, 0, this.size * 3) |
| 1951 | + pop() |
| 1952 | + } |
| 1953 | + |
| 1830 | 1954 | // Rotate based on movement |
| 1831 | 1955 | if (this.state === 'attacking' && this.pattern === 'dive') { |
| 1832 | 1956 | rotate(PI / 2) // Point down when diving |