├── src ├── img │ ├── m0.png │ ├── m1.png │ ├── m10.png │ ├── m11.png │ ├── m12.png │ ├── m13.png │ ├── m14.png │ ├── m15.png │ ├── m16.png │ ├── m17.png │ ├── m18.png │ ├── m19.png │ ├── m1u.png │ ├── m2.png │ ├── m20.png │ ├── m21.png │ ├── m22.png │ ├── m23.png │ ├── m24.png │ ├── m25.png │ ├── m26.png │ ├── m27.png │ ├── m28.png │ ├── m29.png │ ├── m3.png │ ├── m30.png │ ├── m31.png │ ├── m32.png │ ├── m33.png │ ├── m34.png │ ├── m35.png │ ├── m36.png │ ├── m37.png │ ├── m38.png │ ├── m39.png │ ├── m4.png │ ├── m40.png │ ├── m41.png │ ├── m42.png │ ├── m43.png │ ├── m44.png │ ├── m45.png │ ├── m46.png │ ├── m47.png │ ├── m48.png │ ├── m49.png │ ├── m5.png │ ├── m50.png │ ├── m51.png │ ├── m52.png │ ├── m53.png │ ├── m54.png │ ├── m55.png │ ├── m56.png │ ├── m57.png │ ├── m58.png │ ├── m59.png │ ├── m6.png │ ├── m60.png │ ├── m61.png │ ├── m62.png │ ├── m63.png │ ├── m64.png │ ├── m65.png │ ├── m7.png │ ├── m8.png │ ├── m9.png │ ├── o0.png │ ├── o1.png │ ├── o10.png │ ├── o11.png │ ├── o12.png │ ├── o13.png │ ├── o14.png │ ├── o15.png │ ├── o16.png │ ├── o17.png │ ├── o18.png │ ├── o19.png │ ├── o2.png │ ├── o20.png │ ├── o21.png │ ├── o22.png │ ├── o23.png │ ├── o24.png │ ├── o25.png │ ├── o26.png │ ├── o27.png │ ├── o28.png │ ├── o29.png │ ├── o3.png │ ├── o30.png │ ├── o31.png │ ├── o32.png │ ├── o33.png │ ├── o34.png │ ├── o35.png │ ├── o36.png │ ├── o37.png │ ├── o38.png │ ├── o39.png │ ├── o4.png │ ├── o40.png │ ├── o41.png │ ├── o42.png │ ├── o43.png │ ├── o44.png │ ├── o45.png │ ├── o46.png │ ├── o47.png │ ├── o48.png │ ├── o49.png │ ├── o5.png │ ├── o50.png │ ├── o51.png │ ├── o52.png │ ├── o53.png │ ├── o54.png │ ├── o55.png │ ├── o56.png │ ├── o57.png │ ├── o58.png │ ├── o59.png │ ├── o6.png │ ├── o60.png │ ├── o61.png │ ├── o62.png │ ├── o63.png │ ├── o64.png │ ├── o65.png │ ├── o66.png │ ├── o67.png │ ├── o68.png │ ├── o69.png │ ├── o7.png │ ├── o70.png │ ├── o71.png │ ├── o72.png │ ├── o73.png │ ├── o74.png │ ├── o75.png │ ├── o76.png │ ├── o77.png │ ├── o78.png │ ├── o79.png │ ├── o8.png │ ├── o80.png │ ├── o81.png │ ├── o82.png │ ├── o83.png │ ├── o84.png │ ├── o85.png │ ├── o86.png │ ├── o87.png │ ├── o88.png │ ├── o89.png │ ├── o9.png │ ├── o90.png │ ├── o91.png │ ├── o92.png │ ├── o93.png │ ├── o94.png │ ├── o95.png │ ├── o96.png │ ├── o97.png │ ├── o98.png │ ├── o99.png │ ├── w0.png │ ├── w10.png │ ├── w12.png │ ├── w14.png │ ├── w16.png │ ├── w18.png │ ├── w2.png │ ├── w20.png │ ├── w22.png │ ├── w24.png │ ├── w26.png │ ├── w28.png │ ├── w30.png │ ├── w4.png │ ├── w6.png │ ├── w8.png │ ├── m19u.png │ ├── m34u.png │ ├── m39v.png │ ├── m57v.png │ ├── m58v.png │ ├── m59v.png │ ├── m60v.png │ ├── m61v.png │ ├── m62v.png │ ├── m63v.png │ ├── m64v.png │ ├── m65v.png │ ├── o100.png │ ├── o101.png │ ├── dos437.ttf │ ├── favicon.png │ ├── player.png │ ├── NotoSansMono-Regular.ttf │ ├── TopazPlus_a1200_v1.0.ttf │ └── TopazPlus_a500_v1.0.ttf ├── level.js ├── common │ ├── frame.js │ ├── roll.js │ ├── larn_config.js │ ├── patch.js │ ├── live.js │ └── lambda.js ├── workers │ ├── patchWorker.js │ └── compressionWorker.js ├── fullstory.js ├── tv │ ├── larntv.css │ ├── index_local.html │ ├── index.html │ ├── watchlive.js │ ├── _TODO.txt │ └── index.js ├── score │ ├── index.html │ ├── score.css │ └── index_local.html ├── devmode.js ├── gotw.js ├── throne.js ├── larn.html ├── stairs.js ├── larn.css ├── lib │ ├── lz-string.min.js │ ├── mousetrap.min.js │ └── rollbar.js ├── regen.js ├── state.js ├── config.js ├── io.js ├── larn_local.html ├── mcdopes.js ├── spheres.js ├── bill.js ├── altar.js ├── fountain.js ├── help.js └── storedata.js ├── .gitignore ├── babel.config.json ├── LICENSE ├── README.md ├── package.json └── ULARN_SPOILERS.md /src/img/m0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m0.png -------------------------------------------------------------------------------- /src/img/m1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m1.png -------------------------------------------------------------------------------- /src/img/m10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m10.png -------------------------------------------------------------------------------- /src/img/m11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m11.png -------------------------------------------------------------------------------- /src/img/m12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m12.png -------------------------------------------------------------------------------- /src/img/m13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m13.png -------------------------------------------------------------------------------- /src/img/m14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m14.png -------------------------------------------------------------------------------- /src/img/m15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m15.png -------------------------------------------------------------------------------- /src/img/m16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m16.png -------------------------------------------------------------------------------- /src/img/m17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m17.png -------------------------------------------------------------------------------- /src/img/m18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m18.png -------------------------------------------------------------------------------- /src/img/m19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m19.png -------------------------------------------------------------------------------- /src/img/m1u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m1u.png -------------------------------------------------------------------------------- /src/img/m2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m2.png -------------------------------------------------------------------------------- /src/img/m20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m20.png -------------------------------------------------------------------------------- /src/img/m21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m21.png -------------------------------------------------------------------------------- /src/img/m22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m22.png -------------------------------------------------------------------------------- /src/img/m23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m23.png -------------------------------------------------------------------------------- /src/img/m24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m24.png -------------------------------------------------------------------------------- /src/img/m25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m25.png -------------------------------------------------------------------------------- /src/img/m26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m26.png -------------------------------------------------------------------------------- /src/img/m27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m27.png -------------------------------------------------------------------------------- /src/img/m28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m28.png -------------------------------------------------------------------------------- /src/img/m29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m29.png -------------------------------------------------------------------------------- /src/img/m3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m3.png -------------------------------------------------------------------------------- /src/img/m30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m30.png -------------------------------------------------------------------------------- /src/img/m31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m31.png -------------------------------------------------------------------------------- /src/img/m32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m32.png -------------------------------------------------------------------------------- /src/img/m33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m33.png -------------------------------------------------------------------------------- /src/img/m34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m34.png -------------------------------------------------------------------------------- /src/img/m35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m35.png -------------------------------------------------------------------------------- /src/img/m36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m36.png -------------------------------------------------------------------------------- /src/img/m37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m37.png -------------------------------------------------------------------------------- /src/img/m38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m38.png -------------------------------------------------------------------------------- /src/img/m39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m39.png -------------------------------------------------------------------------------- /src/img/m4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m4.png -------------------------------------------------------------------------------- /src/img/m40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m40.png -------------------------------------------------------------------------------- /src/img/m41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m41.png -------------------------------------------------------------------------------- /src/img/m42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m42.png -------------------------------------------------------------------------------- /src/img/m43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m43.png -------------------------------------------------------------------------------- /src/img/m44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m44.png -------------------------------------------------------------------------------- /src/img/m45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m45.png -------------------------------------------------------------------------------- /src/img/m46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m46.png -------------------------------------------------------------------------------- /src/img/m47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m47.png -------------------------------------------------------------------------------- /src/img/m48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m48.png -------------------------------------------------------------------------------- /src/img/m49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m49.png -------------------------------------------------------------------------------- /src/img/m5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m5.png -------------------------------------------------------------------------------- /src/img/m50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m50.png -------------------------------------------------------------------------------- /src/img/m51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m51.png -------------------------------------------------------------------------------- /src/img/m52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m52.png -------------------------------------------------------------------------------- /src/img/m53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m53.png -------------------------------------------------------------------------------- /src/img/m54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m54.png -------------------------------------------------------------------------------- /src/img/m55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m55.png -------------------------------------------------------------------------------- /src/img/m56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m56.png -------------------------------------------------------------------------------- /src/img/m57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m57.png -------------------------------------------------------------------------------- /src/img/m58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m58.png -------------------------------------------------------------------------------- /src/img/m59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m59.png -------------------------------------------------------------------------------- /src/img/m6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m6.png -------------------------------------------------------------------------------- /src/img/m60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m60.png -------------------------------------------------------------------------------- /src/img/m61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m61.png -------------------------------------------------------------------------------- /src/img/m62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m62.png -------------------------------------------------------------------------------- /src/img/m63.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m63.png -------------------------------------------------------------------------------- /src/img/m64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m64.png -------------------------------------------------------------------------------- /src/img/m65.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m65.png -------------------------------------------------------------------------------- /src/img/m7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m7.png -------------------------------------------------------------------------------- /src/img/m8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m8.png -------------------------------------------------------------------------------- /src/img/m9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m9.png -------------------------------------------------------------------------------- /src/img/o0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o0.png -------------------------------------------------------------------------------- /src/img/o1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o1.png -------------------------------------------------------------------------------- /src/img/o10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o10.png -------------------------------------------------------------------------------- /src/img/o11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o11.png -------------------------------------------------------------------------------- /src/img/o12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o12.png -------------------------------------------------------------------------------- /src/img/o13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o13.png -------------------------------------------------------------------------------- /src/img/o14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o14.png -------------------------------------------------------------------------------- /src/img/o15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o15.png -------------------------------------------------------------------------------- /src/img/o16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o16.png -------------------------------------------------------------------------------- /src/img/o17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o17.png -------------------------------------------------------------------------------- /src/img/o18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o18.png -------------------------------------------------------------------------------- /src/img/o19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o19.png -------------------------------------------------------------------------------- /src/img/o2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o2.png -------------------------------------------------------------------------------- /src/img/o20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o20.png -------------------------------------------------------------------------------- /src/img/o21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o21.png -------------------------------------------------------------------------------- /src/img/o22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o22.png -------------------------------------------------------------------------------- /src/img/o23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o23.png -------------------------------------------------------------------------------- /src/img/o24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o24.png -------------------------------------------------------------------------------- /src/img/o25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o25.png -------------------------------------------------------------------------------- /src/img/o26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o26.png -------------------------------------------------------------------------------- /src/img/o27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o27.png -------------------------------------------------------------------------------- /src/img/o28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o28.png -------------------------------------------------------------------------------- /src/img/o29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o29.png -------------------------------------------------------------------------------- /src/img/o3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o3.png -------------------------------------------------------------------------------- /src/img/o30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o30.png -------------------------------------------------------------------------------- /src/img/o31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o31.png -------------------------------------------------------------------------------- /src/img/o32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o32.png -------------------------------------------------------------------------------- /src/img/o33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o33.png -------------------------------------------------------------------------------- /src/img/o34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o34.png -------------------------------------------------------------------------------- /src/img/o35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o35.png -------------------------------------------------------------------------------- /src/img/o36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o36.png -------------------------------------------------------------------------------- /src/img/o37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o37.png -------------------------------------------------------------------------------- /src/img/o38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o38.png -------------------------------------------------------------------------------- /src/img/o39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o39.png -------------------------------------------------------------------------------- /src/img/o4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o4.png -------------------------------------------------------------------------------- /src/img/o40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o40.png -------------------------------------------------------------------------------- /src/img/o41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o41.png -------------------------------------------------------------------------------- /src/img/o42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o42.png -------------------------------------------------------------------------------- /src/img/o43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o43.png -------------------------------------------------------------------------------- /src/img/o44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o44.png -------------------------------------------------------------------------------- /src/img/o45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o45.png -------------------------------------------------------------------------------- /src/img/o46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o46.png -------------------------------------------------------------------------------- /src/img/o47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o47.png -------------------------------------------------------------------------------- /src/img/o48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o48.png -------------------------------------------------------------------------------- /src/img/o49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o49.png -------------------------------------------------------------------------------- /src/img/o5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o5.png -------------------------------------------------------------------------------- /src/img/o50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o50.png -------------------------------------------------------------------------------- /src/img/o51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o51.png -------------------------------------------------------------------------------- /src/img/o52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o52.png -------------------------------------------------------------------------------- /src/img/o53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o53.png -------------------------------------------------------------------------------- /src/img/o54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o54.png -------------------------------------------------------------------------------- /src/img/o55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o55.png -------------------------------------------------------------------------------- /src/img/o56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o56.png -------------------------------------------------------------------------------- /src/img/o57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o57.png -------------------------------------------------------------------------------- /src/img/o58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o58.png -------------------------------------------------------------------------------- /src/img/o59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o59.png -------------------------------------------------------------------------------- /src/img/o6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o6.png -------------------------------------------------------------------------------- /src/img/o60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o60.png -------------------------------------------------------------------------------- /src/img/o61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o61.png -------------------------------------------------------------------------------- /src/img/o62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o62.png -------------------------------------------------------------------------------- /src/img/o63.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o63.png -------------------------------------------------------------------------------- /src/img/o64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o64.png -------------------------------------------------------------------------------- /src/img/o65.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o65.png -------------------------------------------------------------------------------- /src/img/o66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o66.png -------------------------------------------------------------------------------- /src/img/o67.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o67.png -------------------------------------------------------------------------------- /src/img/o68.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o68.png -------------------------------------------------------------------------------- /src/img/o69.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o69.png -------------------------------------------------------------------------------- /src/img/o7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o7.png -------------------------------------------------------------------------------- /src/img/o70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o70.png -------------------------------------------------------------------------------- /src/img/o71.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o71.png -------------------------------------------------------------------------------- /src/img/o72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o72.png -------------------------------------------------------------------------------- /src/img/o73.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o73.png -------------------------------------------------------------------------------- /src/img/o74.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o74.png -------------------------------------------------------------------------------- /src/img/o75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o75.png -------------------------------------------------------------------------------- /src/img/o76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o76.png -------------------------------------------------------------------------------- /src/img/o77.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o77.png -------------------------------------------------------------------------------- /src/img/o78.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o78.png -------------------------------------------------------------------------------- /src/img/o79.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o79.png -------------------------------------------------------------------------------- /src/img/o8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o8.png -------------------------------------------------------------------------------- /src/img/o80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o80.png -------------------------------------------------------------------------------- /src/img/o81.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o81.png -------------------------------------------------------------------------------- /src/img/o82.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o82.png -------------------------------------------------------------------------------- /src/img/o83.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o83.png -------------------------------------------------------------------------------- /src/img/o84.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o84.png -------------------------------------------------------------------------------- /src/img/o85.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o85.png -------------------------------------------------------------------------------- /src/img/o86.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o86.png -------------------------------------------------------------------------------- /src/img/o87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o87.png -------------------------------------------------------------------------------- /src/img/o88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o88.png -------------------------------------------------------------------------------- /src/img/o89.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o89.png -------------------------------------------------------------------------------- /src/img/o9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o9.png -------------------------------------------------------------------------------- /src/img/o90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o90.png -------------------------------------------------------------------------------- /src/img/o91.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o91.png -------------------------------------------------------------------------------- /src/img/o92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o92.png -------------------------------------------------------------------------------- /src/img/o93.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o93.png -------------------------------------------------------------------------------- /src/img/o94.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o94.png -------------------------------------------------------------------------------- /src/img/o95.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o95.png -------------------------------------------------------------------------------- /src/img/o96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o96.png -------------------------------------------------------------------------------- /src/img/o97.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o97.png -------------------------------------------------------------------------------- /src/img/o98.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o98.png -------------------------------------------------------------------------------- /src/img/o99.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o99.png -------------------------------------------------------------------------------- /src/img/w0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w0.png -------------------------------------------------------------------------------- /src/img/w10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w10.png -------------------------------------------------------------------------------- /src/img/w12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w12.png -------------------------------------------------------------------------------- /src/img/w14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w14.png -------------------------------------------------------------------------------- /src/img/w16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w16.png -------------------------------------------------------------------------------- /src/img/w18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w18.png -------------------------------------------------------------------------------- /src/img/w2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w2.png -------------------------------------------------------------------------------- /src/img/w20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w20.png -------------------------------------------------------------------------------- /src/img/w22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w22.png -------------------------------------------------------------------------------- /src/img/w24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w24.png -------------------------------------------------------------------------------- /src/img/w26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w26.png -------------------------------------------------------------------------------- /src/img/w28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w28.png -------------------------------------------------------------------------------- /src/img/w30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w30.png -------------------------------------------------------------------------------- /src/img/w4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w4.png -------------------------------------------------------------------------------- /src/img/w6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w6.png -------------------------------------------------------------------------------- /src/img/w8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/w8.png -------------------------------------------------------------------------------- /src/img/m19u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m19u.png -------------------------------------------------------------------------------- /src/img/m34u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m34u.png -------------------------------------------------------------------------------- /src/img/m39v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m39v.png -------------------------------------------------------------------------------- /src/img/m57v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m57v.png -------------------------------------------------------------------------------- /src/img/m58v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m58v.png -------------------------------------------------------------------------------- /src/img/m59v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m59v.png -------------------------------------------------------------------------------- /src/img/m60v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m60v.png -------------------------------------------------------------------------------- /src/img/m61v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m61v.png -------------------------------------------------------------------------------- /src/img/m62v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m62v.png -------------------------------------------------------------------------------- /src/img/m63v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m63v.png -------------------------------------------------------------------------------- /src/img/m64v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m64v.png -------------------------------------------------------------------------------- /src/img/m65v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/m65v.png -------------------------------------------------------------------------------- /src/img/o100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o100.png -------------------------------------------------------------------------------- /src/img/o101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/o101.png -------------------------------------------------------------------------------- /src/img/dos437.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/dos437.ttf -------------------------------------------------------------------------------- /src/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/favicon.png -------------------------------------------------------------------------------- /src/img/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/player.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dropbox 2 | .DS_Store 3 | .vscode 4 | .eslintrc.json 5 | Icon 6 | node_modules 7 | secrets.sh -------------------------------------------------------------------------------- /src/img/NotoSansMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/NotoSansMono-Regular.ttf -------------------------------------------------------------------------------- /src/img/TopazPlus_a1200_v1.0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/TopazPlus_a1200_v1.0.ttf -------------------------------------------------------------------------------- /src/img/TopazPlus_a500_v1.0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/primeau/Larn/HEAD/src/img/TopazPlus_a500_v1.0.ttf -------------------------------------------------------------------------------- /src/level.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var Level = { 5 | items: [], 6 | monsters: [], 7 | know: [], 8 | }; // Level -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "targets": { 4 | "edge": "15", 5 | "firefox": "53", 6 | "chrome": "55", 7 | "safari": "11.1" 8 | }, 9 | "ignore": ["*/lib/*", "*/aws/*", "*/node_modules/*", "*/cloudflare/broadcast/*" ] 10 | } -------------------------------------------------------------------------------- /src/common/frame.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // a single film frame 4 | // this is the base unit of a recording 5 | class Frame { 6 | constructor() { 7 | this.id = 0; 8 | this.ts = 0; 9 | this.divs = {}; 10 | this.compressed = false; 11 | this.deflated = false; 12 | } 13 | } -------------------------------------------------------------------------------- /src/common/roll.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // a collection of patches 4 | class Roll { 5 | constructor(patches) { 6 | this.patches = patches; 7 | } 8 | 9 | addPatch(patch) { 10 | this.patches.push(patch); 11 | } 12 | 13 | isFull() { 14 | return this.patches.length >= MAX_ROLL_LENGTH; 15 | } 16 | } 17 | 18 | 19 | 20 | function createRoll(roll) { 21 | return new Roll(roll.patches); 22 | } 23 | 24 | 25 | 26 | function decompressRoll(compressed) { 27 | if (!compressed) return null; 28 | // TODO: there's gotta be a way to use decompressFromUTF16 29 | let decompressed = LZString.decompressFromEncodedURIComponent(compressed); 30 | return createRoll(JSON.parse(decompressed)); 31 | } -------------------------------------------------------------------------------- /src/workers/patchWorker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | importScripts('../lib/diff_match_patch.js'); 4 | try { importScripts('../tv/common/patch.js'); } catch (error) { } // prod location 5 | try { importScripts('../common/patch.js'); } catch (error) { } // local testing 6 | 7 | // WORKER STEP 2 - build patch 8 | onmessage = function (event) { 9 | let prevFrame = event.data[0]; 10 | let newFrame = event.data[1]; 11 | let source = event.data[2] || `none`; 12 | let newPatch = buildPatch(prevFrame, newFrame); 13 | // console.log(`${source}:${newFrame.id} worker.buildpatch`); 14 | 15 | // memory management 16 | event.data[0] = null; 17 | event.data[1] = null; 18 | event.data[2] = null; 19 | event.data.length = 0; 20 | 21 | postMessage([newPatch, newFrame]); 22 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-Present Jason Primeau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/workers/compressionWorker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | importScripts('../lib/lz-string.min.js'); 4 | 5 | // WORKER STEP 2 - do compression 6 | onmessage = function (event) { 7 | let id = event.data[0]; 8 | let uncompressed = event.data[1]; 9 | let algo = event.data[2]; 10 | let source = event.data[3] || `none`; 11 | let metadata = event.data[4] || null; 12 | // console.log(`${source}:${id} worker.compress: start size`, uncompressed.length); 13 | let compressed = ``; 14 | if (algo === `UTF16`) { 15 | compressed = LZString.compressToUTF16(uncompressed); 16 | } 17 | else if (algo === `ENCODED_URI`) { 18 | compressed = LZString.compressToEncodedURIComponent(uncompressed); 19 | } 20 | else { 21 | console.error(`worker.compress: no compression algorithm selected`); 22 | } 23 | // console.log(`${source}:${id} worker.compress: end size`, compressed.length); 24 | 25 | // memory management 26 | event.data[0] = null; 27 | event.data[1] = null; 28 | event.data[2] = null; 29 | event.data[3] = null; 30 | event.data[4] = null; 31 | event.data.length = 0; 32 | 33 | postMessage([id, compressed, metadata]); 34 | }; -------------------------------------------------------------------------------- /src/common/larn_config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const VERSION = '12.5.3'; 4 | const BUILD = '552'; 5 | 6 | const ENABLE_DEVMODE = false; // this must be set to false for production releases 7 | 8 | const CF_SCORE_ENDPOINT = 'score'; 9 | const CF_HIGHSCORE_ENDPOINT = 'highscore'; 10 | const CF_ACTIVEGAME_ENDPOINT = 'active'; 11 | const CF_COMPLETEDGAME_ENDPOINT = 'completed'; 12 | const CF_GOTW_ENDPOINT = 'gotw'; 13 | const CF_HIGHSCORES_TABLE = 'highscores'; 14 | 15 | // recorded games 16 | let ENABLE_RECORDING = true; // IDEA: RELEASE A VERSION WITH THIS SET TO FALSE 17 | const MIN_FRAMES_TO_LIST = 1000; 18 | const MAX_ROLL_LENGTH = 200; 19 | const AWS_SCORE_FUNCTION = 'score'; 20 | const AWS_RECORD_FUNCTION = `movie_test`; 21 | function initLambdaCredentials() { 22 | AWS.config.accessKeyId = 'AWS_CONFIG_ACCESSKEYID'; 23 | AWS.config.secretAccessKey = 'AWS_CONFIG_SECRETACCESSKEY'; 24 | } 25 | 26 | // live games 27 | const CF_LOCAL = false; 28 | let ENABLE_RECORDING_REALTIME = true; // IDEA: RELEASE A VERSION WITH THIS SET TO FALSE 29 | const CF_BROADCAST_HOST = CF_LOCAL ? `localhost:8787` : `broadcast.larn.workers.dev`; 30 | const CF_BROADCAST_PROTOCOL = CF_LOCAL ? `http://` : `https://`; 31 | const LIVE_LIST_REFRESH = CF_LOCAL ? 1 : 10; // seconds 32 | const LIVE_METADATA_WAIT = CF_LOCAL ? 1 : 10; // seconds 33 | const LIVE_METADATA_MOVES = CF_LOCAL ? 1 : 11; // moves 34 | -------------------------------------------------------------------------------- /src/fullstory.js: -------------------------------------------------------------------------------- 1 | function fsfunc() { 2 | window['_fs_debug'] = false; 3 | window['_fs_host'] = 'fullstory.com'; 4 | window['_fs_script'] = 'edge.fullstory.com/s/fs.js'; 5 | window['_fs_org'] = 'FULLSTORY_ORG'; 6 | window['_fs_namespace'] = 'FS'; 7 | (function(m,n,e,t,l,o,g,y){ 8 | if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].');} return;} 9 | g=m[e]=function(a,b,s){g.q?g.q.push([a,b,s]):g._api(a,b,s);};g.q=[]; 10 | o=n.createElement(t);o.async=1;o.crossOrigin='anonymous';o.src='https://'+_fs_script; 11 | y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y); 12 | g.identify=function(i,v,s){g(l,{uid:i},s);if(v)g(l,v,s)};g.setUserVars=function(v,s){g(l,v,s)};g.event=function(i,v,s){g('event',{n:i,p:v},s)}; 13 | g.anonymize=function(){g.identify(!!0)}; 14 | g.shutdown=function(){g("rec",!1)};g.restart=function(){g("rec",!0)}; 15 | g.log = function(a,b){g("log",[a,b])}; 16 | g.consent=function(a){g("consent",!arguments.length||a)}; 17 | g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)}; 18 | g.clearUserCookie=function(){}; 19 | g.setVars=function(n, p){g('setVars',[n,p]);}; 20 | g._w={};y='XMLHttpRequest';g._w[y]=m[y];y='fetch';g._w[y]=m[y]; 21 | if(m[y])m[y]=function(){return g._w[y].apply(this,arguments)}; 22 | g._v="1.3.0"; 23 | })(window,document,window['_fs_namespace'],'script','user');} -------------------------------------------------------------------------------- /src/tv/larntv.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: modern; 3 | src: url('../img/NotoSansMono-Regular.ttf'); 4 | } 5 | 6 | @font-face { 7 | font-family: dos437; 8 | src: url('../img/dos437.ttf'); 9 | } 10 | 11 | @font-face { 12 | font-family: amiga500; 13 | src: url('../img/TopazPlus_a500_v1.0.ttf'); 14 | } 15 | 16 | @font-face { 17 | font-family: amiga1200; 18 | src: url('../img/TopazPlus_a1200_v1.0.ttf'); 19 | } 20 | 21 | a:link { 22 | color: #bbbbbb; 23 | text-decoration: none; 24 | } 25 | 26 | a:visited { 27 | color: #FF00FF; 28 | text-decoration: none; 29 | } 30 | 31 | a:hover { 32 | color: #FF77FF; 33 | text-decoration: none; 34 | } 35 | 36 | a:active { 37 | color: #EE0000; 38 | text-decoration: none; 39 | } 40 | 41 | b, 42 | strong { 43 | color: lightgray; 44 | } 45 | 46 | body { 47 | font-family: 'dos', 'Courier New', Courier, monospace; 48 | font-size: 22px; 49 | color: #ABABAB; 50 | margin: 25px; 51 | background-color: #000000; 52 | } 53 | 54 | /* body { 55 | font-family: 'dos', 'Courier New', Courier, monospace; 56 | margin: 0px; 57 | background-color: #000000; 58 | } */ 59 | 60 | 61 | div { 62 | background-position: center center; 63 | background-size: 100% 100%; 64 | background-repeat: no-repeat; 65 | white-space: pre; 66 | } 67 | 68 | div.image { 69 | display: inline-block; 70 | background-image: url('../img/o94.png'); 71 | } 72 | 73 | mark { 74 | background-color: lightgrey; 75 | color: black; 76 | } 77 | 78 | table, 79 | th, 80 | td { 81 | vertical-align: top; 82 | padding-top: 0px; 83 | } -------------------------------------------------------------------------------- /src/score/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Larn Scores 6 | 7 | 8 | 9 | 10 |

