📊 Code Overview

ArcLycée is a 2D RPG written in vanilla JavaScript (ES Modules) without frameworks or bundlers. All rendering is done with HTML5 Canvas, sound effects are generated by code with Web Audio API, background music uses MP3 files with crossfade, and all sprites are drawn procedurally.

MetricValue
JavaScript files53
Total lines of code~37,500
Canvas resolution960 × 540 px (16:9)
Target FPS60
Supported languages3 (Español, Français, English)
Translation keys~1,100+
Procedural sound effects60 methods
Playable worlds12 + world map
Mini-games4 (batú, calibration, programming, wires)

Directory Structure

DirectoryContentsFiles
js/motor/Game loop, input, rendering, sound, save, configuration6
js/escenas/Main menu, character selection, intro, final cutscene4
js/mundos/12 playable levels: caves, settlements, mountain, colonial, aquatic, legal, laboratory, LFSD, lake12
js/mecanicas/Combat, dialogue, inventory, photo album, 4 mini-games, quests, reputation, log11
js/personajes/Player (Pepito/Pepita) + 3 companions (Magnoboot, Viralata, Cemí)4
js/idiomas/ES/FR/EN translations + language manager4
js/mapas/Leaflet map with real data layers (Taíno, colonial, shipwrecks, museums)4
js/clima/Weather system + hurricanes2

Technologies

TechnologyUsage
HTML5 CanvasGame rendering — everything drawn by code, no external sprites
JavaScript ES ModulesModular architecture without frameworks or bundlers
Web Audio API60 procedural sound effects + background music with 24 MP3s
LeafletJS + Stadia MapsInteractive maps with 76 real archaeological sites from DR and Haiti
Kaplay.jsTouch input (analog joystick and D-pad)
CSS3Loading screen, responsive touch controls

Design Principles

🔄 Game Loop and Scene Management

File: js/motor/juego.js (1,659 lines) — the main game orchestrator.

Main Loop

The game runs at ~60 FPS using requestAnimationFrame. Each frame computes delta time and runs two phases: update (logic) and draw (rendering).

Delta time is capped at 0.1 seconds to prevent player "teleportation" when returning from another tab:

dt = Math.min((currentTime - previousTime) / 1000, 0.1)

Scene System

The game has 15 registered scenes. Only one scene is active at a time.

TypeScenes
InterfaceMain menu, character selection, intro cutscene, final cutscene
Playable worlds (9)Pomier Caves, Settlement I, Settlement II, La Isabela, Colonial Zone, Aquatic World, Airport, Laboratory, LFSD
NavigationWorld map (tile map)
SecretLevel selector (Konami Code)

Scene Lifecycle

Each scene implements three methods:

Scene changes use juego.cambiarEscena('name'), which calls salir() on the previous scene, creates the player if the new scene is playable, and calls iniciar(juego) on the new one. Returning to the world map triggers auto-save.

Overlay System

Overlays are drawn on top of the active scene, in this order (bottom to top):

  1. Base scene
  2. Weather effects (rain, storms)
  3. Combat — enemy encounter
  4. Batú — Taíno ball game
  5. LFSD mini-games — calibration, programming, wires
  6. Game Log — quest tracker (L key)
  7. Reputation meter — HUD bar
  8. Photo album — gallery (P key)
  9. Photo/selfie hint (T/G keys)
  10. Inventory — backpack (I key)
  11. Toasts — floating notifications (max 4 stacked)
  12. Secret selector — always on top

Toast System

juego.mostrarToast(text, duration) adds a floating message. They stack vertically (max 4), with 0.3s fade-in and fade-out in the last second. Used for item pickups, quest completion, danger alerts, etc.

Death System

When the player reaches 0 health: comic "wah wah waaah" sound, random death message (4s), 2.5s pause, then respawn at the world map with 30% health restored. A _muerteEnCurso flag prevents triggering death multiple times.

🎮 Input System

File: js/motor/entrada.js (494 lines) — unified abstraction layer for keyboard and touch.

Keyboard Controls

ActionKeysUsage
MoveWASD / ArrowsExploration and menus
JumpSpaceCaves only (platformer)
InteractE / EnterTalk to NPCs, use objects
InventoryIOpen/close backpack
SpecialFMetal detect (Magnoboot)
Return to mapMExit current level
Real mapROpen Leaflet reference map
LogLGame Log (quests)
PhotoTTake photo of nearby entity
SelfieGTake selfie with entity
AlbumPView photo gallery
Zoom+ / -Zoom on world map
Konami Code↑↑↓↓←→←→Secret level selector

