- Add 3 NCE content modules (NCE1-Lesson63-64, NCE2-Lesson3, NCE2-Lesson30) - Integrate NCE modules in content-scanner, game-loader, navigation - Add sentences extracted from stories for better game compatibility - Add meteor spells to NCE1 (15+ word sentences for wizard game) - Enhanced wizard spell effects with particles and casting animations - Update games-config.json with NCE module configurations Note: Architecture needs refactoring - too many interdependencies Current state has issues that need systematic cleanup 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1881 lines
63 KiB
JavaScript
1881 lines
63 KiB
JavaScript
// === WIZARD SPELL CASTER GAME ===
|
|
// Advanced game for 11+ years old - Form sentences to cast magical spells
|
|
|
|
class WizardSpellCaster {
|
|
constructor({ container, content, onScoreUpdate, onGameEnd }) {
|
|
this.container = container;
|
|
this.content = content;
|
|
this.onScoreUpdate = onScoreUpdate;
|
|
this.onGameEnd = onGameEnd;
|
|
|
|
this.score = 0;
|
|
this.enemyHP = 100;
|
|
this.playerHP = 100;
|
|
this.currentSpells = [];
|
|
this.selectedWords = [];
|
|
|
|
// Timer invisible pour bonus de vitesse
|
|
this.spellStartTime = null;
|
|
this.averageSpellTime = 0;
|
|
this.spellCount = 0;
|
|
|
|
// Enemy attack system
|
|
this.enemyAttackTimer = null;
|
|
this.nextEnemyAttack = this.getRandomAttackTime();
|
|
|
|
this.injectCSS();
|
|
this.extractSpells();
|
|
this.init();
|
|
}
|
|
|
|
injectCSS() {
|
|
if (document.getElementById('wizard-spell-caster-styles')) return;
|
|
|
|
const styleSheet = document.createElement('style');
|
|
styleSheet.id = 'wizard-spell-caster-styles';
|
|
styleSheet.textContent = `
|
|
.wizard-game-wrapper {
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
min-height: 100vh;
|
|
color: white;
|
|
font-family: 'Fantasy', serif;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.wizard-hud {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 15px;
|
|
background: rgba(0,0,0,0.3);
|
|
border-bottom: 2px solid #ffd700;
|
|
}
|
|
|
|
.wizard-stats {
|
|
display: flex;
|
|
gap: 20px;
|
|
align-items: center;
|
|
}
|
|
|
|
.health-bar {
|
|
width: 150px;
|
|
height: 20px;
|
|
background: rgba(255,255,255,0.2);
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
border: 2px solid #ffd700;
|
|
}
|
|
|
|
.health-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #ff4757, #ff6b7a);
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.battle-area {
|
|
display: flex;
|
|
height: 60vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.wizard-side {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.enemy-side {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.wizard-character {
|
|
width: 120px;
|
|
height: 120px;
|
|
background: linear-gradient(45deg, #6c5ce7, #a29bfe);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 48px;
|
|
margin-bottom: 20px;
|
|
animation: float 3s ease-in-out infinite;
|
|
box-shadow: 0 0 30px rgba(108, 92, 231, 0.6);
|
|
}
|
|
|
|
.enemy-character {
|
|
width: 150px;
|
|
height: 150px;
|
|
background: linear-gradient(45deg, #ff4757, #ff6b7a);
|
|
border-radius: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 64px;
|
|
margin-bottom: 20px;
|
|
animation: enemyPulse 2s ease-in-out infinite;
|
|
box-shadow: 0 0 40px rgba(255, 71, 87, 0.6);
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% { transform: translateY(0px); }
|
|
50% { transform: translateY(-10px); }
|
|
}
|
|
|
|
@keyframes enemyPulse {
|
|
0%, 100% { transform: scale(1); }
|
|
50% { transform: scale(1.05); }
|
|
}
|
|
|
|
.spell-casting-area {
|
|
background: rgba(0,0,0,0.4);
|
|
border: 2px solid #ffd700;
|
|
border-radius: 15px;
|
|
padding: 20px;
|
|
margin: 20px;
|
|
}
|
|
|
|
.spell-selection {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 15px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.spell-card {
|
|
background: linear-gradient(135deg, #2c2c54, #40407a);
|
|
border: 2px solid #ffd700;
|
|
border-radius: 10px;
|
|
padding: 15px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
text-align: center;
|
|
}
|
|
|
|
.spell-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 10px 25px rgba(255, 215, 0, 0.3);
|
|
border-color: #fff;
|
|
}
|
|
|
|
.spell-card.selected {
|
|
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
|
color: #000;
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.spell-type {
|
|
font-size: 12px;
|
|
color: #ffd700;
|
|
font-weight: bold;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.spell-damage {
|
|
font-size: 14px;
|
|
color: #ff6b7a;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.sentence-builder {
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: 10px;
|
|
padding: 15px;
|
|
margin-bottom: 20px;
|
|
min-height: 80px;
|
|
border: 2px dashed #ffd700;
|
|
}
|
|
|
|
.word-bank {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.word-tile {
|
|
background: linear-gradient(135deg, #5f27cd, #8854d0);
|
|
color: white;
|
|
padding: 8px 15px;
|
|
border-radius: 20px;
|
|
cursor: grab;
|
|
user-select: none;
|
|
transition: all 0.3s ease;
|
|
border: 2px solid transparent;
|
|
}
|
|
|
|
.word-tile:hover {
|
|
transform: scale(1.1);
|
|
box-shadow: 0 5px 15px rgba(95, 39, 205, 0.4);
|
|
}
|
|
|
|
.word-tile.selected {
|
|
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
|
color: #000;
|
|
border-color: #fff;
|
|
}
|
|
|
|
.word-tile:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.cast-button {
|
|
background: linear-gradient(135deg, #ff6b7a, #ff4757);
|
|
border: none;
|
|
color: white;
|
|
padding: 15px 30px;
|
|
border-radius: 25px;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 5px 15px rgba(255, 71, 87, 0.3);
|
|
width: 100%;
|
|
}
|
|
|
|
.cast-button:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 8px 25px rgba(255, 71, 87, 0.5);
|
|
}
|
|
|
|
.cast-button:disabled {
|
|
background: #666;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.damage-number {
|
|
position: absolute;
|
|
font-size: 36px;
|
|
font-weight: bold;
|
|
color: #ff4757;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
|
pointer-events: none;
|
|
animation: damageFloat 1.5s ease-out forwards;
|
|
}
|
|
|
|
@keyframes damageFloat {
|
|
0% {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
transform: translateY(-100px) scale(1.5);
|
|
}
|
|
}
|
|
|
|
.spell-effect {
|
|
position: absolute;
|
|
width: 100px;
|
|
height: 100px;
|
|
border-radius: 50%;
|
|
pointer-events: none;
|
|
animation: spellBlast 0.8s ease-out forwards;
|
|
}
|
|
|
|
.fire-effect {
|
|
background: radial-gradient(circle, #ff6b7a, #ff4757, transparent);
|
|
}
|
|
|
|
.lightning-effect {
|
|
background: radial-gradient(circle, #ffd700, #ffed4e, transparent);
|
|
}
|
|
|
|
.meteor-effect {
|
|
background: radial-gradient(circle, #a29bfe, #6c5ce7, transparent);
|
|
}
|
|
|
|
@keyframes spellBlast {
|
|
0% {
|
|
transform: scale(0);
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
transform: scale(1.5);
|
|
opacity: 0.8;
|
|
}
|
|
100% {
|
|
transform: scale(3);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.mini-enemy {
|
|
position: absolute;
|
|
width: 60px;
|
|
height: 60px;
|
|
background: linear-gradient(45deg, #ff9ff3, #f368e0);
|
|
border-radius: 50%;
|
|
font-size: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
animation: miniEnemyFloat 3s ease-in-out infinite;
|
|
z-index: 100;
|
|
}
|
|
|
|
@keyframes miniEnemyFloat {
|
|
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
|
50% { transform: translateY(-15px) rotate(180deg); }
|
|
}
|
|
|
|
.magic-quirk {
|
|
position: fixed;
|
|
width: 200px;
|
|
height: 200px;
|
|
border-radius: 50%;
|
|
background: conic-gradient(from 0deg, #ff0080, #0080ff, #ff0080);
|
|
animation: magicQuirk 2s ease-in-out;
|
|
z-index: 1000;
|
|
pointer-events: none;
|
|
}
|
|
|
|
@keyframes magicQuirk {
|
|
0% {
|
|
transform: translate(-50%, -50%) scale(0) rotate(0deg);
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
transform: translate(-50%, -50%) scale(1.5) rotate(180deg);
|
|
opacity: 0.8;
|
|
}
|
|
100% {
|
|
transform: translate(-50%, -50%) scale(0) rotate(360deg);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.flying-bird {
|
|
position: fixed;
|
|
font-size: 48px; /* Plus gros ! */
|
|
z-index: 500;
|
|
pointer-events: none;
|
|
width: 60px; /* Plus gros ! */
|
|
height: 60px; /* Plus gros ! */
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.bird-path-1 {
|
|
animation: flyPath1 8s linear infinite; /* Plus rapide ! */
|
|
}
|
|
|
|
.bird-path-2 {
|
|
animation: flyPath2 6s linear infinite; /* Plus rapide ! */
|
|
}
|
|
|
|
.bird-path-3 {
|
|
animation: flyPath3 10s linear infinite; /* Plus rapide ! */
|
|
}
|
|
|
|
.bird-path-4 {
|
|
animation: flyPath4 12s linear infinite; /* Nouveau chemin ! */
|
|
}
|
|
|
|
.bird-path-5 {
|
|
animation: flyPath5 9s linear infinite; /* Encore un autre ! */
|
|
}
|
|
|
|
@keyframes flyPath1 {
|
|
0% {
|
|
left: -100px;
|
|
top: 20vh;
|
|
transform: rotate(0deg) scale(1);
|
|
}
|
|
15% {
|
|
left: 30vw;
|
|
top: 5vh;
|
|
transform: rotate(180deg) scale(1.5); /* Rotation plus douce */
|
|
}
|
|
30% {
|
|
left: 70vw;
|
|
top: 40vh;
|
|
transform: rotate(-90deg) scale(0.5);
|
|
}
|
|
45% {
|
|
left: 100vw;
|
|
top: 15vh;
|
|
transform: rotate(270deg) scale(2);
|
|
}
|
|
60% {
|
|
left: 60vw;
|
|
top: 85vh;
|
|
transform: rotate(-180deg) scale(0.8);
|
|
}
|
|
80% {
|
|
left: 10vw;
|
|
top: 60vh;
|
|
transform: rotate(360deg) scale(1.2);
|
|
}
|
|
100% {
|
|
left: -100px;
|
|
top: 20vh;
|
|
transform: rotate(450deg) scale(1);
|
|
}
|
|
}
|
|
|
|
@keyframes flyPath2 {
|
|
0% {
|
|
left: 50vw;
|
|
top: -80px;
|
|
transform: rotate(0deg) scale(0.5);
|
|
}
|
|
20% {
|
|
left: 80vw;
|
|
top: 20vh;
|
|
transform: rotate(-225deg) scale(2.5);
|
|
}
|
|
40% {
|
|
left: 20vw;
|
|
top: 50vh;
|
|
transform: rotate(315deg) scale(0.3);
|
|
}
|
|
60% {
|
|
left: 90vw;
|
|
top: 80vh;
|
|
transform: rotate(-450deg) scale(3);
|
|
}
|
|
80% {
|
|
left: 30vw;
|
|
top: 30vh;
|
|
transform: rotate(540deg) scale(0.7);
|
|
}
|
|
100% {
|
|
left: 50vw;
|
|
top: -80px;
|
|
transform: rotate(-630deg) scale(0.5);
|
|
}
|
|
}
|
|
|
|
@keyframes flyPath3 {
|
|
0% {
|
|
left: 120vw;
|
|
top: 10vh;
|
|
transform: rotate(0deg) scale(1);
|
|
}
|
|
12% {
|
|
left: 75vw;
|
|
top: 70vh;
|
|
transform: rotate(-360deg) scale(4);
|
|
}
|
|
25% {
|
|
left: 25vw;
|
|
top: 20vh;
|
|
transform: rotate(540deg) scale(0.2);
|
|
}
|
|
37% {
|
|
left: 85vw;
|
|
top: 90vh;
|
|
transform: rotate(-720deg) scale(3.5);
|
|
}
|
|
50% {
|
|
left: 10vw;
|
|
top: 5vh;
|
|
transform: rotate(900deg) scale(0.4);
|
|
}
|
|
62% {
|
|
left: 90vw;
|
|
top: 55vh;
|
|
transform: rotate(-1080deg) scale(2.8);
|
|
}
|
|
75% {
|
|
left: 40vw;
|
|
top: 95vh;
|
|
transform: rotate(1260deg) scale(0.6);
|
|
}
|
|
87% {
|
|
left: 70vw;
|
|
top: 25vh;
|
|
transform: rotate(-1440deg) scale(3.2);
|
|
}
|
|
100% {
|
|
left: 120vw;
|
|
top: 10vh;
|
|
transform: rotate(1800deg) scale(1);
|
|
}
|
|
}
|
|
|
|
@keyframes flyPath4 {
|
|
0% {
|
|
left: 25vw;
|
|
top: 100vh;
|
|
transform: rotate(0deg) scale(1.2);
|
|
}
|
|
20% {
|
|
left: 75vw;
|
|
top: 80vh;
|
|
transform: rotate(180deg) scale(0.7);
|
|
}
|
|
40% {
|
|
left: 90vw;
|
|
top: 40vh;
|
|
transform: rotate(-270deg) scale(2.2);
|
|
}
|
|
60% {
|
|
left: 40vw;
|
|
top: 20vh;
|
|
transform: rotate(360deg) scale(0.4);
|
|
}
|
|
80% {
|
|
left: 10vw;
|
|
top: 70vh;
|
|
transform: rotate(-450deg) scale(1.8);
|
|
}
|
|
100% {
|
|
left: 25vw;
|
|
top: 100vh;
|
|
transform: rotate(540deg) scale(1.2);
|
|
}
|
|
}
|
|
|
|
@keyframes flyPath5 {
|
|
0% {
|
|
left: 100vw;
|
|
top: 50vh;
|
|
transform: rotate(45deg) scale(0.8);
|
|
}
|
|
25% {
|
|
left: 60vw;
|
|
top: 10vh;
|
|
transform: rotate(-135deg) scale(2.5);
|
|
}
|
|
50% {
|
|
left: 20vw;
|
|
top: 80vh;
|
|
transform: rotate(225deg) scale(0.3);
|
|
}
|
|
75% {
|
|
left: 80vw;
|
|
top: 90vh;
|
|
transform: rotate(-315deg) scale(3);
|
|
}
|
|
100% {
|
|
left: 100vw;
|
|
top: 50vh;
|
|
transform: rotate(405deg) scale(0.8);
|
|
}
|
|
}
|
|
|
|
.screen-shake {
|
|
animation: screenShake 0.5s ease-in-out;
|
|
}
|
|
|
|
@keyframes screenShake {
|
|
0%, 100% { transform: translateX(0); }
|
|
10% { transform: translateX(-10px); }
|
|
20% { transform: translateX(10px); }
|
|
30% { transform: translateX(-10px); }
|
|
40% { transform: translateX(10px); }
|
|
50% { transform: translateX(-10px); }
|
|
60% { transform: translateX(10px); }
|
|
70% { transform: translateX(-10px); }
|
|
80% { transform: translateX(10px); }
|
|
90% { transform: translateX(-10px); }
|
|
}
|
|
|
|
.enemy-attack-warning {
|
|
position: absolute;
|
|
top: -30px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: #ff4757;
|
|
color: white;
|
|
padding: 5px 15px;
|
|
border-radius: 15px;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
animation: warningPulse 1s ease-in-out infinite;
|
|
z-index: 100;
|
|
}
|
|
|
|
@keyframes warningPulse {
|
|
0%, 100% { opacity: 1; transform: translateX(-50%) scale(1); }
|
|
50% { opacity: 0.6; transform: translateX(-50%) scale(1.1); }
|
|
}
|
|
|
|
.enemy-attack-effect {
|
|
position: absolute;
|
|
width: 150px;
|
|
height: 150px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle, #ff4757, transparent);
|
|
animation: enemyAttackBlast 1s ease-out;
|
|
pointer-events: none;
|
|
z-index: 200;
|
|
}
|
|
|
|
@keyframes enemyAttackBlast {
|
|
0% {
|
|
transform: scale(0);
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
transform: scale(1.5);
|
|
opacity: 0.8;
|
|
}
|
|
100% {
|
|
transform: scale(3);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.enemy-charging {
|
|
animation: enemyCharging 2s ease-in-out;
|
|
}
|
|
|
|
@keyframes enemyCharging {
|
|
0%, 100% {
|
|
background: linear-gradient(45deg, #ff4757, #ff6b7a);
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
background: linear-gradient(45deg, #ff0000, #ff3333);
|
|
transform: scale(1.1);
|
|
box-shadow: 0 0 60px rgba(255, 0, 0, 0.8);
|
|
}
|
|
}
|
|
|
|
.victory-screen, .defeat-screen {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.8);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.result-title {
|
|
font-size: 48px;
|
|
margin-bottom: 20px;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
|
}
|
|
|
|
.victory-screen .result-title {
|
|
color: #2ed573;
|
|
}
|
|
|
|
.defeat-screen .result-title {
|
|
color: #ff4757;
|
|
}
|
|
|
|
.fail-message {
|
|
position: fixed;
|
|
top: 30%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(255, 71, 87, 0.9);
|
|
color: white;
|
|
padding: 20px 30px;
|
|
border-radius: 15px;
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
z-index: 1000;
|
|
animation: failMessagePop 2s ease-out;
|
|
text-align: center;
|
|
border: 3px solid #ffd700;
|
|
}
|
|
|
|
@keyframes failMessagePop {
|
|
0% {
|
|
transform: translateX(-50%) scale(0);
|
|
opacity: 0;
|
|
}
|
|
20% {
|
|
transform: translateX(-50%) scale(1.2);
|
|
opacity: 1;
|
|
}
|
|
80% {
|
|
transform: translateX(-50%) scale(1);
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
transform: translateX(-50%) scale(0.8);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
/* === ENHANCED SPELL EFFECTS === */
|
|
|
|
/* Particle animations */
|
|
@keyframes fireParticle {
|
|
0% {
|
|
transform: scale(1) translate(0, 0);
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
transform: scale(1.5) translate(var(--random-x, 20px), var(--random-y, -30px));
|
|
opacity: 0.8;
|
|
}
|
|
100% {
|
|
transform: scale(0.5) translate(var(--random-x, 40px), var(--random-y, -60px));
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@keyframes lightningParticle {
|
|
0% {
|
|
transform: scale(1) translate(0, 0);
|
|
opacity: 1;
|
|
filter: brightness(2);
|
|
}
|
|
25% {
|
|
transform: scale(2) translate(var(--random-x, 10px), var(--random-y, -20px));
|
|
opacity: 1;
|
|
filter: brightness(3);
|
|
}
|
|
100% {
|
|
transform: scale(0) translate(var(--random-x, 30px), var(--random-y, -50px));
|
|
opacity: 0;
|
|
filter: brightness(1);
|
|
}
|
|
}
|
|
|
|
@keyframes meteorParticle {
|
|
0% {
|
|
transform: scale(0.5) translate(0, -100px);
|
|
opacity: 0.5;
|
|
}
|
|
30% {
|
|
transform: scale(1.2) translate(var(--random-x, 0px), 0);
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
transform: scale(0.3) translate(var(--random-x, 20px), var(--random-y, 50px));
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
/* Screen effects */
|
|
@keyframes meteorTrail {
|
|
0% {
|
|
opacity: 0;
|
|
transform: translateX(100px) scaleY(0);
|
|
}
|
|
50% {
|
|
opacity: 1;
|
|
transform: translateX(0) scaleY(1);
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
transform: translateX(-100px) scaleY(0.5);
|
|
}
|
|
}
|
|
|
|
@keyframes lightningFlash {
|
|
0% {
|
|
opacity: 0;
|
|
}
|
|
50% {
|
|
opacity: 0.8;
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@keyframes fireRipple {
|
|
0% {
|
|
transform: scale(0.5);
|
|
opacity: 1;
|
|
border-width: 3px;
|
|
}
|
|
50% {
|
|
transform: scale(1.2);
|
|
opacity: 0.6;
|
|
border-width: 2px;
|
|
}
|
|
100% {
|
|
transform: scale(2);
|
|
opacity: 0;
|
|
border-width: 1px;
|
|
}
|
|
}
|
|
|
|
/* Enhanced spell effect improvements */
|
|
.fire-effect {
|
|
background: radial-gradient(circle, #ff6b7a, #ff4757, #ff3742, transparent);
|
|
filter: drop-shadow(0 0 20px #ff4757);
|
|
animation: spellBlast 0.8s ease-out forwards, fireGlow 0.8s ease-out;
|
|
}
|
|
|
|
.lightning-effect {
|
|
background: radial-gradient(circle, #ffd700, #ffed4e, #fff200, transparent);
|
|
filter: drop-shadow(0 0 25px #ffd700);
|
|
animation: spellBlast 0.8s ease-out forwards, lightningPulse 0.8s ease-out;
|
|
}
|
|
|
|
.meteor-effect {
|
|
background: radial-gradient(circle, #a29bfe, #6c5ce7, #5f3dc4, transparent);
|
|
filter: drop-shadow(0 0 30px #6c5ce7);
|
|
animation: spellBlast 0.8s ease-out forwards, meteorImpact 0.8s ease-out;
|
|
}
|
|
|
|
@keyframes fireGlow {
|
|
0%, 100% { filter: drop-shadow(0 0 20px #ff4757) hue-rotate(0deg); }
|
|
50% { filter: drop-shadow(0 0 40px #ff4757) hue-rotate(30deg); }
|
|
}
|
|
|
|
@keyframes lightningPulse {
|
|
0%, 100% { filter: drop-shadow(0 0 25px #ffd700) brightness(1); }
|
|
50% { filter: drop-shadow(0 0 50px #ffd700) brightness(2); }
|
|
}
|
|
|
|
@keyframes meteorImpact {
|
|
0% { filter: drop-shadow(0 0 30px #6c5ce7) contrast(1); }
|
|
30% { filter: drop-shadow(0 0 60px #6c5ce7) contrast(1.5); }
|
|
100% { filter: drop-shadow(0 0 30px #6c5ce7) contrast(1); }
|
|
}
|
|
|
|
/* Spell casting enhancement */
|
|
.spell-card.selected {
|
|
animation: spellCharging 0.5s ease-in-out infinite alternate;
|
|
}
|
|
|
|
@keyframes spellCharging {
|
|
0% {
|
|
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
|
|
transform: scale(1);
|
|
}
|
|
100% {
|
|
box-shadow: 0 0 40px rgba(255, 215, 0, 0.8);
|
|
transform: scale(1.02);
|
|
}
|
|
}
|
|
|
|
/* Casting effect animations */
|
|
@keyframes magicCircleForm {
|
|
0% {
|
|
transform: scale(0.5) rotate(0deg);
|
|
opacity: 0;
|
|
}
|
|
50% {
|
|
transform: scale(1.1) rotate(180deg);
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
transform: scale(1) rotate(360deg);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@keyframes castingSparkle {
|
|
0% {
|
|
transform: scale(1) rotate(0deg);
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
transform: scale(1.5) rotate(180deg);
|
|
opacity: 0.8;
|
|
}
|
|
100% {
|
|
transform: scale(0.5) rotate(360deg);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
`;
|
|
document.head.appendChild(styleSheet);
|
|
}
|
|
|
|
extractSpells() {
|
|
// Extract sentences from content and categorize by length
|
|
this.spells = {
|
|
short: [], // 3-4 words
|
|
medium: [], // 5-6 words
|
|
long: [] // 7+ words
|
|
};
|
|
|
|
// Process story sentences
|
|
if (this.content.story && this.content.story.chapters) {
|
|
this.content.story.chapters.forEach(chapter => {
|
|
chapter.sentences.forEach(sentence => {
|
|
const wordCount = sentence.words.length;
|
|
const spellData = {
|
|
english: sentence.original,
|
|
translation: sentence.translation,
|
|
words: sentence.words,
|
|
damage: this.calculateDamage(wordCount),
|
|
castTime: this.calculateCastTime(wordCount)
|
|
};
|
|
|
|
if (wordCount <= 4) {
|
|
this.spells.short.push(spellData);
|
|
} else if (wordCount <= 6) {
|
|
this.spells.medium.push(spellData);
|
|
} else {
|
|
this.spells.long.push(spellData);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
console.log('Spells extracted:', this.spells);
|
|
}
|
|
|
|
calculateDamage(wordCount) {
|
|
// Augmenter significativement les points pour les phrases longues
|
|
if (wordCount <= 3) return Math.floor(Math.random() * 10) + 15; // 15-25 (phrases courtes)
|
|
if (wordCount <= 5) return Math.floor(Math.random() * 15) + 30; // 30-45 (phrases moyennes)
|
|
if (wordCount <= 7) return Math.floor(Math.random() * 20) + 50; // 50-70 (phrases longues)
|
|
return Math.floor(Math.random() * 30) + 70; // 70-100 (phrases très longues)
|
|
}
|
|
|
|
calculateCastTime(wordCount) {
|
|
if (wordCount <= 4) return 1000; // 1 second
|
|
if (wordCount <= 6) return 2000; // 2 seconds
|
|
return 3000; // 3 seconds
|
|
}
|
|
|
|
init() {
|
|
this.container.innerHTML = `
|
|
<div class="wizard-game-wrapper">
|
|
<div class="wizard-hud">
|
|
<div class="wizard-stats">
|
|
<div>
|
|
<div>Wizard HP</div>
|
|
<div class="health-bar">
|
|
<div class="health-fill" id="player-health" style="width: 100%"></div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div>Score: <span id="current-score">0</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="wizard-stats">
|
|
<div>
|
|
<div>Enemy HP</div>
|
|
<div class="health-bar">
|
|
<div class="health-fill" id="enemy-health" style="width: 100%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="battle-area">
|
|
<div class="wizard-side">
|
|
<div class="wizard-character">🧙♂️</div>
|
|
<div>Wizard Master</div>
|
|
</div>
|
|
<div class="enemy-side">
|
|
<div class="enemy-character">👹</div>
|
|
<div>Grammar Demon</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="spell-casting-area">
|
|
<div class="spell-selection" id="spell-selection">
|
|
<!-- Spell cards will be populated here -->
|
|
</div>
|
|
|
|
<div class="sentence-builder" id="sentence-builder">
|
|
<div style="color: #ffd700; margin-bottom: 10px;">Form your spell incantation:</div>
|
|
<div id="current-sentence" style="font-size: 18px; min-height: 30px;"></div>
|
|
</div>
|
|
|
|
<div class="word-bank" id="word-bank">
|
|
<!-- Words will be populated here -->
|
|
</div>
|
|
|
|
<button class="cast-button" id="cast-button" disabled>🔥 CAST SPELL 🔥</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
this.setupEventListeners();
|
|
this.generateNewSpells();
|
|
this.startEnemyAttackSystem();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
document.getElementById('cast-button').addEventListener('click', () => this.castSpell());
|
|
}
|
|
|
|
generateNewSpells() {
|
|
this.currentSpells = [];
|
|
|
|
// Get one spell of each type
|
|
if (this.spells.short.length > 0) {
|
|
this.currentSpells.push({
|
|
...this.spells.short[Math.floor(Math.random() * this.spells.short.length)],
|
|
type: 'short',
|
|
name: 'Fireball',
|
|
icon: '🔥'
|
|
});
|
|
}
|
|
|
|
if (this.spells.medium.length > 0) {
|
|
this.currentSpells.push({
|
|
...this.spells.medium[Math.floor(Math.random() * this.spells.medium.length)],
|
|
type: 'medium',
|
|
name: 'Lightning',
|
|
icon: '⚡'
|
|
});
|
|
}
|
|
|
|
if (this.spells.long.length > 0) {
|
|
this.currentSpells.push({
|
|
...this.spells.long[Math.floor(Math.random() * this.spells.long.length)],
|
|
type: 'long',
|
|
name: 'Meteor',
|
|
icon: '☄️'
|
|
});
|
|
}
|
|
|
|
this.renderSpellCards();
|
|
this.selectedSpell = null;
|
|
this.selectedWords = [];
|
|
this.updateWordBank();
|
|
this.updateSentenceBuilder();
|
|
}
|
|
|
|
renderSpellCards() {
|
|
const container = document.getElementById('spell-selection');
|
|
container.innerHTML = this.currentSpells.map((spell, index) => `
|
|
<div class="spell-card" data-spell-index="${index}">
|
|
<div class="spell-type">${spell.icon} ${spell.name}</div>
|
|
<div style="margin: 10px 0; font-size: 14px;">${spell.translation}</div>
|
|
<div class="spell-damage">${spell.damage} damage</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Add click listeners
|
|
container.querySelectorAll('.spell-card').forEach(card => {
|
|
card.addEventListener('click', (e) => {
|
|
const spellIndex = parseInt(e.currentTarget.dataset.spellIndex);
|
|
this.selectSpell(spellIndex);
|
|
});
|
|
});
|
|
}
|
|
|
|
selectSpell(index) {
|
|
// Remove previous selection
|
|
document.querySelectorAll('.spell-card').forEach(card => card.classList.remove('selected'));
|
|
|
|
// Select new spell
|
|
this.selectedSpell = this.currentSpells[index];
|
|
document.querySelector(`[data-spell-index="${index}"]`).classList.add('selected');
|
|
|
|
// Démarrer le timer invisible pour le bonus de vitesse
|
|
this.spellStartTime = Date.now();
|
|
|
|
// Reset word selection
|
|
this.selectedWords = [];
|
|
this.updateWordBank();
|
|
this.updateSentenceBuilder();
|
|
}
|
|
|
|
updateWordBank() {
|
|
const container = document.getElementById('word-bank');
|
|
|
|
if (!this.selectedSpell) {
|
|
container.innerHTML = '<div style="color: #ffd700;">Select a spell first</div>';
|
|
return;
|
|
}
|
|
|
|
// Extract the complete sentence including ALL punctuation
|
|
const originalSentence = this.selectedSpell.english;
|
|
const words = [...this.selectedSpell.words];
|
|
|
|
// Extract ALL punctuation from the original sentence
|
|
const punctuationRegex = /[.!?,;:]/g;
|
|
const punctuationMarks = originalSentence.match(punctuationRegex) || [];
|
|
|
|
// Add all punctuation marks as separate word tiles with unique IDs
|
|
punctuationMarks.forEach((punctuation, index) => {
|
|
words.push({
|
|
word: punctuation,
|
|
translation: punctuation,
|
|
type: 'punctuation',
|
|
pronunciation: '',
|
|
uniqueId: `punct_${index}_${Date.now()}_${Math.random()}`
|
|
});
|
|
});
|
|
|
|
// Shuffle the words including punctuation
|
|
const shuffledWords = [...words].sort(() => Math.random() - 0.5);
|
|
|
|
container.innerHTML = shuffledWords.map((wordData, index) => {
|
|
const uniqueId = wordData.uniqueId || `word_${index}_${wordData.word}`;
|
|
return `
|
|
<div class="word-tile" data-word-index="${index}" data-word="${wordData.word}" data-unique-id="${uniqueId}">
|
|
${wordData.word}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
// Add click listeners
|
|
container.querySelectorAll('.word-tile').forEach(tile => {
|
|
tile.addEventListener('click', (e) => {
|
|
const word = e.currentTarget.dataset.word;
|
|
const uniqueId = e.currentTarget.dataset.uniqueId;
|
|
this.toggleWord(word, e.currentTarget, uniqueId);
|
|
});
|
|
});
|
|
}
|
|
|
|
toggleWord(word, element, uniqueId) {
|
|
// Find the word by unique ID instead of just the word text
|
|
const wordIndex = this.selectedWords.findIndex(selectedWord =>
|
|
selectedWord.uniqueId === uniqueId
|
|
);
|
|
|
|
if (wordIndex > -1) {
|
|
// Remove word
|
|
this.selectedWords.splice(wordIndex, 1);
|
|
element.classList.remove('selected');
|
|
} else {
|
|
// Add word with unique ID
|
|
this.selectedWords.push({
|
|
word: word,
|
|
uniqueId: uniqueId
|
|
});
|
|
element.classList.add('selected');
|
|
}
|
|
|
|
this.updateSentenceBuilder();
|
|
this.updateCastButton();
|
|
}
|
|
|
|
updateSentenceBuilder() {
|
|
const container = document.getElementById('current-sentence');
|
|
const sentence = this.buildSentenceFromWords(this.selectedWords);
|
|
container.textContent = sentence;
|
|
}
|
|
|
|
buildSentenceFromWords(words) {
|
|
// Join words and handle punctuation correctly (no space before punctuation)
|
|
let sentence = '';
|
|
for (let i = 0; i < words.length; i++) {
|
|
const wordText = typeof words[i] === 'string' ? words[i] : words[i].word;
|
|
const isPunctuation = ['.', '!', '?', ',', ';', ':'].includes(wordText);
|
|
|
|
if (i === 0) {
|
|
sentence = wordText;
|
|
} else if (isPunctuation) {
|
|
sentence += wordText; // No space before punctuation
|
|
} else {
|
|
sentence += ' ' + wordText; // Space before regular words
|
|
}
|
|
}
|
|
return sentence;
|
|
}
|
|
|
|
updateCastButton() {
|
|
const button = document.getElementById('cast-button');
|
|
|
|
// Always enable the button - let players try and fail!
|
|
button.disabled = false;
|
|
|
|
// Always show the same text - don't reveal if the spell is correct
|
|
if (this.selectedSpell) {
|
|
button.textContent = `🔥 CAST ${this.selectedSpell.name.toUpperCase()} 🔥`;
|
|
} else {
|
|
button.textContent = '🔥 CAST SPELL 🔥';
|
|
}
|
|
}
|
|
|
|
castSpell() {
|
|
if (!this.selectedSpell) {
|
|
this.showFailEffect('noSpell');
|
|
return;
|
|
}
|
|
|
|
// Check if spell is correctly formed (including punctuation)
|
|
const expectedSentence = this.selectedSpell.english;
|
|
const playerSentence = this.buildSentenceFromWords(this.selectedWords);
|
|
|
|
console.log('🔍 Spell check:');
|
|
console.log('Expected:', expectedSentence);
|
|
console.log('Player:', playerSentence);
|
|
console.log('Selected words:', this.selectedWords);
|
|
|
|
const isCorrect = playerSentence === expectedSentence;
|
|
|
|
if (isCorrect) {
|
|
// Successful cast!
|
|
this.showCastingEffect(this.selectedSpell.type);
|
|
|
|
// Delay the main spell effect for dramatic timing
|
|
setTimeout(() => {
|
|
this.showSpellEffect(this.selectedSpell.type);
|
|
}, 500);
|
|
|
|
// Deal damage
|
|
this.enemyHP = Math.max(0, this.enemyHP - this.selectedSpell.damage);
|
|
this.updateEnemyHealth();
|
|
this.showDamageNumber(this.selectedSpell.damage);
|
|
|
|
// Update score - bonus multiplicateur pour phrases longues
|
|
const wordCount = this.selectedWords.length;
|
|
let scoreMultiplier = 10;
|
|
if (wordCount >= 7) scoreMultiplier = 20; // x2 pour phrases très longues
|
|
else if (wordCount >= 5) scoreMultiplier = 15; // x1.5 pour phrases longues
|
|
|
|
// Calculer le bonus de vitesse (invisible)
|
|
let speedBonus = 0;
|
|
if (this.spellStartTime) {
|
|
const spellTime = (Date.now() - this.spellStartTime) / 1000; // en secondes
|
|
this.spellCount++;
|
|
this.averageSpellTime = ((this.averageSpellTime * (this.spellCount - 1)) + spellTime) / this.spellCount;
|
|
|
|
// Bonus de vitesse : plus c'est rapide, plus de points
|
|
if (spellTime < 10) speedBonus = Math.floor((10 - spellTime) * 50); // Jusqu'à 500 bonus
|
|
if (spellTime < 5) speedBonus += 300; // Bonus extra pour super rapide
|
|
if (spellTime < 3) speedBonus += 500; // Bonus énorme pour très rapide
|
|
}
|
|
|
|
this.score += (this.selectedSpell.damage * scoreMultiplier) + speedBonus;
|
|
this.onScoreUpdate(this.score);
|
|
document.getElementById('current-score').textContent = this.score;
|
|
|
|
// Check win condition
|
|
if (this.enemyHP <= 0) {
|
|
this.handleVictory();
|
|
return;
|
|
}
|
|
|
|
// Generate new spells for next round
|
|
setTimeout(() => {
|
|
this.generateNewSpells();
|
|
// Reset timer pour nouveau round
|
|
this.spellStartTime = Date.now();
|
|
}, 1000);
|
|
} else {
|
|
// Spell failed! Random funny effect
|
|
this.showFailEffect();
|
|
}
|
|
}
|
|
|
|
showSpellEffect(type) {
|
|
const enemyChar = document.querySelector('.enemy-character');
|
|
const rect = enemyChar.getBoundingClientRect();
|
|
|
|
// Main spell effect
|
|
const effect = document.createElement('div');
|
|
effect.className = `spell-effect ${type}-effect`;
|
|
effect.style.position = 'fixed';
|
|
effect.style.left = rect.left + rect.width/2 - 50 + 'px';
|
|
effect.style.top = rect.top + rect.height/2 - 50 + 'px';
|
|
document.body.appendChild(effect);
|
|
|
|
// Add spell-specific enhanced effects
|
|
this.createSpellParticles(type, rect);
|
|
this.triggerSpellAnimation(type, enemyChar);
|
|
|
|
setTimeout(() => {
|
|
effect.remove();
|
|
}, 800);
|
|
}
|
|
|
|
createSpellParticles(type, enemyRect) {
|
|
const particleCount = type === 'meteor' ? 15 : type === 'lightning' ? 12 : 8;
|
|
|
|
for (let i = 0; i < particleCount; i++) {
|
|
const particle = document.createElement('div');
|
|
particle.className = `spell-particle ${type}-particle`;
|
|
|
|
// Random position around enemy
|
|
const offsetX = (Math.random() - 0.5) * 200;
|
|
const offsetY = (Math.random() - 0.5) * 200;
|
|
|
|
particle.style.position = 'fixed';
|
|
particle.style.left = enemyRect.left + enemyRect.width/2 + offsetX + 'px';
|
|
particle.style.top = enemyRect.top + enemyRect.height/2 + offsetY + 'px';
|
|
particle.style.width = '6px';
|
|
particle.style.height = '6px';
|
|
particle.style.borderRadius = '50%';
|
|
particle.style.pointerEvents = 'none';
|
|
particle.style.zIndex = '1000';
|
|
|
|
// Spell-specific particle colors and animations
|
|
if (type === 'fire') {
|
|
particle.style.background = 'radial-gradient(circle, #ff6b7a, #ff4757)';
|
|
particle.style.animation = 'fireParticle 1.2s ease-out forwards';
|
|
particle.style.boxShadow = '0 0 10px #ff4757';
|
|
} else if (type === 'lightning') {
|
|
particle.style.background = 'radial-gradient(circle, #ffd700, #ffed4e)';
|
|
particle.style.animation = 'lightningParticle 0.8s ease-out forwards';
|
|
particle.style.boxShadow = '0 0 15px #ffd700';
|
|
} else if (type === 'meteor') {
|
|
particle.style.background = 'radial-gradient(circle, #a29bfe, #6c5ce7)';
|
|
particle.style.animation = 'meteorParticle 1.5s ease-out forwards';
|
|
particle.style.boxShadow = '0 0 20px #6c5ce7';
|
|
}
|
|
|
|
document.body.appendChild(particle);
|
|
|
|
setTimeout(() => {
|
|
particle.remove();
|
|
}, 1500);
|
|
}
|
|
}
|
|
|
|
triggerSpellAnimation(type, enemyChar) {
|
|
// Screen effects based on spell type
|
|
if (type === 'meteor') {
|
|
// Meteor causes screen shake
|
|
document.body.classList.add('screen-shake');
|
|
setTimeout(() => document.body.classList.remove('screen-shake'), 500);
|
|
|
|
// Create meteor trail effect
|
|
this.createMeteorTrail();
|
|
} else if (type === 'lightning') {
|
|
// Lightning flash effect
|
|
this.createLightningFlash();
|
|
} else if (type === 'fire') {
|
|
// Fire ripple effect
|
|
this.createFireRipple(enemyChar);
|
|
}
|
|
|
|
// Enemy hit reaction
|
|
enemyChar.style.transform = 'scale(1.1)';
|
|
enemyChar.style.filter = type === 'fire' ? 'hue-rotate(30deg)' :
|
|
type === 'lightning' ? 'brightness(1.5)' :
|
|
'contrast(1.3)';
|
|
|
|
setTimeout(() => {
|
|
enemyChar.style.transform = '';
|
|
enemyChar.style.filter = '';
|
|
}, 300);
|
|
}
|
|
|
|
createMeteorTrail() {
|
|
const trail = document.createElement('div');
|
|
trail.className = 'meteor-trail';
|
|
trail.style.position = 'fixed';
|
|
trail.style.top = '0';
|
|
trail.style.right = '0';
|
|
trail.style.width = '4px';
|
|
trail.style.height = '100vh';
|
|
trail.style.background = 'linear-gradient(180deg, #a29bfe, transparent)';
|
|
trail.style.animation = 'meteorTrail 0.6s ease-out forwards';
|
|
trail.style.pointerEvents = 'none';
|
|
trail.style.zIndex = '999';
|
|
|
|
document.body.appendChild(trail);
|
|
setTimeout(() => trail.remove(), 600);
|
|
}
|
|
|
|
createLightningFlash() {
|
|
const flash = document.createElement('div');
|
|
flash.style.position = 'fixed';
|
|
flash.style.top = '0';
|
|
flash.style.left = '0';
|
|
flash.style.width = '100vw';
|
|
flash.style.height = '100vh';
|
|
flash.style.background = 'rgba(255, 215, 0, 0.3)';
|
|
flash.style.animation = 'lightningFlash 0.2s ease-out';
|
|
flash.style.pointerEvents = 'none';
|
|
flash.style.zIndex = '998';
|
|
|
|
document.body.appendChild(flash);
|
|
setTimeout(() => flash.remove(), 200);
|
|
}
|
|
|
|
createFireRipple(enemyChar) {
|
|
const ripple = document.createElement('div');
|
|
const rect = enemyChar.getBoundingClientRect();
|
|
|
|
ripple.style.position = 'fixed';
|
|
ripple.style.left = rect.left + rect.width/2 - 100 + 'px';
|
|
ripple.style.top = rect.top + rect.height/2 - 100 + 'px';
|
|
ripple.style.width = '200px';
|
|
ripple.style.height = '200px';
|
|
ripple.style.border = '3px solid #ff4757';
|
|
ripple.style.borderRadius = '50%';
|
|
ripple.style.animation = 'fireRipple 0.8s ease-out forwards';
|
|
ripple.style.pointerEvents = 'none';
|
|
ripple.style.zIndex = '997';
|
|
|
|
document.body.appendChild(ripple);
|
|
setTimeout(() => ripple.remove(), 800);
|
|
}
|
|
|
|
showCastingEffect(spellType) {
|
|
const wizardChar = document.querySelector('.wizard-character');
|
|
const rect = wizardChar.getBoundingClientRect();
|
|
|
|
// Create magical circle around wizard
|
|
this.createMagicCircle(rect, spellType);
|
|
|
|
// Add casting sparkles
|
|
this.createCastingSparkles(rect, spellType);
|
|
|
|
// Wizard glow effect
|
|
wizardChar.style.filter = 'drop-shadow(0 0 20px #ffd700)';
|
|
wizardChar.style.transform = 'scale(1.05)';
|
|
|
|
setTimeout(() => {
|
|
wizardChar.style.filter = '';
|
|
wizardChar.style.transform = '';
|
|
}, 600);
|
|
}
|
|
|
|
createMagicCircle(wizardRect, spellType) {
|
|
const circle = document.createElement('div');
|
|
circle.style.position = 'fixed';
|
|
circle.style.left = wizardRect.left + wizardRect.width/2 - 75 + 'px';
|
|
circle.style.top = wizardRect.top + wizardRect.height/2 - 75 + 'px';
|
|
circle.style.width = '150px';
|
|
circle.style.height = '150px';
|
|
circle.style.borderRadius = '50%';
|
|
circle.style.pointerEvents = 'none';
|
|
circle.style.zIndex = '500';
|
|
|
|
// Spell-specific circle colors
|
|
if (spellType === 'fire') {
|
|
circle.style.border = '3px solid #ff4757';
|
|
circle.style.boxShadow = '0 0 30px #ff4757, inset 0 0 30px rgba(255, 71, 87, 0.3)';
|
|
} else if (spellType === 'lightning') {
|
|
circle.style.border = '3px solid #ffd700';
|
|
circle.style.boxShadow = '0 0 30px #ffd700, inset 0 0 30px rgba(255, 215, 0, 0.3)';
|
|
} else if (spellType === 'meteor') {
|
|
circle.style.border = '3px solid #6c5ce7';
|
|
circle.style.boxShadow = '0 0 30px #6c5ce7, inset 0 0 30px rgba(108, 92, 231, 0.3)';
|
|
}
|
|
|
|
circle.style.animation = 'magicCircleForm 0.6s ease-out forwards';
|
|
|
|
document.body.appendChild(circle);
|
|
setTimeout(() => circle.remove(), 600);
|
|
}
|
|
|
|
createCastingSparkles(wizardRect, spellType) {
|
|
const sparkleCount = 8;
|
|
|
|
for (let i = 0; i < sparkleCount; i++) {
|
|
const sparkle = document.createElement('div');
|
|
sparkle.style.position = 'fixed';
|
|
sparkle.style.width = '4px';
|
|
sparkle.style.height = '4px';
|
|
sparkle.style.borderRadius = '50%';
|
|
sparkle.style.pointerEvents = 'none';
|
|
sparkle.style.zIndex = '501';
|
|
|
|
// Position around wizard
|
|
const angle = (i / sparkleCount) * 2 * Math.PI;
|
|
const radius = 60;
|
|
const x = wizardRect.left + wizardRect.width/2 + Math.cos(angle) * radius;
|
|
const y = wizardRect.top + wizardRect.height/2 + Math.sin(angle) * radius;
|
|
|
|
sparkle.style.left = x + 'px';
|
|
sparkle.style.top = y + 'px';
|
|
|
|
// Spell-specific sparkle colors
|
|
if (spellType === 'fire') {
|
|
sparkle.style.background = '#ff4757';
|
|
sparkle.style.boxShadow = '0 0 8px #ff4757';
|
|
} else if (spellType === 'lightning') {
|
|
sparkle.style.background = '#ffd700';
|
|
sparkle.style.boxShadow = '0 0 8px #ffd700';
|
|
} else if (spellType === 'meteor') {
|
|
sparkle.style.background = '#6c5ce7';
|
|
sparkle.style.boxShadow = '0 0 8px #6c5ce7';
|
|
}
|
|
|
|
sparkle.style.animation = 'castingSparkle 0.6s ease-out forwards';
|
|
|
|
document.body.appendChild(sparkle);
|
|
setTimeout(() => sparkle.remove(), 600);
|
|
}
|
|
}
|
|
|
|
showDamageNumber(damage) {
|
|
const damageEl = document.createElement('div');
|
|
damageEl.className = 'damage-number';
|
|
damageEl.textContent = `-${damage}`;
|
|
|
|
const enemyChar = document.querySelector('.enemy-character');
|
|
const rect = enemyChar.getBoundingClientRect();
|
|
|
|
damageEl.style.position = 'fixed';
|
|
damageEl.style.left = rect.left + rect.width/2 + 'px';
|
|
damageEl.style.top = rect.top + 'px';
|
|
|
|
document.body.appendChild(damageEl);
|
|
|
|
setTimeout(() => {
|
|
damageEl.remove();
|
|
}, 1500);
|
|
}
|
|
|
|
showFailEffect(type = 'random') {
|
|
const effects = ['spawnMinion', 'loseHP', 'magicQuirk', 'flyingBirds'];
|
|
const selectedEffect = type === 'random' ?
|
|
effects[Math.floor(Math.random() * effects.length)] :
|
|
type;
|
|
|
|
// Show fail message first
|
|
this.showFailMessage();
|
|
|
|
// Then trigger specific effect
|
|
switch(selectedEffect) {
|
|
case 'spawnMinion':
|
|
this.spawnMiniEnemy();
|
|
break;
|
|
case 'loseHP':
|
|
this.wizardTakesDamage();
|
|
break;
|
|
case 'magicQuirk':
|
|
this.triggerMagicQuirk();
|
|
break;
|
|
case 'flyingBirds':
|
|
this.summonFlyingBirds();
|
|
break;
|
|
case 'noSpell':
|
|
this.showFailMessage('Select a spell first! 🪄');
|
|
break;
|
|
}
|
|
}
|
|
|
|
showFailMessage(customMessage = null) {
|
|
const messages = [
|
|
"Spell backfired! 💥",
|
|
"Magic went wrong! 🌀",
|
|
"Oops! Wrong incantation! 😅",
|
|
"The magic gods are not pleased! ⚡",
|
|
"Your spell turned into chaos! 🎭",
|
|
"Magic malfunction detected! 🔧"
|
|
];
|
|
|
|
const message = customMessage || messages[Math.floor(Math.random() * messages.length)];
|
|
|
|
const failEl = document.createElement('div');
|
|
failEl.className = 'fail-message';
|
|
failEl.textContent = message;
|
|
|
|
document.body.appendChild(failEl);
|
|
|
|
setTimeout(() => {
|
|
failEl.remove();
|
|
}, 2000);
|
|
}
|
|
|
|
spawnMiniEnemy() {
|
|
console.log('🧌 Spawning mini enemy!');
|
|
|
|
const miniEnemy = document.createElement('div');
|
|
miniEnemy.className = 'mini-enemy';
|
|
miniEnemy.textContent = '👺';
|
|
|
|
// Random position around the main enemy
|
|
const mainEnemy = document.querySelector('.enemy-character');
|
|
const rect = mainEnemy.getBoundingClientRect();
|
|
|
|
miniEnemy.style.position = 'fixed';
|
|
miniEnemy.style.left = (rect.left + Math.random() * 200 - 100) + 'px';
|
|
miniEnemy.style.top = (rect.top + Math.random() * 200 - 100) + 'px';
|
|
|
|
document.body.appendChild(miniEnemy);
|
|
|
|
// Mini enemy disappears after 5 seconds
|
|
setTimeout(() => {
|
|
miniEnemy.remove();
|
|
}, 5000);
|
|
|
|
// Make main enemy slightly stronger
|
|
this.enemyHP = Math.min(100, this.enemyHP + 5);
|
|
this.updateEnemyHealth();
|
|
}
|
|
|
|
wizardTakesDamage() {
|
|
console.log('🔥 Wizard takes damage!');
|
|
|
|
this.playerHP = Math.max(0, this.playerHP - 10);
|
|
document.getElementById('player-health').style.width = this.playerHP + '%';
|
|
|
|
// Screen shake effect
|
|
document.body.classList.add('screen-shake');
|
|
setTimeout(() => {
|
|
document.body.classList.remove('screen-shake');
|
|
}, 500);
|
|
|
|
// Show damage on wizard
|
|
const damageEl = document.createElement('div');
|
|
damageEl.className = 'damage-number';
|
|
damageEl.textContent = '-10';
|
|
damageEl.style.color = '#ff4757';
|
|
|
|
const wizardChar = document.querySelector('.wizard-character');
|
|
const rect = wizardChar.getBoundingClientRect();
|
|
|
|
damageEl.style.position = 'fixed';
|
|
damageEl.style.left = rect.left + rect.width/2 + 'px';
|
|
damageEl.style.top = rect.top + 'px';
|
|
|
|
document.body.appendChild(damageEl);
|
|
|
|
setTimeout(() => {
|
|
damageEl.remove();
|
|
}, 1500);
|
|
|
|
// Check if wizard dies
|
|
if (this.playerHP <= 0) {
|
|
setTimeout(() => {
|
|
this.handleDefeat();
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
triggerMagicQuirk() {
|
|
console.log('🌀 Magic quirk activated!');
|
|
|
|
// Create multiple quirks at random positions
|
|
const numQuirks = 2 + Math.floor(Math.random() * 2); // 2-3 quirks
|
|
|
|
for (let i = 0; i < numQuirks; i++) {
|
|
setTimeout(() => {
|
|
const quirk = document.createElement('div');
|
|
quirk.className = 'magic-quirk';
|
|
|
|
// Random position within viewport
|
|
const x = 20 + Math.random() * 60; // 20% to 80% of viewport width
|
|
const y = 20 + Math.random() * 60; // 20% to 80% of viewport height
|
|
|
|
quirk.style.left = x + '%';
|
|
quirk.style.top = y + '%';
|
|
quirk.style.transform = 'translate(-50%, -50%)';
|
|
|
|
document.body.appendChild(quirk);
|
|
|
|
setTimeout(() => {
|
|
quirk.remove();
|
|
}, 2000);
|
|
}, i * 300); // Stagger the quirks
|
|
}
|
|
|
|
// Scramble the word bank for extra chaos
|
|
setTimeout(() => {
|
|
this.updateWordBank();
|
|
}, 1000);
|
|
}
|
|
|
|
summonFlyingBirds() {
|
|
console.log('🐦 Summoning flying birds!');
|
|
|
|
const birds = ['🐦', '🕊️', '🦅', '🦜', '🐧', '🦆', '🦢', '🐓', '🦃', '🦚', '🐤', '🐣', '🐥'];
|
|
const paths = ['bird-path-1', 'bird-path-2', 'bird-path-3', 'bird-path-4', 'bird-path-5'];
|
|
const numBirds = 5 + Math.floor(Math.random() * 3); // 5-7 birds maintenant !
|
|
|
|
for (let i = 0; i < numBirds; i++) {
|
|
setTimeout(() => {
|
|
const bird = document.createElement('div');
|
|
const pathClass = paths[i % paths.length];
|
|
bird.className = `flying-bird ${pathClass}`;
|
|
bird.textContent = birds[Math.floor(Math.random() * birds.length)];
|
|
|
|
document.body.appendChild(bird);
|
|
|
|
console.log(`🐦 Bird ${i+1} spawned with class: ${bird.className}`);
|
|
|
|
setTimeout(() => {
|
|
bird.remove();
|
|
}, 30000);
|
|
|
|
}, i * 500); // Stagger bird appearances
|
|
}
|
|
}
|
|
|
|
updateEnemyHealth() {
|
|
const healthBar = document.getElementById('enemy-health');
|
|
const percentage = (this.enemyHP / 100) * 100;
|
|
healthBar.style.width = percentage + '%';
|
|
}
|
|
|
|
|
|
getRandomAttackTime() {
|
|
// Enemy attacks every 8-15 seconds randomly
|
|
return 8000 + Math.random() * 7000;
|
|
}
|
|
|
|
startEnemyAttackSystem() {
|
|
this.scheduleNextEnemyAttack();
|
|
}
|
|
|
|
scheduleNextEnemyAttack() {
|
|
this.enemyAttackTimer = setTimeout(() => {
|
|
this.executeEnemyAttack();
|
|
this.scheduleNextEnemyAttack(); // Schedule next attack
|
|
}, this.nextEnemyAttack);
|
|
}
|
|
|
|
executeEnemyAttack() {
|
|
console.log('👹 Enemy is attacking!');
|
|
|
|
const enemyChar = document.querySelector('.enemy-character');
|
|
|
|
// Show attack warning
|
|
this.showEnemyAttackWarning();
|
|
|
|
// Enemy charging animation
|
|
enemyChar.classList.add('enemy-charging');
|
|
|
|
// Attack after 2 seconds warning
|
|
setTimeout(() => {
|
|
enemyChar.classList.remove('enemy-charging');
|
|
this.dealEnemyDamage();
|
|
this.showEnemyAttackEffect();
|
|
}, 2000);
|
|
|
|
// Set next attack time
|
|
this.nextEnemyAttack = this.getRandomAttackTime();
|
|
}
|
|
|
|
showEnemyAttackWarning() {
|
|
const enemyChar = document.querySelector('.enemy-character');
|
|
|
|
// Remove existing warning
|
|
const existingWarning = enemyChar.querySelector('.enemy-attack-warning');
|
|
if (existingWarning) {
|
|
existingWarning.remove();
|
|
}
|
|
|
|
const warning = document.createElement('div');
|
|
warning.className = 'enemy-attack-warning';
|
|
warning.textContent = '⚠️ INCOMING ATTACK!';
|
|
|
|
enemyChar.style.position = 'relative';
|
|
enemyChar.appendChild(warning);
|
|
|
|
// Remove warning after attack
|
|
setTimeout(() => {
|
|
warning.remove();
|
|
}, 2000);
|
|
}
|
|
|
|
dealEnemyDamage() {
|
|
const damage = 12 + Math.floor(Math.random() * 8); // 12-20 damage
|
|
|
|
this.playerHP = Math.max(0, this.playerHP - damage);
|
|
document.getElementById('player-health').style.width = this.playerHP + '%';
|
|
|
|
// Screen shake
|
|
document.body.classList.add('screen-shake');
|
|
setTimeout(() => {
|
|
document.body.classList.remove('screen-shake');
|
|
}, 500);
|
|
|
|
// Show damage number on wizard
|
|
const damageEl = document.createElement('div');
|
|
damageEl.className = 'damage-number';
|
|
damageEl.textContent = `-${damage}`;
|
|
damageEl.style.color = '#ff4757';
|
|
|
|
const wizardChar = document.querySelector('.wizard-character');
|
|
const rect = wizardChar.getBoundingClientRect();
|
|
|
|
damageEl.style.position = 'fixed';
|
|
damageEl.style.left = rect.left + rect.width/2 + 'px';
|
|
damageEl.style.top = rect.top + 'px';
|
|
|
|
document.body.appendChild(damageEl);
|
|
|
|
setTimeout(() => {
|
|
damageEl.remove();
|
|
}, 1500);
|
|
|
|
console.log(`💔 Player took ${damage} damage! HP: ${this.playerHP}`);
|
|
|
|
// Check if player dies
|
|
if (this.playerHP <= 0) {
|
|
setTimeout(() => {
|
|
this.handleDefeat();
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
showEnemyAttackEffect() {
|
|
const effect = document.createElement('div');
|
|
effect.className = 'enemy-attack-effect';
|
|
|
|
const wizardChar = document.querySelector('.wizard-character');
|
|
const rect = wizardChar.getBoundingClientRect();
|
|
|
|
effect.style.position = 'fixed';
|
|
effect.style.left = rect.left + rect.width/2 - 75 + 'px';
|
|
effect.style.top = rect.top + rect.height/2 - 75 + 'px';
|
|
|
|
document.body.appendChild(effect);
|
|
|
|
setTimeout(() => {
|
|
effect.remove();
|
|
}, 1000);
|
|
}
|
|
|
|
handleVictory() {
|
|
clearTimeout(this.enemyAttackTimer);
|
|
|
|
const bonusScore = 1000; // Fixed victory bonus
|
|
this.score += bonusScore;
|
|
|
|
this.container.innerHTML += `
|
|
<div class="victory-screen">
|
|
<div class="result-title">🎉 VICTORY! 🎉</div>
|
|
<div style="font-size: 24px; margin-bottom: 10px;">You defeated the Grammar Demon!</div>
|
|
<div style="font-size: 18px; margin-bottom: 10px;">Final Score: ${this.score}</div>
|
|
<div style="font-size: 16px; margin-bottom: 20px;">Time Bonus: +${bonusScore}</div>
|
|
<button onclick="location.reload()" style="padding: 15px 30px; font-size: 18px; background: #2ed573; color: white; border: none; border-radius: 10px; cursor: pointer;">Play Again</button>
|
|
</div>
|
|
`;
|
|
|
|
this.onGameEnd(this.score);
|
|
}
|
|
|
|
handleDefeat() {
|
|
clearTimeout(this.enemyAttackTimer);
|
|
|
|
this.container.innerHTML += `
|
|
<div class="defeat-screen">
|
|
<div class="result-title">💀 DEFEATED 💀</div>
|
|
<div style="font-size: 24px; margin-bottom: 10px;">The Grammar Demon proved too strong!</div>
|
|
<div style="font-size: 18px; margin-bottom: 20px;">Final Score: ${this.score}</div>
|
|
<button onclick="location.reload()" style="padding: 15px 30px; font-size: 18px; background: #ff4757; color: white; border: none; border-radius: 10px; cursor: pointer;">Try Again</button>
|
|
</div>
|
|
`;
|
|
|
|
this.onGameEnd(this.score);
|
|
}
|
|
|
|
start() {
|
|
// Game starts immediately when initialized
|
|
}
|
|
|
|
destroy() {
|
|
if (this.enemyAttackTimer) {
|
|
clearTimeout(this.enemyAttackTimer);
|
|
}
|
|
|
|
const styleSheet = document.getElementById('wizard-spell-caster-styles');
|
|
if (styleSheet) {
|
|
styleSheet.remove();
|
|
}
|
|
}
|
|
|
|
restart() {
|
|
this.destroy();
|
|
this.score = 0;
|
|
this.enemyHP = 100;
|
|
this.playerHP = 100;
|
|
this.spellStartTime = Date.now();
|
|
this.averageSpellTime = 0;
|
|
this.spellCount = 0;
|
|
this.nextEnemyAttack = this.getRandomAttackTime();
|
|
this.init();
|
|
}
|
|
}
|
|
|
|
// Register the game module
|
|
window.GameModules = window.GameModules || {};
|
|
window.GameModules.WizardSpellCaster = WizardSpellCaster; |