Skip to content

Commit 37a02ba

Browse files
feat: Implement LoveCipher game v1
Implements the initial version of the LoveCipher game, a web-based narrative puzzle game. Features: - HTML structure, CSS styling, and JavaScript game logic. - Simulated chat interface for NPC and player interaction. - Narrative progression with NPC messages and player input. - First puzzle: A riddle presented by the NPC. - Dialogue choices that influence the story and available clues. - Clue collection system with UI display. - Second puzzle: A two-part logic puzzle requiring collected clues. - Basic relationship/progress score tracking and display. - Basic styling, UI polish, and error handling for puzzle inputs.
1 parent 2529c7a commit 37a02ba

File tree

3 files changed

+581
-0
lines changed

3 files changed

+581
-0
lines changed

lovecipher_game/index.html

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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>LoveCipher</title>
7+
<link rel="stylesheet" href="style.css">
8+
</head>
9+
<body>
10+
<div id="game-container">
11+
<header>
12+
<h1>LoveCipher</h1>
13+
</header>
14+
<main>
15+
<div id="chat-display">
16+
<!-- Messages will appear here -->
17+
</div>
18+
<div id="player-input-area">
19+
<input type="text" id="player-input" placeholder="Type your message...">
20+
<button id="send-button">Send</button>
21+
</div>
22+
<div id="dialogue-choices-area" class="hidden">
23+
<!-- Dialogue choice buttons will appear here -->
24+
</div>
25+
<div id="clues-area">
26+
<h2>Clues</h2>
27+
<ul id="clues-list">
28+
<!-- Clues will be listed here -->
29+
</ul>
30+
</div>
31+
<div id="relationship-status">
32+
<h2>Relationship</h2>
33+
<p id="status-text">Just starting...</p>
34+
</div>
35+
</main>
36+
<footer>
37+
<p>&copy; 2024 LoveCipher Game</p>
38+
</footer>
39+
</div>
40+
<script src="script.js"></script>
41+
</body>
42+
</html>