Unified API

All input queries go through entrada.estaPresionada(action), which combines keyboard and touch. Internally uses two Map objects: teclasPresionadas for keyboard and accionesTocadas for touch.

Touch Controls

Two modes switchable from Options menu (preference saved in localStorage):

5 action buttons on the right: A (action), B (cancel), X (inventory), Y (special), JUMP. Use touchstart/touchend instead of click to avoid the 300ms mobile delay.

Input Blocking Pattern

To prevent a key from "repeating" when held, a bloqueoEntrada flag is used: activated on action processing and deactivated only when the key is released. Initialized to true when entering scenes where the activation key might be pressed.

Sticky Key Prevention

The window.blur event clears all pressed keys. This prevents the player from continuing to move when returning from another tab with a movement key pressed.

🖼️ Rendering and Visual Effects

File: js/motor/renderizado.js (199 lines) — Canvas 2D wrapper.

Canvas Configuration

Fixed resolution of 960×540 (half of 1080p, easy to upscale). Image smoothing is disabled (imageSmoothingEnabled = false) to preserve pixel art crispness.

Main Operations

MethodDescription
limpiar()Clears previous frame with clearRect
dibujarRectangulo()Filled rectangle (collisions, placeholders)
dibujarTexto()Text with configurable font, color and alignment
dibujarBarra()Progress bar with clamped percentage [0, 1] and black border

Cave Spotlight Effect

aplicarFiltroCueva(playerX, playerY, lightRadius) creates a torch effect using a radial gradient with transparent center and opaque edges:

Shake Effect

Aquatic worlds use a _sacudida timer that, when activated (by speedboat collision, shark bite or jellyfish), applies a horizontal oscillation to the player sprite:

ctx.translate(Math.sin(totalTime * 50) * amplitude * intensity, 0)
// intensity = _sacudida / initialDuration (decays linearly)

Depth Sorting

In aquatic worlds, entities (NPCs, turtles, jellyfish, player) are sorted by their Y coordinate before drawing, creating a natural depth effect where closer objects overlap further ones.

🔊 Procedural Audio System

File: js/motor/sonido-procedural.js (1,931 lines) — 60 sound effects generated by code.

Sound effects are synthesized in real time by combining oscillators (sine, square, sawtooth, triangle), filters (lowpass, highpass, bandpass), white noise buffers and gain envelopes. Background music is played from MP3 files with automatic crossfade.

Fundamental Patterns

Lazy Initialization

Browsers block Web Audio until the first user interaction. _asegurarContexto() creates the AudioContext only when first needed.

Frequency Sweep

Most sounds use a frequency sweep (exponential or linear ramp) to create a sense of motion:

osc.frequency.setValueAtTime(startFreq, now)
osc.frequency.exponentialRampToValueAtTime(endFreq, now + duration)

Amplitude Envelope (Simplified ADSR)

Controls volume over time: fast attack → sustain → exponential decay to silence.

White Noise

Generated by filling a buffer with random values (-1 to 1), then filtered to create different textures: footsteps (lowpass 800 Hz), sparks (highpass 2000 Hz), crowd (bandpass 600 Hz).

LFO (Low-Frequency Oscillator)

A low-frequency oscillator (4-30 Hz) modulates another parameter (usually the main frequency) to create vibrato, wobble or sirens:

lfo.frequency.value = 4     // 4 Hz wobble
lfoGain.gain.value = 8      // ±8 Hz modulation depth
lfo → lfoGain → osc.frequency

Sound Catalog (60 methods)

Locomotion & Feedback (7)

MethodTechniqueDescription
salto()Square 150→400 HzPlayer jump
aterrizar()Sine 100→40 HzLanding
recoger()Two square notes (523, 659 Hz)Item pickup
paso()White noise 40ms, lowpass 800 HzFootstep
goteo()Sine 800→240 Hz, randomizedWater droplet
dano()Sawtooth 200→50 HzGeneric damage
muerte()Three descending sawtooth notes with LFOComic "wah wah waaah"

Combat (17)

