Spaces:
Running
Running
Interact with NPCs by recruiting them in your party. Maximum 3 NPCs - Follow Up Deployment
Browse files- index.html +225 -2
index.html
CHANGED
@@ -204,6 +204,23 @@
|
|
204 |
</div>
|
205 |
<span class="text-sm">Health: <span id="health-text">100</span>/100</span>
|
206 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
207 |
</div>
|
208 |
</div>
|
209 |
|
@@ -422,7 +439,10 @@
|
|
422 |
combatEnemy: null,
|
423 |
selectedSettlement: null,
|
424 |
gameLog: [],
|
425 |
-
actionsThisTurn: 0
|
|
|
|
|
|
|
426 |
};
|
427 |
|
428 |
// DOM Elements
|
@@ -452,8 +472,43 @@
|
|
452 |
const endTurnBtn = document.getElementById('end-turn');
|
453 |
const mapTooltip = document.getElementById('map-tooltip');
|
454 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
455 |
// Initialize game
|
456 |
function initGame() {
|
|
|
|
|
|
|
457 |
// Set up event listeners
|
458 |
startGameBtn.addEventListener('click', startGame);
|
459 |
endTurnBtn.addEventListener('click', endTurn);
|
@@ -612,9 +667,14 @@
|
|
612 |
<div><span class="font-semibold">Garrison:</span> ${settlement.garrison}</div>
|
613 |
<div><span class="font-semibold">Wealth:</span> ${settlement.wealth}</div>
|
614 |
</div>
|
615 |
-
<div class="text-sm text-gray-400 italic">
|
616 |
${getSettlementDescription(settlement)}
|
617 |
</div>
|
|
|
|
|
|
|
|
|
|
|
618 |
`;
|
619 |
|
620 |
// Enable/disable action buttons based on settlement
|
@@ -636,6 +696,117 @@
|
|
636 |
document.querySelector(`.settlement[data-id="${settlementId}"]`).classList.add('border-2', 'border-white');
|
637 |
}
|
638 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
639 |
function getSettlementDescription(settlement) {
|
640 |
const faction = gameState.factions[settlement.faction];
|
641 |
const playerFaction = gameState.factions[gameState.player.faction];
|
@@ -798,6 +969,36 @@
|
|
798 |
}
|
799 |
|
800 |
// Handle combat retreat
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
801 |
function combatRetreat() {
|
802 |
if (!gameState.inCombat) return;
|
803 |
|
@@ -1145,6 +1346,28 @@
|
|
1145 |
}
|
1146 |
|
1147 |
// Update player info display
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1148 |
function updatePlayerInfo() {
|
1149 |
playerNameEl.textContent = gameState.player.name;
|
1150 |
playerFactionEl.textContent = gameState.factions[gameState.player.faction].name;
|
|
|
204 |
</div>
|
205 |
<span class="text-sm">Health: <span id="health-text">100</span>/100</span>
|
206 |
</div>
|
207 |
+
${gameState.npcParty.length > 0 ? `
|
208 |
+
<div class="border-t border-gray-700 pt-2">
|
209 |
+
<h4 class="font-semibold mb-1">NPC Party (${gameState.npcParty.length}/${gameState.maxNPCs}):</h4>
|
210 |
+
${gameState.npcParty.map(npc => `
|
211 |
+
<div class="flex items-center justify-between py-1">
|
212 |
+
<div class="flex items-center">
|
213 |
+
<i class="fas ${getNPCIcon(npc.type)} ${getNPCColor(npc.type)} mr-2"></i>
|
214 |
+
<span>${npc.name}</span>
|
215 |
+
</div>
|
216 |
+
<button onclick="dismissNPC('${npc.id}')"
|
217 |
+
class="px-2 py-1 bg-red-700 hover:bg-red-800 rounded text-sm">
|
218 |
+
Dismiss
|
219 |
+
</button>
|
220 |
+
</div>
|
221 |
+
`).join('')}
|
222 |
+
</div>
|
223 |
+
` : ''}
|
224 |
</div>
|
225 |
</div>
|
226 |
|
|
|
439 |
combatEnemy: null,
|
440 |
selectedSettlement: null,
|
441 |
gameLog: [],
|
442 |
+
actionsThisTurn: 0,
|
443 |
+
npcs: [],
|
444 |
+
npcParty: [],
|
445 |
+
maxNPCs: 3
|
446 |
};
|
447 |
|
448 |
// DOM Elements
|
|
|
472 |
const endTurnBtn = document.getElementById('end-turn');
|
473 |
const mapTooltip = document.getElementById('map-tooltip');
|
474 |
|
475 |
+
// Generate random NPCs
|
476 |
+
function generateNPCs() {
|
477 |
+
const maleTypes = ['noble son', 'merchant', 'mercenary', 'peasant'];
|
478 |
+
const femaleTypes = ['noble daughter', 'merchant', 'healer', 'witch'];
|
479 |
+
const firstNamesMale = ['Heinrich', 'Konrad', 'Friedrich', 'Ludwig', 'Albrecht', 'Johann', 'Ulrich', 'Werner'];
|
480 |
+
const firstNamesFemale = ['Adelheid', 'Gertrud', 'Elisabeth', 'Mechthild', 'Agnes', 'Hedwig', 'Margarete', 'Katharina'];
|
481 |
+
const lastNames = ['von Bayern', 'von Schwaben', 'von Franken', 'von Sachsen', 'von Lothringen', 'von Habsburg', 'von Wittelsbach', 'von Hohenzollern'];
|
482 |
+
|
483 |
+
// Create 1-2 NPCs per major town
|
484 |
+
gameState.settlements
|
485 |
+
.filter(s => s.type === 'town')
|
486 |
+
.forEach(settlement => {
|
487 |
+
const npcCount = Math.floor(Math.random() * 2) + 1;
|
488 |
+
for (let i = 0; i < npcCount; i++) {
|
489 |
+
const isFemale = Math.random() > 0.5;
|
490 |
+
const types = isFemale ? femaleTypes : maleTypes;
|
491 |
+
const firstNames = isFemale ? firstNamesFemale : firstNamesMale;
|
492 |
+
|
493 |
+
const npc = {
|
494 |
+
id: `npc-${settlement.id}-${i}`,
|
495 |
+
name: `${firstNames[Math.floor(Math.random() * firstNames.length)]} ${lastNames[Math.floor(Math.random() * lastNames.length)]}`,
|
496 |
+
type: types[Math.floor(Math.random() * types.length)],
|
497 |
+
gender: isFemale ? 'female' : 'male',
|
498 |
+
location: settlement.id,
|
499 |
+
relation: Math.floor(Math.random() * 60) - 30, // -30 to 30
|
500 |
+
quest: null
|
501 |
+
};
|
502 |
+
gameState.npcs.push(npc);
|
503 |
+
}
|
504 |
+
});
|
505 |
+
}
|
506 |
+
|
507 |
// Initialize game
|
508 |
function initGame() {
|
509 |
+
// Generate NPCs
|
510 |
+
generateNPCs();
|
511 |
+
|
512 |
// Set up event listeners
|
513 |
startGameBtn.addEventListener('click', startGame);
|
514 |
endTurnBtn.addEventListener('click', endTurn);
|
|
|
667 |
<div><span class="font-semibold">Garrison:</span> ${settlement.garrison}</div>
|
668 |
<div><span class="font-semibold">Wealth:</span> ${settlement.wealth}</div>
|
669 |
</div>
|
670 |
+
<div class="text-sm text-gray-400 italic mb-3">
|
671 |
${getSettlementDescription(settlement)}
|
672 |
</div>
|
673 |
+
<div class="border-t border-gray-700 pt-2">
|
674 |
+
<h4 class="font-semibold mb-1">Notable Persons:</h4>
|
675 |
+
${getSettlementNPCs(settlement.id)}
|
676 |
+
${getRecruitableNPCs(settlement.id)}
|
677 |
+
</div>
|
678 |
`;
|
679 |
|
680 |
// Enable/disable action buttons based on settlement
|
|
|
696 |
document.querySelector(`.settlement[data-id="${settlementId}"]`).classList.add('border-2', 'border-white');
|
697 |
}
|
698 |
|
699 |
+
function getRecruitableNPCs(settlementId) {
|
700 |
+
const recruitableNPCs = gameState.npcs.filter(npc =>
|
701 |
+
npc.location === settlementId &&
|
702 |
+
npc.relation >= 0 &&
|
703 |
+
!gameState.npcParty.some(p => p.id === npc.id)
|
704 |
+
);
|
705 |
+
|
706 |
+
if (recruitableNPCs.length === 0 || gameState.npcParty.length >= gameState.maxNPCs) {
|
707 |
+
return '';
|
708 |
+
}
|
709 |
+
|
710 |
+
return `
|
711 |
+
<div class="mt-3">
|
712 |
+
<h4 class="font-semibold mb-1">Available for Recruitment:</h4>
|
713 |
+
${recruitableNPCs.map(npc => `
|
714 |
+
<div class="flex items-center justify-between py-1">
|
715 |
+
<div class="flex items-center">
|
716 |
+
<i class="fas ${getNPCIcon(npc.type)} ${getNPCColor(npc.type)} mr-2"></i>
|
717 |
+
<span>${npc.name} (${npc.type})</span>
|
718 |
+
</div>
|
719 |
+
<button onclick="recruitNPC('${npc.id}')"
|
720 |
+
class="px-2 py-1 bg-green-700 hover:bg-green-800 rounded text-sm">
|
721 |
+
Recruit
|
722 |
+
</button>
|
723 |
+
</div>
|
724 |
+
`).join('')}
|
725 |
+
</div>
|
726 |
+
`;
|
727 |
+
}
|
728 |
+
|
729 |
+
function getNPCIcon(type) {
|
730 |
+
switch(type) {
|
731 |
+
case 'noble son':
|
732 |
+
case 'noble daughter': return 'fa-crown';
|
733 |
+
case 'merchant': return 'fa-coins';
|
734 |
+
case 'mercenary': return 'fa-swords';
|
735 |
+
case 'peasant': return 'fa-wheat';
|
736 |
+
case 'healer': return 'fa-heart';
|
737 |
+
case 'witch': return 'fa-magic';
|
738 |
+
default: return 'fa-user';
|
739 |
+
}
|
740 |
+
}
|
741 |
+
|
742 |
+
function getNPCColor(type) {
|
743 |
+
switch(type) {
|
744 |
+
case 'noble son':
|
745 |
+
case 'noble daughter': return 'text-yellow-400';
|
746 |
+
case 'merchant': return 'text-green-400';
|
747 |
+
case 'mercenary': return 'text-red-400';
|
748 |
+
case 'peasant': return 'text-brown-400';
|
749 |
+
case 'healer': return 'text-pink-400';
|
750 |
+
case 'witch': return 'text-purple-400';
|
751 |
+
default: return 'text-gray-300';
|
752 |
+
}
|
753 |
+
}
|
754 |
+
|
755 |
+
function getSettlementNPCs(settlementId) {
|
756 |
+
const npcs = gameState.npcs.filter(npc => npc.location === settlementId);
|
757 |
+
if (npcs.length === 0) {
|
758 |
+
return '<div class="text-gray-500 italic">No notable persons present</div>';
|
759 |
+
}
|
760 |
+
|
761 |
+
return npcs.map(npc => {
|
762 |
+
let icon = '';
|
763 |
+
let color = 'text-gray-300';
|
764 |
+
|
765 |
+
switch(npc.type) {
|
766 |
+
case 'noble son':
|
767 |
+
case 'noble daughter':
|
768 |
+
icon = 'fa-crown';
|
769 |
+
color = 'text-yellow-400';
|
770 |
+
break;
|
771 |
+
case 'merchant':
|
772 |
+
icon = 'fa-coins';
|
773 |
+
color = 'text-green-400';
|
774 |
+
break;
|
775 |
+
case 'mercenary':
|
776 |
+
icon = 'fa-swords';
|
777 |
+
color = 'text-red-400';
|
778 |
+
break;
|
779 |
+
case 'peasant':
|
780 |
+
icon = 'fa-wheat';
|
781 |
+
color = 'text-brown-400';
|
782 |
+
break;
|
783 |
+
case 'healer':
|
784 |
+
icon = 'fa-heart';
|
785 |
+
color = 'text-pink-400';
|
786 |
+
break;
|
787 |
+
case 'witch':
|
788 |
+
icon = 'fa-magic';
|
789 |
+
color = 'text-purple-400';
|
790 |
+
break;
|
791 |
+
}
|
792 |
+
|
793 |
+
const relationText = npc.relation > 20 ? 'Friendly' :
|
794 |
+
npc.relation < -20 ? 'Hostile' : 'Neutral';
|
795 |
+
const relationColor = npc.relation > 20 ? 'text-green-400' :
|
796 |
+
npc.relation < -20 ? 'text-red-400' : 'text-gray-400';
|
797 |
+
|
798 |
+
return `
|
799 |
+
<div class="flex items-center justify-between py-1">
|
800 |
+
<div class="flex items-center">
|
801 |
+
<i class="fas ${icon} ${color} mr-2"></i>
|
802 |
+
<span>${npc.name} (${npc.type})</span>
|
803 |
+
</div>
|
804 |
+
<span class="${relationColor} text-sm">${relationText}</span>
|
805 |
+
</div>
|
806 |
+
`;
|
807 |
+
}).join('');
|
808 |
+
}
|
809 |
+
|
810 |
function getSettlementDescription(settlement) {
|
811 |
const faction = gameState.factions[settlement.faction];
|
812 |
const playerFaction = gameState.factions[gameState.player.faction];
|
|
|
969 |
}
|
970 |
|
971 |
// Handle combat retreat
|
972 |
+
function recruitNPC(npcId) {
|
973 |
+
if (gameState.npcParty.length >= gameState.maxNPCs) {
|
974 |
+
addGameLog(`You can only have ${gameState.maxNPCs} NPCs in your party.`);
|
975 |
+
return;
|
976 |
+
}
|
977 |
+
|
978 |
+
const npc = gameState.npcs.find(n => n.id === npcId);
|
979 |
+
if (!npc || npc.relation < 0) {
|
980 |
+
addGameLog("This NPC cannot be recruited.");
|
981 |
+
return;
|
982 |
+
}
|
983 |
+
|
984 |
+
// Add to party
|
985 |
+
gameState.npcParty.push(npc);
|
986 |
+
|
987 |
+
// Remove from settlement
|
988 |
+
const settlementIndex = gameState.settlements.findIndex(s => s.id === npc.location);
|
989 |
+
if (settlementIndex !== -1) {
|
990 |
+
gameState.settlements[settlementIndex].npcIds = gameState.settlements[settlementIndex].npcIds?.filter(id => id !== npc.id) || [];
|
991 |
+
}
|
992 |
+
|
993 |
+
addGameLog(`You recruited ${npc.name} (${npc.type}) to your party!`);
|
994 |
+
|
995 |
+
// Update UI
|
996 |
+
const settlement = gameState.settlements.find(s => s.id === npc.location);
|
997 |
+
if (settlement) {
|
998 |
+
selectSettlement(settlement.id);
|
999 |
+
}
|
1000 |
+
}
|
1001 |
+
|
1002 |
function combatRetreat() {
|
1003 |
if (!gameState.inCombat) return;
|
1004 |
|
|
|
1346 |
}
|
1347 |
|
1348 |
// Update player info display
|
1349 |
+
function dismissNPC(npcId) {
|
1350 |
+
const npcIndex = gameState.npcParty.findIndex(n => n.id === npcId);
|
1351 |
+
if (npcIndex === -1) return;
|
1352 |
+
|
1353 |
+
const npc = gameState.npcParty[npcIndex];
|
1354 |
+
|
1355 |
+
// Return to current location
|
1356 |
+
npc.location = gameState.player.location;
|
1357 |
+
|
1358 |
+
// Remove from party
|
1359 |
+
gameState.npcParty.splice(npcIndex, 1);
|
1360 |
+
|
1361 |
+
addGameLog(`You dismissed ${npc.name} from your party.`);
|
1362 |
+
|
1363 |
+
// Update UI
|
1364 |
+
updatePlayerInfo();
|
1365 |
+
const settlement = gameState.settlements.find(s => s.id === gameState.player.location);
|
1366 |
+
if (settlement) {
|
1367 |
+
selectSettlement(settlement.id);
|
1368 |
+
}
|
1369 |
+
}
|
1370 |
+
|
1371 |
function updatePlayerInfo() {
|
1372 |
playerNameEl.textContent = gameState.player.name;
|
1373 |
playerFactionEl.textContent = gameState.factions[gameState.player.faction].name;
|