Larn High Scores

11 |
12 |
13 | 14 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/score/score.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | text-align: center; 3 | font-size: 110%; 4 | } 5 | 6 | th.pr2ch, 7 | td.pr2ch { 8 | padding-right: 2ch; 9 | } 10 | body { 11 | margin: 2em; 12 | color: #d3d3d3; 13 | font-size: 18px; 14 | } 15 | h1 { 16 | text-align: center; 17 | font-size: 110%; 18 | } 19 | .radio-group label, 20 | .radio-group input[type='radio'] { 21 | font-size: inherit; 22 | } 23 | .radio-group label:hover { 24 | background: #003366; 25 | color: #fff; 26 | border-radius: 4px; 27 | } 28 | .score-list { 29 | list-style: none; 30 | padding: 0; 31 | } 32 | .score-item { 33 | padding: 0; 34 | margin-bottom: 0; 35 | border-bottom: none; 36 | cursor: pointer; 37 | } 38 | .score-item:hover { 39 | background: #003366; 40 | } 41 | .score-item:last-child { 42 | border-bottom: none; 43 | } 44 | .overlay { 45 | position: fixed; 46 | top: 0; 47 | left: 0; 48 | right: 0; 49 | bottom: 0; 50 | background: transparent; 51 | display: flex; 52 | align-items: flex-start; 53 | justify-content: flex-end; 54 | z-index: 1000; 55 | /* pointer-events: none; */ 56 | } 57 | .overlay-content { 58 | position: relative; 59 | background: #222; 60 | padding: 2em 3em; 61 | border-radius: 8px; 62 | box-shadow: 0 2px 16px #000a; 63 | text-align: left; 64 | height: 100vh; 65 | max-width: 75vw; 66 | max-height: 100vh; 67 | overflow-y: auto; 68 | pointer-events: auto; 69 | font-size: 17px; 70 | } 71 | .overlay-close { 72 | margin-top: 1em; 73 | color: #ccc; 74 | font-size: 1.1em; 75 | position: absolute; 76 | top: 10px; 77 | right: 16px; 78 | background: none; 79 | border: none; 80 | } 81 | .overlay-close:hover { 82 | cursor: pointer; 83 | } 84 | #overlayCloseX { 85 | z-index: 1001; 86 | } 87 | #yearSelect, 88 | #gotwSelect { 89 | background: #000; 90 | color: #d3d3d3; 91 | border: 1px solid #444; 92 | width: 140px; 93 | min-width: 140px; 94 | max-width: 140px; 95 | font-family: inherit; 96 | font-size: inherit; 97 | } 98 | -------------------------------------------------------------------------------- /src/devmode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function enableDevmode() { 4 | 5 | try { 6 | Rollbar.configure({ enabled: false }); 7 | } catch (error) { 8 | // 9 | } 10 | 11 | console.error("DEVMODE IN USE"); 12 | 13 | enableDebug(); 14 | eventToggleDebugWTW(); 15 | eventToggleDebugStairs(); 16 | eventToggleDebugOutput(); 17 | eventToggleDebugKnowAll(); 18 | eventToggleDebugStats(); 19 | eventToggleDebugImmortal(); 20 | eventToggleDebugAwareness(); 21 | // eventToggleDebugNoMonsters(); 22 | // player.updateStealth(100000); 23 | // player.updateCancellation(100000); 24 | 25 | wizardmode(`pvnert(x)`); 26 | 27 | // var startShield = createObject(OSHIELD); 28 | // take(startShield); 29 | // player.SHIELD = startShield; 30 | // var startArmor = createObject(OSSPLATE, 25); 31 | // take(startArmor); 32 | // player.WEAR = startArmor; 33 | // var startWeapon = createObject(OLANCE); 34 | // take(startWeapon); 35 | // player.WIELD = startWeapon; 36 | 37 | // player.raiseexperience(5000000); 38 | 39 | // take(createObject(OPOTION, 2)); 40 | // take(createObject(OPOTION, 9)); 41 | // take(createObject(OPOTION, 11)); 42 | // take(createObject(OPOTION, 10)); 43 | // take(createObject(OPOTION, 21)); 44 | // take(createObject(OPOTION, 23)); 45 | 46 | // take(createObject(OSPHTALISMAN)); 47 | // take(createObject(OHANDofFEAR)); 48 | // take(createObject(OLARNEYE)); 49 | // take(createObject(ONOTHEFT)); 50 | // take(createObject(OBRASSLAMP)); 51 | // gtime = 30001; 52 | player.GOLD = 250000; 53 | 54 | // newcavelevel(level + 1); 55 | // setItem(player.x, player.y, createObject(OTRAPDOOR)); 56 | 57 | // createmonster(NYMPH); 58 | // for (let index = 1; index < monsterlist.length; index++) { 59 | // createmonster(index, 1+index, 2); 60 | // } 61 | // revealLevel(); 62 | 63 | // winner: 64 | // take(createObject(OPOTION, 21)); 65 | // gtime = 50001; // out of time 66 | 67 | } 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/score/index_local.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Larn Scores 6 | 7 | 8 | 9 | 10 |

Larn High Scores

11 |
12 |
13 | 14 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/tv/index_local.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Larn TV 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 33 | 34 | 35 |
24 | 25 |
26 | 27 | 28 |
31 |
32 |
36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/common/patch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dmp = new diff_match_patch(); 4 | 5 | 6 | 7 | // the difference between framenum and framenum + 1 8 | class Patch { 9 | constructor() { 10 | this.id; 11 | this.ts; 12 | this.deflated = false; 13 | this.divs = {}; 14 | } 15 | } 16 | 17 | 18 | 19 | // construct a frame from a patch 20 | function buildFrame(patch, frame) { 21 | let newFrame = new Frame(); 22 | 23 | // frame number 24 | newFrame.id = patch.id; 25 | 26 | // timestamp 27 | newFrame.ts = patch.ts; 28 | 29 | // amiga frames are deflated by removing css 30 | newFrame.deflated = patch.deflated; 31 | 32 | // shouldn't happen but just in case 33 | if (frame.compressed) { 34 | decompressFrame(frame); 35 | } 36 | 37 | let divs = Object.entries(patch.divs); 38 | // console.error(`buildFrame(): divs: ${patch.id} ${divs}`); 39 | 40 | for (const [key, value] of divs) { 41 | // console.log(`buildFrame(): k: ${key}`); 42 | // console.log(`buildFrame(): v: ${value}`); 43 | let patches = dmp.patch_fromText(value); 44 | // console.log(`buildFrame(): patch: ${dmp.patch_toText(patches)}`); 45 | let results = dmp.patch_apply(patches, frame.divs[key]); 46 | newFrame.divs[key] = results[0]; 47 | // console.log(`buildFrame(): frame:\`${newFrame.divs[key]}\``); 48 | } 49 | 50 | // console.log(`patch`, patch); 51 | // console.log(`newframe`, newFrame); 52 | 53 | return newFrame; 54 | } 55 | 56 | 57 | 58 | function buildPatch(prevFrame, currentFrame) { 59 | let newPatch = new Patch(); 60 | 61 | newPatch.id = currentFrame.id; 62 | newPatch.ts = currentFrame.ts; 63 | newPatch.deflated = currentFrame.deflated; 64 | 65 | let divs = Object.keys(currentFrame.divs); 66 | 67 | divs.forEach(div => { 68 | // console.log(`buildPatch(): div: ${JSON.stringify(div)}`); 69 | let diff = dmp.diff_main(prevFrame.divs[div] || ``, currentFrame.divs[div]); 70 | if (diff.length > 2) { 71 | dmp.diff_cleanupEfficiency(diff); 72 | } 73 | let patchList = dmp.patch_make(prevFrame.divs[div] || ``, diff); 74 | newPatch.divs[div] = dmp.patch_toText(patchList); 75 | }); 76 | 77 | // console.log(`buildPatch(): patch: ${JSON.stringify(newPatch)}`); 78 | 79 | // console.log(`prevframe`, prevFrame); 80 | // console.log(`newpatch`, newPatch); 81 | 82 | return newPatch; 83 | } -------------------------------------------------------------------------------- /src/tv/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Larn TV 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 33 | 34 | 35 |
24 | 25 |
26 | 27 | 28 |
31 |
32 |
36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/tv/watchlive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let liveMetadata; 4 | let liveFrameCache; 5 | let GET_LIVE_INTERVAL = null; 6 | 7 | 8 | function watchLive() { 9 | initLivePlayer(); 10 | 11 | let liveFrame = new Frame(); 12 | liveFrame.divs = { 13 | LARN: `Waiting for our Hero's next move. This could take a minute...`, 14 | STATS: `` 15 | }; 16 | 17 | setStyle(); 18 | bltFrame(liveFrame); 19 | } 20 | 21 | 22 | 23 | function initLivePlayer() { 24 | let body = document.getElementById('TV_FOOTER'); 25 | if (!body) return; 26 | while (body.firstChild) { 27 | body.firstChild.remove(); 28 | } 29 | } 30 | 31 | 32 | 33 | // cloudlare callback 34 | function setLiveStyleCallback(styleIn) { 35 | setStyle(JSON.parse(styleIn)); 36 | } 37 | 38 | 39 | 40 | function bltLiveFrame(data) { 41 | if (!data) return; 42 | 43 | try { 44 | if (data.message) { 45 | let decompressedFrame = LZString.decompressFromUTF16(data.message); 46 | let newFrame = JSON.parse(decompressedFrame); 47 | 48 | setStyle(liveMetadata); 49 | bltFrame(newFrame); 50 | 51 | liveFrameCache = data; 52 | liveMetadata = newFrame.metadata; // TODO: WHY IS THIS DONE *AFTER*? 53 | let exp = liveMetadata.explored.replaceAll(/\s/g, ``); 54 | document.title = `LarnTV: ${liveMetadata.who} ${exp}`; 55 | 56 | } 57 | } 58 | catch (error) { 59 | console.error(`bltLiveFrame: `, error); 60 | } 61 | } 62 | 63 | 64 | 65 | function downloadliveGamesList(downloadCompleteCallback) { 66 | if (!navigator.onLine) { 67 | console.error(`downloadliveGamesList(): offline`); 68 | return; 69 | } 70 | getLive(downloadCompleteCallback); 71 | GET_LIVE_INTERVAL = setInterval(getLive, LIVE_LIST_REFRESH * 1000, downloadCompleteCallback); 72 | } 73 | 74 | 75 | 76 | async function getLive(downloadCompleteCallback) { 77 | if (!navigator.onLine) { 78 | console.error(`getLive(): offline`); 79 | return; 80 | } 81 | try { 82 | const response = await fetch(`${CF_BROADCAST_PROTOCOL}${CF_BROADCAST_HOST}/api/${CF_ACTIVEGAME_ENDPOINT}`); 83 | if (response.ok) { 84 | const liveGames = await response.json(); 85 | downloadCompleteCallback(liveGames); 86 | } else if (response.status == 403) { // for blocking scrapers 87 | console.error(`getLive(): 403 forbidden, stopping requests`); 88 | clearInterval(GET_LIVE_INTERVAL); 89 | } 90 | } catch (error) { 91 | console.error(`getLive(): no data`); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/gotw.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | async function doGOTWStuff() { 4 | console.log(`GOTW enabled`); 5 | 6 | // let date = new Date(); 7 | // for (let i = 0; i < 12; i++) { 8 | // date.setDate(date.getDate() + 7); 9 | // await uploadGOTW(date); 10 | // } 11 | 12 | return await downloadGOTW(); 13 | } 14 | 15 | // 16 | // 17 | // 18 | // UPLOAD_GOTW 19 | // 20 | // 21 | // 22 | async function uploadGOTW(dateIn) { 23 | console.log(`uploadGOTW()`); 24 | 25 | // clear out old game state 26 | LEVELS = new Array(MAXLEVEL + MAXVLEVEL); 27 | EXPLORED_LEVELS = new Array(MAXLEVEL + MAXVLEVEL).fill(false); // cache needed for GOTW games 28 | USED_MAZES = []; 29 | 30 | // generate levels 31 | for (let level = 0; level < LEVELS.length; level++) { 32 | newcavelevel(level); 33 | } 34 | // clear out unneeded data 35 | gameID = 'GOTW'; 36 | logname = 'GOTW'; 37 | player = { 38 | x: rnd(MAXX - 2), 39 | y: rnd(MAXY - 2), 40 | }; 41 | 42 | const filename = getGotwFilename(dateIn); 43 | let state = JSON.stringify(new GameState(true)); 44 | 45 | console.log(`uploadGOTW(): uploading ${filename} (${state.length})`); 46 | const response = await fetch(`${CF_BROADCAST_PROTOCOL}${CF_BROADCAST_HOST}/admin/CLOUDFLARE_ADMIN_PASSWORD/gotw`, { 47 | method: 'PUT', 48 | body: JSON.stringify({ 49 | filename: encodeURIComponent(filename), 50 | state: state, 51 | }), 52 | headers: { 53 | 'Content-Type': 'application/json', 54 | }, 55 | }).catch((err) => console.error('Error uploading GOTW:', err)); 56 | console.log(`GOTW uploaded:`, response); 57 | } 58 | 59 | // 60 | // 61 | // 62 | // DOWNLOAD_GOTW 63 | // 64 | // 65 | // 66 | async function downloadGOTW() { 67 | try { 68 | // no filename given - the server decides what gotw file you are going to get 69 | const response = await fetch(`${CF_BROADCAST_PROTOCOL}${CF_BROADCAST_HOST}/api/gotw/${GAMENAME.toLocaleLowerCase()},${logname},${playerID}`); 70 | const data = await response.json(); 71 | data.status = response.status; 72 | return data; 73 | } catch (error) { 74 | console.error(`downloadGOTW(): Error downloading GOTW:`, error); 75 | return { error: `Error downloading GOTW`, status: 500 }; 76 | } 77 | } 78 | 79 | // totally duplicated in cf_tools.mjs 80 | function getGotwFilename(dateIn) { 81 | let date = dateIn || new Date(); 82 | const year = getISOYear(date); 83 | return `${year}/${GAMENAME}_${getGotwLabel(date)}.json`.toLocaleLowerCase(); 84 | } 85 | 86 | // totally duplicated in cf_tools.mjs 87 | function getGotwLabel(date) { 88 | const weekNumber = getISOWeek(date); 89 | const year = getISOYear(date); 90 | return `${year}_${weekNumber}`; 91 | } 92 | -------------------------------------------------------------------------------- /src/common/live.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | 5 | // send live frame and style metadata from larn to larntv 6 | let lastLiveFrame; 7 | let lastLiveDataTime = 0; 8 | async function processLiveFrame(frame) { 9 | if (!ENABLE_RECORDING_REALTIME) return; 10 | if (!navigator.onLine) return; 11 | if (!game_started) return; 12 | 13 | try { 14 | 15 | // don't send duplicate frames 16 | if (lastLiveFrame && lastLiveFrame.divs.LARN === frame.divs.LARN && lastLiveFrame.divs.STATS === frame.divs.STATS) { 17 | // console.error(`processLiveFrame() dupe`); 18 | return; 19 | } 20 | 21 | // write game metadata to live game list on cloudflare 22 | let nowSeconds = Date.now() / 1000; 23 | let metadata = getGameData(); 24 | if (GAMEOVER || nowSeconds - lastLiveDataTime > LIVE_METADATA_WAIT && player.MOVESMADE % LIVE_METADATA_MOVES === 0) { 25 | let someoneIsWatchingYou = await writeGameData(metadata, gameID); 26 | lastLiveDataTime = nowSeconds; 27 | 28 | // someone has started watching, open a websocket 29 | if (numWatchers === 0 && someoneIsWatchingYou > 0) { 30 | initCloudFlare(gameID, gameID, null); 31 | } 32 | // nobody's watching any more, shut down websocket 33 | if (numWatchers > 0 && someoneIsWatchingYou === 0) { 34 | closeCloudflare(); 35 | } 36 | 37 | numWatchers = someoneIsWatchingYou; 38 | } 39 | 40 | // write current frame to cloudflare 41 | if (numWatchers > 0) { 42 | frame.metadata = metadata; 43 | lastLiveFrame = frame; 44 | sendLiveFrame(frame, true); 45 | } 46 | } catch (error) { 47 | console.error(`processLiveFrame(): disabling realtime recording`, error); 48 | ENABLE_RECORDING_REALTIME = false; 49 | } 50 | 51 | } 52 | 53 | 54 | 55 | // get live game and style metadata 56 | function getGameData() { 57 | if (!player) return; 58 | 59 | let metadata = {}; 60 | 61 | // game data 62 | metadata.gameID = gameID; 63 | metadata.ularn = ULARN; 64 | metadata.build = BUILD; 65 | metadata.difficulty = getDifficulty(); 66 | metadata.mobuls = elapsedtime(); 67 | metadata.who = logname; 68 | metadata.level = LEVELNAMES[level]; 69 | metadata.lastmove = Date.now(); // this gets overwritten by the server because client clocks can be wrong 70 | metadata.framenum = video ? video.currentFrameNum : 0; 71 | metadata.gameover = game_started && GAMEOVER; 72 | let deadreason = player.reason === DIED_SAVED_GAME ? `saved game` : `dead`; 73 | metadata.explored = metadata.gameover ? (player.winner ? `winner` : deadreason) : getExploredLevels(true); 74 | 75 | // display data 76 | let larnElement = document.getElementById(`LARN`); 77 | metadata.fontFamily = getComputedStyle(larnElement).fontFamily; 78 | 79 | return metadata; 80 | } 81 | -------------------------------------------------------------------------------- /src/throne.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /* For command mode. Perform removal of gems from a jeweled throne */ 5 | function remove_gems() { 6 | cursors(); 7 | var item = itemAt(player.x, player.y); 8 | if (item.matches(ODEADTHRONE)) { 9 | updateLog(`There are no gems to remove!`); 10 | } else if (item.matches(OTHRONE)) { 11 | act_remove_gems(item.arg); 12 | } else { 13 | updateLog(`I see no throne here to remove gems from!`); 14 | } 15 | } 16 | 17 | 18 | 19 | /* 20 | act_remove_gems 21 | 22 | Remove gems from a throne. 23 | 24 | arg is zero if there is a gnome king associated with the throne 25 | 26 | Assumes that cursors() has been called previously, and that a check 27 | has been made that the throne actually has gems. 28 | */ 29 | function act_remove_gems(arg) { 30 | var k = rnd(101); 31 | if (k < 25) { 32 | for (var i = 0; i < rnd(4); i++) { 33 | dropItemNearPlayer(createGem()); /* gems pop off the throne */ 34 | } 35 | var throneArg = itemAt(player.x, player.y).arg; 36 | setItem(player.x, player.y, createObject(ODEADTHRONE, throneArg)); 37 | player.level.know[player.x][player.y] = 0; 38 | } else if (k < 40 && arg == 0 && !isGenocided(GNOMEKING)) { 39 | createmonster(GNOMEKING); 40 | itemAt(player.x, player.y).arg = 1; 41 | player.level.know[player.x][player.y] = 0; 42 | } else { 43 | updateLog(` Nothing happens${period}`); 44 | } 45 | return; 46 | } 47 | 48 | 49 | 50 | /* 51 | For command mode. Perform sitting on a throne. 52 | */ 53 | function sit_on_throne() { 54 | cursors(); 55 | var item = itemAt(player.x, player.y); 56 | if (item.matches(OTHRONE)) { 57 | act_sit_throne(item.arg); 58 | } else if (item.matches(ODEADTHRONE)) { 59 | act_sit_dead_throne(item.arg); 60 | } else { 61 | updateLog(`I see no throne to sit on here!`); 62 | } 63 | } 64 | 65 | 66 | 67 | /* 68 | act_sit_throne 69 | 70 | Sit on a throne. 71 | 72 | arg is zero if there is a gnome king associated with the throne 73 | 74 | Assumes that cursors() has been called previously. 75 | */ 76 | function act_sit_throne(arg) { 77 | var k = rnd(101); 78 | if (k < 30 && arg == 0 && !isGenocided(GNOMEKING)) { 79 | createmonster(GNOMEKING); 80 | itemAt(player.x, player.y).arg = 1; 81 | player.level.know[player.x][player.y] = 0; 82 | } else if (k < 35) { 83 | updateLog(` Zaaaappp! You've been teleported!`); 84 | beep(); 85 | oteleport(0); 86 | } else { 87 | updateLog(` Nothing happens${period}`); 88 | } 89 | } 90 | 91 | 92 | 93 | function act_sit_dead_throne(arg) { 94 | if (ULARN) { 95 | var k = rnd(101); 96 | if (k < 5) { 97 | player.raiselevel(); 98 | } else if (k < 25) { 99 | updateLog(` Zaaaappp! You've been teleported!`); 100 | beep(); 101 | oteleport(0); 102 | } else { 103 | updateLog(` Nothing happens${period}`); 104 | } 105 | return; 106 | } else { 107 | act_sit_throne(arg); 108 | } 109 | } -------------------------------------------------------------------------------- /src/tv/_TODO.txt: -------------------------------------------------------------------------------- 1 | 2 | big, amiga, performance is still shit 3 | http://localhost:8000/tv/index_local.html?gameid=sqcfaiblsz 4 | http://localhost:8001/alpha/tv/?gameid=sqcfaiblsz 5 | 6 | first file doesn't exist 7 | http://localhost:8001/alpha/tv/?gameid=jorz4hhxes 8 | 9 | small, good, savegame at 444 10 | http://localhost:8001/alpha/tv/?gameid=jr0ldn7viw 11 | 12 | small, amiga, runs really slow at times 13 | https://larn.org/larn/tv/index.html?gameid=qjrydqx99l 14 | http://localhost:8001/alpha/tv/?gameid=qjrydqx99l 15 | 16 | big, non-amiga, causes RangeError when scrubbing 17 | https://larn.org/alpha/tv/?gameid=uu5wf8u9al 18 | http://localhost:8001/alpha/tv/?gameid=uu5wf8u9al 19 | 20 | amiga, win by me --> use to test "seeking..." 21 | http://localhost:8000/tv/index_local.html?gameid=q73yvndje4 22 | http://localhost:8001/alpha/tv/?gameid=q73yvndje4 23 | 24 | 25 | 26 | bugs: 27 | *** saving a game 3(?) from the end of a buffer causes a null patch, then error on reload 28 | - scrolling on safari is wonky. seems to be easier when paused 29 | - probably need to await fonts loading on replay 30 | - modern font is too wide on replay 31 | - broken replay: https://larn.org/larn/tv/?gameid=d9vaatk563 32 | - can't load alpha mobile game https://larn.org/larn/tv/?gameid=o0fni21j87 33 | - double screen https://larn.org/larn/tv/?gameid=ve9np2m30v 34 | - made game list but doesn't load 53.json empty file also double screen https://larn.org/larn/tv/?gameid=jch32olu09 35 | - final frames are botched: https://larn.org/larn/tv/?gameid=ai2n1hrgnq 36 | - 106.json is broken: https://larn.org/larn/tv/?gameid=n8l1xbhg2j -> coming back from save game 37 | --> bug mitigated, but cause is a missing frame on save (or frame # is one too high on reload) 38 | 39 | todo: 40 | * amiga games are much bigger. don't record them for now. it also totally doesn't work... 41 | - download multiple .json in parallel 42 | - Link from watch screen back to list 43 | - Save playback speed via cookie 44 | - move game replays to cloudflare 45 | - add play options to larntv list 46 | - restrict aws uploads to .json/etc 47 | - compress completed games into a single large file, or a few larger files 48 | - https://dev.to/lineup-ninja/zip-files-on-s3-with-aws-lambda-and-node-1nm1 49 | - https://stackoverflow.com/questions/38633577/create-a-zip-file-on-s3-from-files-on-s3-using-lambda-node/50397276#50397276 50 | - put a size limit on uploads in the lambda 51 | - VIDEO.WRITETOS3() <- TODO this could be basis of cloud savegames too 52 | - hold inventory views (and others?) for a little longer somehow? 53 | - clean up s3/dynamo boilerplate 54 | 55 | live mode: 56 | - support classic fonts 57 | - mobile fonts have big gaps in the walls 58 | - message to viewer when player disconnects? 59 | - # of watchers doesn't reset when cf worker restarted 60 | 61 | 62 | alternative data structure 63 | -------------------------- 64 | recording > bundles > diffs > frames > divs 65 | 66 | div 67 | - id 68 | - content 69 | 70 | frame 71 | - num 72 | - timestamp 73 | - divs[] 74 | - compressedFormat??? 75 | 76 | diff 77 | - num 78 | - timestamp 79 | - divs[] 80 | - uncompressedFormat??? 81 | 82 | bundle 83 | - id 84 | - diffs[] 85 | - compress() 86 | - decompress() 87 | 88 | recording 89 | - gameid 90 | - bundles[] -------------------------------------------------------------------------------- /src/larn.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Play Larn Online - A classic DOS/Amiga/Unix Roguelike Video Game 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | LARN is a dungeon type adventure game similar in concept to HACK, ROGUE or 32 | MORIA, but with a different feel and winning criteria. 33 | 34 | Unfortunately if you're seeing this page it means that there was an error 35 | loading the game. 36 | 37 | This version of Larn requires a browser from circa 2016 to work properly. 38 | Please check to make sure you are running Firefox 53+, Chrome 55+, Edge 15+, 39 | or Safari 11+. Internet Explorer is not supported. 40 | 41 | Sometimes, you'll get this message when a new version is released. You can 42 | usually get the game working again with a full refresh of your browser. 43 | - On Windows: Hold down Ctrl and press F5 44 | - On Mac: 45 | - Chrome & Firefox: Hold down Shift and click the Reload button 46 | - Safari: Hold down the Option and Command key then press the ‘E’ key 47 | 48 | If you're still having trouble, please send a message to eye@larn.org 49 | and I'll get to the bottom of it! 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JS LARN Version 12.5.3 2 | 3 | ## Introduction 4 | 5 | LARN is a dungeon type adventure game similar in concept to HACK, ROGUE 6 | or MORIA, but with a different feel and winning criteria. 7 | 8 | 9 | ## Play Online 10 | [You can play Larn online at larn.org](https://larn.org "You can play Larn online here") 11 | 12 | 13 | ## Play Locally 14 | - [Download a zipfile of the repository](https://github.com/primeau/Larn/archive/refs/heads/master.zip "download a zipfile of the repository") 15 | - Open the `src` folder then open `larn_local.html` in your browser and play 16 | 17 | 18 | ## Play Locally on a Mobile Device 19 | 1. Install the ***iSH Shell*** app 20 | 2. Open iSH Shell and type the following commands: 21 | ``` 22 | # apk update 23 | # apk add python3 24 | # apk add git 25 | # git clone https://github.com/primeau/Larn.git 26 | # python3 -m http.server 27 | ``` 28 | 3. Switch to your browser and view this link (and wait, it might take a while): http://127.0.0.1:8000/Larn/src/larn_local.html 29 | 30 | 31 | ## Build Instructions For Development 32 | ``` 33 | # git clone https://github.com/primeau/Larn.git 34 | # cd Larn 35 | # npm install 36 | # npm run build 37 | # python3 -m http.server 8000 38 | # http://localhost:8000/dist/larn/larn.html 39 | ``` 40 | 41 | ## History and Other Information 42 | Noah Morgan originally created LARN 12.0 and released the UNIX 43 | version to the USENET in 1986. Don Kneller ported the UNIX 44 | version to MSDOS (both IBM PCs and DEC Rainbows). 45 | 46 | Other contributors from this era include: 47 | - James McNamara: UNIX install notes, source and patch distribution 48 | - Fred Fish: Termcap support for VMS port 49 | - Daniel Kegel: Enhanced ansi terminal decoding for DOS 50 | - Alexander Perry: Port for Linux 2.x kernel and GCC 3.x 51 | 52 | Kevin Routley contributed various LARN enhancements. Version 12.1 had 53 | a limited distribution. Version 12.2 was distributed to the Usenet 54 | community. Version 12.3 was the last version released by Kevin. 55 | 56 | Someone made 12.4 through 12.4.2, possibly copx according to 57 | roguebasin. Edwin Denicholas took 12.4 alpha 2 and caressed it into 58 | 12.4.3 for Win32. Version 12.4.4 includes a bugfix from Joe Neff. 59 | 60 | Ularn was written in 1987 by Phil Cordier at UC Santa Cruz. 61 | Version 1.5.1 was released by David Richerby and future 1.5.x versions 62 | were developed by Josh Brandt. Version 1.6 was a Windows 32 conversion 63 | and refactor by Julian Olds. Ularn is currently maintained by Josh Bressers. 64 | 65 | Other editions of Larn have been distributed by others, namely 66 | LARN13, dLarn, NLarn, ReLarn, and XLarn. 67 | 68 | JSLarn 12.5.3 is a JavaScript port of Larn and Ularn, by Jason Primeau. 69 | It primarily references the Larn 12.4.4 and Ularn 1.5.4 codebases but takes 70 | inspiration from other versions as well. It includes bug fixes, a global 71 | scoreboard, watching live replays, a weekly challenge mode, mobile support 72 | and updates to balance the game at higher difficulties. 73 | 74 | I hope you enjoy this version of LARN. 75 | 76 | [Larn's version history can be found here](https://github.com/primeau/Larn/blob/master/history.md "Larn's version history can be found here") 77 | 78 | 79 | ## WIZARD mode 80 | There is a WIZARD mode for testing features of the game. To get into WIZARD 81 | mode, type in an underscore '_' and answer the prompt for the password with 82 | '`pvnert(x)`' (do not enter the quotes). Wizards are non-scoring characters that 83 | get magic mapping, everlasting expanded awareness and one of every object in 84 | the game. 85 | 86 | Since it's easy to lose your game due to a browser crash, accidental back 87 | button press, or closed tab, there are 2 new 'wizard' passwords. Using the 88 | password 'checkpoint' will load a backup of a checkpoint file, and 'savegame' 89 | will resurrect a previous save game. These games are excluded from the 90 | scoreboard due to rampant cheating. -------------------------------------------------------------------------------- /src/stairs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /* 5 | For command mode. Checks that player is actually standing at a set of 6 | up stairs or volcanic shaft. 7 | */ 8 | function up_stairs() { 9 | var item = itemAt(player.x, player.y); 10 | 11 | if (item.matches(OSTAIRSDOWN)) { 12 | updateLog(` The stairs don't go up!`); 13 | dropflag = 1; 14 | } else if (item.matches(OVOLUP)) 15 | act_up_shaft(); 16 | 17 | else if (!item.matches(OSTAIRSUP)) { 18 | updateLog(` I see no way to go up here!`); 19 | dropflag = 1; 20 | } else 21 | act_up_stairs(); 22 | } 23 | 24 | 25 | 26 | /* 27 | For command mode. Checks that player is actually standing at a set of 28 | down stairs or volcanic shaft. 29 | */ 30 | function down_stairs() { 31 | var item = itemAt(player.x, player.y); 32 | 33 | if (item.matches(OSTAIRSUP)) { 34 | updateLog(` The stairs don't go down!`); 35 | dropflag = 1; 36 | } else if (item.matches(OVOLDOWN)) 37 | act_down_shaft(); 38 | 39 | else if (item.matches(OENTRANCE)) 40 | enter(); 41 | 42 | else if (!item.matches(OSTAIRSDOWN)) { 43 | updateLog(` I see no way to go down here!`); 44 | dropflag = 1; 45 | } else 46 | act_down_stairs(); 47 | } 48 | 49 | 50 | 51 | /* 52 | assumes that cursors() has been called and that a check has been made that 53 | the user is actually standing at a set of up stairs. 54 | */ 55 | function act_up_stairs() { 56 | let deadend = level <= 1; // no up on H, D1 57 | deadend |= level == MAXLEVEL; // no up on V1 58 | if (ULARN) deadend |= level == DBOTTOM; // no up on D15 59 | if (!deadend) { 60 | newcavelevel(level - 1); 61 | } else { 62 | updateLog(` The stairs lead to a dead end!`); 63 | dropflag = 1; 64 | } 65 | } 66 | 67 | 68 | 69 | /* 70 | assumes that cursors() has been called and that a check has been made that 71 | the user is actually standing at a set of down stairs. 72 | */ 73 | function act_down_stairs() { 74 | let deadend = level == 0; 75 | deadend |= level == DBOTTOM; // no down at bottom of dungeon 76 | deadend |= level == VBOTTOM; // no down at bottom of volcano 77 | if (ULARN) deadend |= level >= VBOTTOM - 2; // ularn no down stairs on V3/V4 78 | if (!deadend) { 79 | newcavelevel(level + 1); 80 | } else { 81 | updateLog(` The stairs lead to a dead end!`); 82 | dropflag = 1; 83 | } 84 | } 85 | 86 | 87 | 88 | /* 89 | Perform the act of climbing down the volcanic shaft. Assumes 90 | cursors() has been called and that a check has been made that 91 | are actually at a down shaft. 92 | */ 93 | function act_down_shaft() { 94 | setMazeMode(true); 95 | 96 | if (level != 0) { 97 | updateLog(` The shaft only extends 5 feet downward!`); 98 | return; 99 | } 100 | 101 | /* 102 | v12.4.5 - far too many newbie players go into the volcanic shaft, die, 103 | and never play again. this seemed like the least restrictive 104 | way to prevent that from happening. 105 | */ 106 | // if (LEVELS[1] == null && !wizard) { // always true in GOTW games 107 | if (!isLevelVisited(1) && !wizard) { 108 | updateLog(` You feel a foreboding sense of doom, and back away`); 109 | return; 110 | } 111 | 112 | if (packweight() > 45 + 3 * (player.STRENGTH + player.STREXTRA)) { 113 | updateLog(` You slip and fall down the shaft${period}`); 114 | lastnum = DIED_VOLCANO_SLIP; /* slipped on a volcano shaft */ 115 | player.losehp(30 + rnd(20)); 116 | } 117 | 118 | newcavelevel(MAXLEVEL); 119 | // moveNear(OVOLUP, false); // this is a larn 12.0 `feature` 120 | 121 | } 122 | 123 | 124 | 125 | /* 126 | Perform the action of climbing up the volcanic shaft. Assumes 127 | cursors() has been called and that a check has been made that 128 | are actually at an up shaft. 129 | */ 130 | function act_up_shaft() { 131 | setMazeMode(true); 132 | 133 | if (level != MAXLEVEL) { 134 | updateLog(` The shaft only extends 8 feet upwards before you find a blockage!`); 135 | return; 136 | } 137 | 138 | if (packweight() > (ULARN ? 40 : 45) + 5 * (player.STRENGTH + player.STREXTRA)) { 139 | updateLog(` You slip and fall down the shaft${period}`); 140 | lastnum = DIED_VOLCANO_SLIP; /* slipped on a volcano shaft */ 141 | player.losehp(15 + rnd(20)); 142 | return; 143 | } 144 | 145 | newcavelevel(0); 146 | moveNear(OVOLDOWN, false); 147 | } 148 | -------------------------------------------------------------------------------- /src/larn.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: modern; 3 | 4 | /* experiments to try fixing too-wide fonts on non-chrome browsers https://developer.mozilla.org/en-US/docs/Learn/CSS/Styling_text/Fundamentals */ 5 | /* for future font-noodling https://fonts.google.com/?category=Monospace&preview.text=a%E2%96%92%E2%96%92%E2%96%92%C2%B7@%C2%B7%E2%96%92%C2%B7%E2%96%92A!!0%3F&preview.text_type=custom */ 6 | 7 | /* works in firefox, not safari */ 8 | /* src: url('img/PTMono-Regular.ttf'); */ 9 | 10 | /* no firefox or safari */ 11 | src: url('img/NotoSansMono-Regular.ttf'); 12 | /* src: url('img/NotoSansMono-Light.ttf'); */ 13 | } 14 | 15 | @font-face { 16 | font-family: dos437; 17 | src: url('img/dos437.ttf'); 18 | } 19 | 20 | @font-face { 21 | font-family: amiga500; 22 | src: url('img/TopazPlus_a500_v1.0.ttf'); 23 | } 24 | 25 | @font-face { 26 | font-family: amiga1200; 27 | src: url('img/TopazPlus_a1200_v1.0.ttf'); 28 | } 29 | 30 | a:link { 31 | color: #bbbbbb; 32 | text-decoration: none; 33 | } 34 | 35 | a:visited { 36 | color: #FF00FF; 37 | text-decoration: none; 38 | } 39 | 40 | a:hover { 41 | color: #FF77FF; 42 | text-decoration: none; 43 | } 44 | 45 | a:active { 46 | color: #EE0000; 47 | text-decoration: none; 48 | } 49 | 50 | b, 51 | strong { 52 | color: lightgray; 53 | } 54 | 55 | body { 56 | /* border: 1px solid orange; */ 57 | font-family: 'dos', 'Courier New', Courier, monospace; 58 | margin: 0px; 59 | background-color: #000000; 60 | } 61 | 62 | div { 63 | background-position: center center; 64 | background-size: 100% 100%; 65 | background-repeat: no-repeat; 66 | } 67 | 68 | .image { 69 | display: inline-block; 70 | background-image: url('img/o94.png'); 71 | } 72 | 73 | mark { 74 | background-color: lightgrey; 75 | color: black; 76 | } 77 | 78 | .button, 79 | .narrowbutton, 80 | .variablebutton, 81 | .verticalbutton { 82 | /* color: black; 83 | background-color: #FFD700; */ 84 | color: lightgrey; 85 | background-color: #0071e3; 86 | border: none; 87 | text-align: center; 88 | text-decoration: none; 89 | display: inline-block; 90 | cursor: pointer; 91 | -webkit-touch-callout: none; 92 | -webkit-user-select: none; 93 | -khtml-user-select: none; 94 | -moz-user-select: none; 95 | -ms-user-select: none; 96 | appearance: none; 97 | -moz-appearance: none; 98 | -webkit-appearance: none; 99 | user-select: none; 100 | } 101 | 102 | .narrowbutton { 103 | width: 45px; 104 | } 105 | 106 | .button { 107 | width: 70px; 108 | } 109 | 110 | .verticalbutton { 111 | display: block; 112 | width: 100%; 113 | height: 100%; 114 | } 115 | 116 | .button:disabled, 117 | .narrowbutton:disabled, 118 | .variablebutton:disabled, 119 | .verticalbutton:disabled, 120 | .button[disabled], 121 | .narrowbutton[disabled], 122 | .variablebutton[disabled], 123 | .verticalbutton[disabled] { 124 | border: 1px solid #999999; 125 | background-color: #cccccc; 126 | color: #666666; 127 | cursor: not-allowed; 128 | pointer-events: none; 129 | } 130 | 131 | 132 | 133 | .box, 134 | .larngrid { 135 | position: absolute; 136 | /* border: 1px solid white; */ 137 | -webkit-user-select: none; 138 | -webkit-touch-callout: none; 139 | -moz-user-select: none; 140 | -ms-user-select: none; 141 | user-select: none; 142 | } 143 | 144 | 145 | larn { 146 | white-space: pre; 147 | margin: 0px; 148 | /* border: 1px solid green; */ 149 | } 150 | 151 | larngrid { 152 | /* border: 1px solid blue; */ 153 | width: 100%; 154 | height: 100%; 155 | } 156 | 157 | foot { 158 | /* border: 1px solid cyan; */ 159 | } 160 | 161 | inventorybuttons { 162 | /* border: 1px solid pink; */ 163 | position: static; 164 | } 165 | 166 | keyboard { 167 | /* border: 1px solid lightblue; */ 168 | } 169 | 170 | help { 171 | /* border: 1px solid violet; */ 172 | } 173 | 174 | stats { 175 | white-space: pre; 176 | /* border: 1px solid yellow; */ 177 | } 178 | 179 | movebuttons { 180 | /* border: 1px solid orange; */ 181 | } 182 | 183 | contextbuttons { 184 | /* border: 1px solid brown; */ 185 | } 186 | 187 | run { 188 | /* border: 1px solid red; */ 189 | /* background: white; */ 190 | transform-origin: top left; 191 | rotate: 90deg; 192 | } -------------------------------------------------------------------------------- /src/lib/lz-string.min.js: -------------------------------------------------------------------------------- 1 | var LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;ne;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return"";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;ie;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u)}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var t,i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(t=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return"";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else{if(l!==d)return null;v=s+s.charAt(0)}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString); -------------------------------------------------------------------------------- /src/regen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* subroutine to regenerate player hp and spells */ 4 | function regen() { 5 | 6 | if (!player) return; 7 | 8 | player.MOVESMADE++; 9 | 10 | /* for stop time spell */ 11 | if (player.TIMESTOP > 0) { 12 | if (player.updateTimeStop(-1) <= 0) recalc(); 13 | return; 14 | } 15 | 16 | // is this where gtime should be incremented? 17 | 18 | if (player.HP != player.HPMAX) { 19 | if (player.REGENCOUNTER-- <= 0) /* regenerate hit points */ { 20 | player.REGENCOUNTER = 22 + (getDifficulty() << 1) - player.LEVEL; 21 | player.raisehp(player.REGEN); 22 | } 23 | } 24 | 25 | /* regenerate spells */ 26 | if (player.SPELLS < player.SPELLMAX) { 27 | if (player.ECOUNTER-- <= 0) { 28 | player.ECOUNTER = 100 + 4 * (getDifficulty() - player.LEVEL - player.ENERGY); 29 | player.setSpells(player.SPELLS + 1); 30 | } 31 | } 32 | 33 | if (player.HERO) { 34 | if (--player.HERO <= 0) { 35 | player.setStrength(player.STRENGTH - 10); 36 | player.setIntelligence(player.INTELLIGENCE - 10); 37 | player.setWisdom(player.WISDOM - 10); 38 | player.setConstitution(player.CONSTITUTION - 10); 39 | player.setDexterity(player.DEXTERITY - 10); 40 | player.setCharisma(player.CHARISMA - 10); 41 | } 42 | } 43 | 44 | if (player.COKED) { 45 | if (--player.COKED <= 0) { 46 | player.setStrength(player.STRENGTH - 34); 47 | player.setIntelligence(player.INTELLIGENCE - 34); 48 | player.setWisdom(player.WISDOM - 34); 49 | player.setConstitution(player.CONSTITUTION - 34); 50 | player.setDexterity(player.DEXTERITY - 34); 51 | player.setCharisma(player.CHARISMA - 34); 52 | } 53 | } 54 | 55 | if (player.STEALTH) player.updateStealth(-1); 56 | if (player.UNDEADPRO) player.updateUndeadPro(-1); 57 | if (player.SPIRITPRO) player.updateSpiritPro(-1); 58 | if (player.CHARMCOUNT) player.updateCharmCount(-1); 59 | if (player.HOLDMONST) player.updateHoldMonst(-1); 60 | if (player.FIRERESISTANCE) player.updateFireResistance(-1); 61 | if (player.SCAREMONST) player.updateScareMonst(-1); 62 | if (player.HASTESELF) player.updateHasteSelf(-1); 63 | if (player.CANCELLATION) player.updateCancellation(-1); 64 | if (player.INVISIBILITY) player.updateInvisibility(-1); 65 | if (player.WTW) player.updateWTW(-1); 66 | 67 | if (player.GIANTSTR) player.updateGiantStr(-1); 68 | if (player.DEXCOUNT) player.updateDexCount(-1); 69 | if (player.STRCOUNT) player.updateStrCount(-1); 70 | if (player.ALTPRO) player.updateAltPro(-1); 71 | if (player.PROTECTIONTIME) player.updateProtectionTime(-1); 72 | 73 | if (player.GLOBE) if (--player.GLOBE <= 0) player.setMoreDefenses(player.MOREDEFENSES - 10); 74 | if (player.BLINDCOUNT) if (--player.BLINDCOUNT <= 0) updateLog(`The blindness lifts${period}`); 75 | if (player.CONFUSE) if (--player.CONFUSE <= 0) updateLog(`You regain your senses${period}`); 76 | if (player.HALFDAM) if (--player.HALFDAM <= 0) updateLog(`You now feel better${period}`); 77 | 78 | if (player.AGGRAVATE) --player.AGGRAVATE; 79 | if (player.AWARENESS) if (!isCarrying(OORB)) --player.AWARENESS; 80 | if (player.HASTEMONST) --player.HASTEMONST; 81 | 82 | if (player.SEEINVISIBLE) { 83 | if (ULARN) { 84 | if (isCarrying(OAMULET)) { 85 | /* See inv doesn't wear off if player has amulet of invisibility */ 86 | player.SEEINVISIBLE++; 87 | } 88 | } 89 | if (--player.SEEINVISIBLE <= 0) { 90 | if (!player.BLINDCOUNT) { 91 | updateLog(`You feel your vision return to normal${period}`); 92 | } 93 | } 94 | } 95 | 96 | if (player.ITCHING) { 97 | if (player.ITCHING > 1) 98 | if ((player.WEAR) || (player.SHIELD)) 99 | if (rnd(100) < 50) { 100 | player.WEAR = null; 101 | player.SHIELD = null; 102 | updateLog(`The hysteria of itching forces you to remove your armor!`); 103 | } 104 | if (--player.ITCHING <= 0) { 105 | updateLog(`You now feel the irritation subside!`); 106 | } 107 | } 108 | 109 | if (player.CLUMSINESS) { 110 | if (player.WIELD) 111 | if (player.CLUMSINESS > 1) 112 | if (itemAt(player.x, player.y).matches(OEMPTY)) /* only if nothing there */ 113 | if (rnd(100) < 33) {/* drop your weapon due to clumsiness */ 114 | var dropindex = getCharFromIndex(player.inventory.indexOf(player.WIELD)); 115 | drop_object(dropindex); 116 | } 117 | if (--player.CLUMSINESS <= 0) { 118 | updateLog(`You now feel less awkward!`); 119 | } 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/lib/mousetrap.min.js: -------------------------------------------------------------------------------- 1 | /* mousetrap v1.6.3 craig.is/killing/mice */ 2 | (function(q,u,c){function v(a,b,g){a.addEventListener?a.addEventListener(b,g,!1):a.attachEvent("on"+b,g)}function z(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return n[a.which]?n[a.which]:r[a.which]?r[a.which]:String.fromCharCode(a.which).toLowerCase()}function F(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function w(a){return"shift"==a||"ctrl"==a||"alt"==a|| 3 | "meta"==a}function A(a,b){var g,d=[];var e=a;"+"===e?e=["+"]:(e=e.replace(/\+{2}/g,"+plus"),e=e.split("+"));for(g=0;gc||n.hasOwnProperty(c)&&(p[n[c]]=c)}g=p[e]?"keydown":"keypress"}"keypress"==g&&d.length&&(g="keydown");return{key:m,modifiers:d,action:g}}function D(a,b){return null===a||a===u?!1:a===b?!0:D(a.parentNode,b)}function d(a){function b(a){a= 4 | a||{};var b=!1,l;for(l in p)a[l]?b=!0:p[l]=0;b||(x=!1)}function g(a,b,t,f,g,d){var l,E=[],h=t.type;if(!k._callbacks[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(l=0;l":".","?":"/","|":"\\"},B={option:"alt",command:"meta","return":"enter", 9 | escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p;for(c=1;20>c;++c)n[111+c]="f"+c;for(c=0;9>=c;++c)n[c+96]=c.toString();d.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};d.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};d.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};d.prototype.reset=function(){this._callbacks={}; 10 | this._directMap={};return this};d.prototype.stopCallback=function(a,b){if(-1<(" "+b.className+" ").indexOf(" mousetrap ")||D(b,this.target))return!1;if("composedPath"in a&&"function"===typeof a.composedPath){var c=a.composedPath()[0];c!==a.target&&(b=c)}return"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};d.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};d.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b]);p=null}; 11 | d.init=function(){var a=d(u),b;for(b in a)"_"!==b.charAt(0)&&(d[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};d.init();q.Mousetrap=d;"undefined"!==typeof module&&module.exports&&(module.exports=d);"function"===typeof define&&define.amd&&define(function(){return d})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null); -------------------------------------------------------------------------------- /src/state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // TODO, make this the canonical source for data? 4 | // i.e. replace cheat = ... with status.cheat etc. 5 | 6 | 7 | /* additions for JS Larn */ 8 | var LEVELS; 9 | let EXPLORED_LEVELS; 10 | var LOG; 11 | var player; 12 | var playerID; 13 | var playerIP = `0`; 14 | var PARAMS = {}; 15 | var recording = {}; 16 | var showConfigButtons = true; 17 | 18 | var newsphereflag = false; /* JRP hack to not move sphere twice after cast */ 19 | var GAMEOVER = true; 20 | var game_started = false; 21 | var mazeMode = false; 22 | var napping = false; /* prevent keyboard input while a nap event is happening */ 23 | var original_objects = true; 24 | var keyboard_hints = false; 25 | var auto_pickup = false; 26 | var side_inventory = true; 27 | var show_color = true; 28 | var bold_objects = true; 29 | var retro_mode = false; 30 | var dnd_item = null; 31 | var genocide = []; 32 | var amiga_mode = false; 33 | var gameID = Math.random().toString(36).substr(2, 10); 34 | // var gameID = `testgameid`; 35 | var debug_used = 0; 36 | 37 | var logname = `Adventurer`; 38 | var cheat = 0; /* 1 if the player has fudged save file */ 39 | var level = -1; /* cavelevel player is on = cdesc[CAVELEVEL] */ 40 | var wizard = 0; /* the wizard mode flag */ 41 | var gtime = 0; /* the clock for the game */ 42 | var HARDGAME = 0; /* game difficulty */ 43 | 44 | /* these function were added as a defensive measure to find a pesky bug, 45 | and now it's easier to just leave them here 46 | */ 47 | function getDifficulty() { 48 | if (HARDGAME == null || HARDGAME === `` || isNaN(Number(HARDGAME))) { 49 | console.log('get: invalid difficulty: ' + HARDGAME); 50 | console.trace(); 51 | } 52 | return Math.min(128, HARDGAME); 53 | } 54 | 55 | function setDifficulty(diff) { 56 | if (diff == null || diff === `` || isNaN(Number(diff))) { 57 | console.log('set: invalid difficulty: ' + diff); 58 | console.trace(); 59 | } 60 | if (diff > 128) { 61 | console.log(`capping difficulty at 128`); 62 | diff = 128; 63 | } 64 | HARDGAME = diff; 65 | } 66 | 67 | var lastmonst = ``; /* name of the last monster to hit the player */ 68 | var lastnum = 0; /* the number of the monster last hitting player */ 69 | var hitflag = 0; /* flag for if player has been hit when running */ 70 | var lastpx = 0; 71 | var lastpy = 0; 72 | var lasthx = 0; /* location of monster last hit by player */ 73 | var lasthy = 0; /* location of monster last hit by player */ 74 | var prayed = 1; /* did player pray at an altar? */ 75 | var course = []; /* the list of courses taken */ 76 | var outstanding_taxes = 0; /* present tax bill from score file */ 77 | var dropflag = 0; /* if 1 then don't lookforobject() next round */ 78 | var rmst = 120; /* random monster creation counter */ 79 | var nomove = 0; /* if (nomove) then don't count next iteration as a move */ 80 | var viewflag = 0; /* if viewflag then we have done a 99 stay here and don't showcell in the main loop */ 81 | var lasttime = 0; /* last time in bank */ 82 | var spheres = []; 83 | 84 | 85 | 86 | function GameState(save) { 87 | this.LEVELS = LEVELS; 88 | this.LOG = LOG; 89 | this.player = player; 90 | 91 | if (ENABLE_RECORDING) { 92 | this.recording = getRecordingInfo(); 93 | if (save) { 94 | this.recording.frames += 3; // hack because three more frames get added before a game is done saving 95 | } else { 96 | // this.recording.frames -= 1; // hack because 1 frame is added before a game is loaded 97 | this.recording.rolls -= 1; // hack because 1 roll is added before a game is loaded 98 | } 99 | } 100 | 101 | this.newsphereflag = newsphereflag; 102 | this.GAMEOVER = GAMEOVER; 103 | this.mazeMode = mazeMode; 104 | this.napping = napping; 105 | this.original_objects = original_objects; 106 | this.keyboard_hints = keyboard_hints; 107 | this.auto_pickup = auto_pickup; 108 | this.side_inventory = side_inventory; 109 | this.show_color = show_color; 110 | this.bold_objects = bold_objects; 111 | this.dnd_item = dnd_item; 112 | this.genocide = genocide; 113 | this.amiga_mode = amiga_mode; 114 | this.gameID = gameID; 115 | this.debug_used = debug_used; 116 | 117 | this.logname = logname; 118 | this.cheat = cheat; 119 | this.level = level; 120 | this.wizard = wizard; 121 | this.gtime = gtime; 122 | this.HARDGAME = getDifficulty(); 123 | this.lastmonst = lastmonst; 124 | this.lastnum = lastnum; 125 | this.hitflag = hitflag; 126 | this.lastpx = lastpx; 127 | this.lastpy = lastpy; 128 | this.lasthx = lasthx; 129 | this.lasthy = lasthy; 130 | this.prayed = prayed; 131 | this.course = course; 132 | this.outstanding_taxes = outstanding_taxes; 133 | this.dropflag = dropflag; 134 | this.rmst = rmst; 135 | this.nomove = nomove; 136 | this.viewflag = viewflag; 137 | this.lasttime = lasttime; 138 | this.spheres = spheres; 139 | } -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function setGameConfig() { 4 | 5 | // GAME NAME 6 | GAMENAME = ULARN ? `Ularn` : `Larn`; 7 | 8 | // TIME 9 | TIMELIMIT = ULARN ? 40000 : 30000; 10 | 11 | // DUNGEON AND VOLCANO LEVELS 12 | MAXLEVEL = ULARN ? 16 : 11; 13 | MAXVLEVEL = ULARN ? 5 : 3; 14 | DBOTTOM = (MAXLEVEL - 1); 15 | VBOTTOM = (MAXLEVEL + MAXVLEVEL - 1); 16 | 17 | // MAZES 18 | LEVELS = new Array(MAXLEVEL + MAXVLEVEL); 19 | EXPLORED_LEVELS = new Array(MAXLEVEL + MAXVLEVEL).fill(false); // cache needed for GOTW games 20 | 21 | MAZES = COMMON_MAZES.concat(ULARN ? ULARN_MAZES : LARN_MAZES); 22 | 23 | // MONSTERS 24 | monsterlist = ULARN ? ULARN_monsterlist : LARN_monsterlist; 25 | 26 | // SPELLS 27 | splev = ULARN ? ULARN_splev : LARN_splev; 28 | spelweird = ULARN ? ULARN_spelweird : LARN_spelweird; 29 | 30 | // BUILDINGS 31 | STORE_INVENTORY = ULARN ? ULARN_STORE_INVENTORY : LARN_STORE_INVENTORY; 32 | MAXITM = STORE_INVENTORY.length; 33 | 34 | MAX_BANK_BALANCE = ULARN ? 1000000 : 500000; 35 | OBANK.desc = `the bank of ${GAMENAME}`; 36 | OBANK2.desc = `the ${ULARN ? 8 : 5}th branch of the Bank of ${GAMENAME}`; 37 | 38 | OSCHOOL.desc = `the College of ${GAMENAME}`; 39 | 40 | OLRS.desc = `the ${GAMENAME} Revenue Service`; 41 | 42 | if (ULARN) OTRADEPOST.desc = `the Ularn trading Post`; 43 | 44 | // ITEMS 45 | FORTUNES = COMMON_FORTUNES.concat(ULARN ? ULARN_FORTUNES : LARN_FORTUNES); 46 | 47 | if (ULARN) { 48 | DEATH_REASONS.set(DIED_BOTTOMLESS_TRAPDOOR, `fell through a trap door to HELL`); 49 | DEATH_REASONS.set(DIED_BOTTOMLESS_PIT, `fell into a pit to HELL`); 50 | } 51 | 52 | // MISC NIT-PICKING FOR MESSAGES 53 | youFound = ULARN ? `You find` : `You have found`; 54 | period = ULARN ? `.` : ``; 55 | 56 | // MONSTER COLOURS 57 | if (ULARN) monsterlist[LEMMING].color = `rosybrown`; else monsterlist[BAT].color = `brown`; 58 | monsterlist[GNOME].color = `darkkhaki`; 59 | monsterlist[HOBGOBLIN].color = `salmon`; 60 | monsterlist[JACKAL].color = `sandybrown`; 61 | if (ULARN) monsterlist[KOBOLD].color = `brown`; else monsterlist[KOBOLD].color = `rosybrown`; 62 | monsterlist[ORC].color = `tan`; 63 | monsterlist[SNAKE].color = `olivedrab`; 64 | monsterlist[CENTIPEDE].color = `orangered`; 65 | monsterlist[JACULI].color = `burlywood`; 66 | monsterlist[TROGLODYTE].color = `navajowhite`; 67 | monsterlist[ANT].color = `indianred`; 68 | monsterlist[EYE].color = `mediumorchid`; 69 | monsterlist[LEPRECHAUN].color = `mediumseagreen`; 70 | monsterlist[NYMPH].color = `lightpink`; 71 | monsterlist[QUASIT].color = `yellowgreen`; 72 | monsterlist[RUSTMONSTER].color = `goldenrod`; 73 | monsterlist[ZOMBIE].color = `gray`; 74 | monsterlist[ASSASSINBUG].color = `forestgreen`; 75 | if (ULARN) monsterlist[BITBUG].color = `chocolate`; else monsterlist[BUGBEAR].color = `chocolate`; 76 | monsterlist[HELLHOUND].color = `crimson`; 77 | monsterlist[ICELIZARD].color = `white`; 78 | monsterlist[CENTAUR].color = `sandybrown`; 79 | monsterlist[TROLL].color = `seagreen`; 80 | monsterlist[YETI].color = `mintcream`; 81 | monsterlist[WHITEDRAGON].color = `snow`; 82 | monsterlist[ELF].color = `mediumseagreen`; 83 | monsterlist[CUBE].color = `turquoise`; 84 | monsterlist[METAMORPH].color = `moccasin`; 85 | monsterlist[VORTEX].color = `lightcyan`; 86 | monsterlist[ZILLER].color = `cadetblue`; 87 | monsterlist[VIOLETFUNGI].color = `violet`; 88 | monsterlist[WRAITH].color = `dimgray`; 89 | monsterlist[FORVALAKA].color = `thistle`; 90 | if (ULARN) monsterlist[LAMANOBE].color = `powderblue`; else monsterlist[LAWLESS].color = `powderblue`; 91 | monsterlist[OSEQUIP].color = null; 92 | monsterlist[ROTHE].color = `sienna`; 93 | monsterlist[XORN].color = `brown`; 94 | monsterlist[VAMPIRE].color = `slategray`; 95 | monsterlist[INVISIBLESTALKER].color = null; 96 | monsterlist[POLTERGEIST].color = `lavender`; 97 | monsterlist[DISENCHANTRESS].color = `antiquewhite`; 98 | monsterlist[SHAMBLINGMOUND].color = `olivedrab`; 99 | monsterlist[YELLOWMOLD].color = `palegoldenrod`; 100 | monsterlist[UMBERHULK].color = `peru`; 101 | monsterlist[GNOMEKING].color = `steelblue`; 102 | monsterlist[MIMIC].color = `beige`; 103 | monsterlist[WATERLORD].color = `cornflowerblue`; 104 | monsterlist[BRONZEDRAGON].color = `goldenrod`; 105 | monsterlist[GREENDRAGON].color = `palegreen`; 106 | monsterlist[PURPLEWORM].color = `purple`; 107 | monsterlist[XVART].color = `teal`; 108 | monsterlist[SPIRITNAGA].color = `mediumslateblue`; 109 | monsterlist[SILVERDRAGON].color = `paleturquoise`; 110 | monsterlist[PLATINUMDRAGON].color = `lightsteelblue`; 111 | monsterlist[GREENURCHIN].color = `lime`; 112 | monsterlist[REDDRAGON].color = `indianred`; 113 | 114 | } -------------------------------------------------------------------------------- /src/common/lambda.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function invokeLambda(requestPayload, successCallback, failCallback) { 4 | if (AWS.config.accessKeyId === `AWS_CONFIG_ACCESSKEYID`) { 5 | console.log(`AWS credentials not set`); 6 | return; 7 | } 8 | 9 | let params = { 10 | FunctionName: AWS_RECORD_FUNCTION, 11 | Payload: JSON.stringify(requestPayload), 12 | InvocationType: 'RequestResponse', 13 | LogType: 'None' 14 | }; 15 | 16 | try { 17 | lambda.invoke(params, function (error, data) { 18 | if (!error) { 19 | if (data) { 20 | if (data.StatusCode == 200) { 21 | if (successCallback) { 22 | let responsePayload = JSON.parse(data.Payload); 23 | successCallback(responsePayload.body, responsePayload.File, responsePayload.Metadata); 24 | } 25 | } else { 26 | console.error(`invokeLambda(): error ${data.StatusCode}`); 27 | updateMessage(`couldn't load: ${data.StatusCode}`); 28 | if (failCallback) failCallback(data.StatusCode); 29 | } 30 | } 31 | } else { 32 | console.error(`invokeLambda(): error: ${error}`); 33 | updateMessage(`${error}`); 34 | if (failCallback) failCallback(error); 35 | } 36 | }); 37 | } catch (error) { 38 | console.error(`invokeLambda():`, error); 39 | } 40 | 41 | } 42 | 43 | 44 | 45 | 46 | async function invokeLambdaAsync(payload) { 47 | if (AWS.config.accessKeyId === `AWS_CONFIG_ACCESSKEYID`) { 48 | console.log(`AWS credentials not set`); 49 | return; 50 | } 51 | 52 | const params = { 53 | FunctionName: AWS_RECORD_FUNCTION, 54 | Payload: JSON.stringify(payload), 55 | InvocationType: 'RequestResponse', 56 | LogType: 'None' 57 | }; 58 | 59 | try { 60 | const response = await lambda.invoke(params).promise(); 61 | 62 | let payload = {}; 63 | if (response.StatusCode === 200 && response.Payload) { 64 | try { 65 | payload = JSON.parse(response.Payload); 66 | } catch (parseError) { 67 | payload = { rawPayload: response.Payload }; 68 | } 69 | if (payload.statusCode === 200) { 70 | return payload; 71 | } else { 72 | console.error(`invokeLambdaAsync(): payload error: ${payload.statusCode}`); 73 | return null; 74 | } 75 | } else { 76 | console.error(`invokeLambdaAsync(): response error: ${response.StatusCode}`); 77 | return null; 78 | } 79 | } catch (error) { 80 | console.error(`invokeLambdaAsync():`, error); 81 | } 82 | } 83 | 84 | 85 | 86 | function lambdaFail(err) { 87 | console.log(`lambdaFail(): `, err); 88 | } 89 | 90 | 91 | 92 | function loadStyles(gameID, successCallback) { 93 | console.log(`loadStyles()`); 94 | let requestPayload = { 95 | action: `loadstyle`, 96 | gameID: gameID 97 | }; 98 | invokeLambda(requestPayload, successCallback, null); 99 | } 100 | 101 | 102 | 103 | function downloadFile(gameID, filename, successCallback, failCallback) { 104 | console.log(`downloadFile(): ${gameID}/${filename}`); 105 | let requestPayload = { 106 | action: `read`, 107 | gameID: gameID, 108 | filename: filename, 109 | }; 110 | invokeLambda(requestPayload, successCallback, failCallback); 111 | } 112 | 113 | async function downloadFileAsync(gameID, filename) { 114 | console.log(`downloadFileAsync(): ${gameID}/${filename}`); 115 | let requestPayload = { 116 | action: `read`, 117 | gameID: gameID, 118 | filename: filename, 119 | }; 120 | const response = await invokeLambdaAsync(requestPayload); 121 | if (!response) { 122 | console.error(`downloadFileAsync(): ${gameID}/${filename} empty file`); 123 | } 124 | return response; 125 | } 126 | 127 | 128 | 129 | async function downloadRoll(gameID, rollNum) { 130 | let filename = `${rollNum}.json`; 131 | // console.log(`downloadRoll(): ${gameID}/${filename}`); 132 | let data = await downloadFileAsync(gameID, filename); 133 | if (data && data.File) { 134 | let roll = decompressRoll(data.File); 135 | roll.metadata = data.Metadata; 136 | return roll; 137 | } else { 138 | console.error(`downloadRoll(): ${gameID}/${filename} empty file`); 139 | return null; 140 | } 141 | } 142 | 143 | 144 | async function downloadStyleInfo(gameID) { 145 | let filename = `${gameID}.css`; 146 | let data = await downloadFileAsync(gameID, filename); 147 | if (data && data.File) { 148 | const styleInfo = JSON.parse(data.File); 149 | return styleInfo; 150 | } else { 151 | console.error(`downloadStyleInfo(): ${gameID}/${filename} empty file`); 152 | return null; 153 | } 154 | } 155 | 156 | 157 | 158 | function uploadFile(gameID, filename, filecontents, isLastFile, metadata) { 159 | if (!filecontents) { 160 | console.error(`uploadFile(): no file contents for ${gameID}/${JSON.stringify(filename)}`); 161 | return; 162 | } 163 | console.log(`uploadFile(): ${gameID}/${filename} (${filecontents.length})`); 164 | let requestPayload = { 165 | action: isLastFile ? `writelast` : `write`, 166 | gameID: gameID, 167 | filename: filename, 168 | file: filecontents, 169 | Metadata: metadata 170 | }; 171 | invokeLambda(requestPayload, null, null); 172 | } -------------------------------------------------------------------------------- /src/io.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cursorx = 1; 4 | var cursory = 1; 5 | 6 | var display = initGrid(80, 24); 7 | 8 | 9 | 10 | function lprintf(str, width) { 11 | if (width != null) { 12 | lprcat(padString(str, width)); 13 | } else { 14 | lprcat(str); 15 | } 16 | } 17 | 18 | 19 | var START_MARK = ``; 20 | var END_MARK = ``; 21 | var START_BOLD = ``; 22 | var END_BOLD = ``; 23 | 24 | 25 | 26 | function lprint(str) { 27 | lprcat(str); 28 | blt(); 29 | } 30 | 31 | 32 | 33 | function lprcat(str, width) { 34 | DEBUG_LPRCAT++; 35 | 36 | if (alternativeDisplay) { 37 | alternativeDisplay += str; 38 | return; 39 | } 40 | 41 | if (width) { 42 | lprintf(str, width); 43 | return; 44 | } 45 | 46 | // Some people, when confronted with a problem, think, “I know, 47 | // I'll use regular expressions.” Now they have two problems. 48 | 49 | var markup = null; 50 | var len = str.length; 51 | 52 | for (var i = 0; i < len; i++) { 53 | var c = str[i]; 54 | 55 | if (c === '<') { 56 | if (str.substr(i, 3).toLowerCase() === START_BOLD) { 57 | markup = START_BOLD; 58 | i += 2; 59 | if (amiga_mode) { 60 | continue; 61 | } else { 62 | c = START_BOLD; 63 | } 64 | } else if (str.substr(i, 4).toLowerCase() === END_BOLD) { 65 | markup = null; 66 | i += 3; 67 | if (amiga_mode) { 68 | continue; 69 | } else { 70 | let diff = 1; 71 | if (cursorx > 80) diff = cursorx - 80; 72 | cursorx -= diff; 73 | c = str.substr(i - 3 - diff, diff) + END_BOLD; 74 | } 75 | } else if (str.substr(i, 6).toLowerCase() === START_MARK) { 76 | markup = START_MARK; 77 | i += 5; 78 | if (amiga_mode) { 79 | continue; 80 | } else { 81 | c = START_MARK; 82 | } 83 | } else if (str.substr(i, 7).toLowerCase() === END_MARK) { 84 | markup = null; 85 | i += 6; 86 | if (amiga_mode) { 87 | continue; 88 | } else { 89 | let diff = 1; 90 | if (cursorx > 80) diff = cursorx - 80; 91 | cursorx -= diff; 92 | c = str.substr(i - 6 - diff, diff) + END_MARK; 93 | } 94 | } 95 | } 96 | 97 | lprc(c, markup); 98 | 99 | } 100 | } 101 | 102 | 103 | 104 | function cursor(x, y) { 105 | cursorx = x; 106 | cursory = y; 107 | } 108 | 109 | 110 | 111 | function cursors() { 112 | cursor(1, 24); 113 | } 114 | 115 | 116 | 117 | function lprc(ch, markup) { 118 | DEBUG_LPRC++; 119 | 120 | if (alternativeDisplay) { 121 | alternativeDisplay += ch; 122 | return; 123 | } 124 | 125 | if (ch == '\b') { 126 | cursorx--; 127 | os_put_font(' ', cursorx - 1, cursory - 1); 128 | } else if (ch == '\n') { 129 | cursorx = 1; 130 | cursory++; 131 | } else { 132 | // var n = 1; 133 | // if (ch == '\t') { 134 | // ch = ' '; 135 | // n = 4; 136 | // } 137 | // while (n--) { 138 | os_put_font(ch, cursorx - 1, cursory - 1, markup); 139 | cursorx++; 140 | // } 141 | } 142 | } 143 | 144 | 145 | var HACK_URL_TEXT = `url`; 146 | 147 | function os_put_font(ch, x, y, markup) { 148 | if (x >= 0 && x < 80 && y >= 0 && y < 24) { 149 | if (!amiga_mode) { 150 | 151 | if (DEBUG_PROXIMITY) { 152 | if (!screen[x] || !screen[x][y] || screen[x][y] == 127 || screen[x][y] == 0) { 153 | // do nothing 154 | } else { 155 | if (!monsterAt(x, y)) { 156 | ch = (screen[x][y] < 10) ? `` + screen[x][y] : `` + (screen[x][y] % 10); 157 | } 158 | else { 159 | ch = `${ch}`; 160 | } 161 | } 162 | if (x == player.x && y == player.y) ch = `#`; 163 | } 164 | 165 | display[x][y] = ch; 166 | 167 | // TODO: setup for not repainting in text mode 168 | // TODO: need to update io.js:os_put_font(), display.js:blt(), larn.js:play() 169 | // TODO: this will break scoreboard rendering 170 | if (altrender) { 171 | setChar(x, y, ch, markup); 172 | } 173 | 174 | } else { 175 | ch = `${ch}`; // workaround: amiga larn bank buttons are numbers, not strings 176 | 177 | // HACK HACk HAck Hack hack 178 | if (ch.substring(0, 3) === HACK_URL_TEXT) { 179 | setImage(x, y, ch); 180 | } else { 181 | setChar(x, y, ch, markup); 182 | } 183 | } 184 | } 185 | } 186 | 187 | 188 | 189 | function clear() { 190 | cl_dn(1, 1); 191 | } 192 | 193 | 194 | 195 | function cltoeoln() { 196 | var x = cursorx; 197 | var n = 80 + 1 - x; 198 | while (n-- > 0) { 199 | lprc(' ', null); 200 | setImage(80 - n - 1, cursory - 1, OUNKNOWN.getChar()); 201 | } 202 | cursorx = x; 203 | } 204 | 205 | 206 | 207 | function cl_up(x, y) { 208 | for (var i = 1; i <= y; i++) { 209 | cursor(1, i); 210 | cltoeoln(); 211 | } 212 | cursor(x, y); 213 | } 214 | 215 | 216 | 217 | function cl_dn(x, y) { 218 | for (var i = y; i <= 24; i++) { 219 | cursor(1, i); 220 | cltoeoln(); 221 | } 222 | cursor(x, y); 223 | } 224 | 225 | 226 | 227 | function lflush() { 228 | LOG = Array(LOG_SAVE_SIZE).join(' ').split(''); 229 | } 230 | -------------------------------------------------------------------------------- /src/larn_local.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Play Larn Online - A classic DOS/Amiga/Unix Roguelike Video Game 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | LARN is a dungeon type adventure game similar in concept to HACK, ROGUE or 84 | MORIA, but with a different feel and winning criteria. 85 | 86 | Unfortunately if you're seeing this page it means that there was an error 87 | loading the game. 88 | 89 | This version of Larn requires a browser from circa 2016 to work properly. 90 | Please check to make sure you are running Firefox 53+, Chrome 55+, Edge 15+, 91 | or Safari 11+. Internet Explorer is not supported. 92 | 93 | Sometimes, you'll get this message when a new version is released. You can 94 | usually get the game working again with a full refresh of your browser. 95 | - On Windows: Hold down Ctrl and press F5 96 | - On Mac: 97 | - Chrome & Firefox: Hold down Shift and click the Reload button 98 | - Safari: Hold down the Option and Command key then press the ‘E’ key 99 | 100 | If you're still having trouble, please send a message to eye@larn.org 101 | and I'll get to the bottom of it! 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "larn", 3 | "version": "12.5.2", 4 | "description": "JS Larn", 5 | "main": "larn.js", 6 | "scripts": { 7 | "clean": "npm run tempclean ; npm run distclean", 8 | "tempclean": "rm -rf larn-temp || true", 9 | "distclean": "rm -rf dist || true", 10 | "alphaclean": "rm -rf ../larn-deploy/alpha || true", 11 | "betaclean": "rm -rf ../larn-deploy/beta || true", 12 | "prodclean": "rm -rf ../larn-deploy/larn || true", 13 | "histclean": "rm -rf ../larn-deploy/history || true", 14 | "imgclean": "rm -rf ../larn-deploy/img || true", 15 | "webclean": "rm ../larn-deploy/*.xml ; rm ../larn-deploy/*.png ; rm ../larn-deploy/*.html ; rm ../larn-deploy/*.css ; rm ../larn-deploy/*.txt ; rm ../larn-deploy/*.md || true", 16 | "deployclean": "npm run clean ; npm run alphaclean ; npm run betaclean ; npm run prodclean ; npm run webclean ; npm run histclean ; npm run imgclean", 17 | 18 | "transpile": "babel src --out-dir larn-temp/ ; ./secrets.sh 'larn-temp' || true", 19 | "minify": "uglifyjs --mangle --compress -o larn-temp/larn.min.js larn-temp/common/util.js larn-temp/common/larn_config.js larn-temp/common/frame.js larn-temp/common/patch.js larn-temp/common/roll.js larn-temp/common/lambda.js larn-temp/common/cloudflare.js larn-temp/common/movie.js larn-temp/common/live.js larn-temp/config.js larn-temp/larn.js larn-temp/main.js larn-temp/object.js larn-temp/global.js larn-temp/monster.js larn-temp/monsterdata.js larn-temp/player.js larn-temp/mazes.js larn-temp/level.js larn-temp/create.js larn-temp/data.js larn-temp/parse.js larn-temp/buttons.js larn-temp/scores.js larn-temp/inventory.js larn-temp/movem.js larn-temp/action.js larn-temp/io.js larn-temp/display.js larn-temp/storedata.js larn-temp/store.js larn-temp/mcdopes.js larn-temp/savelev.js larn-temp/spells.js larn-temp/spellsinfo.js larn-temp/regen.js larn-temp/spheres.js larn-temp/help.js larn-temp/state.js larn-temp/bill.js larn-temp/altar.js larn-temp/fountain.js larn-temp/potion.js larn-temp/scroll.js larn-temp/stairs.js larn-temp/throne.js larn-temp/devmode.js larn-temp/fullstory.js larn-temp/gotw.js", 20 | "minifyscore": "uglifyjs --mangle --compress -o larn-temp/score/score.min.js larn-temp/common/util.js larn-temp/common/larn_config.js larn-temp/larn.js larn-temp/player.js larn-temp/data.js larn-temp/object.js larn-temp/state.js larn-temp/inventory.js larn-temp/potion.js larn-temp/scroll.js larn-temp/savelev.js larn-temp/score/score.js", 21 | 22 | "_copy": "echo $envt ; mkdir -p dist/$envt/lib ; mkdir dist/$envt/img ; mkdir dist/$envt/workers ; cp src/lib/*.js dist/$envt/lib ; cp src/img/* dist/$envt/img ; cp src/larn.html dist/$envt ; cp src/larn.css dist/$envt ; cp larn-temp/larn.min.js dist/$envt ; cp larn-temp/workers/*.js dist/$envt/workers || true", 23 | "alphacopy": "export envt=alpha ; npm run _copy", 24 | "betacopy": "export envt=beta ; npm run _copy", 25 | "prodcopy": "export envt=larn ; npm run _copy", 26 | "webcopy": "cp -rv ../larn-www/* ../larn-deploy || true", 27 | "_tvcopy": "mkdir -p dist/$envt/tv/common ; cp larn-temp/tv/* dist/$envt/tv ; cp larn-temp/common/* dist/$envt/tv/common ; cp src/tv/index.html dist/$envt/tv ; cp src/tv/larntv.css dist/$envt/tv || true", 28 | "_scorecopy": "mkdir -p dist/$envt/score ; cp larn-temp/score/* dist/$envt/score ; cp src/score/index.html dist/$envt/score ; cp src/score/score.css dist/$envt/score || true", 29 | 30 | "alphatv": "export envt=alpha ; npm run _tvcopy", 31 | "betatv": "export envt=beta ; npm run _tvcopy", 32 | "prodtv": "export envt=larn ; npm run _tvcopy", 33 | 34 | "alphascore": "export envt=alpha ; npm run _scorecopy", 35 | "betascore": "export envt=beta ; npm run _scorecopy", 36 | "prodscore": "export envt=larn ; npm run _scorecopy", 37 | 38 | "_dist": "echo $envt ; rm -rf ../larn-deploy/$envt ; mv dist/$envt ../larn-deploy || true", 39 | "distalpha": "export envt=alpha ; npm run _dist", 40 | "distbeta": "export envt=beta ; npm run _dist", 41 | "distprod": "export envt=larn ; npm run _dist", 42 | "distweb": "npm run webclean ; npm run webcopy || true", 43 | "dist": "npm run distalpha ; npm run distbeta ; npm run distprod ; npm run distweb || true", 44 | 45 | "buildalpha": "npm run alphaclean ; npm run transpile ; npm run minify ; npm run minifyscore ; npm run alphacopy ; npm run alphatv ; npm run alphascore ; npm run distalpha", 46 | "buildbeta": "npm run betaclean ; npm run transpile ; npm run minify ; npm run minifyscore ; npm run betacopy ; npm run betatv ; npm run betascore ; npm run distbeta", 47 | "buildprod": "npm run prodclean ; npm run transpile ; npm run minify ; npm run minifyscore ; npm run prodcopy ; npm run prodtv ; npm run prodscore ; npm run distprod", 48 | 49 | "build": "echo '=== STARTING BUILD ==' ; npm run distclean ; npm run transpile ; npm run minify ; npm run minifyscore ; npm run prodcopy ; npm run tempclean", 50 | "buildall": "echo '=== STARTING FULL BUILD ==' ; npm run deployclean ; npm run transpile ; npm run minify ; npm run minifyscore ; npm run alphacopy ; npm run betacopy ; npm run prodcopy ; npm run alphatv ; npm run betatv ; npm run prodtv ; npm run alphascore ; npm run betascore ; npm run prodscore ; npm run dist ; npm run clean", 51 | 52 | "watch": "npm run tempclean ; babel src --watch --out-dir larn-temp/", 53 | "localserver": "cd ../larn-local ; python3 -m http.server 8000", 54 | "deployserver": "cd ../larn-deploy ; python3 -m http.server 8001" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "git+https://github.com/primeau/Larn.git" 59 | }, 60 | "keywords": [ 61 | "larn", 62 | "ularn", 63 | "roguelike", 64 | "game" 65 | ], 66 | "author": "Jason Primeau", 67 | "license": "MIT", 68 | "bugs": { 69 | "url": "https://github.com/primeau/Larn/issues" 70 | }, 71 | "homepage": "https://github.com/primeau/Larn#readme", 72 | "dependencies": {}, 73 | "devDependencies": { 74 | "@babel/cli": "^7.14.5", 75 | "@babel/core": "^7.14.5", 76 | "@babel/preset-env": "^7.14.5", 77 | "uglify-js": "^3.13.9" 78 | } 79 | } -------------------------------------------------------------------------------- /src/mcdopes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* inventory */ 4 | var drug = [true, true, true, true, true]; 5 | 6 | 7 | 8 | /* 9 | function to display the header info for the pad 10 | */ 11 | function pad_hd() { 12 | cl_up(1, 18); 13 | cursor(1, 1); 14 | lprcat(`Hey man, welcome to Dealer McDope's Pad! I gots the some of the finest shit\n`); 15 | lprcat(`you'll find anywhere in Ularn -- check it out...\n\n\n`); 16 | lprcat(` The Stash The Cash\n\n`); 17 | 18 | if (drug[0]) lprcat(` a) Killer Speed 100 bucks`); 19 | lprc(`\n`); 20 | if (drug[1]) lprcat(` b) Groovy Acid 250 bucks`); 21 | lprc(`\n`); 22 | if (drug[2]) lprcat(` c) Monster Hash 500 bucks`); 23 | lprc(`\n`); 24 | if (drug[3]) lprcat(` d) Trippy Shrooms 1000 bucks`); 25 | lprc(`\n`); 26 | if (drug[4]) lprcat(` e) Cool Coke 5000 bucks`); 27 | lprc(`\n`); 28 | 29 | let plural = player.GOLD == 1 ? `` : `s`; 30 | cursor(30, 18); 31 | lprcat(`Looks like you got about ${player.GOLD} buck${plural} on you. `); 32 | 33 | lprcat(`\n\nSo, whaddya want [escape to split] ?`); 34 | } 35 | 36 | 37 | 38 | function opad() { 39 | setMazeMode(false); 40 | 41 | // clear(); 42 | pad_hd(); 43 | setCharCallback(parse_mcdopes); 44 | } 45 | 46 | 47 | 48 | function parse_mcdopes(key) { 49 | if (key == ESC) { 50 | return exitbuilding(); 51 | } 52 | 53 | if (!isalpha(key)) return false; 54 | 55 | cursor(39, 20); 56 | lprc(`${key}\n`); 57 | 58 | switch (key) { 59 | case `a`: 60 | /* speed */ 61 | dodeal(OSPEED, 100, 0); 62 | break; 63 | case `b`: 64 | /* acid */ 65 | dodeal(OACID, 250, 1); 66 | break; 67 | case `c`: 68 | /* hash */ 69 | dodeal(OHASH, 500, 2); 70 | break; 71 | case `d`: 72 | /* shrooms */ 73 | dodeal(OSHROOMS, 1000, 3); 74 | break; 75 | case `e`: 76 | /* coke */ 77 | dodeal(OCOKE, 5000, 4); 78 | break; 79 | } /* end switch */ 80 | 81 | pad_hd(); 82 | return false; 83 | } /* end pad() */ 84 | 85 | 86 | 87 | function dodeal(whichdrug, price, index) { 88 | if (!drug[index]) { 89 | nomore(); 90 | return; 91 | } 92 | if (player.GOLD < price) { 93 | nocash(); 94 | return; 95 | } 96 | else if (snag(whichdrug)) { 97 | player.GOLD -= price; 98 | drug[index] = false; 99 | } 100 | } 101 | 102 | 103 | 104 | function snag(itm) { 105 | if (pocketfull()) { 106 | lprcat(`\nHey, you can't carry any more.`); 107 | cltoeoln(); 108 | return false; 109 | } 110 | let contraband = createObject(itm); 111 | take(contraband); 112 | lprcat(`\nOk, here ya go.`); 113 | cltoeoln(); 114 | return true; 115 | } 116 | 117 | 118 | 119 | function nomore() { 120 | lprcat(`\nSorry man, I ain't got no more of that shit.`); 121 | cltoeoln(); 122 | // nap(2200); 123 | } 124 | 125 | 126 | 127 | function nocash() { 128 | lprcat(`\nWhattaya trying to pull on me? You aint got the cash!`); 129 | cltoeoln(); 130 | // nap(1200); 131 | } 132 | 133 | 134 | 135 | function doSpeed() { 136 | appendLog(` snort!`); 137 | updateLog(`Ohwowmanlikethingstotallyseemtoslowdown!`); 138 | player.updateHasteSelf(200 + player.LEVEL); 139 | player.HALFDAM += 300 + rnd(200); 140 | player.setIntelligence(player.INTELLIGENCE - 2); 141 | player.setWisdom(player.WISDOM - 2); 142 | player.setConstitution(player.CONSTITUTION - 2); 143 | player.setDexterity(player.DEXTERITY - 2); 144 | player.setStrength(player.STRENGTH - 2); 145 | } 146 | 147 | 148 | 149 | function eatShrooms() { 150 | appendLog(` eat!`); 151 | updateLog(`Things start to get real spacey...`); 152 | player.HASTEMONST += rnd(75) + 25; 153 | player.CONFUSE += 30 + rnd(10); 154 | player.setWisdom(player.WISDOM + 2); 155 | player.setCharisma(player.CHARISMA + 2); 156 | 157 | } 158 | 159 | 160 | 161 | function dropAcid() { 162 | appendLog(` eat!`); 163 | updateLog(`You are now frying your ass off!`); 164 | player.CONFUSE += 30 + rnd(10); 165 | player.setIntelligence(player.INTELLIGENCE + 2); 166 | player.setWisdom(player.WISDOM + 2); 167 | player.AWARENESS += 1500; 168 | player.AGGRAVATE += 1500; 169 | // heal monsters 170 | for (let j = 0; j < MAXY; j++) 171 | for (let i = 0; i < MAXX; i++) 172 | if (player.level.monsters[i][j]) 173 | player.level.monsters[i][j].hitpoints = monsterlist[player.level.monsters[i][j].arg].hitpoints; 174 | } 175 | 176 | 177 | 178 | function smokeHash() { 179 | appendLog(` smoke!`); 180 | updateLog(`WOW! You feel stooooooned...`); 181 | player.HASTEMONST += rnd(75) + 25; 182 | player.setIntelligence(player.INTELLIGENCE + 2); 183 | player.setWisdom(player.WISDOM + 2); 184 | player.setConstitution(player.CONSTITUTION - 2); 185 | player.setDexterity(player.DEXTERITY - 2); 186 | player.HALFDAM += 300 + rnd(200); 187 | player.CLUMSINESS += rnd(1800) + 200; 188 | } 189 | 190 | 191 | 192 | function doCoke() { 193 | appendLog(` snort!`); 194 | updateLog(`Your nose begins to bleed!`); 195 | player.setDexterity(player.DEXTERITY - 2); 196 | player.setConstitution(player.CONSTITUTION - 2); 197 | player.setCharisma(player.CHARISMA + 3); 198 | 199 | player.setStrength(player.STRENGTH + 33); 200 | player.setIntelligence(player.INTELLIGENCE + 33); 201 | player.setWisdom(player.WISDOM + 33); 202 | player.setConstitution(player.CONSTITUTION + 33); 203 | player.setDexterity(player.DEXTERITY + 33); 204 | player.setCharisma(player.CHARISMA + 33); 205 | 206 | player.COKED += 10; 207 | } 208 | 209 | 210 | -------------------------------------------------------------------------------- /src/lib/rollbar.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var _rollbarConfig = { 3 | accessToken: "3ff371a699404d4c9ec0d02f09252ee8", 4 | captureUncaught: true, 5 | captureUnhandledRejections: true, 6 | payload: { 7 | environment: "production" 8 | } 9 | }; 10 | // Rollbar Snippet 11 | (()=>{"use strict";var r={349:r=>{r.exports={captureUncaughtExceptions:function(r,o,e){if(r){var n;if("function"==typeof o._rollbarOldOnError)n=o._rollbarOldOnError;else if(r.onerror){for(n=r.onerror;n._rollbarOldOnError;)n=n._rollbarOldOnError;o._rollbarOldOnError=n}o.handleAnonymousErrors();var t=function(){var e=Array.prototype.slice.call(arguments,0);!function(r,o,e,n){r._rollbarWrappedError&&(n[4]||(n[4]=r._rollbarWrappedError),n[5]||(n[5]=r._rollbarWrappedError._rollbarContext),r._rollbarWrappedError=null);var t=o.handleUncaughtException.apply(o,n);e&&e.apply(r,n),"anonymous"===t&&(o.anonymousErrorsPending+=1)}(r,o,n,e)};e&&(t._rollbarOldOnError=n),r.onerror=t}},captureUnhandledRejections:function(r,o,e){if(r){"function"==typeof r._rollbarURH&&r._rollbarURH.belongsToShim&&r.removeEventListener("unhandledrejection",r._rollbarURH);var n=function(r){var e,n,t;try{e=r.reason}catch(r){e=void 0}try{n=r.promise}catch(r){n="[unhandledrejection] error getting `promise` from event"}try{t=r.detail,!e&&t&&(e=t.reason,n=t.promise)}catch(r){}e||(e="[unhandledrejection] error getting `reason` from event"),o&&o.handleUnhandledRejection&&o.handleUnhandledRejection(e,n)};n.belongsToShim=e,r._rollbarURH=n,r.addEventListener("unhandledrejection",n)}}}},202:r=>{function o(r,e){this.impl=r(e,this),this.options=e,function(r){for(var o=function(r){return function(){var o=Array.prototype.slice.call(arguments,0);if(this.impl[r])return this.impl[r].apply(this.impl,o)}},e="log,debug,info,warn,warning,error,critical,global,configure,handleUncaughtException,handleAnonymousErrors,handleUnhandledRejection,_createItem,wrap,loadFull,shimId,captureEvent,captureDomContentLoaded,captureLoad".split(","),n=0;n{var n=e(349),t=e(965);function a(r){return function(){try{return r.apply(this,arguments)}catch(r){try{console.error("[Rollbar]: Internal error",r)}catch(r){}}}}var l=0;function i(r,o){this.options=r,this._rollbarOldOnError=null;var e=l++;this.shimId=function(){return e},"undefined"!=typeof window&&window._rollbarShims&&(window._rollbarShims[e]={handler:o,messages:[]})}var s=e(202),d=function(r,o){return new i(r,o)},p=function(r){return new s(d,r)};function c(r){return a((function(){var o=Array.prototype.slice.call(arguments,0),e={shim:this,method:r,args:o,ts:new Date};window._rollbarShims[this.shimId()].messages.push(e)}))}i.prototype.loadFull=function(r,o,e,n,t){var l=!1,i=o.createElement("script"),s=o.getElementsByTagName("script")[0],d=s.parentNode;i.crossOrigin="",i.src=n.rollbarJsUrl,e||(i.async=!0),i.onload=i.onreadystatechange=a((function(){if(!(l||this.readyState&&"loaded"!==this.readyState&&"complete"!==this.readyState)){i.onload=i.onreadystatechange=null;try{d.removeChild(i)}catch(r){}l=!0,function(){var o;if(void 0===r._rollbarDidLoad){o=new Error("rollbar.js did not load");for(var e,n,a,l,i=0;e=r._rollbarShims[i++];)for(e=e.messages||[];n=e.shift();)for(a=n.args||[],i=0;i{r.exports=function(r){return function(o){if(!o&&!window._rollbarInitialized){for(var e,n,t=(r=r||{}).globalAlias||"Rollbar",a=window.rollbar,l=function(r){return new a(r)},i=0;e=window._rollbarShims[i++];)n||(n=e.handler),e.handler._swapAndProcessMessages(l,e.messages);window[t]=n,window._rollbarInitialized=!0}}}},965:r=>{function o(r,o,e){if(o.hasOwnProperty&&o.hasOwnProperty("addEventListener")){for(var n=o.addEventListener;n._rollbarOldAdd&&n.belongsToShim;)n=n._rollbarOldAdd;var t=function(o,e,t){n.call(this,o,r.wrap(e),t)};t._rollbarOldAdd=n,t.belongsToShim=e,o.addEventListener=t;for(var a=o.removeEventListener;a._rollbarOldRemove&&a.belongsToShim;)a=a._rollbarOldRemove;var l=function(r,o,e){a.call(this,r,o&&o._rollbar_wrapped||o,e)};l._rollbarOldRemove=a,l.belongsToShim=e,o.removeEventListener=l}}r.exports=function(r,e,n){if(r){var t,a,l="EventTarget,Window,Node,ApplicationCache,AudioTrackList,ChannelMergerNode,CryptoOperation,EventSource,FileReader,HTMLUnknownElement,IDBDatabase,IDBRequest,IDBTransaction,KeyOperation,MediaController,MessagePort,ModalWindow,Notification,SVGElementInstance,Screen,TextTrack,TextTrackCue,TextTrackList,WebSocket,WebSocketWorker,Worker,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload".split(",");for(t=0;t{var r=e(758),o=e(157);_rollbarConfig=_rollbarConfig||{},_rollbarConfig.rollbarJsUrl=_rollbarConfig.rollbarJsUrl||"https://cdn.rollbar.com/rollbarjs/refs/tags/v2.26.4/rollbar.min.js",_rollbarConfig.async=void 0===_rollbarConfig.async||_rollbarConfig.async;var n=r.setupShim(window,_rollbarConfig),t=o(_rollbarConfig);window.rollbar=r.Rollbar,n.loadFull(window,document,!_rollbarConfig.async,_rollbarConfig,t)})()})(); 12 | // End Rollbar Snippet -------------------------------------------------------------------------------- /ULARN_SPOILERS.md: -------------------------------------------------------------------------------- 1 | # Ularn Spoiler Guide (Updated 2020) 2 | 3 | Adapted from Phil Cordier's web site: 4 | http://www.cordier.com/ularn/index.html 5 | 6 | 7 | ## A Note on Lemmings - 2020 8 | Lemmings are annoying, but they have been part of the game for 30+ years and 9 | they aren't being changed now. Your best strategy is to fight them in a 10 | dead-end hallway so they can't reproduce. Also, don't forget that they can 11 | be Rambo's best friend. 12 | 13 | ## Lance of Death & Strategy 14 | 15 | In Larn, the basic strategy was to get enough money to buy a Lance of Death, 16 | usually by getting to dungeon level 10, killing or getting around the demon 17 | lord, and getting and then selling the Eye of Larn. This would usually get 18 | you enough money to buy the Lance from the dnd store. You could then go into 19 | the volcano and kill everything in sight with one hit from the Lance. 20 | Well, the Lance is still deadly to all monsters. Demons, on the other hand, 21 | are merely tickled. And there are a LOT more demons (and relatives) in Ularn 22 | than in Larn. 23 | 24 | ## Dungeon and Volcano levels 25 | 26 | There are now 15 levels to the Dungeon, and 5 to the Volcano. Larn had 10 27 | and 3. 28 | 29 | Levels 11 - 15 of the dungeon contain, in addition to all the other nasty 30 | monsters that show up at low levels: 31 | - level 11: 1 demon lord 32 | - level 12: 2 demon lords 33 | - level 13: 3 demon lords 34 | - level 14: 4 demon lords 35 | - level 15: 5 demon lords + 1 demon prince guarding the Eye of Larn. 36 | 37 | The stairs up from level 15 lead to a dead end. You'll need to find another 38 | way up. 39 | 40 | Levels 1 - 5 of the Volcano contain: 41 | - level 1: 1 demon prince 42 | - level 2: 2 demon princes 43 | - level 3: 3 demon princes 44 | - level 4: 4 demon princes 45 | - level 5: 5 demon princes + 1 God of Hellfire guarding the potion of cure 46 | dianthroritis. 47 | 48 | In addition, there are no stairs from level 3 to 4, or from 4 to 5. The only 49 | way to go down is via a pit or trap door. There is guaranteed at least 1 50 | bottomless pit and bottomless trap door on level 5. The rock on levels 3, 4, 51 | and 5 cannot be destroyed via a vaporize rock spell, or in any other way. 52 | 53 | ## Magic Items 54 | 55 | Many of the following magic items will appear in the game: 56 | 57 | #### Eye of Larn 58 | 59 | The Eye now has a very beneficial magic property. If you are carrying the 60 | Eye, demon lords, demon princes, and the God of Hellfire are visible to you. 61 | Otherwise, they are invisible. They will show as follows: 62 | 63 | Monster Character on screen 64 | - Type I Demon Lord: 1 65 | - Type II DemonLord: 2 66 | - Type III Demon Lord: 3 67 | - Type IV Demon Lord: 4 68 | - Type V Demon Lord: 5 69 | - Type VI Demon Lord: 6 70 | - Type VII Demon Lord: 7 71 | - Demon Prince: 9 72 | - God of Hellfire: 0 73 | 74 | #### Sword of Slashing 75 | The Sword of Slashing is quite strong and light, and is impervious to rust. 76 | It raises dexterity by 5. 77 | 78 | #### Bessman's Flailing Hammer 79 | The Hammer is the strongest weapon in the game (excluding the Lance of 80 | Death). It lowers your intelligence by 10, but raises your dexterity and 81 | strength by 10. 82 | 83 | #### Vorpal Blade 84 | The Vorpal Blade is a good all-around weapon that has a 1/20 chance of 85 | beheading any monster that has a head. 86 | 87 | #### Orb of Enlightenment 88 | Carrying the Orb gives permanent expanded awareness. 89 | 90 | #### Orb of Dragon Slaying 91 | Causes attacks against dragons to be much more effective. 92 | 93 | #### Scarab of Negate Spirit 94 | Spirit Nagas and Poltergeists damage is halved if you have the Scarab of 95 | Negate spirit. 96 | 97 | #### Amulet of Invisibility 98 | Causes the 'inv' spell to last much longer. Also extends see invisible while 99 | being carried. 100 | 101 | #### Cube of Undead Control 102 | Vampires, Wraiths, and Zombies damage is halved you have the Cube of Undead 103 | Control. In addition, they can perform no special attacks upon you, like 104 | draining your experience level. 105 | 106 | #### Device of Theft Prevention 107 | If you have this, Nymphs and Disenchantresses cannot steal anything from 108 | you. 109 | 110 | #### Brass Lamp 111 | A genie lives in the brass lamp. If you are lucky, by rubbing the lamp, the 112 | genie will grant you a spell. Be warned though, the genie does not usually 113 | like to be disturbed, and may react unpleasantly. If the genie disappears 114 | (along with the lamp) without granting you a wish, it is still possible to 115 | find him again. But if you are granted a wish, the genie will not wish to be 116 | disturbed again (you only get one wish). 117 | 118 | #### Hand of Fear 119 | Causes the scare monster spell to last much longer. 120 | 121 | #### Talisman of the Sphere 122 | Normally, demons can dispel spheres of annihilation, disenchantresses can 123 | cancel them, and if you have the spell 'cancel' cast, that may cancel the 124 | sphere too. You can also die by touching the sphere. 125 | Carrying the Talisman revokes all of the above 126 | 127 | #### Wand of Wonder 128 | You will not fall down any pits or trap doors if you are carrying the Wand 129 | of Wonder. 130 | 131 | #### Staff of Power 132 | Carrying the staff of power will cancel any attack by a demon lord, 133 | demon prince, wraith, or vampire 75% of the time. It increases wisdom by 10. 134 | 135 | #### Slayer 136 | Demon lords and demon princes attacks are halved if you are wielding the sword 137 | Slayer. Slayer essentially acts as a Lance of Death, but only against 138 | demons. It is otherwise a good strong weapon against other monsters. You 139 | will only find Slayer somewhere below dungeon level 10, or in the volcano. 140 | It increases intelligence by 10. 141 | 142 | #### Elven Chain 143 | Strong and light, impervious to rust. 144 | 145 | #### Amulet of Life Preservation (added in Ularn 1.6) 146 | Prevents drain life attacks. 147 | 148 | ## New spell 149 | I've added a 'make wall' spell, that creates a wall in the place you 150 | specify. The code is 'mkw'. 151 | 152 | ## Elevators 153 | 154 | Watch out for express elevators. They can be good or bad, depending on which 155 | direction they go (up or down). 156 | 157 | ## Time limit 158 | 159 | The time limit for solving the game has been increased from 300 mobuls to 160 | 400 mobuls. 161 | 162 | ## Dealer McDope's 163 | On request from several unsavory types here at UC Santa Cruz, I added Dealer 164 | McDope's Pad. He will show up usually somewhere in the lower level of the 165 | dungeon. Visiting Dealer McDope's will offer you the chance to buy such 166 | interesting and stimulating items as: 167 | - Killer Speed 168 | - Groovy Acid 169 | - Monster Hash 170 | - Trippy Shrooms 171 | - Cool Coke 172 | 173 | You "use" a drug by dropping it, and then moving on top of it for more 174 | instructions. Drugs have varied effects. They can be fun, but are generally 175 | not worth the trouble. 176 | 177 | ## NOTES 178 | - NEVER NEVER cast 'vpr' when you are next to an altar! 179 | - Interesting things may happen when you sit on a dead throne. 180 | - Ularn hates a cheapskate at the altar. 181 | -------------------------------------------------------------------------------- /src/spheres.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* this is the structure for maintaining & moving the spheres of annihilation */ 4 | var Sphere = function (x, y, dir, lifetime, lev) { 5 | this.x = x; /* location of the sphere */ 6 | this.y = y; 7 | this.level = lev; 8 | this.direction = dir; /* direction sphere is going in */ 9 | this.lifetime = lifetime; /* duration of the sphere */ 10 | //console.log(`${x} ${y} ${this.level} ${lifetime}`) 11 | }; 12 | 13 | 14 | 15 | /* 16 | * newsphere(x,y,dir,lifetime) Function to create a new sphere of annihilation 17 | * int x,y,dir,lifetime; 18 | * 19 | * Enter with the coordinates of the sphere in x,y 20 | * the direction (0-8 diroffx format) in dir, and the lifespan of the 21 | * sphere in lifetime (in turns) 22 | * Returns the number of spheres currently in existence 23 | */ 24 | function newsphere(x, y, dir, life, lev) { 25 | if (dir >= 9) dir = 0; /* no movement if direction not found */ 26 | if (lev == 0) { 27 | /* don't go out of bounds */ 28 | x = vx(x); 29 | y = vy(y); 30 | } else { 31 | if (x < 1) x = 1; 32 | if (x >= MAXX - 1) x = MAXX - 2; 33 | if (y < 1) y = 1; 34 | if (y >= MAXY - 1) y = MAXY - 2; 35 | } 36 | 37 | var monster = monsterAt(x, y); 38 | 39 | if (!isCarrying(OSPHTALISMAN)) { // talisman of the sphere negates many things 40 | if (monster && monster.isDemon()) /* demons dispel spheres */ { 41 | show1cell(x, y); /* show the demon (ha ha) */ 42 | cursors(); 43 | updateLog(`The ${monster} dispels the sphere!`); 44 | rmsphere(x, y); /* remove any spheres that are here */ 45 | return; 46 | } 47 | if (monster && monster.matches(DISENCHANTRESS)) /* disenchantress cancels spheres */ { 48 | cursors(); 49 | updateLog(`The ${monster} causes cancellation of the sphere!`); 50 | sphboom(x, y); /* blow up stuff around sphere */ 51 | rmsphere(x, y); /* remove any spheres that are here */ 52 | return; 53 | } 54 | if (player.CANCELLATION) /* cancellation cancels spheres */ { 55 | cursors(); 56 | updateLog(`As the cancellation spell takes effect, you hear a great earth shaking blast!`); 57 | sphboom(x, y); /* blow up stuff around sphere */ 58 | rmsphere(x, y); /* remove any spheres that are here */ 59 | return; 60 | } 61 | if (player.x == x && player.y == y) /* collision of sphere and player! */ { 62 | cursors(); 63 | updateLog(`You have been enveloped by the zone of nothingness!`); 64 | rmsphere(x, y); /* remove any spheres that are here */ 65 | //nap(2000); 66 | died(DIED_ANNIHILATED_SELF, false); /* self - annihilated */ 67 | return; 68 | } 69 | } 70 | 71 | if (itemAt(x, y).matches(OANNIHILATION)) /* collision of spheres detonates spheres */ { 72 | cursors(); 73 | updateLog(`Two spheres of annihilation collide! You hear a great earth shaking blast!`); 74 | sphboom(x, y); /* blow up stuff around sphere */ 75 | rmsphere(x, y); /* remove any spheres that are here */ 76 | return; 77 | } 78 | 79 | setItem(x, y, OANNIHILATION); 80 | 81 | updateWalls(x, y, 1); 82 | 83 | /* talisman protects monsters too */ 84 | if (monster && monster.isCarrying(OSPHTALISMAN)) { 85 | updateLog(`The ${monster} is unaffected by the sphere of annihilation!`); 86 | } 87 | else { 88 | killMonster(x,y); 89 | } 90 | 91 | player.level.know[x][y] = 1; 92 | show1cell(x, y); /* show the new sphere */ 93 | 94 | /* one more sphere in the world */ 95 | var sp = new Sphere(x, y, dir, life, lev); 96 | spheres.push(sp); 97 | 98 | return; 99 | } 100 | 101 | 102 | 103 | /* 104 | * rmsphere(x,y) Function to delete a sphere of annihilation from list 105 | * int x,y; 106 | * 107 | * Enter with the coordinates of the sphere (on current level) 108 | * Returns the number of spheres currently in existence 109 | */ 110 | function rmsphere(x, y) { 111 | for (var i = 0; i < spheres.length; i++) { 112 | var sp = spheres[i]; 113 | if (!sp) continue; 114 | if (sp.level == level) { 115 | /* is sphere on this level? */ 116 | if (x == sp.x && y == sp.y) /* locate sphere at this location */ { 117 | setItem(x, y, OEMPTY); 118 | // player.level.monsters[x][y] = null; 119 | player.level.know[x][y] = 1; 120 | show1cell(x, y); /* show the now missing sphere */ 121 | spheres.splice(i, 1); // remove the sphere from the list 122 | //break; 123 | } 124 | } 125 | } 126 | 127 | /* return number of spheres in the world */ 128 | return spheres.length; 129 | } 130 | 131 | 132 | 133 | /* 134 | * sphboom(x,y) Function to perform the effects of a sphere detonation 135 | * int x,y; 136 | * 137 | * Enter with the coordinates of the blast, Returns no value 138 | */ 139 | function sphboom(x, y) { 140 | if (player.HOLDMONST) player.updateHoldMonst(-player.HOLDMONST); // set to 0 141 | if (player.CANCELLATION) player.updateCancellation(-player.CANCELLATION); // set to 0 142 | for (var j = Math.max(1, x - 2); j < Math.min(x + 3, MAXX - 1); j++) { 143 | for (var i = Math.max(1, y - 2); i < Math.min(y + 3, MAXY - 1); i++) { 144 | setItem(j, i, OEMPTY); 145 | let monster = monsterAt(j, i); 146 | if (monster && monster.isCarrying(OSPHTALISMAN)) { 147 | updateLog(`The ${monster} is unaffected by the blast!`); 148 | } else { 149 | killMonster(j, i); 150 | } 151 | show1cell(j, i); 152 | if (!isCarrying(OSPHTALISMAN)) { // sphere of talisman protects 153 | if (player.x == j && player.y == i) { 154 | cursors(); 155 | updateLog(`You were too close to the sphere!`); 156 | //nap(2000); 157 | died(DIED_ANNIHILATED, false); /* player killed in explosion */ 158 | } 159 | } 160 | } 161 | } 162 | updateWalls(x, y, 3); 163 | } 164 | 165 | 166 | 167 | /* 168 | * movsphere() Function to look for and move spheres of annihilation 169 | * 170 | * This function works on the sphere linked list, first duplicating the list 171 | * (the act of moving changes the list), then processing each sphere in order 172 | * to move it. They eat anything in their way, including stairs, volcanic 173 | * shafts, potions, etc, except for upper level demons, who can dispel 174 | * spheres. 175 | * No value is returned. 176 | */ 177 | function movsphere() { 178 | for (var i = spheres.length; i >= 0; --i) /* look through sphere list */ { 179 | var sp = spheres[i]; 180 | if (!sp) continue; 181 | if (sp.level != level) continue; 182 | if (!itemAt(sp.x, sp.y).matches(OANNIHILATION)) continue; /* not really there */ 183 | if (--sp.lifetime < 0) /* has sphere run out of gas? */ { 184 | rmsphere(sp.x, sp.y); /* delete sphere */ 185 | continue; 186 | } 187 | switch (rnd(Math.max(7, player.INTELLIGENCE >> 1))) /* time to move the sphere */ { 188 | case 1: 189 | case 2: 190 | /* change direction to a random one */ 191 | sp.direction = rnd(8); 192 | // deliberate fall through 193 | // eslint-disable-next-line no-fallthrough 194 | default: 195 | /* move in normal direction */ 196 | rmsphere(sp.x, sp.y); 197 | newsphere(sp.x + diroffx[sp.direction], sp.y + diroffy[sp.direction], sp.direction, sp.lifetime, sp.level); 198 | } 199 | } 200 | } -------------------------------------------------------------------------------- /src/bill.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var winnerHardlev; 5 | var winnerLetterGender = 'Male'; 6 | 7 | function readmail() { 8 | var gold = 0; 9 | winnerHardlev = 0; 10 | try { 11 | var winner = localStorageGetObject(logname); 12 | if (winner) { 13 | winnerLetterGender = winner.gender; 14 | gold = winner.score; 15 | winnerHardlev = winner.hardlev + 1; 16 | } 17 | } catch (error) { 18 | console.error(`readmail: caught ${error}`); 19 | } 20 | letter1(gold); 21 | } 22 | 23 | 24 | 25 | /* 26 | * function to create the tax bill for the user 27 | */ 28 | function letter1(gold) { 29 | clear(); 30 | 31 | const TAX_OWED = amiga_mode ? `0`: `${Math.round(gold * TAXRATE).toLocaleString()}`; 32 | 33 | lprcat(`From: The LRS (${GAMENAME} Revenue Service)`); 34 | lprcat(` ${lt}lrs@larn.org${gt}`); 35 | lprcat(`\n`); 36 | lprcat(`\nSubject: Undeclared income\n`); 37 | 38 | lprcat(`\n We heard you survived the caverns of ${GAMENAME}. Let me be the`); 39 | lprcat(`\nfirst to congratulate you on your success. It is quite a feat.`); 40 | lprcat(`\nIt must also have been very profitable for you.`); 41 | lprcat(`\n\n The Dungeon Master has informed us that you brought`); 42 | lprcat(`\n${Number(gold).toLocaleString()} gold pieces back with you from your journey. As the`); 43 | lprcat(`\ncounty of ${GAMENAME} is in dire need of funds, we have spared no time`); 44 | lprcat(`\nin preparing your tax bill. You owe ${TAX_OWED} gold pieces as`); 45 | lprcat(`\nof this notice, and is due within 5 days. Failure to pay will`); 46 | lprcat(`\nmean penalties. Once again, congratulations, We look forward`); 47 | lprcat(`\nto your future successful expeditions.\n`); 48 | 49 | cursors(); 50 | lprcat(` --- press space to continue ---\n`); 51 | setCharCallback(letter2); 52 | 53 | return (1); 54 | } 55 | 56 | 57 | 58 | function letter2(key) { 59 | 60 | let letter2title = `Champion`; 61 | if (winnerLetterGender === `Male`) letter2title = `Knight`; 62 | if (winnerLetterGender === `Female`) letter2title = `Lady`; 63 | 64 | if (key != ' ') return 0; 65 | 66 | clear(); 67 | 68 | lprcat(`From: His Majesty King Wilfred of Larndom`); 69 | lprcat(` ${lt}wilfred@larn.org${gt}`); 70 | lprcat(`\n`); 71 | lprcat(`\nSubject: A noble deed\n`); 72 | 73 | lprcat(`\n I have heard of your magnificent feat, and I, King Wilfred,`); 74 | lprcat(`\nforthwith declare today to be a national holiday. Furthermore,`); 75 | lprcat(`\nhence three days, Ye be invited to the castle to receive the`); 76 | lprcat(`\nhonour of ${letter2title} of the realm. Upon thy name shall it be written. . .`); 77 | lprcat(`\nBravery and courage be yours.`); 78 | lprcat(`\nMay you live in happiness forevermore . . .\n`); 79 | 80 | cursors(); 81 | lprcat(` --- press space to continue ---\n`); 82 | setCharCallback(letter3); 83 | 84 | return (1); 85 | } 86 | 87 | 88 | 89 | function letter3(key) { 90 | 91 | let letter3insult = `Jerk`; 92 | if (winnerLetterGender === `Male`) letter3insult = `Bastard`; 93 | if (winnerLetterGender === `Female`) letter3insult = `Bitch`; 94 | 95 | if (key != ' ') return 0; 96 | 97 | clear(); 98 | 99 | lprcat(`From: Count Endelford`); 100 | lprcat(` ${lt}endelford@larn.org${gt}`); 101 | lprcat(`\n`); 102 | lprcat(`\nSubject: You ${letter3insult}!\n`); 103 | 104 | lprcat(`\n I heard (from sources) of your journey. Congratulations!`); 105 | lprcat(`\nYou ${letter3insult}! With several attempts I have yet to endure the`); 106 | lprcat(` caves,\nand you, a nobody, makes the journey! From this time`); 107 | lprcat(` onward, bewarned\nupon our meeting you shall pay the price!\n`); 108 | 109 | cursors(); 110 | lprcat(` --- press space to continue ---\n`); 111 | setCharCallback(letter4); 112 | 113 | return (1); 114 | } 115 | 116 | 117 | 118 | function letter4(key) { 119 | 120 | if (key != ' ') return 0; 121 | 122 | clear(); 123 | 124 | lprcat(`From: Mainair, Duke of Larnty`); 125 | lprcat(` ${lt}mainair@larn.org${gt}`); 126 | lprcat(`\n`); 127 | lprcat(`\nSubject: High Praise\n`); 128 | 129 | lprcat(`\n With a certainty a hero I declare to be amongst us! A nod of`); 130 | lprcat(`\nfavour I send to thee. Me thinks Count Endelford this day of`); 131 | lprcat(`\nright breath'eth fire as of dragon of whom ye are slayer. I`); 132 | lprcat(`\nyearn to behold his anger and jealously. Should ye choose to`); 133 | lprcat(`\nunleash some of thy wealth upon those who be unfortunate, I,`); 134 | lprcat(`\nDuke Mainair, Shall equal thy gift also.\n`); 135 | 136 | cursors(); 137 | lprcat(` --- press space to continue ---\n`); 138 | setCharCallback(letter5); 139 | 140 | return (1); 141 | } 142 | 143 | 144 | 145 | function letter5(key) { 146 | 147 | let letter5plea = `person's`; 148 | if (winnerLetterGender === `Male`) letter5plea = `man's`; 149 | if (winnerLetterGender === `Female`) letter5plea = `woman's`; 150 | 151 | if (key != ' ') return 0; 152 | 153 | clear(); 154 | 155 | lprcat(`From: St. Mary's Children's Home`); 156 | lprcat(` ${lt}stmarys@larn.org${gt}`); 157 | lprcat(`\n`); 158 | lprcat(`\nSubject: These poor children\n`); 159 | 160 | lprcat(`\n News of your great conquests has spread to all of Larndom.`); 161 | lprcat(`\nMight I have a moment of a great ${letter5plea} time. We here at St.`); 162 | lprcat(`\nMary's Children's Home are very poor, and many children are`); 163 | lprcat(`\nstarving. Disease is widespread and very often fatal without`); 164 | lprcat(`\ngood food. Could you possibly find it in your heart to help us`); 165 | lprcat(`\nin our plight? Whatever you could give will help much.`); 166 | lprcat(`\n(your gift is tax deductible)\n`); 167 | 168 | cursors(); 169 | lprcat(` --- press space to continue ---\n`); 170 | setCharCallback(letter6); 171 | 172 | return (1); 173 | } 174 | 175 | 176 | 177 | function letter6(key) { 178 | 179 | let letter6disease = `Cancer`; 180 | if (ULARN) letter6disease = `Dianthroritis` 181 | 182 | if (key != ' ') return 0; 183 | 184 | clear(); 185 | 186 | lprcat(`From: The National ${letter6disease} Society of ${GAMENAME}`); 187 | lprcat(` ${lt}nds@larn.org${gt}`); 188 | lprcat(`\n`); 189 | lprcat(`\nSubject: Hope\n`); 190 | 191 | lprcat(`\n Congratulations on your successful expedition. We are sure much`); 192 | lprcat(`\ncourage and determination were needed on your quest. There are`); 193 | lprcat(`\nmany though, that could never hope to undertake such a journey`); 194 | lprcat(`\ndue to an enfeebling disease -- ${letter6disease}. We at the National`); 195 | lprcat(`\n${letter6disease} Society of ${GAMENAME} wish to appeal to your philanthropy in`); 196 | lprcat(`\norder to save many good people -- possibly even yourself a few`); 197 | lprcat(`\nyears from now. Much work needs to be done in researching this`); 198 | lprcat(`\ndreaded disease, and you can help today. Could you please see it`); 199 | lprcat(`\nin your heart to give generously? Your continued good health`); 200 | lprcat(`\ncan be your everlasting reward.\n`); 201 | 202 | cursors(); 203 | lprcat(` --- press space to continue ---\n`); 204 | 205 | setCharCallback(setdiff); 206 | 207 | return (1); 208 | } 209 | -------------------------------------------------------------------------------- /src/altar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /* For command mode. Perform the act of descecrating an altar */ 5 | function desecrate_altar() { 6 | cursors(); 7 | if (itemAt(player.x, player.y).matches(OALTAR)) { 8 | act_desecrate_altar(); 9 | } else { 10 | updateLog(`I see no altar to desecrate here!`); 11 | } 12 | } 13 | 14 | 15 | 16 | /* For command mode. Perform the act of praying at an altar */ 17 | function pray_at_altar() { 18 | cursors(); 19 | if (!itemAt(player.x, player.y).matches(OALTAR)) { 20 | updateLog(`I see no altar to pray at here!`); 21 | } else { 22 | updateLog(` How much do you donate? `); 23 | setNumberCallback(act_donation_pray, true); 24 | nomove = 1; 25 | } 26 | } 27 | 28 | 29 | 30 | /* 31 | Perform the actions associated with praying at an altar and giving a 32 | donation. 33 | */ 34 | function act_donation_pray(offering) { 35 | if (offering == ESC) { 36 | appendLog(` cancelled${period}`); 37 | nomove = 1; 38 | prayed = 0; 39 | return 1; 40 | } 41 | 42 | if (offering == '*') { 43 | offering = player.GOLD; 44 | } 45 | offering = Number(offering); 46 | 47 | /* Player donates more gold than they have. Loop back around so 48 | player can't escape the altar for free. */ 49 | if (offering > player.GOLD) { 50 | updateLog(` You don't have that much!`); 51 | prayed = 0; 52 | dropflag = 1; 53 | return 1; 54 | } 55 | 56 | /* make giving zero gold equivalent to 'just pray'ing. Allows player to 57 | 'just pray' in command mode, without having to add yet another command. */ 58 | if (offering == 0) { 59 | // // for testing 60 | // var outcome; 61 | // var cnt = 0; 62 | // while(outcome != 'crumble') { 63 | // outcome = act_just_pray(); 64 | // cnt++; 65 | // if (outcome) console.log(cnt, outcome); 66 | // } 67 | act_just_pray(); 68 | dropflag = 1; 69 | prayed = 1; 70 | return 1; 71 | } 72 | 73 | if (player.GOLD >= offering) { 74 | prayed = 1; 75 | dropflag = 1; 76 | 77 | var oneTenth = player.GOLD / 10; 78 | player.setGold(player.GOLD - offering); 79 | 80 | if (ULARN) { 81 | // less than 10% of post offering gold 82 | if (offering < (player.GOLD / 10) && rnd(60) < 30) { 83 | updateLog(`Cheapskate! The Gods are insulted by such a tiny offering!`); 84 | forget(); /* remember to destroy the altar */ 85 | // ularn does NOT appreciate cheapskates 86 | createmonster(DEMONPRINCE); 87 | player.AGGRAVATE += 1500; 88 | return 1; 89 | } 90 | // less than 10% of original gold 91 | else if (offering < oneTenth || offering < rnd(50)) { 92 | createmonster(makemonst(level + 2)); 93 | player.AGGRAVATE += 500; 94 | return 1; 95 | } 96 | // 97 | else { 98 | var p = rund(16); 99 | if (p < 4) { 100 | updateLog(`Thank you${period}`); 101 | return 1; 102 | } else if (p < 6) { 103 | enchantarmor(ENCH_ALTAR); 104 | enchantarmor(ENCH_ALTAR); 105 | } else if (p < 8) { 106 | enchweapon(ENCH_ALTAR); 107 | enchweapon(ENCH_ALTAR); 108 | } else { 109 | act_prayer_heard(); 110 | } 111 | 112 | /* 113 | v12.4.5 - prevents our hero from buying too many +AC/WC 114 | */ 115 | if (rnd(43) == 13) { 116 | crumble_altar(); 117 | } 118 | 119 | return 1; 120 | } 121 | } // end ULARN 122 | else { 123 | //if player gave less than 10% of _original_ gold, make a monster 124 | if (offering < oneTenth || offering < rnd(50)) { 125 | createmonster(makemonst(level + 1)); 126 | player.AGGRAVATE += 200; 127 | return 1; 128 | } 129 | if (rnd(101) > 50) { 130 | act_prayer_heard(); 131 | return 1; 132 | } 133 | if (rnd(43) == 5) { 134 | enchantarmor(ENCH_ALTAR); 135 | return 1; 136 | } 137 | if (rnd(43) == 8) { 138 | enchweapon(ENCH_ALTAR); 139 | return 1; 140 | } 141 | 142 | /* 143 | v12.4.5 - prevents our hero from buying too many +AC/WC 144 | */ 145 | if (rnd(43) == 13) { 146 | crumble_altar(); 147 | return 1; 148 | } 149 | 150 | updateLog(` Thank You${period}`); 151 | return 1; 152 | 153 | } 154 | } 155 | 156 | } 157 | 158 | 159 | 160 | /* 161 | Performs the actions associated with 'just praying' at the altar. Called 162 | when the user responds 'just pray' when in prompt mode, or enters 0 to 163 | the money prompt when praying. 164 | */ 165 | function act_just_pray() { 166 | 167 | /* 168 | v12.4.5 - prevents our hero from getting too many free +AC/WC 169 | */ 170 | if (rnd(43) == 10) { 171 | crumble_altar(); 172 | return 'crumble'; 173 | } 174 | 175 | if (ULARN) { 176 | var p = rund(100); 177 | if (p < 12) { 178 | createmonster(makemonst(level + 2)); 179 | } else if (p < 17) { 180 | enchweapon(ENCH_ALTAR); 181 | } else if (p < 22) { 182 | enchantarmor(ENCH_ALTAR); 183 | } else if (p < 27) { 184 | act_prayer_heard(); 185 | } else { 186 | updateLog(` Nothing happens${period}`); 187 | } 188 | } else { 189 | if (rnd(100) < 75) { 190 | updateLog(` Nothing happens${period}`); 191 | } else if (rnd(43) == 10) { 192 | enchantarmor(ENCH_ALTAR); 193 | return 'ac'; 194 | } else if (rnd(43) == 10) { 195 | if (player.WIELD) { 196 | updateLog(` You feel your weapon vibrate for a moment${period}`); 197 | } 198 | enchweapon(); 199 | return 'wc'; 200 | } else { 201 | createmonster(makemonst(level + 1)); 202 | } 203 | return; 204 | } 205 | 206 | } 207 | 208 | 209 | 210 | /* 211 | Perform the actions associated with Altar desecration. 212 | */ 213 | function act_desecrate_altar() { 214 | if (rnd(100) < 60) { 215 | var monstBoost = (ULARN ? 3 : 2); 216 | createmonster(makemonst(level + monstBoost) + 8); 217 | player.AGGRAVATE += 2500; 218 | } else if (ULARN && rnd(100) < 5) { 219 | player.raiselevel(); 220 | } else if (rnd(101) < 30) { 221 | crumble_altar(); 222 | } else 223 | updateLog(` Nothing happens${period}`); 224 | return; 225 | } 226 | 227 | 228 | /* 229 | Destroys the Altar 230 | */ 231 | function crumble_altar() { 232 | updateLog(` The altar crumbles into a pile of dust before your eyes${period}`); 233 | forget(); /* remember to destroy the altar */ 234 | } 235 | 236 | 237 | 238 | /* 239 | Performs the act of ignoring an altar. 240 | 241 | Assumptions: cursors() has been called. 242 | */ 243 | function act_ignore_altar(x, y) { 244 | if (rnd(100) < 30) { 245 | var monstBoost = (ULARN ? 2 : 1); 246 | createmonster(makemonst(level + monstBoost), x, y); 247 | player.AGGRAVATE += rnd(450); 248 | } else 249 | updateLog(` Nothing happens${period}`); 250 | return; 251 | } 252 | 253 | 254 | 255 | /* 256 | function to cast a +3 protection on the player 257 | */ 258 | function act_prayer_heard() { 259 | updateLog(` You have been heard!`); 260 | var protime; 261 | if (ULARN) { 262 | protime = 800; 263 | } else { 264 | protime = 500; 265 | } 266 | player.updateAltPro(protime); /* protection field */ 267 | } 268 | 269 | 270 | 271 | function autoPray() { 272 | if (player.GOLD < 50 || !itemAt(player.x, player.y).matches(OALTAR)) { 273 | return; 274 | } 275 | let donation = `${Math.max(50, Math.ceil(player.GOLD / 10))}` 276 | simulateKeypress('p'); 277 | for (let char of donation) { 278 | simulateKeypress(char); 279 | } 280 | simulateKeypress(ENTER); 281 | } -------------------------------------------------------------------------------- /src/fountain.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /* 5 | Code to perform the action of drinking at a fountain. Assumes that 6 | cursors() has already been called, and that a check has been made that 7 | the player is actually standing at a live fountain. 8 | */ 9 | function act_drink_fountain() { 10 | 11 | if (rnd(1501) < (ULARN ? 4 : 2)) { 12 | var sleepExclaim = ULARN ? `OH MY GOD!! You` : `Oops! You seem to`; 13 | updateLog(` ${sleepExclaim} have caught the dreadful sleep!`); 14 | beep(); 15 | died(DIED_DREADFUL_SLEEP, false); /* fell into the dreadful sleep */ 16 | return; 17 | } 18 | 19 | var x = rnd(100); 20 | 21 | if (ULARN) { 22 | if (x == 1) { 23 | player.raiselevel(); 24 | } else if (x < 11) { 25 | var hitloss = rnd((level << 2) + 2); 26 | updateLog(` Bleah! The water tasted like stale gatorade! You lose ${hitloss} hit point`); 27 | exclaim(hitloss); 28 | lastnum = DIED_BAD_WATER; 29 | player.losehp(hitloss); 30 | } else if (x < 14) { 31 | player.HALFDAM += 200 + rnd(200); 32 | updateLog(` The water makes you vomit${period}`); 33 | } else if (x < 17) { 34 | quaffpotion(createObject(OPOTION, 17), false); /* giant strength, but don't know the potion */ 35 | } else if (x < 45) { 36 | updateLog(` Nothing seems to have happened${period}`); 37 | } else if (rnd(3) != 2) { 38 | fntchange(1); /* change char levels upward */ 39 | } else { 40 | fntchange(-1); /* change char levels downward */ 41 | } 42 | } else { 43 | if (x < 7) { 44 | player.HALFDAM += 200 + rnd(200); 45 | updateLog(` You feel a sickness coming on${period}`); 46 | } else if (x < 13) { 47 | quaffpotion(createObject(OPOTION, 23), false); /* see invisible, but don't know the potion */ 48 | } else if (x < 45) { 49 | updateLog(` Nothing seems to have happened${period}`); 50 | } else if (rnd(3) != 2) { 51 | fntchange(1); /* change char levels upward */ 52 | } else { 53 | fntchange(-1); /* change char levels downward */ 54 | } 55 | } 56 | 57 | if (rnd(12) < 3) { 58 | updateLog(` The fountains bubbling slowly quiets${period}`); 59 | setItem(player.x, player.y, ODEADFOUNTAIN); /* dead fountain */ 60 | player.level.know[player.x][player.y] = 0; 61 | } 62 | } 63 | 64 | 65 | 66 | /* 67 | Code to perform the action of washing at a fountain. Assumes that 68 | cursors() has already been called and that a check has been made that 69 | the player is actually standing at a live fountain. 70 | */ 71 | function act_wash_fountain() { 72 | if (rnd(100) < 11) { 73 | var hitloss = rnd((level << 2) + 2); 74 | var washExclaim = ` Oh no! The water was foul! You suffer ${hitloss} hit point`; 75 | if (ULARN) washExclaim = ` The water burns like acid! You lose ${hitloss} hit point`; 76 | updateLog(washExclaim); 77 | exclaim(hitloss); 78 | lastnum = DIED_BAD_WATER; /* drank some poisonous water */ 79 | player.losehp(hitloss); 80 | } else if (rnd(100) < 29) { 81 | if (ULARN) updateLog(` You are now clean${period}`); 82 | else updateLog(` You got the dirt off!`); 83 | 84 | /* 12.4.5 85 | remove one negative wc/ac point, or remove itchyness 86 | */ 87 | if (player.ITCHING) { 88 | player.ITCHING = 1; // interestingly, this was also added in ularn 1.6.3! 89 | } else { 90 | if (rnd(100) < 50) { 91 | enchantarmor(ENCH_FOUNTAIN); 92 | } else { 93 | enchweapon(ENCH_FOUNTAIN); 94 | } 95 | } 96 | 97 | } else if (rnd(100) < 31) { 98 | if (ULARN) updateLog(` This water seems to be hard water! The dirt didn't come off!`); 99 | else updateLog(` This water needs soap -- the dirt didn't come off${period}`); 100 | } else if (rnd(100) < 34 && !isGenocided(WATERLORD)) { 101 | createmonster(WATERLORD); /* make water lord */ 102 | } else { 103 | updateLog(` Nothing seems to have happened${period}`); 104 | } 105 | 106 | /* 12.4.5 107 | since getting the dirt off is a good bonus, need to limit it somehow 108 | */ 109 | if (rnd(12) < 3) { 110 | updateLog(` The fountains bubbling slowly quiets${period}`); 111 | setItem(player.x, player.y, ODEADFOUNTAIN); /* dead fountain */ 112 | player.level.know[player.x][player.y] = 0; 113 | } 114 | } 115 | 116 | 117 | 118 | /* 119 | a subroutine to raise or lower character levels 120 | if x > 0 they are raised if x < 0 they are lowered 121 | */ 122 | function fntchange(how) { 123 | how = how / Math.abs(how); 124 | switch (rnd(9)) { 125 | case 1: 126 | updateLog(` Your strength`); 127 | player.setStrength(player.STRENGTH + how); 128 | fch(how); 129 | break; 130 | case 2: 131 | updateLog(` Your intelligence`); 132 | player.setIntelligence(player.INTELLIGENCE + how); 133 | fch(how); 134 | break; 135 | case 3: 136 | updateLog(` Your wisdom`); 137 | player.setWisdom(player.WISDOM + how); 138 | fch(how); 139 | break; 140 | case 4: 141 | updateLog(` Your constitution`); 142 | player.setConstitution(player.CONSTITUTION + how); 143 | fch(how); 144 | break; 145 | case 5: 146 | updateLog(` Your dexterity`); 147 | player.setDexterity(player.DEXTERITY + how); 148 | fch(how); 149 | break; 150 | case 6: 151 | updateLog(` Your charm`); 152 | player.setCharisma(player.CHARISMA + how); 153 | fch(how); 154 | break; 155 | case 7: 156 | var hpchange = rnd(level + 1); 157 | if (how < 0) { 158 | updateLog(` You lose ${hpchange} hit point`); 159 | exclaim(hpchange); 160 | player.losemhp(hpchange); 161 | } else { 162 | updateLog(` You gain ${hpchange} hit point`); 163 | exclaim(hpchange); 164 | player.raisemhp(hpchange); 165 | } 166 | break; 167 | 168 | case 8: 169 | var spellchange = rnd(level + 1); 170 | if (how > 0) { 171 | updateLog(` You just gained ${spellchange} spell`); 172 | exclaim(spellchange); 173 | player.raisemspells(spellchange); 174 | } else { 175 | updateLog(` You just lost ${spellchange} spell`); 176 | exclaim(spellchange); 177 | player.losemspells(spellchange); 178 | } 179 | break; 180 | 181 | case 9: 182 | var xpchange = 5 * rnd((level + 1) * (level + 1)); 183 | if (how < 0) { 184 | updateLog(` You just lost ${xpchange} experience point`); 185 | exclaim(xpchange); 186 | player.loseexperience(xpchange); 187 | } else { 188 | updateLog(` You just gained ${xpchange} experience point`); 189 | exclaim(xpchange); 190 | player.raiseexperience(xpchange); 191 | } 192 | break; 193 | } 194 | } 195 | 196 | 197 | 198 | /* 199 | subroutine to process an up/down of a character attribute for ofountain 200 | */ 201 | function fch(how) { 202 | if (how < 0) { 203 | appendLog(` went down by one!`); 204 | } else { 205 | appendLog(` went up by one!`); 206 | } 207 | } 208 | 209 | 210 | 211 | function exclaim(num) { 212 | if (num > 1) { 213 | appendLog(`s!`); 214 | } else { 215 | appendLog(`!`); 216 | } 217 | } 218 | 219 | 220 | 221 | /* 222 | For command mode. Perform drinking at a fountain. 223 | */ 224 | function drink_fountain() { 225 | var item = itemAt(player.x, player.y); 226 | if (item.matches(ODEADFOUNTAIN)) { 227 | updateLog(`There is no water to drink!`); 228 | } else if (!item.matches(OFOUNTAIN)) { 229 | updateLog(`I see no fountain to drink from here!`); 230 | } else { 231 | act_drink_fountain(); 232 | } 233 | return; 234 | } 235 | 236 | 237 | 238 | /* 239 | For command mode. Perform washing (tidying up) at a fountain. 240 | */ 241 | function wash_fountain() { 242 | var item = itemAt(player.x, player.y); 243 | if (item.matches(ODEADFOUNTAIN)) { 244 | updateLog(`There is no water to wash in!`); 245 | } else if (!item.matches(OFOUNTAIN)) { 246 | updateLog(`I see no fountain to wash at here!`); 247 | } else { 248 | act_wash_fountain(); 249 | } 250 | return; 251 | } -------------------------------------------------------------------------------- /src/help.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const helppages = []; 4 | 5 | let currentpage = 0; 6 | 7 | 8 | 9 | function parse_help(key) { 10 | nomove = 1; 11 | if (key == ESC) { 12 | return exitbuilding(); 13 | } else if (key == ' ') { 14 | print_help(); 15 | } 16 | } 17 | 18 | 19 | 20 | function print_help() { 21 | mazeMode = false; 22 | 23 | clear(); 24 | 25 | if (++currentpage > helppages.length - 1) { 26 | currentpage = 1; 27 | } 28 | lprcat(helppages[currentpage]); 29 | cursors(); 30 | lprcat(` ---- Press space for more help, escape to exit ----`); 31 | 32 | blt(); 33 | } 34 | 35 | function initHelpPages() { 36 | 37 | helppages[0] = !GOTW ? 38 | `Welcome to the game of ${GAMENAME}. At this moment, you face a great problem.\n\ 39 | Your daughter has contracted a strange disease, and none of your home remedies\n\ 40 | seem to have any effect. You sense that she is in mortal danger, and you must\n\ 41 | try to save her. Time ago you heard of a land of great danger and opportunity.\n\ 42 | Perhaps here is the solution you need.\n\ 43 | \n\ 44 | It has been said that there once was a great magician who called himself\n\ 45 | Polinneaus. Many years ago, after having many miraculous successes, Polinneaus\n\ 46 | retired to the caverns of ${GAMENAME}, where he devoted most of his time to the\n\ 47 | creation of magic. Rumors have it that one day Polinneaus set out to dispel\n\ 48 | an attacking army in a forest some distance to the north. It is believed that\n\ 49 | here he met his demise.\n\ 50 | \n\ 51 | The caverns of ${GAMENAME}, it is thought, must be magnificent in design,\n\ 52 | and contain much magic and treasure. One option you have is to undertake a\n\ 53 | journey into these caverns.\n\ 54 | \n\ 55 | \n\ 56 | Good Luck! You're going to need it!\n\ 57 | ` 58 | : 59 | // Subject: A Weekly Ordeal for One So Unworthy\n\ 60 | ` Count Endelford's Weekly Dungeon \n\ 61 | \n\ 62 | From: Count Endelford ${lt}endelford@larn.org${gt}\n\ 63 | Subject: Thinking of you\n\ 64 | \n\ 65 | I have devised a trial for you, worm. Your reputation precedes you though \n\ 66 | I find it laughable.\n\ 67 | \n\ 68 | The rules for this challenge are as follows:\n\ 69 | o The contest resets as Sunday falls (23:59 UTC).\n\ 70 | o You may play only once. Should you attempt a second journey, expect no mercy.\n\ 71 | o Saving progress is forbidden. Only the weak seek refuge in such cowardice.\n\ 72 | o Wizard mode is strictly and severely forbidden. Invoke it, and my wrath will\n\ 73 | be swift and absolute.\n\ 74 | \n\ 75 | I present this challenge because I despise you and the opportunity to watch\n\ 76 | you fail weekly delights me. I eagerly await your inevitable demise.\n\ 77 | \n\ 78 | With deepest contempt,\n\ 79 | Endelford\n\ 80 | `; 81 | 82 | helppages[1] = 83 | ` Help File for The Caverns of ${GAMENAME} \n\ 84 | shift+ \n\ 85 | y|Y move|run northwest k|K move|run up u|U move|run northeast ↖↑↗ \n\ 86 | h|H move|run left . stay here l|L move|run right ← → \n\ 87 | b|B move|run southwest j|J move|run down n|N move|run southeast ↙↓↘ \n\ 88 | to run \n\ 89 | A desecrate an altar < go up stairs \n\ 90 | c cast a spell C close a door > go down stairs \n\ 91 | d drop an item D drink at a fountain \n\ 92 | e eat something E enter a store, dungeon ^ identify a trap \n\ 93 | f tidy up at a fountain or volcanic shaft \n\ 94 | g get present pack weight ! toggle key hints \n\ 95 | i inventory your pockets I list all known items @ toggle auto-pickup \n\ 96 | o open a door or chest # toggle inventory \n\ 97 | p pray at an altar P give tax status \n\ 98 | q quaff a potion Q quit the game { toggle retro fonts \n\ 99 | r read a scroll or book R remove gems from throne } toggle between \n\ 100 | s sit on a throne S save the game classic, hack, \n\ 101 | t take an item T take off armor and amiga graphics \n\ 102 | v print program version V view conducts \n\ 103 | w wield a weapon W wear armor \n\ 104 | z show scores Z teleport yourself ? this help screen \n\ 105 | `; 106 | 107 | 108 | helppages[2] = 109 | ` Special Notes \n\ 110 | \n\ 111 | To drop gold, press '.' after pressing 'd'. When dropping gold, if you\n\ 112 | type '*' as your amount, all your gold is dropped.\n\ 113 | \n\ 114 | In general, typing in '*' means all of what you're interested in. This is true\n\ 115 | when visiting the bank, or when contributing at altars.\n\ 116 | \n\ 117 | When in the store, trading post, school, or home, an escape will get you out.\n\ 118 | \n\ 119 | When casting a spell, if you need a list of spells you can cast, type space \n\ 120 | at any time and the available list of spells will be shown.\n\ 121 | \n\ 122 | When an inventory list is on the screen from a drop, quaff, read, or similar\n\ 123 | command, you can type the letter of the object that you wish to act apon,\n\ 124 | without having to type a space to get back to the prompt.\n\ 125 | \n\ 126 | If NumLock is off, the Keypad functions in the obvious way for movement. Hold\n\ 127 | Shift when pressing any direction on the Keypad to run in that direction. The\n\ 128 | 5 key on the Keypad is the same as 'stay here', which really means skip your\n\ 129 | turn.\n\ 130 | `; 131 | 132 | helppages[3] = 133 | ` Background Information for ${GAMENAME} \n\ 134 | \n\ 135 | Welcome to the game of ${GAMENAME}. At this moment, you face a great problem.\n\ 136 | Your daughter has contracted a strange disease, and none of your home remedies\n\ 137 | seem to have any effect. You sense that she is in mortal danger, and you must\n\ 138 | try to save her. Time ago you heard of a land of great danger and opportunity.\n\ 139 | Perhaps here is the solution you need.\n\ 140 | \n\ 141 | It has been said that there once was a great magician who called himself\n\ 142 | Polinneaus. Many years ago, after having many miraculous successes, Polinneaus\n\ 143 | retired to the caverns of ${GAMENAME}, where he devoted most of his time to the\n\ 144 | creation of magic. Rumors have it that one day Polinneaus set out to dispel\n\ 145 | an attacking army in a forest some distance to the north. It is believed that\n\ 146 | here he met his demise.\n\ 147 | \n\ 148 | The caverns of ${GAMENAME}, it is thought, must be magnificent in design,\n\ 149 | and contain much magic and treasure. One option you have is to undertake a\n\ 150 | journey into these caverns.\n\ 151 | \n\ 152 | Good Luck! You're going to need it!\n\ 153 | `; 154 | 155 | helppages[4] = 156 | ` Explanation of the ${GAMENAME} scoreboard facility \n\ 157 | \n\ 158 | ${GAMENAME} supports TWO scoreboards, one for winners, and one for deceased\n\ 159 | characters. Each player (by the name entered when you start the game)\n\ 160 | is allowed one slot on each scoreboard. This design helps ensure that \n\ 161 | frequent players of ${GAMENAME} do not hog the scoreboard, and gives more\n\ 162 | players a chance for glory. Level of difficulty is also noted on the\n\ 163 | scoreboards, and this takes precedence over score for determining what\n\ 164 | entry is on the scoreboard. For example: if 'Yar, the Bug Slayer' has\n\ 165 | a score of 128003 on the scoreboard at diff 0, then a game at diff 1\n\ 166 | and a score of 4112 would replace the previous entry on the scoreboard.\n\ 167 | Note that when a player dies, the inventory is stored in the scoreboard\n\ 168 | so that everyone can see what items the player had at the time of death.\n\ 169 | `; 170 | 171 | } -------------------------------------------------------------------------------- /src/storedata.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /************************************************/ 4 | /* never, ever, never use a code formatter here */ 5 | /************************************************/ 6 | 7 | var DNDItem = function DNDItem(price, itemId, arg, qty) { 8 | this.price = price; 9 | this.itemId = itemId; 10 | this.arg = arg; 11 | this.qty = qty; 12 | } 13 | 14 | var STORE_INVENTORY; // set in config.js 15 | var MAXITM; // set in config.js now 16 | 17 | const LARN_STORE_INVENTORY = [ 18 | /* cost name arg how many */ 19 | [20, OLEATHER.id, 0, 3], 20 | [100, OSTUDLEATHER.id, 0, 2], 21 | [400, ORING.id, 0, 2], 22 | [850, OCHAIN.id, 0, 2], 23 | [2200, OSPLINT.id, 0, 1], 24 | [4000, OPLATE.id, 0, 1], 25 | [9000, OPLATEARMOR.id, 0, 1], 26 | [26000, OSSPLATE.id, 0, 1], 27 | [1500, OSHIELD.id, 0, 1], 28 | [20, ODAGGER.id, 0, 3], 29 | [200, OSPEAR.id, 0, 3], 30 | [800, OFLAIL.id, 0, 2], 31 | [1500, OBATTLEAXE.id, 0, 2], 32 | [4500, OLONGSWORD.id, 0, 2], 33 | [10000, O2SWORD.id, 0, 2], 34 | [50000, OSWORD.id, 0, 1], 35 | [165000, OLANCE.id, 0, 1], 36 | [60000, OSWORDofSLASHING.id, 0, 0], 37 | [100000, OHAMMER.id, 0, 0], 38 | [1500, OPROTRING.id, 1, 1], 39 | [850, OSTRRING.id, 1, 1], 40 | [1200, ODEXRING.id, 1, 1], 41 | [1200, OCLEVERRING.id, 1, 1], 42 | [1800, OENERGYRING.id, 0, 1], 43 | [1250, ODAMRING.id, 0, 1], 44 | [2200, OREGENRING.id, 0, 1], 45 | [10000, ORINGOFEXTRA.id, 0, 1], 46 | [2800, OBELT.id, 0, 1], 47 | [4000, OAMULET.id, 0, 1], 48 | [65000, OORBOFDRAGON.id, 0, 0], 49 | [55000, OSPIRITSCARAB.id, 0, 0], 50 | [50000, OCUBEofUNDEAD.id, 0, 0], 51 | [60000, ONOTHEFT.id, 0, 0], 52 | [5900, OCHEST.id, 6, 1], 53 | [2000, OBOOK.id, 8, 1], 54 | [100, OCOOKIE.id, 0, 3], 55 | [200, OPOTION.id, 0, 6], 56 | [900, OPOTION.id, 1, 5], 57 | [5200, OPOTION.id, 2, 1], 58 | [1000, OPOTION.id, 3, 2], 59 | [500, OPOTION.id, 4, 2], 60 | [1500, OPOTION.id, 5, 2], 61 | [700, OPOTION.id, 6, 1], 62 | [300, OPOTION.id, 7, 7], 63 | [2000, OPOTION.id, 8, 1], 64 | [500, OPOTION.id, 9, 1], 65 | [800, OPOTION.id, 10, 1], 66 | [300, OPOTION.id, 11, 3], 67 | [200, OPOTION.id, 12, 5], 68 | [400, OPOTION.id, 13, 3], 69 | [350, OPOTION.id, 14, 2], 70 | [5200, OPOTION.id, 15, 1], 71 | [900, OPOTION.id, 16, 2], 72 | [2000, OPOTION.id, 17, 2], 73 | [2200, OPOTION.id, 18, 4], 74 | [800, OPOTION.id, 19, 6], 75 | [3700, OPOTION.id, 20, 3], 76 | [500, OPOTION.id, 22, 1], 77 | [1500, OPOTION.id, 23, 3], 78 | [1000, OSCROLL.id, 0, 2], 79 | [1250, OSCROLL.id, 1, 2], 80 | [600, OSCROLL.id, 2, 4], 81 | [100, OSCROLL.id, 3, 4], 82 | [1000, OSCROLL.id, 4, 3], 83 | [2000, OSCROLL.id, 5, 2], 84 | [1100, OSCROLL.id, 6, 1], 85 | [5000, OSCROLL.id, 7, 2], 86 | [2000, OSCROLL.id, 8, 2], 87 | [2500, OSCROLL.id, 9, 4], 88 | [200, OSCROLL.id, 10, 5], 89 | [300, OSCROLL.id, 11, 3], 90 | [3400, OSCROLL.id, 12, 1], 91 | [3400, OSCROLL.id, 13, 1], 92 | [3000, OSCROLL.id, 14, 2], 93 | [4000, OSCROLL.id, 15, 2], 94 | [5000, OSCROLL.id, 16, 2], 95 | [10000, OSCROLL.id, 17, 1], 96 | [5000, OSCROLL.id, 18, 1], 97 | [3400, OSCROLL.id, 19, 2], 98 | [2200, OSCROLL.id, 20, 3], 99 | [39000, OSCROLL.id, 21, 0], 100 | [6100, OSCROLL.id, 22, 1], 101 | [30000, OSCROLL.id, 23, 0] 102 | ]; 103 | 104 | const ULARN_STORE_INVENTORY = [ 105 | /* cost name arg how many */ 106 | [20, OLEATHER.id, 0, 3], 107 | [100, OSTUDLEATHER.id, 0, 2], 108 | [400, ORING.id, 0, 2], 109 | [850, OCHAIN.id, 0, 2], 110 | [2200, OSPLINT.id, 0, 1], 111 | [4000, OPLATE.id, 0, 1], 112 | [9000, OPLATEARMOR.id, 0, 1], 113 | [26000, OSSPLATE.id, 0, 1], 114 | [1500, OSHIELD.id, 0, 1], 115 | [50000, OELVENCHAIN.id, 0, 0], 116 | [10000, OORB.id, 0, 0], 117 | [100000, OSLAYER.id, 0, 0], 118 | [20, ODAGGER.id, 0, 3], 119 | [200, OSPEAR.id, 0, 3], 120 | [800, OFLAIL.id, 0, 2], 121 | [1500, OBATTLEAXE.id, 0, 2], 122 | [4500, OLONGSWORD.id, 0, 2], 123 | [10000, O2SWORD.id, 0, 2], 124 | [50000, OSWORD.id, 0, 1], 125 | [200000, OLANCE.id, 0, 1], 126 | [20000, OSWORDofSLASHING.id, 0, 0], 127 | [75000, OHAMMER.id, 0, 0], 128 | [1500, OPROTRING.id, 1, 1], 129 | [850, OSTRRING.id, 1, 1], 130 | [1200, ODEXRING.id, 1, 1], 131 | [1200, OCLEVERRING.id, 1, 1], 132 | [1800, OENERGYRING.id, 0, 1], 133 | [1250, ODAMRING.id, 0, 1], 134 | [2200, OREGENRING.id, 0, 1], 135 | [10000, ORINGOFEXTRA.id, 0, 1], 136 | [2800, OBELT.id, 0, 1], 137 | [4000, OAMULET.id, 0 /*5*/, 1], 138 | [5000, OCUBEofUNDEAD.id, 0, 0], 139 | [6000, ONOTHEFT.id, 0, 0], 140 | [5900, OCHEST.id, 3, 1], 141 | [2000, OBOOK.id, 2, 1], 142 | [100, OCOOKIE.id, 0, 3], 143 | [6660, OHANDofFEAR.id, 0, 0], 144 | [200, OPOTION.id, 0, 6], 145 | [900, OPOTION.id, 1, 5], 146 | [5200, OPOTION.id, 2, 1], 147 | [1000, OPOTION.id, 3, 2], 148 | [500, OPOTION.id, 4, 2], 149 | [1500, OPOTION.id, 5, 2], 150 | [700, OPOTION.id, 6, 1], 151 | [300, OPOTION.id, 7, 7], 152 | [2000, OPOTION.id, 8, 1], 153 | [500, OPOTION.id, 9, 1], 154 | [800, OPOTION.id, 10, 1], 155 | [300, OPOTION.id, 11, 3], 156 | [200, OPOTION.id, 12, 5], 157 | [400, OPOTION.id, 13, 3], 158 | [350, OPOTION.id, 14, 2], 159 | [5200, OPOTION.id, 15, 1], 160 | [900, OPOTION.id, 16, 2], 161 | [2000, OPOTION.id, 17, 2], 162 | [2200, OPOTION.id, 18, 4], 163 | [800, OPOTION.id, 19, 6], 164 | [3700, OPOTION.id, 20, 3], 165 | [500, OPOTION.id, 22, 1], 166 | [1500, OPOTION.id, 23, 3], 167 | [8500, OORBOFDRAGON.id, 0, 0], 168 | [7500, OSPIRITSCARAB.id, 0, 0], 169 | [80000, OVORPAL.id, 0, 0], 170 | [1000, OSCROLL.id, 0, 2], 171 | [1250, OSCROLL.id, 1, 2], 172 | [600, OSCROLL.id, 2, 4], 173 | [100, OSCROLL.id, 3, 4], 174 | [1000, OSCROLL.id, 4, 3], 175 | [2000, OSCROLL.id, 5, 2], 176 | [1100, OSCROLL.id, 6, 1], 177 | [5000, OSCROLL.id, 7, 2], 178 | [2000, OSCROLL.id, 8, 2], 179 | [2500, OSCROLL.id, 9, 4], 180 | [200, OSCROLL.id, 10, 5], 181 | [300, OSCROLL.id, 11, 3], 182 | [3400, OSCROLL.id, 12, 1], 183 | [3400, OSCROLL.id, 13, 1], 184 | [3000, OSCROLL.id, 14, 2], 185 | [4000, OSCROLL.id, 15, 2], 186 | [5000, OSCROLL.id, 16, 2], 187 | [10000, OSCROLL.id, 17, 1], 188 | [5000, OSCROLL.id, 18, 1], 189 | [3400, OSCROLL.id, 19, 2], 190 | [2200, OSCROLL.id, 20, 3], 191 | [39000, OSCROLL.id, 21, 0], 192 | [6100, OSCROLL.id, 22, 1], 193 | [30000, OSCROLL.id, 23, 0], 194 | [3000, OSPHTALISMAN.id, 0, 0], 195 | [1500, OWWAND.id, 0, 0], 196 | [500, OBRASSLAMP.id, 0, 0], 197 | [95000, OPSTAFF.id, 0, 0], 198 | [100000, OLIFEPRESERVER.id, 0, 0] 199 | ]; 200 | -------------------------------------------------------------------------------- /src/tv/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let lambda; 4 | 5 | let GAMENAME = `TV`; 6 | let ULARN = false; // this is a hack to get around localstoragegetobject issues 7 | 8 | const TV_CHANNEL_LIST = `list`; 9 | const TV_CHANNEL_RECORDED = `recorded`; 10 | const TV_CHANNEL_LIVE = `live`; 11 | let TV_CHANNEL; 12 | 13 | let frameCompressionWorker; /* web worker to compress live frames */ 14 | 15 | go(); 16 | 17 | 18 | 19 | async function go() { 20 | initRollbar(); 21 | initAWS(); 22 | console.log(`build`, BUILD); 23 | console.log(`cloudflare`, CF_BROADCAST_HOST); 24 | 25 | const urlParams = loadURLParameters(); 26 | window.addEventListener('resize', onResize); 27 | 28 | // WORKER STEP 0 - initialization 29 | if (window.Worker) { 30 | frameCompressionWorker = new Worker('../workers/compressionWorker.js'); 31 | frameCompressionWorker.onmessage = frameCompressionCallback; 32 | } 33 | 34 | await loadFonts(); 35 | 36 | if (urlParams.gameid) { 37 | TV_CHANNEL = TV_CHANNEL_RECORDED; 38 | watchRecorded(urlParams.gameid); 39 | } else if (urlParams.live) { 40 | TV_CHANNEL = TV_CHANNEL_LIVE; 41 | const logname = localStorageGetObject('logname', '-'); 42 | initCloudFlare(`larntv:${logname}:${rund(1000000)}`, urlParams.live, bltLiveFrame); 43 | watchLive(urlParams.live); 44 | } else { 45 | TV_CHANNEL = TV_CHANNEL_LIST; 46 | initList(); 47 | downloadRecordings(recordedGamesLoaded, MIN_FRAMES_TO_LIST); 48 | downloadliveGamesList(liveGamesLoaded); 49 | } 50 | 51 | onResize(); 52 | } 53 | 54 | 55 | 56 | function initRollbar() { 57 | try { 58 | let enableRollbar = !isLocal() && !isFile(); 59 | if (Rollbar) Rollbar.configure({ 60 | enabled: (enableRollbar), 61 | payload: { 62 | code_version: BUILD, 63 | client: { 64 | javascript: { 65 | code_version: BUILD, 66 | } 67 | } 68 | } 69 | }); 70 | } catch (error) { 71 | console.log(`caught`, error); 72 | } 73 | } 74 | 75 | 76 | 77 | function initAWS() { 78 | try { 79 | initLambdaCredentials(); 80 | lambda = new AWS.Lambda({ 81 | region: 'us-east-1', 82 | apiVersion: '2015-03-31' 83 | }); 84 | } catch (error) { 85 | console.error(`go(): not loading aws credentials: ${error}`); 86 | } 87 | } 88 | 89 | 90 | 91 | function setIP(ip) { 92 | // do nothing for larntv 93 | } 94 | 95 | 96 | 97 | function onResize() { 98 | 99 | switch (TV_CHANNEL) { 100 | 101 | case TV_CHANNEL_LIST: { 102 | let spriteWidth = computeSpriteWidth(); 103 | let widthMultiple = 1.66; 104 | let fontSize = spriteWidth * widthMultiple; 105 | let fontFamily = `Courier New`; 106 | let font = `${fontSize}px ${fontFamily}`; 107 | document.body.style.font = font; 108 | break; 109 | } 110 | 111 | case TV_CHANNEL_RECORDED: { 112 | if (video && video.currentFrameNum && video.currentFrameNum >= 0) { 113 | bltRecordedFrame(video.getCurrentFrame()); 114 | } else { 115 | return; 116 | } 117 | break; 118 | } 119 | 120 | case TV_CHANNEL_LIVE: { 121 | if (liveFrameCache) { 122 | bltLiveFrame(liveFrameCache); 123 | } 124 | break; 125 | } 126 | 127 | default: { 128 | console.error(`unknown channel, have you tried adjusting the antenna?`); 129 | } 130 | } 131 | 132 | } 133 | 134 | 135 | let bltFrameCache = -1; 136 | function bltFrame(frame) { 137 | if (!frame) return; 138 | 139 | if (frame.compressed) { 140 | decompressFrame(frame); 141 | // console.log(`bltFrame(): decompressed`, frame.id); 142 | } 143 | 144 | let larnDivs = frame.divs.LARN; 145 | 146 | if (frame.deflated) { 147 | larnDivs = inflate(larnDivs); 148 | } 149 | 150 | if (larnDivs === ``) frame.divs.LARN = EMPTY_LARN_FRAME; 151 | 152 | // TODO: if bltFrameCache is set to a value it prevents rewinding 153 | if (frame.id > bltFrameCache) { 154 | // if (frame.ts > bltFrameCache) { 155 | setDiv(`TV_LARN`, larnDivs); 156 | setDiv(`TV_STATS`, frame.divs.STATS); 157 | // bltFrameCache = frame.id; 158 | //bltFrameCache = frame.ts; 159 | 160 | // compressFrame(frame, true); // handled by frameCompressionJob 161 | } else { 162 | console.log(`bltFrame(): out of order frame`, frame.id, bltFrameCache); 163 | // console.log(`bltFrame(): out of order frame`, frame.ts, bltFrameCache); 164 | return; 165 | } 166 | 167 | // for amiga mode 168 | let sw = computeSpriteWidth(); 169 | document.querySelectorAll(`.image`).forEach((div) => { 170 | // set new width and height 171 | div.style.width = `${sw}px`; 172 | div.style.height = `${sw * 2}px`; 173 | // adjust background image location 174 | div.style.backgroundImage = div.style.backgroundImage.replace(/img\//, `../img/`); 175 | }); // this takes ~0.003 seconds on an m1 mac... 176 | } 177 | 178 | 179 | 180 | // shares most code with setMode(amiga, retro, original) 181 | // todo: consolidate, probable with some sort of a "setFont()" option 182 | function setStyle(styleIn) { 183 | if (!styleIn) { 184 | // console.log(`setStyle(): no style data available`); 185 | styleIn = { fontFamily: `Courier New` }; 186 | // return; 187 | } 188 | 189 | // defaults for courier new 190 | let fontFamily = `Courier New`; 191 | let textColour = `lightgrey`; 192 | let heightMultiple = 1.88; 193 | let letterSpacing = `normal`; 194 | let spacing = 0; 195 | let fontSize = 10; 196 | 197 | let spriteWidth = computeSpriteWidth(); 198 | 199 | if (styleIn.fontFamily.includes(`amiga`)) { 200 | fontFamily = styleIn.fontFamily; 201 | textColour = `lightgrey`; 202 | heightMultiple = 2; 203 | letterSpacing = `normal`; 204 | spacing = 0; 205 | fontSize = spriteWidth * 2; 206 | } 207 | else { 208 | const testfont = `12px ${styleIn.fontFamily}`; 209 | const testtext = `ABCDEFGHIJKLMNOPQRSTUVWXYZ`; 210 | const isBoldWider = getTextWidth(testtext, testfont, true) != getTextWidth(testtext, testfont, false); 211 | fontFamily = isBoldWider ? `Courier New` : styleIn.fontFamily; 212 | 213 | // console.log(testfont, isBoldWider, getTextWidth(testtext, testfont, true), getTextWidth(testtext, testfont, false)); 214 | 215 | if (fontFamily === `modern`) { 216 | fontFamily = `modern`; 217 | textColour = `lightgrey`; 218 | heightMultiple = 1.88; 219 | letterSpacing = `normal`; 220 | spacing = 0; 221 | } else if (fontFamily === `dos437`) { 222 | fontFamily = `dos437`; 223 | textColour = `#ABABAB`; 224 | heightMultiple = 1.93; 225 | letterSpacing = '-1px'; 226 | spacing = -1; 227 | } 228 | fontSize = computeFontSize(fontFamily, spriteWidth, spacing); 229 | } 230 | 231 | let font = `${fontSize}px ${fontFamily}`; 232 | document.body.style.font = font; 233 | document.body.style.fontFamily = fontFamily; 234 | document.body.style.color = textColour; 235 | document.body.style.letterSpacing = letterSpacing; 236 | 237 | if (TV_CHANNEL_LIST) { 238 | let bar = document.getElementById('progressbar'); 239 | if (bar) bar.style.font = `${fontSize}px Courier New`; 240 | let box = document.getElementById('realtime'); 241 | if (box) box.style.font = `${fontSize}px Courier New`; 242 | if (box && box.label) box.label.style.font = `${fontSize}px Courier New`; 243 | } 244 | // console.log(font, fontFamily, textColour, letterSpacing, heightMultiple); 245 | 246 | // do this last for some reason 247 | document.body.style.lineHeight = `${spriteWidth * heightMultiple}px`; 248 | } 249 | 250 | 251 | 252 | // copied & adapted from display.js 253 | function computeSpriteWidth() { 254 | let browserWidth = window.innerWidth; 255 | let browserHeight = window.innerHeight; 256 | 257 | /* 258 | "a) a magic potion of cure dianthroritis" -> 39 characters 259 | width: 80 for game area, 39 for side inventory 260 | height: 24 for game area, 6 for the playback buttons and scrollbar 261 | */ 262 | let rawSpriteW = (browserWidth) / (80 + 39); 263 | let rawSpriteH = (browserHeight) / (24 + 6); 264 | 265 | let spriteWidth = Math.min(rawSpriteW, rawSpriteH / 2); 266 | spriteWidth *= 10; 267 | spriteWidth = Math.floor(spriteWidth); 268 | spriteWidth /= 10; 269 | 270 | return Math.max(4, spriteWidth); 271 | } 272 | 273 | 274 | 275 | function isAmigaMode() { 276 | if (recordedMetadata && recordedMetadata.fontFamily) { 277 | return recordedMetadata.fontFamily.includes(`amiga`); 278 | } else { 279 | return false; 280 | } 281 | } --------------------------------------------------------------------------------