MethodDescription
combateAtacar()Aggressive sawtooth 400→100 Hz
combateHablar()Two simultaneous sines (300, 350 Hz), peaceful
combateNegociar()Two alternating square notes with pause
combateRedesSociales()Two sine "ding-ding" (phone notification)
combateProtestas()Three square pulses (protest chant rhythm)
combateDenuncia()Noise + sine 150→60 Hz (official stamp)
combateViaLegal()Triangle 180→90 Hz (judicial gavel)
combateAtrapar()Noise bandpass 1200 Hz (fishing net)
combatePescar()Sine 200→800 Hz + noise lowpass (harpoon + splash)
combateProtegerCoral()Warm three-note major chord
combateAlertarBuzos()Square 700 Hz with 8 Hz LFO (underwater whistle)
combateLey318()Triangle 200→100 Hz (judicial strike)
combateEvidenciaForense()Two sine beeps (camera/scanner)
combateInterpol()Square 600→800→600 Hz (police siren chirp)
combateUnesco()Solemn major chord (diplomatic authority)
combateVictoria()Ascending triumphant arpeggio
combatePacificado()Sustained major chord 0.5s (peace achieved)

Batú (5)

MethodDescription
batuGolpe()Triangle 120→60 Hz (ball-on-stone impact)
batuRebote()Sine 300→150 Hz (rubber bounce)
batuPunto()Three-note ascending arpeggio (scoring)
batuSaque()Sine 600→800 Hz (serve whistle)
batuMultitud()White noise bandpass 600 Hz (crowd roar)

Aquatic Ambience (4)

MethodDescription
cantoBallenaCerca()Three long sine notes with individual LFO, lowpass 600 Hz (humpback whale vocalization)
manatiLlamada()Sine 80→120→90→110 Hz wobble (manatee undulant call)
lanchaImpacto()3 layers: triangle impact 200→40 Hz + noise splash + synthetic Wilhelm scream (sawtooth with 30 Hz LFO)
mordidaTiburon()3 layers: jaw crunch (highpass noise 3000 Hz) + deep impact + growl with 12 Hz LFO

LFSD Mini-games (15)

MethodDescription
calibracionTono(freq)Sine at given frequency, 0.2s
calibracionExito()Ascending major arpeggio (4 notes)
calibracionFallo()Two descending sawtooth pulses
calibracionAjuste()Highpass noise 2000 Hz, 20ms (dial click)
bloqueColocar()Square 800→400 Hz + noise (block snapped)
bloqueQuitar()Sine 300→600 Hz ascending (block removed)
robotMoverse()Modulated sawtooth 120→180→120 Hz (servo motor)
robotExito()Triumphant 4-note square arpeggio
robotFallo()Two descending square beeps
cableConectar()Sine 1200→600 Hz + contact noise
cableError()Sawtooth + highpass noise 3000 Hz (electrical spark)
circuitoCompleto()Exponential sine + triangle (circuit power-up)
robotFLLIniciar()Continuous sawtooth motor whir (FLL chain-track robot start)
robotFLLDetener()Stops the motor with smooth fade-out (FLL robot stop)
robotFLLClick()Short mechanical click (currently unused)

Ambient & UI (4)

MethodDescription
dialogo()Quick square "bleep" 380±40 Hz (Undertale-style text character)
fotoCaptura()Noise 0.08s + square 1200→300 Hz (shutter + mechanical click)
selfieCaptura()Noise 0.06s + sine 400→1200 Hz ascending (shutter + flash)
lluviaAmbiente()Continuous loop: filtered noise + periodic drops every 150-350ms

Background Music (js/motor/musica.js)

The SistemaMusica class manages the game's background music using MP3 files. Unlike the sound effects (procedural), the music uses 24 real audio files organized into 12 thematic groups (2 tracks per group).

Music Groups (12 groups, 24 MP3s)

GroupScene(s)Description
menumenuPrincipalMain menu music
cuevascuevasPomierCave ambience
tainoTaíno worlds (settlements)Indigenous music
colonialColonial worldsColonial era
acuaticoAquatic WorldUnderwater ambience
aeropuertoLegal WorldPunta Cana Airport
museoLaboratory WorldMuseum interior
lfsdLFSDRobotics classroom
mapamapaPrincipalWorld map
creditosfinalCinematicaFinal cutscene and credits

A-B Crossfade

Each group has 2 tracks (A and B). When one track ends, the system crossfades over 2 seconds to the other track, creating a continuous loop with no silence (A → B → A → B...). Both tracks use HTMLAudioElement and volume is linearly interpolated during the transition.

Combat and Batú Override

