Advertisement
Guest User

rts.html

a guest
Apr 15th, 2025
60
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 36.78 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>Simple WW2 RTS</title>
  7.     <style>
  8.         body { margin: 0; overflow: hidden; background-color: #222; color: #eee; font-family: sans-serif; }
  9.         canvas { display: block; background-color: #6B8E23; /* Olive Drab */ cursor: crosshair; }
  10.         #ui { position: absolute; bottom: 10px; left: 10px; background-color: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px; color: #fff; }
  11.         #info { position: absolute; top: 10px; right: 10px; background-color: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px; color: #fff; min-width: 150px; }
  12.         button { margin: 2px; padding: 5px; cursor: pointer; }
  13.     </style>
  14. </head>
  15. <body>
  16.  
  17. <canvas id="gameCanvas"></canvas>
  18. <div id="ui">
  19.     <div>Supplies: <span id="supplies">0</span></div>
  20.     <div id="build-buttons">
  21.         <!-- Buttons added dynamically based on selection -->
  22.     </div>
  23. </div>
  24. <div id="info">
  25.     Selected: <span id="selected-info">None</span><br>
  26.     HP: <span id="selected-hp">-</span>
  27. </div>
  28.  
  29. <script>
  30.     const canvas = document.getElementById('gameCanvas');
  31.     const ctx = canvas.getContext('2d');
  32.     const ui = document.getElementById('ui');
  33.     const suppliesDisplay = document.getElementById('supplies');
  34.     const buildButtonsDiv = document.getElementById('build-buttons');
  35.     const selectedInfoDisplay = document.getElementById('selected-info');
  36.     const selectedHpDisplay = document.getElementById('selected-hp');
  37.  
  38.     canvas.width = window.innerWidth;
  39.     canvas.height = window.innerHeight;
  40.  
  41.     // --- Game Constants ---
  42.     const TILE_SIZE = 32; // Not really used for grid, just scaling
  43.     const HQ_COST = 0; // Starting building
  44.     const BARRACKS_COST = 150;
  45.     const WORKER_COST = 50;
  46.     const RIFLEMAN_COST = 75;
  47.     const GATHER_AMOUNT = 10;
  48.     const GATHER_RADIUS = 50;
  49.     const UNIT_SPEED = 2;
  50.     const ATTACK_RANGE = 80;
  51.     const ATTACK_DAMAGE = 5;
  52.     const ATTACK_COOLDOWN = 60; // frames
  53.     const AI_UPDATE_INTERVAL = 120; // frames
  54.  
  55.     // --- Game State ---
  56.     let gameObjects = [];
  57.     let resources = [];
  58.     let playerSupplies = 200;
  59.     let aiSupplies = 200;
  60.     let selectedObject = null;
  61.     let frameCount = 0;
  62.     let gameOver = false;
  63.     let winner = null;
  64.  
  65.     // --- Helper Functions ---
  66.     function distance(x1, y1, x2, y2) {
  67.         return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
  68.     }
  69.  
  70.     function isColliding(obj1, obj2) {
  71.       if (!obj1 || !obj2 || obj1 === obj2) return false;
  72.       const dist = distance(obj1.x, obj1.y, obj2.x, obj2.y);
  73.       return dist < (obj1.size / 2 + obj2.size / 2);
  74.    }
  75.  
  76.    function findClosest(obj, list, filterFn = () => true) {
  77.         let closest = null;
  78.         let minDist = Infinity;
  79.         for (const target of list) {
  80.             if (obj === target || !filterFn(target)) continue;
  81.             const d = distance(obj.x, obj.y, target.x, target.y);
  82.             if (d < minDist) {
  83.                minDist = d;
  84.                closest = target;
  85.            }
  86.        }
  87.        return { target: closest, distance: minDist };
  88.    }
  89.  
  90.     function getObjectsInRadius(x, y, radius, list, filterFn = () => true) {
  91.         const nearby = [];
  92.         for (const obj of list) {
  93.             if (!filterFn(obj)) continue;
  94.             const d = distance(x, y, obj.x, obj.y);
  95.             if (d <= radius) {
  96.                nearby.push(obj);
  97.            }
  98.        }
  99.        return nearby;
  100.    }
  101.  
  102.  
  103.    // --- Game Object Classes (using functions for simplicity) ---
  104.  
  105.    function createBuilding(x, y, type, owner) {
  106.        const building = {
  107.            id: Date.now() + Math.random(),
  108.            x, y, type, owner,
  109.            maxHp: type === 'HQ' ? 1000 : 500,
  110.            hp: type === 'HQ' ? 1000 : 500,
  111.            size: type === 'HQ' ? 80 : 60,
  112.            isBuilding: true,
  113.            productionQueue: [],
  114.            productionProgress: 0,
  115.            rallyPoint: { x: x + 60, y: y + 60},
  116.            target: null, // Needed for AI targeting consistency
  117.  
  118.            draw() {
  119.                ctx.fillStyle = owner === 'player' ? 'blue' : 'red';
  120.                ctx.fillRect(this.x - this.size / 2, this.y - this.size / 2, this.size, this.size);
  121.                ctx.strokeStyle = 'black';
  122.                ctx.strokeRect(this.x - this.size / 2, this.y - this.size / 2, this.size, this.size);
  123.  
  124.                // Health bar
  125.                const hpRatio = this.hp / this.maxHp;
  126.                ctx.fillStyle = 'red';
  127.                ctx.fillRect(this.x - this.size / 2, this.y - this.size / 2 - 10, this.size, 5);
  128.                ctx.fillStyle = 'green';
  129.                ctx.fillRect(this.x - this.size / 2, this.y - this.size / 2 - 10, this.size * hpRatio, 5);
  130.  
  131.                // Rally point
  132.                if (selectedObject === this && this.rallyPoint) {
  133.                    ctx.strokeStyle = 'yellow';
  134.                    ctx.beginPath();
  135.                    ctx.moveTo(this.x, this.y);
  136.                    ctx.lineTo(this.rallyPoint.x, this.rallyPoint.y);
  137.                    ctx.stroke();
  138.                    ctx.strokeRect(this.rallyPoint.x - 5, this.rallyPoint.y - 5, 10, 10);
  139.                }
  140.  
  141.                // Production progress
  142.                if (this.productionQueue.length > 0) {
  143.                      const progressRatio = this.productionProgress / getProductionTime(this.productionQueue[0]);
  144.                      ctx.fillStyle = 'grey';
  145.                      ctx.fillRect(this.x - this.size / 2, this.y + this.size / 2 + 2, this.size, 5);
  146.                      ctx.fillStyle = 'lightblue';
  147.                      ctx.fillRect(this.x - this.size / 2, this.y + this.size / 2 + 2, this.size * progressRatio, 5);
  148.                 }
  149.             },
  150.             update() {
  151.                 if (this.hp <= 0) {
  152.                    this.remove();
  153.                    if (this.type === 'HQ') {
  154.                        gameOver = true;
  155.                        winner = this.owner === 'player' ? 'AI' : 'Player';
  156.                    }
  157.                    return;
  158.                }
  159.  
  160.                 // Production
  161.                if (this.productionQueue.length > 0) {
  162.                     this.productionProgress++;
  163.                     const neededTime = getProductionTime(this.productionQueue[0]);
  164.                     if (this.productionProgress >= neededTime) {
  165.                         const unitType = this.productionQueue.shift();
  166.                         const spawnPos = this.rallyPoint || {x: this.x + this.size/2 + 20, y: this.y};
  167.                         const newUnit = createUnit(spawnPos.x, spawnPos.y, unitType, this.owner);
  168.                         gameObjects.push(newUnit);
  169.                          // If rally point is on an enemy, set attack move
  170.                         const rallyTarget = findClosest(newUnit, gameObjects, o => o.owner !== this.owner && distance(spawnPos.x, spawnPos.y, o.x, o.y) < 15);
  171.                         if(rallyTarget.target){
  172.                             newUnit.command = { type: 'attack', target: rallyTarget.target };
  173.                         } else {
  174.                            newUnit.command = { type: 'move', x: spawnPos.x, y: spawnPos.y }; // Move to rally point initially
  175.                         }
  176.  
  177.                         this.productionProgress = 0;
  178.                     }
  179.                 }
  180.             },
  181.             queueUnit(unitType) {
  182.                  if ( (this.type === 'HQ' && unitType === 'Worker') ||
  183.                      (this.type === 'Barracks' && unitType === 'Rifleman') ) {
  184.                    const cost = unitType === 'Worker' ? WORKER_COST : RIFLEMAN_COST;
  185.                     if (this.owner === 'player' && playerSupplies >= cost) {
  186.                        playerSupplies -= cost;
  187.                         this.productionQueue.push(unitType);
  188.                     } else if (this.owner === 'ai' && aiSupplies >= cost) {
  189.                        aiSupplies -= cost;
  190.                         this.productionQueue.push(unitType);
  191.                     }
  192.                  }
  193.             },
  194.             setRallyPoint(x, y) {
  195.                 this.rallyPoint = {x, y};
  196.                 // If right click on enemy unit/building, set as attack target for rally
  197.                 let targetObject = null;
  198.                  for (const obj of gameObjects) {
  199.                      if (obj.owner !== this.owner && distance(x, y, obj.x, obj.y) < obj.size / 2) {
  200.                         targetObject = obj;
  201.                          break;
  202.                      }
  203.                  }
  204.                  // We won't implement attack-rally here for simplicity, just move rally.
  205.             },
  206.              remove() {
  207.                 gameObjects = gameObjects.filter(o => o.id !== this.id);
  208.                 if (selectedObject === this) selectedObject = null;
  209.             }
  210.         };
  211.         return building;
  212.     }
  213.  
  214.      function getProductionTime(unitType) {
  215.         switch(unitType) {
  216.             case 'Worker': return 180; // 3 seconds at 60fps
  217.             case 'Rifleman': return 300; // 5 seconds at 60fps
  218.             default: return 60;
  219.         }
  220.     }
  221.  
  222.     function createUnit(x, y, type, owner) {
  223.         const unit = {
  224.             id: Date.now() + Math.random(),
  225.             x, y, type, owner,
  226.             maxHp: type === 'Worker' ? 50 : 100,
  227.             hp: type === 'Worker' ? 50 : 100,
  228.             size: type === 'Worker' ? 15 : 20,
  229.             speed: UNIT_SPEED,
  230.             isBuilding: false,
  231.             command: { type: 'idle' }, // { type: 'move', x, y }, { type: 'gather', targetResource, targetHQ }, { type: 'attack', target }
  232.             target: null, // Current move/attack target object
  233.             attackCooldown: 0,
  234.             resourceCarried: 0,
  235.  
  236.             draw() {
  237.                 ctx.beginPath();
  238.                 ctx.arc(this.x, this.y, this.size / 2, 0, Math.PI * 2);
  239.                 ctx.fillStyle = owner === 'player' ? (type === 'Worker' ? 'lightblue' : 'darkblue') : (type === 'Worker' ? 'lightcoral' : 'darkred');
  240.                 ctx.fill();
  241.                 ctx.strokeStyle = 'black';
  242.                 ctx.stroke();
  243.  
  244.                  // Health bar
  245.                 const hpRatio = this.hp / this.maxHp;
  246.                 ctx.fillStyle = 'red';
  247.                 ctx.fillRect(this.x - this.size / 2, this.y - this.size / 2 - 8, this.size, 3);
  248.                 ctx.fillStyle = 'green';
  249.                 ctx.fillRect(this.x - this.size / 2, this.y - this.size / 2 - 8, this.size * hpRatio, 3);
  250.  
  251.                  // Resource carried indicator (for worker)
  252.                  if (this.type === 'Worker' && this.resourceCarried > 0) {
  253.                    ctx.fillStyle = 'gold';
  254.                     ctx.beginPath();
  255.                     ctx.arc(this.x + this.size/3, this.y - this.size/3, 4, 0, Math.PI*2);
  256.                     ctx.fill();
  257.                  }
  258.             },
  259.  
  260.             update() {
  261.                  if (this.hp <= 0) {
  262.                    this.remove();
  263.                    return;
  264.                 }
  265.  
  266.                this.attackCooldown = Math.max(0, this.attackCooldown - 1);
  267.  
  268.                 // --- Command Execution ---
  269.                 if (this.command.type === 'move') {
  270.                    this.target = null; // Clear attack target when moving
  271.                    const dx = this.command.x - this.x;
  272.                    const dy = this.command.y - this.y;
  273.                    const dist = Math.sqrt(dx * dx + dy * dy);
  274.                    if (dist > this.speed) {
  275.                         this.x += (dx / dist) * this.speed;
  276.                         this.y += (dy / dist) * this.speed;
  277.                     } else {
  278.                         this.x = this.command.x;
  279.                         this.y = this.command.y;
  280.                         this.command = { type: 'idle' };
  281.                     }
  282.                  } else if (this.command.type === 'gather') {
  283.                      if (this.resourceCarried === 0) { // Move to resource
  284.                          const targetRes = this.command.targetResource;
  285.                          if (!targetRes || targetRes.amount <= 0) { // Resource depleted or destroyed
  286.                            this.command = { type: 'idle'};
  287.                            // Find new nearby resource? (Simplification: just idle)
  288.                            return;
  289.                         }
  290.                         const dx = targetRes.x - this.x;
  291.                         const dy = targetRes.y - this.y;
  292.                         const dist = Math.sqrt(dx * dx + dy * dy);
  293.                         if (dist > this.speed + targetRes.size/2) {
  294.                              this.x += (dx / dist) * this.speed;
  295.                              this.y += (dy / dist) * this.speed;
  296.                          } else {
  297.                              // Arrived at resource node
  298.                              this.resourceCarried = GATHER_AMOUNT;
  299.                              targetRes.amount -= GATHER_AMOUNT; // Simplification: instant gather
  300.                              if (targetRes.amount <= 0) {
  301.                                 resources = resources.filter(r => r.id !== targetRes.id);
  302.                                  // Potentially need to re-assign other workers targeting this
  303.                              }
  304.                          }
  305.                      } else { // Have resources, move to HQ
  306.                         const targetHQ = this.command.targetHQ;
  307.                         if(!targetHQ || targetHQ.hp <= 0) { // HQ destroyed
  308.                            this.command = { type: 'idle'};
  309.                            // Drop resources? (Simplification: just idle)
  310.                            this.resourceCarried = 0; // Lose resources
  311.                            return;
  312.                        }
  313.                         const dx = targetHQ.x - this.x;
  314.                         const dy = targetHQ.y - this.y;
  315.                         const dist = Math.sqrt(dx * dx + dy * dy);
  316.                          if (dist > this.speed + targetHQ.size/2) {
  317.                              this.x += (dx / dist) * this.speed;
  318.                              this.y += (dy / dist) * this.speed;
  319.                          } else {
  320.                              // Arrived at HQ
  321.                              if (this.owner === 'player') playerSupplies += this.resourceCarried;
  322.                              else aiSupplies += this.resourceCarried;
  323.                              this.resourceCarried = 0;
  324.                              // Automatically go back to gathering? (Simplification: yes)
  325.                              if (this.command.targetResource && this.command.targetResource.amount > 0) {
  326.                                // command stays 'gather', loop continues
  327.                             } else {
  328.                                 // Find new resource if original depleted? (Simplification: idle)
  329.                                 this.command = {type: 'idle'};
  330.                              }
  331.                          }
  332.                      }
  333.  
  334.                  } else if (this.command.type === 'attack') {
  335.                     this.target = this.command.target;
  336.                     if (!this.target || this.target.hp <= 0) {
  337.                        this.command = { type: 'idle' };
  338.                        this.target = null;
  339.                        return;
  340.                    }
  341.                    const distToTarget = distance(this.x, this.y, this.target.x, this.target.y);
  342.  
  343.                    if (distToTarget <= ATTACK_RANGE) {
  344.                        // Stop moving and attack
  345.                        if (this.attackCooldown <= 0) {
  346.                            this.target.hp -= ATTACK_DAMAGE;
  347.                            this.attackCooldown = ATTACK_COOLDOWN;
  348.                             // Basic attack animation/sound placeholder
  349.                             // console.log(`${this.owner} ${this.type} attacking ${this.target.type}`);
  350.                        }
  351.                    } else {
  352.                         // Move towards target
  353.                         const dx = this.target.x - this.x;
  354.                         const dy = this.target.y - this.y;
  355.                         const dist = Math.sqrt(dx * dx + dy * dy);
  356.                         if (dist > this.speed) {
  357.                              this.x += (dx / dist) * this.speed;
  358.                              this.y += (dy / dist) * this.speed;
  359.                          }
  360.                     }
  361.                  } else { // Idle or finished command
  362.                     // Auto-attack nearby enemies if not worker
  363.                      if (this.type === 'Rifleman' && this.attackCooldown <= 0) {
  364.                        const nearbyEnemies = getObjectsInRadius(this.x, this.y, ATTACK_RANGE * 1.5, gameObjects, // Scan slightly further than attack range
  365.                            (obj) => obj.owner !== this.owner && obj.hp > 0);
  366.                         if (nearbyEnemies.length > 0) {
  367.                              // Find the closest enemy
  368.                              let closestEnemy = null;
  369.                              let minDist = Infinity;
  370.                              for(const enemy of nearbyEnemies){
  371.                                  const d = distance(this.x, this.y, enemy.x, enemy.y);
  372.                                  if(d < minDist){
  373.                                     minDist = d;
  374.                                     closestEnemy = enemy;
  375.                                 }
  376.                             }
  377.                            if (closestEnemy) {
  378.                                this.command = { type: 'attack', target: closestEnemy };
  379.                            }
  380.                        }
  381.                    }
  382.                 }
  383.  
  384.  
  385.                // Basic collision avoidance (push apart) - VERY basic
  386.                for (const other of gameObjects) {
  387.                   if (this !== other && !other.isBuilding && isColliding(this, other)) {
  388.                      const dx = other.x - this.x;
  389.                      const dy = other.y - this.y;
  390.                      const dist = Math.sqrt(dx*dx + dy*dy);
  391.                      const overlap = (this.size / 2 + other.size / 2) - dist;
  392.                      if (overlap > 0 && dist > 0.1) { // prevent division by zero
  393.                         const pushX = (dx / dist) * overlap * 0.1; // Push gently
  394.                          const pushY = (dy / dist) * overlap * 0.1;
  395.                          this.x -= pushX;
  396.                          this.y -= pushY;
  397.                          // other.x += pushX; // Avoid double processing
  398.                          // other.y += pushY;
  399.                       } else if (dist <= 0.1) { // Directly on top? Nudge randomly
  400.                         this.x += (Math.random() - 0.5) * 2;
  401.                         this.y += (Math.random() - 0.5) * 2;
  402.                      }
  403.                   }
  404.                }
  405.  
  406.                // Keep units within bounds
  407.                this.x = Math.max(this.size / 2, Math.min(canvas.width - this.size / 2, this.x));
  408.                this.y = Math.max(this.size / 2, Math.min(canvas.height - this.size / 2, this.y));
  409.  
  410.            },
  411.            remove() {
  412.                 gameObjects = gameObjects.filter(o => o.id !== this.id);
  413.                  // If this unit was a target, clear the attacker's command
  414.                  gameObjects.forEach(obj => {
  415.                      if (obj.command && obj.command.target === this) {
  416.                         obj.command = { type: 'idle' };
  417.                          obj.target = null;
  418.                      }
  419.                      if(obj.target === this) {
  420.                          obj.target = null;
  421.                      }
  422.                  });
  423.                  if (selectedObject === this) selectedObject = null;
  424.             },
  425.              setCommand(command) {
  426.                  // If gathering, find closest HQ
  427.                  if (command.type === 'gather') {
  428.                      const { target: closestHQ } = findClosest(this, gameObjects, obj => obj.owner === this.owner && obj.type === 'HQ');
  429.                      if (closestHQ) {
  430.                          command.targetHQ = closestHQ;
  431.                          this.command = command;
  432.                      } else {
  433.                          console.log("No HQ found for drop-off!"); // Cannot gather without HQ
  434.                          this.command = { type: 'idle' };
  435.                      }
  436.                  } else {
  437.                     this.command = command;
  438.                  }
  439.              }
  440.         };
  441.         return unit;
  442.     }
  443.  
  444.     function createResourceNode(x, y, amount) {
  445.         const resource = {
  446.             id: Date.now() + Math.random(),
  447.             x, y, amount,
  448.             maxAmount: amount,
  449.             size: 40,
  450.             isResource: true, // For type checking if needed
  451.             draw() {
  452.                 ctx.fillStyle = 'gold';
  453.                 ctx.beginPath();
  454.                 // Simple representation: multiple small circles
  455.                 for(let i=0; i< 5; i++){
  456.                     ctx.arc(
  457.                         this.x + (Math.random() - 0.5) * this.size * 0.6,
  458.                         this.y + (Math.random() - 0.5) * this.size * 0.6,
  459.                         this.size / 5, 0, Math.PI * 2);
  460.                }
  461.                ctx.fill();
  462.                 ctx.strokeStyle = 'darkgoldenrod';
  463.                 ctx.stroke();
  464.  
  465.                // Optional: Show remaining amount visually (e.g., text or bar)
  466.                 ctx.fillStyle = 'black';
  467.                 ctx.font = '10px sans-serif';
  468.                 ctx.textAlign = 'center';
  469.                 ctx.fillText(Math.round(this.amount), this.x, this.y + this.size/2 + 10);
  470.            },
  471.            update() {
  472.                // Resources themselves don't do much, maybe regenerate slowly?
  473.                 if (this.amount <= 0 && resources.includes(this)) {
  474.                    resources = resources.filter(r => r.id !== this.id);
  475.                      // Ensure workers targeting this stop
  476.                       gameObjects.forEach(obj => {
  477.                          if (obj.command && obj.command.type === 'gather' && obj.command.targetResource === this) {
  478.                             obj.command = { type: 'idle' };
  479.                          }
  480.                       });
  481.                  }
  482.             }
  483.         };
  484.         return resource;
  485.     }
  486.  
  487.  
  488.     // --- AI Logic ---
  489.     let aiState = {
  490.         lastActionFrame: 0,
  491.         desiredWorkers: 5,
  492.         desiredSoldiers: 10,
  493.         attackTriggerThreshold: 5, // Attack when having this many soldiers
  494.         isAttacking: false
  495.     };
  496.  
  497.     function updateAI() {
  498.         if (gameOver) return;
  499.         if (frameCount - aiState.lastActionFrame < AI_UPDATE_INTERVAL) return;
  500.        aiState.lastActionFrame = frameCount;
  501.  
  502.        const aiHQ = gameObjects.find(o => o.owner === 'ai' && o.type === 'HQ');
  503.         const aiBarracks = gameObjects.find(o => o.owner === 'ai' && o.type === 'Barracks');
  504.         const aiWorkers = gameObjects.filter(o => o.owner === 'ai' && o.type === 'Worker');
  505.         const aiSoldiers = gameObjects.filter(o => o.owner === 'ai' && o.type === 'Rifleman');
  506.         const playerBuildings = gameObjects.filter(o => o.owner === 'player' && o.isBuilding);
  507.         const playerUnits = gameObjects.filter(o => o.owner === 'player' && !o.isBuilding);
  508.         const allPlayerObjects = [...playerBuildings, ...playerUnits];
  509.  
  510.  
  511.         if (!aiHQ) return; // AI lost
  512.  
  513.         // 1. Build Workers if needed and possible
  514.         if (aiWorkers.length < aiState.desiredWorkers && aiSupplies >= WORKER_COST && aiHQ.productionQueue.length === 0) {
  515.            aiHQ.queueUnit('Worker');
  516.             // console.log("AI queuing Worker");
  517.             return; // Do one action per update interval for simplicity
  518.         }
  519.  
  520.         // 2. Assign Idle Workers to gather
  521.         const idleWorkers = aiWorkers.filter(w => w.command.type === 'idle' || (w.command.type === 'gather' && (!w.command.targetResource || w.command.targetResource.amount <= 0)));
  522.         if (idleWorkers.length > 0) {
  523.              const { target: closestResource } = findClosest(idleWorkers[0], resources, r => r.amount > 0);
  524.              if (closestResource) {
  525.                 idleWorkers[0].setCommand({type: 'gather', targetResource: closestResource});
  526.                 // console.log("AI assigning Worker to gather");
  527.                 return;
  528.              }
  529.         }
  530.  
  531.          // 3. Build Barracks if none exists and affordable
  532.         if (!aiBarracks && aiSupplies >= BARRACKS_COST) {
  533.             // Find a suitable location near HQ, not overlapping
  534.             const buildX = aiHQ.x + aiHQ.size + 40;
  535.              const buildY = aiHQ.y;
  536.              const newBarracks = createBuilding(buildX, buildY, 'Barracks', 'ai');
  537.              gameObjects.push(newBarracks);
  538.              aiSupplies -= BARRACKS_COST;
  539.              // console.log("AI building Barracks");
  540.              return;
  541.         }
  542.  
  543.         // 4. Build Soldiers if Barracks exists and affordable
  544.         if (aiBarracks && aiSoldiers.length < aiState.desiredSoldiers && aiSupplies >= RIFLEMAN_COST && aiBarracks.productionQueue.length === 0) {
  545.             aiBarracks.queueUnit('Rifleman');
  546.              // console.log("AI queuing Rifleman");
  547.              return;
  548.         }
  549.  
  550.         // 5. Basic Attack Logic
  551.          const idleSoldiers = aiSoldiers.filter(s => s.command.type === 'idle');
  552.         if ((aiSoldiers.length >= aiState.attackTriggerThreshold || aiState.isAttacking) && idleSoldiers.length > 0) {
  553.             aiState.isAttacking = true; // Latch attacking state once triggered
  554.              const playerHQ = gameObjects.find(o => o.owner === 'player' && o.type === 'HQ');
  555.              let target = playerHQ; // Default target HQ
  556.  
  557.               // Find closest player object if no HQ or want more dynamic target
  558.              if (!target && allPlayerObjects.length > 0) {
  559.                 const { target: closestPlayerThing } = findClosest(idleSoldiers[0], allPlayerObjects);
  560.                  target = closestPlayerThing;
  561.              }
  562.  
  563.              if (target) {
  564.                 // Send all idle soldiers to attack
  565.                 idleSoldiers.forEach(soldier => {
  566.                     soldier.setCommand({ type: 'attack', target: target });
  567.                 });
  568.                 // console.log(`AI sending ${idleSoldiers.length} soldiers to attack`);
  569.              } else {
  570.                  // No targets left? Stop attacking state maybe?
  571.                  aiState.isAttacking = false;
  572.              }
  573.         }
  574.  
  575.          // If not attacking and many soldiers idle, maybe move them defensively? (Simplification: skip)
  576.  
  577.     }
  578.  
  579.  
  580.     // --- Input Handling ---
  581.     function handleMouseClick(event) {
  582.         if (gameOver) return;
  583.         const rect = canvas.getBoundingClientRect();
  584.         const x = event.clientX - rect.left;
  585.         const y = event.clientY - rect.top;
  586.  
  587.         let clickedObject = null;
  588.         // Check units first (smaller targets)
  589.         for (const obj of gameObjects.filter(o => !o.isBuilding).reverse()) { // Check top-most first
  590.             if (distance(x, y, obj.x, obj.y) < obj.size / 2) {
  591.                clickedObject = obj;
  592.                break;
  593.            }
  594.        }
  595.        // Check buildings if no unit clicked
  596.        if (!clickedObject) {
  597.            for (const obj of gameObjects.filter(o => o.isBuilding).reverse()) {
  598.                 if (x >= obj.x - obj.size / 2 && x <= obj.x + obj.size / 2 &&
  599.                    y >= obj.y - obj.size / 2 && y <= obj.y + obj.size / 2) {
  600.                    clickedObject = obj;
  601.                     break;
  602.                 }
  603.             }
  604.         }
  605.  
  606.         if (clickedObject && clickedObject.owner === 'player') {
  607.            selectedObject = clickedObject;
  608.         } else {
  609.             selectedObject = null; // Clicked empty space or enemy unit
  610.         }
  611.         updateUI();
  612.     }
  613.  
  614.     function handleRightClick(event) {
  615.         event.preventDefault(); // Prevent context menu
  616.         if (gameOver || !selectedObject || selectedObject.owner !== 'player') return;
  617.  
  618.         const rect = canvas.getBoundingClientRect();
  619.         const x = event.clientX - rect.left;
  620.         const y = event.clientY - rect.top;
  621.  
  622.         // Check if right-clicking on an enemy unit/building
  623.         let targetObject = null;
  624.         for (const obj of gameObjects) {
  625.             if (obj.owner === 'ai' && distance(x, y, obj.x, obj.y) < obj.size / 2) {
  626.                targetObject = obj;
  627.                 break;
  628.             }
  629.         }
  630.          // Check if right-clicking on a resource node (only for workers)
  631.         let targetResource = null;
  632.          if (selectedObject.type === 'Worker') {
  633.              for (const res of resources) {
  634.                  if (distance(x, y, res.x, res.y) < res.size / 2) {
  635.                     targetResource = res;
  636.                     break;
  637.                 }
  638.             }
  639.         }
  640.  
  641.  
  642.        if (selectedObject.isBuilding) {
  643.            selectedObject.setRallyPoint(x, y);
  644.  
  645.        } else { // Selected object is a Unit
  646.            if (targetObject) { // Right-clicked an enemy
  647.                if (selectedObject.type === 'Rifleman') { // Only riflemen attack
  648.                     selectedObject.setCommand({ type: 'attack', target: targetObject });
  649.                 } else { // Workers just move near enemy if right-clicked
  650.                      selectedObject.setCommand({ type: 'move', x: x, y: y });
  651.                 }
  652.            } else if (targetResource && selectedObject.type === 'Worker') { // Right-clicked a resource with worker selected
  653.                 selectedObject.setCommand({ type: 'gather', targetResource: targetResource });
  654.            }
  655.             else { // Right-clicked empty ground or friendly unit
  656.                selectedObject.setCommand({ type: 'move', x: x, y: y });
  657.            }
  658.        }
  659.         updateUI(); // Update UI in case command changed something relevant
  660.    }
  661.  
  662.    // --- UI Update ---
  663.    function updateUI() {
  664.        suppliesDisplay.textContent = playerSupplies;
  665.  
  666.        if (selectedObject) {
  667.            selectedInfoDisplay.textContent = `${selectedObject.owner}'s ${selectedObject.type}`;
  668.            selectedHpDisplay.textContent = `${selectedObject.hp} / ${selectedObject.maxHp}`;
  669.            buildButtonsDiv.innerHTML = ''; // Clear previous buttons
  670.            if (selectedObject.type === 'HQ') {
  671.                const btn = document.createElement('button');
  672.                btn.textContent = `Build Worker (${WORKER_COST} S)`;
  673.                btn.onclick = () => {
  674.                      if (playerSupplies >= WORKER_COST) {
  675.                         selectedObject.queueUnit('Worker');
  676.                         updateUI(); // Refresh supply count immediately
  677.                      } else {
  678.                         console.log("Not enough supplies!"); // Basic feedback
  679.                      }
  680.                 };
  681.                  if (playerSupplies < WORKER_COST || selectedObject.productionQueue.length > 0) btn.disabled = true; // Basic check
  682.                 buildButtonsDiv.appendChild(btn);
  683.  
  684.                  const btnBarracks = document.createElement('button');
  685.                 btnBarracks.textContent = `Build Barracks (${BARRACKS_COST} S)`;
  686.                 btnBarracks.onclick = () => {
  687.                      if (playerSupplies >= BARRACKS_COST) {
  688.                           // Simplification: Build next to HQ
  689.                           const buildX = selectedObject.x + selectedObject.size + 40;
  690.                           const buildY = selectedObject.y;
  691.                          // TODO: Add placement logic / ghost building
  692.                          const newBarracks = createBuilding(buildX, buildY, 'Barracks', 'player');
  693.                          gameObjects.push(newBarracks);
  694.                          playerSupplies -= BARRACKS_COST;
  695.                          updateUI();
  696.                      } else {
  697.                          console.log("Not enough supplies!");
  698.                      }
  699.                  };
  700.                   if (playerSupplies < BARRACKS_COST) btnBarracks.disabled = true;
  701.                  buildButtonsDiv.appendChild(btnBarracks);
  702.  
  703.            } else if (selectedObject.type === 'Barracks') {
  704.                const btn = document.createElement('button');
  705.                btn.textContent = `Train Rifleman (${RIFLEMAN_COST} S)`;
  706.                btn.onclick = () => {
  707.                      if (playerSupplies >= RIFLEMAN_COST) {
  708.                          selectedObject.queueUnit('Rifleman');
  709.                          updateUI();
  710.                      } else {
  711.                          console.log("Not enough supplies!");
  712.                      }
  713.                 };
  714.                  if (playerSupplies < RIFLEMAN_COST || selectedObject.productionQueue.length > 0) btn.disabled = true;
  715.                 buildButtonsDiv.appendChild(btn);
  716.             }
  717.             // Add more buttons for other selected types here...
  718.  
  719.         } else {
  720.             selectedInfoDisplay.textContent = 'None';
  721.             selectedHpDisplay.textContent = '-';
  722.             buildButtonsDiv.innerHTML = ''; // Clear buttons
  723.         }
  724.     }
  725.  
  726.  
  727.     // --- Game Loop ---
  728.     function gameLoop() {
  729.         if (gameOver) {
  730.             ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
  731.             ctx.fillRect(0, 0, canvas.width, canvas.height);
  732.             ctx.fillStyle = "white";
  733.             ctx.font = "48px sans-serif";
  734.             ctx.textAlign = "center";
  735.             ctx.fillText(`${winner} Wins!`, canvas.width / 2, canvas.height / 2);
  736.             ctx.font = "24px sans-serif";
  737.             ctx.fillText("Refresh page to play again", canvas.width / 2, canvas.height / 2 + 50);
  738.             return; // Stop the loop
  739.         }
  740.  
  741.         // Clear canvas
  742.         ctx.clearRect(0, 0, canvas.width, canvas.height);
  743.  
  744.         // Update AI
  745.         updateAI();
  746.  
  747.         // Update game objects
  748.         resources.forEach(res => res.update());
  749.         gameObjects.forEach(obj => obj.update()); // Update positions, handle commands, production etc.
  750.  
  751.  
  752.         // Draw game objects
  753.         resources.forEach(res => res.draw());
  754.         gameObjects.sort((a, b) => a.y - b.y); // Basic depth sorting (draw lower objects first)
  755.         gameObjects.forEach(obj => obj.draw());
  756.  
  757.         // Draw selection indicator
  758.         if (selectedObject) {
  759.             ctx.strokeStyle = 'yellow';
  760.             ctx.lineWidth = 2;
  761.             if (selectedObject.isBuilding) {
  762.                 ctx.strokeRect(selectedObject.x - selectedObject.size / 2, selectedObject.y - selectedObject.size / 2, selectedObject.size, selectedObject.size);
  763.             } else {
  764.                 ctx.beginPath();
  765.                 ctx.arc(selectedObject.x, selectedObject.y, selectedObject.size / 2 + 3, 0, Math.PI * 2);
  766.                 ctx.stroke();
  767.             }
  768.             ctx.lineWidth = 1; // Reset line width
  769.         }
  770.  
  771.         // Update UI (supplies are updated constantly here, specific selections on click/action)
  772.          suppliesDisplay.textContent = playerSupplies;
  773.          // AI supplies not shown in this simple UI
  774.  
  775.  
  776.         frameCount++;
  777.         requestAnimationFrame(gameLoop);
  778.     }
  779.  
  780.     // --- Initialization ---
  781.     function init() {
  782.         // Create starting HQs
  783.         const playerHQ = createBuilding(150, canvas.height / 2, 'HQ', 'player');
  784.         const aiHQ = createBuilding(canvas.width - 150, canvas.height / 2, 'HQ', 'ai');
  785.         gameObjects.push(playerHQ, aiHQ);
  786.  
  787.         // Create starting workers
  788.         gameObjects.push(createUnit(playerHQ.x + playerHQ.size, playerHQ.y, 'Worker', 'player'));
  789.         gameObjects.push(createUnit(playerHQ.x + playerHQ.size, playerHQ.y + 30, 'Worker', 'player'));
  790.         gameObjects.push(createUnit(aiHQ.x - aiHQ.size, aiHQ.y, 'Worker', 'ai'));
  791.         gameObjects.push(createUnit(aiHQ.x - aiHQ.size, aiHQ.y + 30, 'Worker', 'ai'));
  792.  
  793.  
  794.         // Create resource patches
  795.         resources.push(createResourceNode(300, canvas.height / 2 - 100, 500));
  796.         resources.push(createResourceNode(300, canvas.height / 2 + 100, 500));
  797.         resources.push(createResourceNode(canvas.width - 300, canvas.height / 2 - 100, 500));
  798.         resources.push(createResourceNode(canvas.width - 300, canvas.height / 2 + 100, 500));
  799.         resources.push(createResourceNode(canvas.width/2, 100, 800)); // Center contested?
  800.         resources.push(createResourceNode(canvas.width/2, canvas.height - 100, 800));
  801.  
  802.         // Add event listeners
  803.         canvas.addEventListener('click', handleMouseClick);
  804.         canvas.addEventListener('contextmenu', handleRightClick); // Use contextmenu for right-click
  805.  
  806.          window.addEventListener('resize', () => {
  807.             canvas.width = window.innerWidth;
  808.             canvas.height = window.innerHeight;
  809.             // Could potentially reposition camera or objects on resize, but keep simple
  810.          });
  811.  
  812.         updateUI();
  813.         // Start the game loop
  814.         gameLoop();
  815.     }
  816.  
  817.     // --- Start the game ---
  818.     init();
  819.  
  820. </script>
  821.  
  822. </body>
  823. </html>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement