Skip to content

Commit a14c8a6

Browse files
author
web3blind
committed
Build year-sized magical forecast pool
1 parent c0fd12e commit a14c8a6

8 files changed

Lines changed: 165 additions & 17 deletions

File tree

app/css/main.css

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,3 +1616,61 @@ body,
16161616
margin-left: 0;
16171617
line-height: 1.4;
16181618
}
1619+
1620+
1621+
.magical-forecast {
1622+
display: grid;
1623+
grid-template-columns: repeat(3, minmax(0, 1fr));
1624+
gap: var(--space-sm);
1625+
align-items: stretch;
1626+
}
1627+
1628+
.forecast-card {
1629+
min-width: 0;
1630+
padding: var(--space-sm);
1631+
border: 1px solid var(--color-border);
1632+
border-radius: var(--radius-sm);
1633+
background: rgba(255,255,255,0.03);
1634+
}
1635+
1636+
.forecast-icon {
1637+
display: block;
1638+
font-size: 1.6rem;
1639+
line-height: 1;
1640+
margin-bottom: 6px;
1641+
}
1642+
1643+
.forecast-kicker {
1644+
display: block;
1645+
color: var(--color-primary);
1646+
font-size: 0.78rem;
1647+
font-weight: 700;
1648+
margin-bottom: 4px;
1649+
text-transform: uppercase;
1650+
letter-spacing: 0.02em;
1651+
}
1652+
1653+
.forecast-line,
1654+
.forecast-omen,
1655+
.magical-forecast .season-bonus {
1656+
margin: 0;
1657+
line-height: 1.4;
1658+
overflow-wrap: anywhere;
1659+
}
1660+
1661+
.forecast-line {
1662+
color: var(--color-text);
1663+
font-weight: 650;
1664+
}
1665+
1666+
.forecast-omen,
1667+
.magical-forecast .season-bonus {
1668+
color: var(--color-text-muted);
1669+
font-size: 0.82rem;
1670+
}
1671+
1672+
@media (max-width: 560px) {
1673+
.magical-forecast {
1674+
grid-template-columns: 1fr;
1675+
}
1676+
}

app/index.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<link rel="icon" type="image/png" sizes="192x192" href="assets/icons/viz-magic-192.png?v=20260712c">
1414
<link rel="icon" type="image/png" sizes="512x512" href="assets/icons/viz-magic-512.png?v=20260712c">
1515
<link rel="apple-touch-icon" href="assets/icons/viz-magic-192.png?v=20260712c">
16-
<link rel="stylesheet" href="css/main.css?v=20260712b">
16+
<link rel="stylesheet" href="css/main.css?v=20260712e">
1717
<link rel="stylesheet" href="css/themes.css">
1818
<link rel="stylesheet" href="css/accessibility.css">
1919
</head>
@@ -116,8 +116,8 @@
116116
<script src="js/config.js?v=20260621m"></script>
117117

118118
<!-- I18n -->
119-
<script src="js/i18n/ru.js?v=20260712d"></script>
120-
<script src="js/i18n/en.js?v=20260712d"></script>
119+
<script src="js/i18n/ru.js?v=20260712e"></script>
120+
<script src="js/i18n/en.js?v=20260712e"></script>
121121

122122
<!-- Utilities -->
123123
<script src="js/utils/helpers.js?v=20260621l"></script>
@@ -161,7 +161,7 @@
161161
<script src="js/engine/marketplace.js"></script>
162162
<script src="js/engine/checkpoint.js"></script>
163163
<script src="js/engine/block-processor.js"></script>
164-
<script src="js/engine/world-events.js?v=20260712d"></script>
164+
<script src="js/engine/world-events.js?v=20260712e"></script>
165165
<script src="js/engine/world-boss.js"></script>
166166
<script src="js/engine/quest-system.js?v=20260621e"></script>
167167
<script src="js/engine/loci.js"></script>
@@ -182,7 +182,7 @@
182182
<script src="js/ui/screens/landing.js"></script>
183183
<script src="js/ui/screens/login.js?v=20260711a"></script>
184184
<script src="js/ui/screens/onboarding.js"></script>
185-
<script src="js/ui/screens/home.js?v=20260712c"></script>
185+
<script src="js/ui/screens/home.js?v=20260712e"></script>
186186
<script src="js/ui/screens/character.js?v=20260621l"></script>
187187
<script src="js/ui/screens/hunt.js"></script>
188188
<script src="js/ui/screens/inventory.js?v=20260621l"></script>