When combat or a batú match starts, the background music is temporarily paused. When the overlay ends, the scene's music is automatically restored.

Volume Control

Music volume (0-100%) is configured from the Options menu. The preference is saved to localStorage under the key arclycee_volumen_musica and restored when the game starts.

Browser Autoplay

Browsers block automatic audio playback until the first user interaction. If the initial playback attempt fails, the system automatically retries when the first keypress is detected in the main menu.

⚔️ Combat System

File: js/mecanicas/combate.js (932 lines) — Undertale-style turn-based combat with pacifist route.

Concept

Combat in ArcLycée is not traditional mutual damage. The goal is to convince the enemy through diplomatic actions. Two emotional meters:

Turn Flow

  1. Player's turn: choose from options (←→ to navigate, E to confirm)
  2. Feedback: golden message with the action result (1.8-2.5s)
  3. Enemy's turn: enemy responds based on the player's last action
  4. Repeat until patience = 100 (pacifist victory) or enemy HP = 0 (force victory)

Custom Enemy Options

Each enemy can have opcionesPersonalizadas — an array of unique actions with random patience/hostility ranges and specific counter-responses:

EnemyOptionsLabel
Constructor MéndezSocial media, Protests, Report, Legal route"Convinced:"
LionfishCapture, Fish, Protect coral, Alert divers"Controlled:"
Rodrigo TorresLaw 318, Forensic evidence, INTERPOL, UNESCO 1970"Evidence:"

Enemy Sprites

4 sprite types drawn procedurally: _dibujarSoldado(), _dibujarConstructor(), _dibujarPezLeon(), _dibujarTraficante(). Each with unique design and animation.

Companion Attack Sub-menu

When the player chooses to attack and has active companions, a sub-menu appears asking "Who do you attack with?" showing: Solo / Magnoboot +3 / Viralata +2 / Cemí +4. Navigate with arrows (up/down), E to confirm, Q to cancel and return to the main combat menu.

Help Panel (H key)

A toggleable overlay activated with the H key that explains combat objectives, the differences between the pacifist and aggressive paths, the meaning of the meters (patience, hostility, health), and the reputation rewards depending on the type of victory.

Status Indicator

Dynamic text showing who is winning the combat. Changes based on patience and hostility values: "Almost convinced", "Very hostile", "Making progress", "Tense situation".

[E] Continue

Both player action messages and enemy counter-attack messages are displayed for 2.5 seconds. Afterwards, a pulsing [E] Continue button appears that the player must press to advance to the next turn.

Hidden Enemy HP Bar

For enemies that have opcionesPersonalizadas, the enemy HP bar is hidden. These enemies are defeated by conviction (patience = 100), not by HP damage.

Reputation Integration

Pacifist victory = +15 reputation. Force victory = +5. Additionally, the player's current reputation gives an initial advantage: initialPatience = min(reputation / 2, 25).

💬 Dialogue System

File: js/mecanicas/dialogo.js (326 lines) — dialogue engine with typewriter effect.

Typewriter Effect

Characters appear one by one. A _caracteresVisibles counter increments each frame, and the displayed text is text.substring(0, Math.floor(characters)). Each character triggers a dialogue "bleep" (procedural sound).

Response Options

Dialogue lines can include opciones: [{texto, valor}]. When options are present, advancement is blocked until the player selects one (↑↓ to navigate, E to confirm).

Chaining Callback

alTerminar allows chaining events after a dialogue ends: start combat, give an item, discover a quest, etc.

Rotative Dialogue

Mentor NPCs (Roberto Cassá, Lcda. Carmen Vidal) use a counter that increments after each conversation. The index is calculated as counter % array.length to cycle infinitely through topics.

🤝 Companion System

Directory: js/personajes/companeros/ — three companions that follow the player across scenes.

Common Pattern

All share: tipo property (string), activo flag, activar()/desactivar() methods, and linear interpolation (lerp) movement for smooth following instead of teleportation.

The Three Companions

NameTypeAbilityLerp FactorRelative Position
MagnobootExcavator robotMetal detector (F key), excavation (costs 10 energy)0.05Right of player (+30, +5)
ViralataTracker dogSmell (50 px radius), tail wags when objects detected0.06Left of player (-25, +10)
Cemí MurciélagoCave spirit6 power levels unlocked with botijas (echo-location → cemí vision)0.08Above-left (-20, -25) with sinusoidal floating

Persistence

