@@ -23,13 +23,15 @@ class Spider { |
| 23 | // DAWN PHASE: Check and consume stamina | 23 | // DAWN PHASE: Check and consume stamina |
| 24 | if (gamePhase === 'DAWN') { | 24 | if (gamePhase === 'DAWN') { |
| 25 | if (jumpStamina < jumpCost) { | 25 | if (jumpStamina < jumpCost) { |
| 26 | - // Not enough stamina to jump | | |
| 27 | isExhausted = true | 26 | isExhausted = true |
| 28 | - return // Can't jump | 27 | + return |
| 29 | } | 28 | } |
| 30 | - // Consume stamina for jump | | |
| 31 | jumpStamina -= jumpCost | 29 | jumpStamina -= jumpCost |
| 32 | stats.totalJumps++ | 30 | stats.totalJumps++ |
| | 31 | + // Delay stamina regen after each jump during DAWN |
| | 32 | + if (gamePhase === 'DAWN') { |
| | 33 | + staminaRegenCooldown = 60 // 1s at 60fps |
| | 34 | + } |
| 33 | } | 35 | } |
| 34 | | 36 | |
| 35 | // PHASE 4B: Track wind jumps | 37 | // PHASE 4B: Track wind jumps |
@@ -60,7 +62,12 @@ class Spider { |
| 60 | this.vel = direction | 62 | this.vel = direction |
| 61 | this.isAirborne = true | 63 | this.isAirborne = true |
| 62 | this.canJump = false | 64 | this.canJump = false |
| 63 | - this.lastAnchorPoint = this.pos.copy() | 65 | + |
| | 66 | + // FIX: Ensure lastAnchorPoint is set to edge, not center |
| | 67 | + if (!this.lastAnchorPoint) { |
| | 68 | + // If no anchor point set yet, use current position |
| | 69 | + this.lastAnchorPoint = this.pos.copy() |
| | 70 | + } |
| 64 | // Record jump time for touch debounce | 71 | // Record jump time for touch debounce |
| 65 | if (typeof window !== 'undefined') { | 72 | if (typeof window !== 'undefined') { |
| 66 | window.lastJumpTime = millis() | 73 | window.lastJumpTime = millis() |
@@ -301,10 +308,20 @@ class Spider { |
| 301 | // Only land if we're actually airborne | 308 | // Only land if we're actually airborne |
| 302 | if (!this.isAirborne) return | 309 | if (!this.isAirborne) return |
| 303 | | 310 | |
| | 311 | + // Calculate angle from obstacle center to spider |
| 304 | let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x) | 312 | let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x) |
| | 313 | + |
| | 314 | + // Place spider on the edge of the circular collision boundary |
| 305 | this.pos.x = obstacle.x + cos(angle) * (obstacle.radius + this.radius) | 315 | this.pos.x = obstacle.x + cos(angle) * (obstacle.radius + this.radius) |
| 306 | this.pos.y = obstacle.y + sin(angle) * (obstacle.radius + this.radius) | 316 | this.pos.y = obstacle.y + sin(angle) * (obstacle.radius + this.radius) |
| 307 | - this.attachedObstacle = obstacle // Track which obstacle we're on | 317 | + |
| | 318 | + // FIX: Set anchor point at the edge, not center |
| | 319 | + this.lastAnchorPoint = createVector( |
| | 320 | + obstacle.x + cos(angle) * obstacle.radius, |
| | 321 | + obstacle.y + sin(angle) * obstacle.radius |
| | 322 | + ) |
| | 323 | + |
| | 324 | + this.attachedObstacle = obstacle |
| 308 | this.land() | 325 | this.land() |
| 309 | } | 326 | } |
| 310 | | 327 | |
@@ -340,34 +357,48 @@ class Spider { |
| 340 | | 357 | |
| 341 | // FIX: Check if we're actually landing on something valid | 358 | // FIX: Check if we're actually landing on something valid |
| 342 | let landedOnSomething = false | 359 | let landedOnSomething = false |
| | 360 | + let landingPoint = null // Store where we're landing for anchor |
| 343 | | 361 | |
| 344 | // Check if on ground | 362 | // Check if on ground |
| 345 | if (this.pos.y >= height - this.radius - 5) { | 363 | if (this.pos.y >= height - this.radius - 5) { |
| 346 | landedOnSomething = true | 364 | landedOnSomething = true |
| | 365 | + landingPoint = createVector(this.pos.x, height) |
| 347 | } | 366 | } |
| 348 | | 367 | |
| 349 | // Check if on an obstacle | 368 | // Check if on an obstacle |
| 350 | - for (let obstacle of obstacles) { | 369 | + if (!landedOnSomething) { |
| 351 | - if (this.checkObstacleCollision(obstacle)) { | 370 | + for (let obstacle of obstacles) { |
| 352 | - landedOnSomething = true | 371 | + if (this.checkObstacleCollision(obstacle)) { |
| 353 | - break | 372 | + landedOnSomething = true |
| | 373 | + // Calculate edge point for anchor |
| | 374 | + let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x) |
| | 375 | + landingPoint = createVector( |
| | 376 | + obstacle.x + cos(angle) * obstacle.radius, |
| | 377 | + obstacle.y + sin(angle) * obstacle.radius |
| | 378 | + ) |
| | 379 | + break |
| | 380 | + } |
| 354 | } | 381 | } |
| 355 | } | 382 | } |
| 356 | | 383 | |
| 357 | // Check if on a web strand | 384 | // Check if on a web strand |
| 358 | - for (let strand of webStrands) { | 385 | + if (!landedOnSomething) { |
| 359 | - if ( | 386 | + for (let strand of webStrands) { |
| 360 | - strand !== currentStrand && | 387 | + if ( |
| 361 | - !strand.broken && | 388 | + strand !== currentStrand && |
| 362 | - this.checkStrandCollision(strand) | 389 | + !strand.broken && |
| 363 | - ) { | 390 | + this.checkStrandCollision(strand) |
| 364 | - landedOnSomething = true | 391 | + ) { |
| 365 | - break | 392 | + landedOnSomething = true |
| | 393 | + // For web strands, use spider position as anchor |
| | 394 | + landingPoint = this.pos.copy() |
| | 395 | + break |
| | 396 | + } |
| 366 | } | 397 | } |
| 367 | } | 398 | } |
| 368 | | 399 | |
| 369 | // Check if on home branch | 400 | // Check if on home branch |
| 370 | - if (window.homeBranch) { | 401 | + if (!landedOnSomething && window.homeBranch) { |
| 371 | let branch = window.homeBranch | 402 | let branch = window.homeBranch |
| 372 | let branchStart = Math.min(branch.startX, branch.endX) | 403 | let branchStart = Math.min(branch.startX, branch.endX) |
| 373 | let branchEnd = Math.max(branch.startX, branch.endX) | 404 | let branchEnd = Math.max(branch.startX, branch.endX) |
@@ -386,21 +417,25 @@ class Spider { |
| 386 | | 417 | |
| 387 | if (abs(this.pos.y - branchSurfaceY) < this.radius + 10) { | 418 | if (abs(this.pos.y - branchSurfaceY) < this.radius + 10) { |
| 388 | landedOnSomething = true | 419 | landedOnSomething = true |
| | 420 | + landingPoint = createVector(this.pos.x, branchSurfaceY) |
| 389 | } | 421 | } |
| 390 | } | 422 | } |
| 391 | } | 423 | } |
| 392 | | 424 | |
| 393 | // FIX: If we're deploying web but didn't land on anything valid, destroy the web | 425 | // FIX: If we're deploying web but didn't land on anything valid, destroy the web |
| 394 | if (currentStrand && isDeployingWeb && (spacePressed || touchHolding)) { | 426 | if (currentStrand && isDeployingWeb && (spacePressed || touchHolding)) { |
| 395 | - if (landedOnSomething) { | 427 | + if (landedOnSomething && landingPoint) { |
| 396 | - // Valid landing - finalize the web | 428 | + // Valid landing - finalize the web at the landing point |
| 397 | - currentStrand.end = this.pos.copy() | 429 | + currentStrand.end = landingPoint.copy() // Use edge point, not spider center |
| 398 | if (!currentStrand.path || currentStrand.path.length === 0) { | 430 | if (!currentStrand.path || currentStrand.path.length === 0) { |
| 399 | - currentStrand.path = [this.pos.copy()] | 431 | + currentStrand.path = [landingPoint.copy()] |
| 400 | } else { | 432 | } else { |
| 401 | - currentStrand.path.push(this.pos.copy()) | 433 | + currentStrand.path.push(landingPoint.copy()) |
| 402 | } | 434 | } |
| 403 | - webNodes.push(new WebNode(this.pos.x, this.pos.y)) | 435 | + webNodes.push(new WebNode(landingPoint.x, landingPoint.y)) |
| | 436 | + |
| | 437 | + // Update last anchor for next web |
| | 438 | + this.lastAnchorPoint = landingPoint.copy() |
| 404 | } else { | 439 | } else { |
| 405 | // Invalid landing in mid-air - destroy the web! | 440 | // Invalid landing in mid-air - destroy the web! |
| 406 | if ( | 441 | if ( |
@@ -426,6 +461,9 @@ class Spider { |
| 426 | } | 461 | } |
| 427 | } | 462 | } |
| 428 | } | 463 | } |
| | 464 | + } else if (landedOnSomething && landingPoint) { |
| | 465 | + // Update last anchor point even when not deploying web |
| | 466 | + this.lastAnchorPoint = landingPoint.copy() |
| 429 | } | 467 | } |
| 430 | | 468 | |
| 431 | currentStrand = null | 469 | currentStrand = null |
@@ -869,42 +907,50 @@ class Obstacle { |
| 869 | updateAttachedStrands () { | 907 | updateAttachedStrands () { |
| 870 | // Update web strands that are connected to this obstacle | 908 | // Update web strands that are connected to this obstacle |
| 871 | for (let strand of webStrands) { | 909 | for (let strand of webStrands) { |
| 872 | - // Check if strand starts at this obstacle | 910 | + // Check if strand starts near this obstacle's edge |
| 873 | - if ( | 911 | + let startDist = dist(strand.start.x, strand.start.y, this.x, this.y) |
| 874 | - dist(strand.start.x, strand.start.y, this.x, this.y) < | 912 | + if (startDist >= this.radius - 5 && startDist <= this.radius + 15) { |
| 875 | - this.radius + 10 | 913 | + // Strand is attached to edge - update to maintain edge connection |
| 876 | - ) { | 914 | + let angle = atan2(strand.start.y - this.y, strand.start.x - this.x) |
| 877 | - strand.start.x = this.x | 915 | + strand.start.x = this.x + cos(angle) * this.radius |
| 878 | - strand.start.y = this.y | 916 | + strand.start.y = this.y + sin(angle) * this.radius |
| 879 | if (strand.path && strand.path.length > 0) { | 917 | if (strand.path && strand.path.length > 0) { |
| 880 | - strand.path[0].x = this.x | 918 | + strand.path[0].x = strand.start.x |
| 881 | - strand.path[0].y = this.y | 919 | + strand.path[0].y = strand.start.y |
| 882 | } | 920 | } |
| 883 | } | 921 | } |
| 884 | | 922 | |
| 885 | - // Check if strand ends at this obstacle | 923 | + // Check if strand ends near this obstacle's edge |
| 886 | - if ( | 924 | + if (strand.end) { |
| 887 | - strand.end && | 925 | + let endDist = dist(strand.end.x, strand.end.y, this.x, this.y) |
| 888 | - dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10 | 926 | + if (endDist >= this.radius - 5 && endDist <= this.radius + 15) { |
| 889 | - ) { | 927 | + // Strand is attached to edge - update to maintain edge connection |
| 890 | - strand.end.x = this.x | 928 | + let angle = atan2(strand.end.y - this.y, strand.end.x - this.x) |
| 891 | - strand.end.y = this.y | 929 | + strand.end.x = this.x + cos(angle) * this.radius |
| 892 | - if (strand.path && strand.path.length > 0) { | 930 | + strand.end.y = this.y + sin(angle) * this.radius |
| 893 | - strand.path[strand.path.length - 1].x = this.x | 931 | + if (strand.path && strand.path.length > 0) { |
| 894 | - strand.path[strand.path.length - 1].y = this.y | 932 | + strand.path[strand.path.length - 1].x = strand.end.x |
| | 933 | + strand.path[strand.path.length - 1].y = strand.end.y |
| | 934 | + } |
| 895 | } | 935 | } |
| 896 | } | 936 | } |
| 897 | } | 937 | } |
| 898 | } | 938 | } |
| 899 | | 939 | |
| 900 | breakAttachedStrands () { | 940 | breakAttachedStrands () { |
| 901 | - // Break any strands attached to this beetle that has drifted too far | 941 | + // Check for strands attached to this obstacle's edge |
| 902 | for (let strand of webStrands) { | 942 | for (let strand of webStrands) { |
| | 943 | + // Check if attached to edge (not center) |
| | 944 | + let startDist = dist(strand.start.x, strand.start.y, this.x, this.y) |
| 903 | let attachedToStart = | 945 | let attachedToStart = |
| 904 | - dist(strand.start.x, strand.start.y, this.x, this.y) < this.radius + 10 | 946 | + startDist >= this.radius - 5 && startDist <= this.radius + 15 |
| 905 | - let attachedToEnd = | 947 | + |
| 906 | - strand.end && | 948 | + let attachedToEnd = false |
| 907 | - dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10 | 949 | + if (strand.end) { |
| | 950 | + let endDist = dist(strand.end.x, strand.end.y, this.x, this.y) |
| | 951 | + attachedToEnd = |
| | 952 | + endDist >= this.radius - 5 && endDist <= this.radius + 15 |
| | 953 | + } |
| 908 | | 954 | |
| 909 | if (attachedToStart || attachedToEnd) { | 955 | if (attachedToStart || attachedToEnd) { |
| 910 | // Mark strand as broken | 956 | // Mark strand as broken |
@@ -1056,7 +1102,11 @@ class Obstacle { |
| 1056 | // Matte fabric shading (subtle, non-glossy) | 1102 | // Matte fabric shading (subtle, non-glossy) |
| 1057 | noStroke() | 1103 | noStroke() |
| 1058 | // Soft radial shading toward top-left to imply ambient light without specular shine | 1104 | // Soft radial shading toward top-left to imply ambient light without specular shine |
| 1059 | - for (let r = this.radius * 1.2; r > this.radius * 0.2; r -= this.radius * 0.15) { | 1105 | + for ( |
| | 1106 | + let r = this.radius * 1.2; |
| | 1107 | + r > this.radius * 0.2; |
| | 1108 | + r -= this.radius * 0.15 |
| | 1109 | + ) { |
| 1060 | fill(255, 255, 255, 8) // very low alpha | 1110 | fill(255, 255, 255, 8) // very low alpha |
| 1061 | ellipse(-this.radius * 0.25, -this.radius * 0.35, r * 0.25, r * 0.18) | 1111 | ellipse(-this.radius * 0.25, -this.radius * 0.35, r * 0.25, r * 0.18) |
| 1062 | } | 1112 | } |
@@ -1622,19 +1672,51 @@ class Bird { |
| 1622 | // Update target position while diving | 1672 | // Update target position while diving |
| 1623 | if (frameCount % 8 === 0) { | 1673 | if (frameCount % 8 === 0) { |
| 1624 | this.targetX = spider.pos.x | 1674 | this.targetX = spider.pos.x |
| 1625 | - this.targetY = spider.pos.y | 1675 | + // Keep steering intent downward; don't let target sit above our per-dive floor |
| | 1676 | + this.targetY = |
| | 1677 | + typeof this.pullUpY === 'number' |
| | 1678 | + ? Math.min(spider.pos.y, this.pullUpY - 4) |
| | 1679 | + : spider.pos.y |
| 1626 | } | 1680 | } |
| 1627 | | 1681 | |
| 1628 | // Only consider bailing out after a minimum number of dive frames | 1682 | // Only consider bailing out after a minimum number of dive frames |
| 1629 | - let canBailOut = this.diveFrames > 15 | 1683 | + let canBailOut = this.diveFrames > 22 |
| 1630 | // Use pullUpY for stable bailout check | 1684 | // Use pullUpY for stable bailout check |
| 1631 | - let reachedPullUpY = (typeof this.pullUpY === 'number') ? (this.y > this.pullUpY) : false | 1685 | + let reachedPullUpY = |
| | 1686 | + typeof this.pullUpY === 'number' ? this.y > this.pullUpY : false |
| 1632 | let reachedBottom = this.y > height - 20 // Go almost to canvas bottom | 1687 | let reachedBottom = this.y > height - 20 // Go almost to canvas bottom |
| 1633 | - let closeToSpider = dist(this.x, this.y, spider.pos.x, spider.pos.y) < 50 | 1688 | + const hitCollision = |
| 1634 | - | 1689 | + dist(this.x, this.y, spider.pos.x, spider.pos.y) <= |
| 1635 | - if (canBailOut && (reachedPullUpY || reachedBottom || closeToSpider)) { | 1690 | + this.size * 0.5 + spider.radius |
| | 1691 | + const nearButNotHit = |
| | 1692 | + !hitCollision && |
| | 1693 | + abs(this.x - spider.pos.x) < 30 && |
| | 1694 | + abs(this.y - spider.pos.y) < 24 |
| | 1695 | + |
| | 1696 | + if (canBailOut && (hitCollision || reachedPullUpY || reachedBottom)) { |
| | 1697 | + // Convert near-miss near the floor into a sweep instead of an early bail |
| | 1698 | + if ( |
| | 1699 | + !hitCollision && |
| | 1700 | + reachedPullUpY && |
| | 1701 | + spider.pos.y > height - 30 && |
| | 1702 | + !this.sweeping |
| | 1703 | + ) { |
| | 1704 | + this.sweeping = true |
| | 1705 | + this.y = spider.pos.y // lock to spider height |
| | 1706 | + this.vy = 0 |
| | 1707 | + const sweepDirection = spider.pos.x > this.x ? 1 : -1 |
| | 1708 | + this.vx = sweepDirection * 8 |
| | 1709 | + setTimeout(() => { |
| | 1710 | + this.sweeping = false |
| | 1711 | + this.state = 'retreating' |
| | 1712 | + this.attacking = false |
| | 1713 | + this.diveFrames = 0 |
| | 1714 | + this.pullUpY = null |
| | 1715 | + }, 500) |
| | 1716 | + return // skip normal bailout path |
| | 1717 | + } |
| 1636 | // If spider is at bottom and we haven't hit it yet, do a horizontal sweep | 1718 | // If spider is at bottom and we haven't hit it yet, do a horizontal sweep |
| 1637 | - if (spider.pos.y > height - 30 && !closeToSpider && !this.sweeping) { | 1719 | + if (spider.pos.y > height - 30 && !hitCollision && !this.sweeping) { |
| 1638 | this.sweeping = true | 1720 | this.sweeping = true |
| 1639 | this.y = spider.pos.y // Match spider height | 1721 | this.y = spider.pos.y // Match spider height |
| 1640 | this.vy = 0 // Stop vertical movement | 1722 | this.vy = 0 // Stop vertical movement |