app/js/engine/world-events.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,25 @@ var WorldEvents = (function() {
302302
}
303303
];
304304

305+
306+
307+
/** Everyday magical sky signs. Combined with omens, this gives a year-sized forecast pool. */
308+
var SKY_SIGNS = [
309+
{ id: 'sun_cloud', icon: '\u26C5', summaryKey: 'sky_sun_cloud' },
310+
{ id: 'rain', icon: '\uD83C\uDF27\uFE0F', summaryKey: 'sky_rain' },
311+
{ id: 'hail', icon: '\uD83C\uDF28\uFE0F', summaryKey: 'sky_hail' },
312+
{ id: 'lightning', icon: '\uD83C\uDF29\uFE0F', summaryKey: 'sky_lightning' },
313+
{ id: 'hurricane', icon: '\uD83C\uDF2A\uFE0F', summaryKey: 'sky_hurricane' },
314+
{ id: 'dry_heat', icon: '\uD83C\uDF35', summaryKey: 'sky_dry_heat' },
315+
{ id: 'hard_frost', icon: '\u2744\uFE0F', summaryKey: 'sky_hard_frost' },
316+
{ id: 'dust_storm', icon: '\uD83C\uDF2B\uFE0F', summaryKey: 'sky_dust_storm' },
317+
{ id: 'sudden_thaw', icon: '\uD83E\uDDCA', summaryKey: 'sky_sudden_thaw' },
318+
{ id: 'red_dawn', icon: '\uD83C\uDF05', summaryKey: 'sky_red_dawn' },
319+
{ id: 'black_snow', icon: '\u26C4', summaryKey: 'sky_black_snow' },
320+
{ id: 'silver_fog', icon: '\uD83C\uDF01', summaryKey: 'sky_silver_fog' },
321+
{ id: 'double_rainbow', icon: '\uD83C\uDF08', summaryKey: 'sky_double_rainbow' }
322+
];
323+
305324
/**
306325
* Get the current season based on block number.
307326
* @param {number} blockNum
@@ -336,14 +355,36 @@ var WorldEvents = (function() {
336355

337356

338357

358+
359+
360+
/**
361+
* Get current sky sign based on block number.
362+
* @param {number} blockNum
363+
* @returns {Object}
364+
*/
365+
function getCurrentSky(blockNum) {
366+
var idx = Math.floor(blockNum / 28800) % SKY_SIGNS.length;
367+
if (idx < 0) idx = 0;
368+
return SKY_SIGNS[idx];
369+
}
370+
371+
/**
372+
* Count available daily forecast combinations.
373+
* @returns {number}
374+
*/
375+
function getForecastVariantCount() {
376+
return SKY_SIGNS.length * WEATHER.length;
377+
}
378+
339379
/**
340380
* Get current magical weather based on block number.
341381
* This is a deterministic in-game forecast and affects hunts.
342382
* @param {number} blockNum
343383
* @returns {Object}
344384
*/
345385
function getCurrentWeather(blockNum) {
346-
var idx = Math.floor(blockNum / 28800) % WEATHER.length;
386+
var day = Math.floor(blockNum / 28800);
387+
var idx = Math.floor(day / SKY_SIGNS.length) % WEATHER.length;
347388
if (idx < 0) idx = 0;
348389
return WEATHER[idx];
349390
}
@@ -519,6 +560,8 @@ var WorldEvents = (function() {
519560
getCurrentSeason: getCurrentSeason,
520561
getSeasonalBonuses: getSeasonalBonuses,
521562
getCurrentWeather: getCurrentWeather,
563+
getCurrentSky: getCurrentSky,
564+
getForecastVariantCount: getForecastVariantCount,
522565
getActiveEvents: getActiveEvents,
523566
checkEventTriggers: checkEventTriggers,
524567
checkWeaveSurge: checkWeaveSurge,

app/js/i18n/en.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,7 @@ var LangEN = {
743743

744744
weather_forecast_label: 'Magical weather forecast',
745745
weather_forecast_title: 'Magical weather',
746+
weather_sky_title: 'Sky',
746747
season_effect_prefix: 'Affects hunts',
747748
weather_frog_rain: 'Frog rain. Watch the cauldron and avoid the croaking circles.',
748749
weather_frog_rain_effect: 'Creatures hit 5% harder.',
@@ -804,6 +805,19 @@ var LangEN = {
804805
weather_wrong_echo_effect: 'Creatures hit 9% harder, character defense is 3% lower.',
805806
weather_crown_of_rain: 'A crown of rain hangs over the road. Today even puddles demand a title.',
806807
weather_crown_of_rain_effect: 'Creatures and character defense are 5% stronger.',
808+
sky_sun_cloud: 'Sun and cloud are arguing. No winner yet, but it is beautiful.',
809+
sky_rain: 'Sharp rain drums on roofs like an apprentice on a kettle.',
810+
sky_hail: 'Hail arrives suddenly and proudly: the world has a continental temper.',
811+
sky_lightning: 'Lightning walks low, as if looking for someone to deliver a letter to.',
812+
sky_hurricane: 'A hurricane tries roads on for size. Not all roads fit.',
813+
sky_dry_heat: 'Dry heat crackles in the air. Grass pretends to be parchment.',
814+
sky_hard_frost: 'Frost grabs words by the tail and holds them until evening.',
815+
sky_dust_storm: 'A dust storm erases horizons and edits the map a little.',
816+
sky_sudden_thaw: 'Sudden thaw. Snow has reconsidered being snow.',
817+
sky_red_dawn: 'Red dawn is watching too carefully.',
818+
sky_black_snow: 'Black snow falls quietly. Footprints sound louder than steps.',
819+
sky_silver_fog: 'Silver fog makes the world richer and more suspicious.',
820+
sky_double_rainbow: 'A double rainbow. One is real; the other handles consequences.',
807821

808822
// World Events
809823
event_weave_surge: 'Weave Surge',

app/js/i18n/ru.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,7 @@ var LangRU = {
743743

744744
weather_forecast_label: 'Магическая сводка погоды',
745745
weather_forecast_title: 'Магическая погода',
746+
weather_sky_title: 'Небо',
746747
season_effect_prefix: 'Влияет на охоту',
747748
weather_frog_rain: 'Дождь с лягушками. Следите за котлом и не наступайте на квакающие круги.',
748749
weather_frog_rain_effect: 'Существа бьют на 5% сильнее.',
@@ -804,6 +805,19 @@ var LangRU = {
804805
weather_wrong_echo_effect: 'Существа бьют на 9% сильнее, защита персонажа ниже на 3%.',
805806
weather_crown_of_rain: 'Корона дождя висит над дорогой. Сегодня даже лужи требуют титул.',
806807
weather_crown_of_rain_effect: 'Существа и защита персонажа усилены на 5%.',
808+
sky_sun_cloud: 'Солнце спорит с тучей. Победителя пока нет, зато красиво.',
809+
sky_rain: 'Резкий дождь бьёт по крышам, как ученик по барабану.',
810+
sky_hail: 'Град сыплет внезапно и гордо: резко континентальный характер мира.',
811+
sky_lightning: 'Молнии ходят низко, будто ищут, кому передать письмо.',
812+
sky_hurricane: 'Ураган примеряет дороги на себя. Не все дороги подходят.',
813+
sky_dry_heat: 'Сухой жар хрустит в воздухе. Трава делает вид, что она пергамент.',
814+
sky_hard_frost: 'Мороз хватает слова за хвост и держит до вечера.',
815+
sky_dust_storm: 'Пыльная буря стирает горизонты и слегка правит карту.',
816+
sky_sudden_thaw: 'Внезапная оттепель. Снег передумал быть снегом.',
817+
sky_red_dawn: 'Красный рассвет смотрит слишком внимательно.',
818+
sky_black_snow: 'Чёрный снег падает тихо. Следы на нём слышнее, чем шаги.',
819+
sky_silver_fog: 'Серебряный туман делает мир богаче и подозрительнее.',
820+
sky_double_rainbow: 'Двойная радуга. Одна настоящая, вторая отвечает за последствия.',
807821

808822
// World Events
809823
event_weave_surge: 'Всплеск Плетения',

app/js/ui/screens/home.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,17 +160,29 @@ var HomeScreen = (function() {
160160
var season = WorldEvents.getCurrentSeason(blockNum);
161161
if (!season) return '';
162162

163+
var sky = WorldEvents.getCurrentSky ? WorldEvents.getCurrentSky(blockNum) : null;
163164
var weather = WorldEvents.getCurrentWeather ? WorldEvents.getCurrentWeather(blockNum) : null;
165+
var skyText = sky ? t(sky.summaryKey) : '';
164166
var forecast = weather ? t(weather.summaryKey) : '';
165167
var effect = weather ? t(weather.effectKey) : '';
166168
return '<section class="season-indicator magical-forecast" aria-label="' + t('weather_forecast_label') + '">' +
167-
'<div class="forecast-head">' +
168-
'<span class="season-icon" aria-hidden="true">' + season.icon + '</span>' +
169-
'<span class="season-name">' + t('weather_forecast_title') + ': ' + t(season.nameKey) + '</span>' +
169+
'<div class="forecast-card forecast-card-season">' +
170+
'<span class="forecast-icon" aria-hidden="true">' + season.icon + '</span>' +
171+
'<span class="forecast-kicker">' + t('weather_forecast_title') + '</span>' +
172+
'<p class="forecast-line">' + t(season.nameKey) + '</p>' +
173+
'</div>' +
174+
'<div class="forecast-card forecast-card-sky">' +
175+
'<span class="forecast-icon" aria-hidden="true">' + (sky ? sky.icon : '\u26C5') + '</span>' +
176+
'<span class="forecast-kicker">' + t('weather_sky_title') + '</span>' +
177+
'<p class="forecast-line">' + skyText + '</p>' +
178+
'<p class="forecast-omen">' + forecast + '</p>' +
179+
'</div>' +
180+
'<div class="forecast-card forecast-card-effect">' +
181+
'<span class="forecast-icon" aria-hidden="true">\u2694\uFE0F</span>' +
182+
'<span class="forecast-kicker">' + t('season_effect_prefix') + '</span>' +
183+
'<p class="season-bonus">' + t('school_' + season.dominant) + ' +20%, ' +
184+
t('school_' + season.secondary) + ' +10%. ' + effect + '</p>' +
170185
'</div>' +
171-
'<p class="forecast-line">' + (weather ? '<span aria-hidden="true">' + weather.icon + '</span> ' : '') + forecast + '</p>' +
172-
'<p class="season-bonus">' + t('season_effect_prefix') + ': ' + t('school_' + season.dominant) + ' +20%, ' +
173-
t('school_' + season.secondary) + ' +10%. ' + effect + '</p>' +
174186
'</section>';
175187
}
176188

app/sw.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Viz Magic — Service Worker
2-
var CACHE_NAME = 'viz-magic-v37';
2+
var CACHE_NAME = 'viz-magic-v38';
33
var ASSETS = [
44
'/',
55
'/index.html',

tests/player-bug-regressions.test.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ test('high-traffic UI narration, screen announcements, and inventory stat labels
337337

338338
test('service worker updates quickly and keeps navigations network-first', function () {
339339
const swJs = read('app/sw.js');
340-
assert.ok(/viz-magic-v37/.test(swJs), 'service worker cache version should be bumped');
340+
assert.ok(/viz-magic-v38/.test(swJs), 'service worker cache version should be bumped');
341341
assert.ok(/self\.skipWaiting\(\)/.test(swJs), 'service worker should activate new cache without waiting for all tabs to close');
342342
assert.ok(/self\.clients\.claim\(\)/.test(swJs), 'service worker should claim clients after activation');
343343
assert.ok(/event\.request\.mode === 'navigate'[\s\S]*fetch\(event\.request\)/.test(swJs), 'navigation requests should prefer network to avoid stale cached index');
@@ -384,7 +384,7 @@ test('mobile entry helpers cover keyboard paste, home-screen shortcut, nav parit
384384
assert.ok(/SoundManager\.setVolume\(sfxVolume \/ 100\)/.test(read('app/js/ui/screens/settings.js')), 'settings should apply stored SFX volume on render');
385385
assert.ok(/localStorage\.setItem\(STORAGE_PREFIX \+ 'sfx_volume'/.test(read('app/js/ui/sound.js')), 'sound manager should persist SFX volume');
386386
assert.ok(/var volume = _getStoredNumber\('sfx_volume', 0\.5\)/.test(read('app/js/ui/sound.js')), 'sound manager should restore persisted SFX volume');
387-
assert.ok(/viz-magic-v37/.test(read('app/sw.js')), 'service worker cache should be bumped for UI changes');
387+
assert.ok(/viz-magic-v38/.test(read('app/sw.js')), 'service worker cache should be bumped for UI changes');
388388
});
389389

390390

@@ -434,8 +434,15 @@ test('magical weather is labelled and affects hunts', function () {
434434
assert.ok(/function getCurrentWeather/.test(worldEventsJs), 'world events should expose deterministic magical weather');
435435
assert.ok(/weather_frog_rain/.test(worldEventsJs + ruJs + enJs), 'magical forecast copy should exist');
436436
assert.ok((worldEventsJs.match(/summaryKey: 'weather_/g) || []).length >= 30, 'magical forecast should have at least 30 rotating templates');
437-
assert.ok(/i18n\/ru.js\?v=20260712d/.test(indexHtml), 'Russian weather copy must be cache-busted');
438-
assert.ok(/i18n\/en.js\?v=20260712d/.test(indexHtml), 'English weather copy must be cache-busted');
437+
assert.ok(/var SKY_SIGNS/.test(worldEventsJs), 'forecast should combine omens with sky signs');
438+
assert.ok(/function getForecastVariantCount/.test(worldEventsJs), 'forecast should expose total variant count');
439+
assert.ok(((worldEventsJs.match(/summaryKey: 'sky_/g) || []).length * (worldEventsJs.match(/summaryKey: 'weather_/g) || []).length) >= 365, 'forecast pool should cover a year of daily surprise');
440+
assert.ok(/forecast-card-effect/.test(homeJs + mainCss), 'forecast effect column needs its own thematic icon/card');
441+
assert.ok(/i18n\/ru.js\?v=20260712e/.test(indexHtml), 'Russian weather copy must be cache-busted');
442+
assert.ok(/i18n\/en.js\?v=20260712e/.test(indexHtml), 'English weather copy must be cache-busted');
443+
assert.ok(/home.js\?v=20260712e/.test(indexHtml), 'home forecast layout must be cache-busted');
444+
assert.ok(/world-events.js\?v=20260712e/.test(indexHtml), 'world events forecast pool must be cache-busted');
445+
assert.ok(/main.css\?v=20260712e/.test(indexHtml), 'forecast grid CSS must be cache-busted');
439446
assert.ok(/season_effect_prefix/.test(homeJs + ruJs + enJs), 'home forecast should explain gameplay effect');
440447
assert.ok(/seasonBonuses\[spell\.school\]/.test(combatJs), 'season school bonus should affect spell attack');
441448
assert.ok(/creatureAttackMod/.test(combatJs), 'weather should affect creature danger in hunt combat');

0 commit comments

Comments
 (0)