Companions persist across scenes via juego.companeros[]. In saves they are serialized as {tipo, activo} and reconstructed with _crearCompaneroBasico(tipo, activo).

🎒 Inventory System

File: js/mecanicas/inventario.js (742 lines) — visual backpack with 20-slot grid.

Design

5 columns × 4 rows grid. Each slot is 52×52 px with 4 px margin. The panel is centered on the canvas. Navigation with arrows, use item with E (if esUsable).

Healing Items

ItemHealingSource
Guanábana+30 HPGuarionex (Settlement II)
Healing vessel+35 HPAnacaona (Settlement I)
Casabe+25 HPVarious

Procedural Icons

Over 24 unique icons drawn with Canvas 2D: torch with light rays, golden cemí with eyes, compass with red/white needles, colonial arquebus, coin with crown, etc. _dibujarIconoObjeto(ctx, id, x, y, size) dispatches to the correct icon by ID.

📸 Photo Album

File: js/mecanicas/album-fotos.js (1,140 lines) — capture system with dedicated per-entity-type rendering.

Capture Types

Per-Entity-Type Rendering

The tipoEntidad field determines which renderer to use. Scenes expose a fotografiables[] array for additional entities beyond NPCs:

tipoEntidadRendererDetail
'npc'_renderizarNPCCentrado()Draws NPC on auxiliary 200×200 canvas, detects bounding box by pixels, crops and centers
'tortuga'_renderizarTortuga()4 dedicated sprites: hawksbill (olive with amber stripes), leatherback (blue-black with 7 ridges), loggerhead (brown-red with heart mark), green (olive with spots)
'coral'_renderizarCoral()4 sprites: brain (hemisphere with meandering grooves), staghorn (5 bifurcated branches), fan (purple-pink with radial veins), table (elliptical platform on trunk)
'petroglifo'_renderizarPetroglifo()4 types: sun, bat, face, spiral
'objeto'_renderizarObjeto()Generic collectible with glow

Detection Range

Photo target detection uses Euclidean distance with an 80 px radius (RANGO_FOTO). Searches NPCs first, then objects, then fotografiables[].

🗺️ Tile Map (Hispaniola)

File: js/mundos/mapa-tiles.js (784 lines) — procedural terrain generation for the island.

The Bitmap

Hispaniola's coastline is defined with ISLA_BITMAP: an array of 68 strings of 128 characters each. Each character is '1' (land) or '0' (water). Manually traced from a NASA reference image, scaled 2× from a 64×34 original bitmap.

Terrain Generation Pipeline

  1. Initialize all tiles as DEEP_WATER
  2. Mark land tiles from ISLA_BITMAP
  3. Add shallow water adjacent to coast (1-tile border)
  4. Paint beaches (land adjacent to water)
  5. Apply mountain ranges using point-to-segment distance (8 cordilleras)
  6. Add interior lakes (Enriquillo, Saumâtre) via ellipse collision
  7. Generate forests with pseudo-random hash (40% probability per tile)
  8. Draw 5 rivers with Bresenham algorithm (Yaque del Norte/Sur, Yuna, Ozama, Artibonite)

8 Terrain Types

TypeWalkableDecoration
Deep waterNoAnimated wave curves
Shallow waterYesFoam
SandYesGrain texture
PrairieYes
ForestYesTree crowns (circles)
MountainNoTriangles with rocky peaks
RiverYesAnimated current lines
PathYes

Bresenham Algorithm for Rivers

Rivers are traced connecting waypoints with Bresenham's line algorithm. Only tiles that are already land are painted (water tiles are skipped), creating rivers that flow naturally along the terrain.

Mountain Ranges by Distance

8 mountain ranges defined as waypoint sequences. For each land tile, the minimum distance to each range segment is calculated. If below 3.6 tiles, it becomes a mountain (non-walkable).

Viewport Culling

dibujarTilesVisibles() only renders tiles within the camera viewport, calculating start/end column and row. This is crucial for performance given the map is 128×68 = 8,704 tiles total.

🧭 World Map

File: js/mundos/mapa-principal.js (809 lines) — navigation between level nodes.

Camera System

Level Nodes

13 nodes placed at verified walkable tile coordinates. Each has: ID, position, type (cave, village, city, shipwreck, etc.), destination scene, and previous node in the progression chain.

Visualization: circle (18-22 px) + border + emoji icon + name. Colors indicate state: gray (locked), gold (active), green (completed). A pulsing golden [E] indicator appears when the player is nearby.