lovecipher_game/script.js

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
console.log("LoveCipher script loaded!");
2+
3+
document.addEventListener('DOMContentLoaded', () => {
4+
const playerInput = document.getElementById('player-input');
5+
const sendButton = document.getElementById('send-button');
6+
const chatDisplay = document.getElementById('chat-display');
7+
const dialogueChoicesArea = document.getElementById('dialogue-choices-area');
8+
const cluesList = document.getElementById('clues-list');
9+
const statusText = document.getElementById('status-text');
10+
11+
// --- Game State & Data ---
12+
const conversations = {
13+
intro: [
14+
{ sender: 'npc', text: "Hello there... stranger. Found my little message in a bottle, did you?" },
15+
{ sender: 'npc', text: "I'm a bit surprised anyone did. It's been a while." },
16+
{ sender: 'npc', text: "So, tell me, what are you seeking?" } // Player responds via text input
17+
],
18+
post_intro_reply: [ // After player's first text reply
19+
{ sender: 'npc', text: "Interesting. Seeking, are we? Well, perhaps you can solve something for me first." },
20+
{ sender: 'npc', text: "I have cities, but no houses. I have mountains, but no trees. I have water, but no fish. What am I?", puzzle: 'map_riddle', answer: 'a map' }
21+
],
22+
riddle_correct: [
23+
{ sender: 'npc', text: "Impressive! 'A map' it is. You're sharper than you look." },
24+
{ sender: 'npc', text: "Maybe you are interesting after all. Tell me..." },
25+
{
26+
sender: 'npc',
27+
text: "Are you the type to rush into things, or do you prefer to observe from the shadows?",
28+
choices: [
29+
{ text: "I jump in head first!", nextStage: 'choice_rush' },
30+
{ text: "I like to watch and learn.", nextStage: 'choice_observe' },
31+
{ text: "A bit of both, really.", nextStage: 'choice_both' }
32+
]
33+
}
34+
],
35+
riddle_incorrect: [
36+
{ sender: 'npc', text: "Hmm, not quite. Think harder." } // Stays on riddle
37+
],
38+
choice_rush: [
39+
{ sender: 'npc', text: "A thrill-seeker, eh? I can appreciate that. Sometimes." },
40+
{ sender: 'npc', text: "We'll see how that pans out for you." } // Leads to next phase
41+
],
42+
choice_observe: [
43+
{ sender: 'npc', text: "The cautious type. Wise, perhaps. Or perhaps too timid." },
44+
{ sender: 'npc', text: "Observation can only get you so far." } // Leads to next phase
45+
],
46+
choice_both: [
47+
{ sender: 'npc', text: "Balanced, or just indecisive? Time will tell." },
48+
{ sender: 'npc', text: "Let's move on, but remember this number: 7." , clue: "Number: 7"}, // Clue 1
49+
{ sender: 'npc', text: "And this word: 'whisper'." , clue: "Word: whisper"}, // Clue 2
50+
{ sender: 'npc', text: "They might be useful if you're observant.", nextStage: 'puzzle_gate_intro' }
51+
],
52+
choice_rush: [
53+
{ sender: 'npc', text: "A thrill-seeker, eh? I can appreciate that. Sometimes." },
54+
{ sender: 'npc', text: "You like speed? Then maybe you'll appreciate the swiftness of a 'comet'." , clue: "Speed: comet"}, // Clue 1
55+
{ sender: 'npc', text: "And also, keep the number '3' in mind." , clue: "Number: 3"}, // Clue 2
56+
{ sender: 'npc', text: "We'll see how that pans out for you.", nextStage: 'puzzle_gate_intro' }
57+
],
58+
choice_observe: [
59+
{ sender: 'npc', text: "The cautious type. Wise, perhaps. Or perhaps too timid." },
60+
{ sender: 'npc', text: "You watch for details? Then note the color 'indigo'." , clue: "Color: indigo"}, // Clue 1
61+
{ sender: 'npc', text: "And the number '5'." , clue: "Number: 5"}, // Clue 2
62+
{ sender: 'npc', text: "Observation can only get you so far.", nextStage: 'puzzle_gate_intro' }
63+
],
64+
puzzle_gate_intro: [
65+
{ sender: 'npc', text: "Alright, enough chit-chat for a moment." },
66+
{ sender: 'npc', text: "I have a little gate here. It needs two specific things you might have picked up from our conversation to open." },
67+
{ sender: 'npc', text: "Tell me the first one, then the second. What's the first part of the code?", puzzle: 'gate_puzzle_part1', expects: 'clue1_value' }
68+
// expects 'clue1_value' will be dynamically determined based on path taken
69+
],
70+
puzzle_gate_part2: [
71+
{ sender: 'npc', text: "Okay, and the second part?", puzzle: 'gate_puzzle_part2', expects: 'clue2_value' }
72+
],
73+
puzzle_gate_correct: [
74+
{ sender: 'npc', text: "Click. The gate swings open. Well done." },
75+
{ sender: 'npc', text: "You're proving to be quite capable." }
76+
],
77+
puzzle_gate_incorrect: [
78+
{ sender: 'npc', text: "That doesn't seem right. The gate remains shut." }
79+
// This will loop back to the current puzzle part
80+
]
81+
};
82+
83+
let currentStage = 'intro';
84+
let currentMessages = [...conversations.intro];
85+
let messageIndex = 0;
86+
let expectingPuzzleAnswer = null; // e.g. { id: 'gate_puzzle_part1', expects: 'clue1_value' }
87+
let relationshipScore = 0;
88+
let collectedClues = []; // Stores objects like { text: "Number: 7" }
89+
let playerPathClues = []; // Stores the actual clue values player should have based on path, e.g. ['comet', '3']
90+
let tempPlayerInputForPuzzle = null; // To store the first part of a two-part puzzle answer
91+
92+
// --- UI Functions ---
93+
function setInputMode(mode) { // 'text' or 'choice'
94+
if (mode === 'choice') {
95+
playerInput.disabled = true;
96+
sendButton.disabled = true;
97+
playerInput.placeholder = "Choose an option above";
98+
dialogueChoicesArea.classList.remove('hidden');
99+
} else { // 'text'
100+
playerInput.disabled = false;
101+
sendButton.disabled = false;
102+
playerInput.placeholder = "Type your message...";
103+
dialogueChoicesArea.classList.add('hidden');
104+
dialogueChoicesArea.innerHTML = ''; // Clear old choices
105+
}
106+
}
107+
108+
// --- Message & Choice Display Functions ---
109+
110+
function addMessageToChat(sender, message) {
111+
const messageElement = document.createElement('div');
112+
messageElement.classList.add('message');
113+
messageElement.classList.add(sender === 'player' ? 'player-message' : 'npc-message');
114+
messageElement.textContent = message;
115+
chatDisplay.appendChild(messageElement);
116+
chatDisplay.scrollTop = chatDisplay.scrollHeight; // Auto-scroll to bottom
117+
}
118+
119+
function updateCluesDisplay() {
120+
cluesList.innerHTML = ''; // Clear existing clues
121+
collectedClues.forEach(clue => {
122+
const listItem = document.createElement('li');
123+
listItem.textContent = clue.text; // Display the full clue text e.g. "Number: 7"
124+
cluesList.appendChild(listItem);
125+
});
126+
}
127+
128+
function displayChoices(choices) {
129+
dialogueChoicesArea.innerHTML = ''; // Clear previous choices
130+
setInputMode('choice');
131+
132+
choices.forEach(choice => {
133+
const button = document.createElement('button');
134+
button.classList.add('choice-button');
135+
button.textContent = choice.text;
136+
button.addEventListener('click', () => handleChoiceSelection(choice));
137+
dialogueChoicesArea.appendChild(button);
138+
});
139+
}
140+
141+
function displayNextMessage() {
142+
if (messageIndex < currentMessages.length) {
143+
const messageData = currentMessages[messageIndex];
144+
145+
if (messageData.sender === 'npc') {
146+
setTimeout(() => {
147+
addMessageToChat(messageData.sender, messageData.text);
148+
149+
if (messageData.clue) {
150+
const newClue = { text: messageData.clue };
151+
if (!collectedClues.some(c => c.text === newClue.text)) {
152+
collectedClues.push(newClue);
153+
updateCluesDisplay();
154+
const clueValue = messageData.clue.split(": ")[1].toLowerCase();
155+
if (playerPathClues.length < 2) {
156+
playerPathClues.push(clueValue);
157+
}
158+
}
159+
}
160+
161+
messageIndex++;
162+
163+
if (messageData.choices) {
164+
displayChoices(messageData.choices);
165+
} else if (messageData.puzzle) {
166+
expectingPuzzleAnswer = { id: messageData.puzzle, expects: messageData.expects };
167+
setInputMode('text');
168+
} else if (messageData.nextStage && messageIndex >= currentMessages.length) {
169+
currentStage = messageData.nextStage;
170+
currentMessages = [...(conversations[currentStage] || [])];
171+
messageIndex = 0;
172+
displayNextMessage(); // Auto-progress
173+
} else if (messageIndex < currentMessages.length && currentMessages[messageIndex].sender === 'npc') {
174+
displayNextMessage();
175+
} else { // NPC turn ends
176+
if(!messageData.choices && !messageData.puzzle) { // and no choices/puzzle pending from this message
177+
setInputMode('text'); // enable text input for player
178+
}
179+
}
180+
}, 700);
181+
}
182+
} else { // End of currentMessages
183+
if (expectingPuzzleAnswer) {
184+
// Current messages finished, but still expecting a puzzle answer (e.g., after incorrect feedback)
185+
// Re-prompt for the current puzzle.
186+
let puzzleStageKey = null;
187+
for (const stageKey in conversations) {
188+
if (conversations[stageKey].some(m => m.puzzle === expectingPuzzleAnswer.id)) {
189+
puzzleStageKey = stageKey;
190+
break;
191+
}
192+
}
193+
if (puzzleStageKey) {
194+
currentStage = puzzleStageKey; // This might be redundant if stage didn't change
195+
currentMessages = [...conversations[currentStage]];
196+
messageIndex = 0;
197+
// Find the specific message that poses the puzzle to start from there.
198+
const puzzleMessageIndex = currentMessages.findIndex(m => m.puzzle === expectingPuzzleAnswer.id);
199+
if (puzzleMessageIndex !== -1) {
200+
messageIndex = puzzleMessageIndex;
201+
}
202+
setTimeout(() => displayNextMessage(), 300); // Display the puzzle prompt again
203+
} else {
204+
// Fallback: couldn't find puzzle stage, enable text input
205+
console.error("Could not find stage for puzzle:", expectingPuzzleAnswer.id);
206+
setInputMode('text');
207+
}
208+
} else if (dialogueChoicesArea.classList.contains('hidden')) {
209+
// No puzzle pending, no choices displayed, so enable text input.
210+
setInputMode('text');
211+
}
212+
}
213+
}
214+
215+
// --- Player Action Handlers ---
216+
217+
function handleChoiceSelection(choice) {
218+
addMessageToChat('player', choice.text);
219+
currentStage = choice.nextStage;
220+
// Reset playerPathClues when a choice is made that leads to new clues
221+
if (currentStage === 'choice_rush' || currentStage === 'choice_observe' || currentStage === 'choice_both') {
222+
playerPathClues = [];
223+
// Clues will be added by displayNextMessage as it processes the new stage's messages
224+
}
225+
currentMessages = [...(conversations[currentStage] || [])];
226+
messageIndex = 0;
227+
228+
// Example: Update relationshipScore based on choice
229+
if (choice.scoreEffect) { // This property isn't used yet, but good for future
230+
relationshipScore += choice.scoreEffect;
231+
}
232+
if (currentStage === 'choice_rush') {
233+
relationshipScore += 5;
234+
// playerPathClues are now ['comet', '3'] via displayNextMessage
235+
} else if (currentStage === 'choice_observe') {
236+
relationshipScore += 5;
237+
// playerPathClues are now ['indigo', '5']
238+
} else if (currentStage === 'choice_both') {
239+
relationshipScore += 2;
240+
// playerPathClues are now ['7', 'whisper']
241+
}
242+
243+
statusText.textContent = `Relationship Score: ${relationshipScore}`;
244+
245+
setInputMode('text');
246+
dialogueChoicesArea.innerHTML = '';
247+
displayNextMessage();
248+
}
249+
250+
function processPlayerTextInput(text) {
251+
const cleanedText = text.toLowerCase().trim();
252+
253+
if (expectingPuzzleAnswer) {
254+
let puzzleSolvedForThisTurn = false;
255+
256+
if (expectingPuzzleAnswer.id === 'map_riddle') {
257+
const riddleDefinition = conversations.post_intro_reply.find(m => m.puzzle === expectingPuzzleAnswer.id);
258+
if (riddleDefinition && cleanedText === riddleDefinition.answer) {
259+
currentStage = 'riddle_correct';
260+
relationshipScore += 10;
261+
statusText.textContent = `She's impressed! Score: ${relationshipScore}`;
262+
expectingPuzzleAnswer = null; // Puzzle fully solved
263+
puzzleSolvedForThisTurn = true;
264+
} else {
265+
currentMessages = [...conversations.riddle_incorrect];
266+
statusText.textContent = `Not quite... Score: ${relationshipScore}`;
267+
// expectingPuzzleAnswer for map_riddle remains, displayNextMessage will re-prompt
268+
}
269+
} else if (expectingPuzzleAnswer.id === 'gate_puzzle_part1') {
270+
if (playerPathClues.length > 0 && cleanedText === playerPathClues[0]) {
271+
tempPlayerInputForPuzzle = cleanedText;
272+
currentStage = 'puzzle_gate_part2'; // Moves to ask for the second part
273+
// expectingPuzzleAnswer will be updated by displayNextMessage when it processes puzzle_gate_part2
274+
puzzleSolvedForThisTurn = true; // Part 1 is correct
275+
} else {
276+
currentMessages = [...conversations.puzzle_gate_incorrect];
277+
// expectingPuzzleAnswer for gate_puzzle_part1 remains, displayNextMessage will re-prompt
278+
}
279+
} else if (expectingPuzzleAnswer.id === 'gate_puzzle_part2') {
280+
if (playerPathClues.length > 1 && tempPlayerInputForPuzzle && cleanedText === playerPathClues[1]) {
281+
currentStage = 'puzzle_gate_correct';
282+
relationshipScore += 15;
283+
statusText.textContent = `Gate opened! Score: ${relationshipScore}`;
284+
expectingPuzzleAnswer = null; // Puzzle fully solved
285+
puzzleSolvedForThisTurn = true;
286+
tempPlayerInputForPuzzle = null;
287+
} else {
288+
currentMessages = [...conversations.puzzle_gate_incorrect];
289+
tempPlayerInputForPuzzle = null;
290+
// Add the restart message if not already there to loop back.
291+
if (!conversations.puzzle_gate_incorrect.find(m => m.nextStage === 'puzzle_gate_intro')) {
292+
conversations.puzzle_gate_incorrect.push({ sender: 'npc', text: "Let's try that gate sequence again from the start.", nextStage: 'puzzle_gate_intro'});
293+
}
294+
// expectingPuzzleAnswer remains for gate_puzzle_part2, but the nextStage in the incorrect message
295+
// will effectively reset it by going to puzzle_gate_intro.
296+
}
297+
}
298+
299+
// If puzzle was solved (or part 1 of gate puzzle was correct), load the new stage's messages.
300+
// Otherwise, currentMessages is already set to the "incorrect" feedback.
301+
if (puzzleSolvedForThisTurn) {
302+
currentMessages = [...(conversations[currentStage] || [])];
303+
}
304+
305+
messageIndex = 0;
306+
displayNextMessage();
307+
308+
} else {
309+
// General conversation progression
310+
if (currentStage === 'intro') {
311+
currentStage = 'post_intro_reply';
312+
currentMessages = [...(conversations[currentStage] || [])];
313+
messageIndex = 0;
314+
displayNextMessage();
315+
}
316+
}
317+
}
318+
319+
function handlePlayerSend() {
320+
const messageText = playerInput.value.trim();
321+
if (messageText && !playerInput.disabled) {
322+
addMessageToChat('player', messageText);
323+
playerInput.value = '';
324+
processPlayerTextInput(messageText);
325+
}
326+
}
327+
328+
// --- Event Listeners ---
329+
if (sendButton) {
330+
sendButton.addEventListener('click', handlePlayerSend);
331+
}
332+
333+
if (playerInput) {
334+
playerInput.addEventListener('keypress', (event) => {
335+
if (event.key === 'Enter' && !playerInput.disabled) {
336+
handlePlayerSend();
337+
}
338+
});
339+
}
340+
341+
// --- Game Initialization ---
342+
function startGame() {
343+
console.log("Game initialized.");
344+
statusText.textContent = `Relationship Score: ${relationshipScore}`;
345+
setInputMode('text'); // Start with text input
346+
displayNextMessage();
347+
}
348+
349+
startGame();
350+
});

0 commit comments

Comments
 (0)