Mini-Map

Top-right corner (120×75 px). Shows simplified tiles (step 2 for performance), nodes colored by state, player position and camera viewport.

Collision

_puedeMoverse() tests the sprite's 4 corners (with 4 px margin) against the tile map's esCaminable().

🌊 Aquatic Mechanics

Files: js/mundos/acuatico/mundo-acuatico.js (2,174 lines) + santuario-manati.js (2,905 lines).

Underwater Movement

ConditionSpeed Factor
Normal (underwater)× 0.7
With jellyfish effect× 0.4
With shark effect× 0.4 (2s duration)

Passive Jellyfish

4 jellyfish with sinusoidal movement between two waypoints: t = (sin(phase + dt*0.8) + 1) / 2 → lerp between pointA and pointB. Contact (radius 15-20 px) = 5 HP damage + 2.5s slowness + 2s invulnerability.

Marine Life

Oxygen System (Manatee Sanctuary)

The player swims on lung power with a snorkel. O₂ bar from 100 to 0:

Sharks

3 sharks patrol between waypoints with phase offset (0, π/2, π). Bite: -8 HP + 2s slowness + shake effect (0.5s) + mordidaTiburon() sound.

Speedboats

Spawn every 5-15s in the surface zone. Collision: -10 HP + shake (0.6s) + synthetic Wilhelm scream (lanchaImpacto()). The propeller zone causes periodic damage (-2 HP with cooldown). Warning toast shown once per zone entry.

Ecological Actions

⛈️ Weather System

Files: js/clima/clima.js (442 lines) + huracan.js (324 lines).

6 Weather States

StateProbabilityIntensityPlayer Effect
Sunny45%0.1None
Cloudy25%0.3None
Rain20%0.6Speed × (1 − 0.15×i)
Storm7%0.8Speed × (1 − 0.25×i) + thunder
Hurricane2%1.0Wind push 2px×i + speed × (1 − 0.35×i)
Earthquake1%2% per frame stumble chance

Particles

Up to 500 recycled particles. Spawn rate: 3-8 particles/frame based on intensity. Each particle has gravity + wind push.

Hurricanes (Advanced)

Categories 1-5 with wind speed = category × 2 px/frame. Three phases: onset (0-25%, intensity ramps up) → peak (25-75%, max intensity) → end (75-100%, intensity drops). Debris physics with up to 500 objects bouncing off terrain. Direction: typically from the east (realistic for the Caribbean).

Ambient Sound

Continuous rain (lluviaAmbiente()) with dynamic intensity during transitions. Random thunder every 4-15s during storms.

🎲 Mini-Games

LFSD World — Robotics Class

File: js/mundos/lfsd/mundo-lfsd.js — interior level of the Lycée Français de Santo Domingo robotics classroom.

Prof. Nicolas Droulers (white hair and white beard) guides the player. The classroom contains work tables with Scratch programming screens, a whiteboard, a 3D printer with a translated label matching the active language, and a FIRST LEGO League table with black line-following paths where an animated LEGO robot with chain tracks follows the paths with motor sound (robotFLLIniciar/robotFLLDetener). 9 student NPCs (3 quest-givers with colored T-shirts) and 4 workstations.


Batú — Taíno Ball Game

File: js/mecanicas/batu.js (935 lines) — 2D physics with adaptive AI.

Physics

ParameterValue
Gravity480 px/s²
Bounce coefficient (ground)0.8
Bounce coefficient (walls)0.9
Air friction0.998 per frame
Ball radius8 px
Win conditionFirst to 3 points

Hit Types (by contact height)

ZoneBody PartSpeedAngle
< 25%Head290 px/s-70° (nearly vertical)
25-50%Shoulder425 px/s-60°
50-75%Hip/yugo500 px/s (max)-45° (strongest)
> 75%Knee280 px/s-30° (defensive)

Each hit has ±25% speed variation and ±8° angle variation for unpredictability.

AI Opponent

Educational Data

Between each point, a historical fact about batú is displayed: ball material (cupey tree latex), batey petroglyphs, the ceremonial yugo, areíto festivals, peaceful conflict resolution, and real archaeological sites (Chacuey, La Aleta).

Phases

intro → serve → playing → point → educational → serve → ... → gameEnd


Signal Calibration ("Good Vibrations")

File: js/mecanicas/calibracion-senal.js (330 lines) — oscilloscope puzzle.

Mechanic

3 dials (frequency, amplitude, phase) controlling a sine wave displayed on an oscilloscope (green on black). The player must match the target wave within ±15% tolerance.

Waveform Formula

y = centerY + sin(t + phase) × amplitude
where t = (x − 40) / (width − 80) × π × 2 × frequency

Controls

↑↓ to switch dials, ←→ to adjust (±2), E to confirm. Time limit: 45 seconds.

Reward: calibrated magnetometer (progreso.magnetometroCalibrado = true), +15 reputation.


Robot Programming ("Full Metal Archeologist")

File: js/mecanicas/programacion-bloques.js (491 lines) — block-based navigation.

Mechanic

Arrange 5-6 instruction blocks (advance, turn left/right, scan, dive, ascend) to guide a submarine robot through a 6×4 grid. Robot starts at (0,1) facing right, target at (5,2).

Execution

Max 12 blocks. Execution interval: 0.6s between steps. Failure: robot exits grid or hits obstacle (if not submerged). Programming phase: 60s. Execution: automatic, no time limit.

Reward: archaeological sites layer on Leaflet map (progreso.robotProgramado = true), +15 reputation.


Wire Connection ("Weird Science")

File: js/mecanicas/conexion-cables.js (408 lines) — matching puzzle on a circuit board.

Mechanic

Connect 6 inputs (left) to 6 outputs (right) by color and symbol. Outputs in random order. Connections drawn as cubic Bézier curves. Wrong connection = spark animation.

Controls

↑↓ to select input/output, E to connect. Time limit: 30 seconds.

Reward: +15 reputation (bonus +5 if completed in <15s), newspaper article in inventory.

Reputation and Side Quests

Reputation System

File: js/mecanicas/reputacion.js (74 lines) — score from 0-100 with 4 levels.

RangeLevelBar Color
0-24UnknownRed
25-49KnownYellow
50-74RespectedGreen
75-100LegendaryGold

Reputation affects combat (initial patience bonus) and is shown in the final epilogue. Gained through pacifist victories (+15), force victories (+5), mini-games (+15 each) and ecological actions (+10).

5 Side Quests

File: js/mecanicas/misiones-secundarias.js (89 lines) — state machine per quest.

QuestDescriptionTrigger
BatúPlay Taíno ball game with HigüemotaTalk to Higüemota in Settlement II
Manatee RescueFree manatee + clean reefTalk to Dra. Sofía in Sanctuary
Good VibrationsCalibrate the magnetometerTalk to NPC in La Isabela
Full Metal ArcheologistProgram submarine robotTalk to biologist in Sanctuary
Weird ScienceRepair analysis equipmentTalk to mentor in Laboratory

Each quest progresses: undiscovered → discovered → in_progress → completed. The Game Log (L key) shows all quest states in two tabs: Main Story (13 nodes with ✅/🔓/🔒) and Side Quests (with checkmarks).

💾 Save System

File: js/motor/guardado.js (243 lines) — localStorage persistence.

Auto-save

The game auto-saves when returning to the world map. "Continue Game" in the main menu restores all state and goes to the map.

Serialized Data

Companion Reconstruction

Companions are saved as {tipo: 'magnoboot', activo: true} and reconstructed as real instances via _crearCompaneroBasico(tipo, activo) using the Magnoboot, Viralata and CemiMurcielago imports.

🌐 Reference Map (LeafletJS)

Directory: js/mapas/ (4 files, ~700 lines) — real-world geographic map with archaeological sites.

Configuration

Center: 19.0°N, -70.5°W (Dominican Republic). Default zoom: 8. Opens with R key from any playable scene (not just the world map). Stadia Maps tiles (6 aesthetic layers: Watercolor, Terrain, Toner, Dark, Smooth, OSM Bright) with CARTO Voyager fallback.

Data Layers (76 real sites)

LayerIconCount
Taíno sites🗿16 (settlements, caves, petroglyphs in DR and Haiti)
Colonial sites🏰8 (La Isabela, Colonial Zone, Cap-Haïtien, etc.)
Shipwrecks12 (Santa María 1492, San Miguel 1551, Concepción 1641, etc.)
Museums🏛30 (DR + Haiti)
Unexplored sites🔍8 (only visible after completing robot mini-game)

Interaction

Markers with popup (name + description) on click. Click-to-travel: window._viajarANodo(id) for animated fly and scene change. Visual state: completed (green border + brightness), locked (grayscale + opacity 0.4), active (white border).

🎬 Multiple Endings

File: js/escenas/final-cinematica.js (673 lines) — 5 endings + cinematic credits.

Ending Types

EndingConditionTitle
Complete8 nodes + 7 sidequests + more pacified than violent"Legend of Quisqueya"
Pacifist8 nodes + more pacified, no violent"Heritage Guardian"
Ecological3+ ecological actions + no violence"Marine Ecosystem Protector"
DarkMore violent combats than pacified"Heritage deserves another path"
MuseumDefault fallback"Curator of History"

Sequence

5 text steps with auto-advance (5-6s each), skip with E. Themed backgrounds per ending: golden gradients (complete/pacifist), marine blue with bubbles (ecological), red with fractures (dark), warm with light spot (museum). Reputation level shown in epilogue.

Credits

Automatic vertical scroll at 40 px/s. Lists the 8 creators (les fous du robot), the Liceo Francés de Santo Domingo and the year 2026. E to speed up/skip.

🌍 Internationalization

Directory: js/idiomas/ (4 files, ~4,400 lines).

Structure

3 translation files (es.js, en.js, fr.js) with ~1,100+ keys each, organized as nested JavaScript objects. A manager idiomas.js (73 lines) provides:

Main Sections

SectionContent
menuTitles, main menu options
seleccionCharacter selection
interfazHUD (health, oxygen, inventory), death phrases (10 variants)
dialogosAll dialogues by world (cave, village, isabela, colonial, aquatic, legal, laboratory, LFSD, endings)
combateCombat actions and messages
batu25 mini-game keys (rules, facts, messages)
objetos32+ items with name and description
inventarioInventory UI
climaWeather state names
guardadoSave system strings

Code Access

// In scenes and mechanics:
const texts = juego.idiomas.traducciones[juego.idiomas.idiomaActual];
const name = texts.menu.titulo;

// Or using the dot-notation helper:
const text = juego.idiomas.t('dialogos.cueva.petroSol');

🧑 Player (Pepito/Pepita)

File: js/personajes/pepito.js (258 lines).

Two Movement Modes

Main Properties

PropertyValue
Dimensions28×32 px (narrower than 32 px tile for easier passage)
Max health100 HP
Gender'pepito' (male) or 'pepita' (female)

Walking Animation

Legs animate with Math.sin(cuadroAnimacion * 5) * 3 — a sinusoidal movement simulating steps. Arms with Math.sin(cuadroAnimacion * 2) * 1. Animation only advances when esAnimando = true.

Character Selection

js/escenas/seleccion-personaje.js (291 lines): the game sprite is shown scaled ×2.5 with live walking animation. Pepito is blue (#4488ff) with spiky hair; Pepita is purple (#aa44ff) with braids.

⚙️ Configuration and Constants

File: js/motor/configuracion.js (95 lines) — central configuration point.

ConstantValueUsage
ANCHO_JUEGO960Canvas width in pixels
ALTO_JUEGO540Canvas height (16:9)
TAMANO_TILE32Standard tile size
GRAVEDAD0.6Vertical acceleration in platformer
VELOCIDAD_JUGADOR3Pixels per frame
FPS_OBJETIVO60Frames per second
ESCALA_MINIMA0.5Minimum canvas scale
ESCALA_MAXIMA3.0Maximum canvas scale
CLAVE_GUARDADO'arclycee_guardado'localStorage key

Sprite Dimensions

TypeWidth × Height
Player32 × 32
Companion24 × 24
Cemí16 × 16
NPC32 × 32
Boss64 × 64
Large Boss96 × 96
Item16 × 16
Petroglyph48 × 48

📁 Top Files by Size

#FileLinesResponsibility
1santuario-manati.js2,905Sanctuary: oxygen, sharks, speedboats, ecology
2mundo-acuatico.js2,174Shipwreck: jellyfish, turtles, whales, combat
3sonido-procedural.js1,93160 code-generated audio effects
4zona-colonial.js1,923Colonial Zone: NPCs, combat, activism
5juego.js1,659Game loop, scenes, overlays, toasts
6es.js1,523Spanish translations
7fr.js1,472French translations
8en.js1,408English translations
9cuevas-pomier.js1,340Cave platformer with spotlight
10mundo-juridico.js1,335Airport: legal combat, INTERPOL