Adding patch notes from git, UI, and loading files from the main install path properly.
Also fixed: * flying wands not pathing properly. * Lantern particles offset * Wand fire offset * Water spray from flask
This commit is contained in:
1 parent
06bb712a46
commit
9bb9797271
19 files changed
+1550
-115
No files matched your search
@@ -67,12 +67,20 @@ target_include_directories(ngblosc INTERFACE ${CMAKE_SOURCE_DIR}/third_party/c-b
|
|||||||
|
|
||||||
add_executable(noita_gpu src/app/main.cpp src/audio/fmod_audio.cpp)
|
add_executable(noita_gpu src/app/main.cpp src/audio/fmod_audio.cpp)
|
||||||
target_link_libraries(noita_gpu PRIVATE ngspawn ngblosc nggpulua Vulkan::Vulkan)
|
target_link_libraries(noita_gpu PRIVATE ngspawn ngblosc nggpulua Vulkan::Vulkan)
|
||||||
|
|
||||||
|
#for changelog
|
||||||
# the short git hash of the sources at configure time (the GIF metadata's "build", docs/GIF_METADATA.md)
|
# the short git hash of the sources at configure time (the GIF metadata's "build", docs/GIF_METADATA.md)
|
||||||
execute_process(COMMAND git rev-parse --short HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE NG_GIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
execute_process(COMMAND git rev-parse --short HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE NG_GIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
||||||
if(NOT NG_GIT_HASH)
|
if(NOT NG_GIT_HASH)
|
||||||
set(NG_GIT_HASH "dev")
|
set(NG_GIT_HASH "dev")
|
||||||
endif()
|
endif()
|
||||||
target_compile_definitions(noita_gpu PRIVATE NG_BUILD="${NG_GIT_HASH}")
|
target_compile_definitions(noita_gpu PRIVATE NG_BUILD="${NG_GIT_HASH}")
|
||||||
|
# the release notes of the menu (src/app/menu.h): the git log as "hash|date|subject" lines, beside the executable and in the demo folder
|
||||||
|
execute_process(COMMAND git log -n 400 --date=short "--pretty=format:%h|%ad|%s" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE NG_GIT_LOG ERROR_QUIET)
|
||||||
|
file(WRITE ${CMAKE_BINARY_DIR}/release_notes.txt "${NG_GIT_LOG}\n")
|
||||||
|
add_custom_command(TARGET noita_gpu POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/release_notes.txt $<TARGET_FILE_DIR:noita_gpu>/release_notes.txt
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/release_notes.txt ${CMAKE_SOURCE_DIR}/demo/release_notes.txt)
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
target_compile_options(noita_gpu PRIVATE /W4 /permissive- /bigobj)
|
target_compile_options(noita_gpu PRIVATE /W4 /permissive- /bigobj)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ layout(std430, set = 0, binding = 23) buffer Ctrl {
|
|||||||
ivec4 hand1; // host: held wand: texel offset, w, h
|
ivec4 hand1; // host: held wand: texel offset, w, h
|
||||||
vec4 hand2; // host: held wand: pivot
|
vec4 hand2; // host: held wand: pivot
|
||||||
ivec4 burnN; // rigid_burn.comp: x the burnt pixels; y: the player's respawns, z: saved by SAVING_GRACE, w: an extra life used (the host reads and clears them)
|
ivec4 burnN; // rigid_burn.comp: x the burnt pixels; y: the player's respawns, z: saved by SAVING_GRACE, w: an extra life used (the host reads and clears them)
|
||||||
|
ivec4 burnEv[256]; ivec4 reactN; ivec4 reactEv[64];
|
||||||
|
ivec4 extra; // host: x = 1: a dead player stays dead where it fell (the game over of the menus) instead of respawning
|
||||||
} ctrl;
|
} ctrl;
|
||||||
#include "hit_liquid.glsl"
|
#include "hit_liquid.glsl"
|
||||||
|
|
||||||
@@ -303,10 +305,15 @@ void main() {
|
|||||||
if (hp <= 0.0 && (e.ex8.x & 16) != 0 && hpBeforeHit > 0.04) { hp = 0.04; atomicAdd(ctrl.burnN.z, 1); }
|
if (hp <= 0.0 && (e.ex8.x & 16) != 0 && hpBeforeHit > 0.04) { hp = 0.04; atomicAdd(ctrl.burnN.z, 1); }
|
||||||
else if (hp <= 0.0 && (ents[id].ex8.x & 131072) != 0) { hp = min(4.0, e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0); atomicAnd(ents[id].ex8.x, ~131072); atomicAdd(ctrl.burnN.w, 1); }
|
else if (hp <= 0.0 && (ents[id].ex8.x & 131072) != 0) { hp = min(4.0, e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0); atomicAnd(ents[id].ex8.x, ~131072); atomicAdd(ctrl.burnN.w, 1); }
|
||||||
if (hp <= 0.0 && (e.ex8.x & 32768) == 0) { // respawn at the spawn point (a polymorphed body is not: the host ends the polymorph, 32768)
|
if (hp <= 0.0 && (e.ex8.x & 32768) == 0) { // respawn at the spawn point (a polymorphed body is not: the host ends the polymorph, 32768)
|
||||||
|
if (ctrl.extra.x != 0) { // the game over (menu.h): the body stays where it fell, dead - no controls, counted once for the host
|
||||||
|
if ((flags & ST_DEAD) == 0) atomicAdd(ctrl.burnN.y, 1);
|
||||||
|
flags |= ST_DEAD; hp = 0.0; target = 0.0; want_jump = false; thrust = false;
|
||||||
|
} else {
|
||||||
pos = vec2(float(e.x.z), float(e.x.w)); vel = vec2(0.0); ip = ivec2(floor(pos));
|
pos = vec2(float(e.x.z), float(e.x.w)); vel = vec2(0.0); ip = ivec2(floor(pos));
|
||||||
hp = e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0;
|
hp = e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0;
|
||||||
atomicAdd(ctrl.burnN.y, 1);
|
atomicAdd(ctrl.burnN.y, 1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (kind == 1 || kind == 2) { // ---- creatures ----
|
} else if (kind == 1 || kind == 2) { // ---- creatures ----
|
||||||
// the target of the creature: the player; a charmed creature (game effect CHARM) fights the nearest creature of another herd instead
|
// the target of the creature: the player; a charmed creature (game effect CHARM) fights the nearest creature of another herd instead
|
||||||
vec4 tp = pl; bool tAlive = player_alive; int tgt = 0;
|
vec4 tp = pl; bool tAlive = player_alive; int tgt = 0;
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ void main() {
|
|||||||
if (mirror) hl.y = -hl.y;
|
if (mirror) hl.y = -hl.y;
|
||||||
hand = root + vec2(c * hl.x - s * hl.y, s * hl.x + c * hl.y);
|
hand = root + vec2(c * hl.x - s * hl.y, s * hl.x + c * hl.y);
|
||||||
}
|
}
|
||||||
|
if (ctrl.hand1.w == 1) hand += vec2(mirror ? -0.5 : 0.5, 0.5); // flask entity transform, matching the host spray origin
|
||||||
{ // the item's recoil (00b51670): hand2.z px back along the aim, turned muzzle-up by hand0.w radians (the arm keeps the aim)
|
{ // the item's recoil (00b51670): hand2.z px back along the aim, turned muzzle-up by hand0.w radians (the arm keeps the aim)
|
||||||
const float ia = ang + (mirror ? ctrl.hand0.w : -ctrl.hand0.w);
|
const float ia = ang + (mirror ? ctrl.hand0.w : -ctrl.hand0.w);
|
||||||
const float ic = cos(ia), is = sin(ia);
|
const float ic = cos(ia), is = sin(ia);
|
||||||
|
|||||||
+11
-10
@@ -62,7 +62,8 @@ void displaced_particle(uint id, FPart p) {
|
|||||||
fparts[id].c.w = p.c.w + 1u;
|
fparts[id].c.w = p.c.w + 1u;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const float g = 0.1667;
|
const bool spray = (p.c.w & 0x200000u) != 0u;
|
||||||
|
const float g = spray ? PART_GRAV : 0.1667;
|
||||||
vec2 target = pos + vel;
|
vec2 target = pos + vel;
|
||||||
vel.y += g * (kind == 1u ? 0.25 : 1.0);
|
vel.y += g * (kind == 1u ? 0.25 : 1.0);
|
||||||
if (kind == 1u && vel.y > 0.0) kind = 2u;
|
if (kind == 1u && vel.y > 0.0) kind = 2u;
|
||||||
@@ -81,14 +82,14 @@ void displaced_particle(uint id, FPart p) {
|
|||||||
uint m = cell_mat(cells[q.y * pc.gw + q.x]);
|
uint m = cell_mat(cells[q.y * pc.gw + q.x]);
|
||||||
if (m != 0u) {
|
if (m != 0u) {
|
||||||
uint f = mflags(m);
|
uint f = mflags(m);
|
||||||
if ((f & F_STATIC) != 0u) { dead = true; break; }
|
if ((f & F_STATIC) != 0u && !(spray && kind == 1u)) { dead = true; break; }
|
||||||
if ((f & F_LIQUID) != 0u) {
|
if ((f & F_LIQUID) != 0u || (spray && (f & F_STATIC) != 0u)) {
|
||||||
if (kind == 2u) { dead = true; break; } // mode 0 stops at liquid (and powder)
|
if (kind == 2u) { dead = true; break; } // mode 0 stops at liquid (and powder)
|
||||||
float eff = (vel.y > -0.83 || (p.c.w & 0x100000u) != 0u) ? 0.0 : dens; // mode 1: effective density 0 (an upward speed below 50 px/s, or a draw at creation) unless moving up fast; it must be lighter than the target
|
float eff = (vel.y > -0.83 || (p.c.w & 0x100000u) != 0u) ? 0.0 : dens; // mode 1: effective density 0 (an upward speed below 50 px/s, or a draw at creation) unless moving up fast; it must be lighter than the target
|
||||||
if (!(eff < mdensity(m)) || budget == 0u) { dead = true; break; }
|
if (!(eff < mdensity(m)) || budget == 0u) { dead = true; break; }
|
||||||
budget -= 1u;
|
budget -= 1u;
|
||||||
}
|
} else if (spray) kind = 2u;
|
||||||
}
|
} else if (spray) kind = 2u; // 00712390: leaving material ends the mode-1 penetration.
|
||||||
cur = nx;
|
cur = nx;
|
||||||
}
|
}
|
||||||
if (!dead && int(mlen) <= 60) { // the target cell itself
|
if (!dead && int(mlen) <= 60) { // the target cell itself
|
||||||
@@ -98,15 +99,15 @@ void displaced_particle(uint id, FPart p) {
|
|||||||
uint m = cell_mat(cells[q.y * pc.gw + q.x]);
|
uint m = cell_mat(cells[q.y * pc.gw + q.x]);
|
||||||
if (m != 0u) {
|
if (m != 0u) {
|
||||||
uint f = mflags(m);
|
uint f = mflags(m);
|
||||||
if ((f & F_STATIC) != 0u) dead = true;
|
if ((f & F_STATIC) != 0u && !(spray && kind == 1u)) dead = true;
|
||||||
else if ((f & F_LIQUID) != 0u) {
|
else if ((f & F_LIQUID) != 0u || (spray && (f & F_STATIC) != 0u)) {
|
||||||
if (kind == 2u) dead = true;
|
if (kind == 2u) dead = true;
|
||||||
else {
|
else {
|
||||||
float eff = (vel.y > -0.83 || (p.c.w & 0x100000u) != 0u) ? 0.0 : dens;
|
float eff = (vel.y > -0.83 || (p.c.w & 0x100000u) != 0u) ? 0.0 : dens;
|
||||||
if (!(eff < mdensity(m)) || budget == 0u) dead = true; else budget -= 1u;
|
if (!(eff < mdensity(m)) || budget == 0u) dead = true; else budget -= 1u;
|
||||||
}
|
}
|
||||||
}
|
} else if (spray) kind = 2u;
|
||||||
}
|
} else if (spray) kind = 2u;
|
||||||
if (!dead) cur = target;
|
if (!dead) cur = target;
|
||||||
}
|
}
|
||||||
} else if (!dead) cur = target; // beyond the 60 probes the end point is taken as it is
|
} else if (!dead) cur = target; // beyond the 60 probes the end point is taken as it is
|
||||||
@@ -116,7 +117,7 @@ void displaced_particle(uint id, FPart p) {
|
|||||||
kind = 3u; // no place yet: it keeps trying
|
kind = 3u; // no place yet: it keeps trying
|
||||||
}
|
}
|
||||||
fparts[id].a = vec4(pos, vel);
|
fparts[id].a = vec4(pos, vel);
|
||||||
fparts[id].c = uvec4(cw, kind, budget, (p.c.w & 0x100000u) | ((p.c.w & 0xFFFFFu) + 1u));
|
fparts[id].c = uvec4(cw, kind, budget, (p.c.w & 0x300000u) | ((p.c.w & 0xFFFFFu) + 1u));
|
||||||
ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
|
ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
|
||||||
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) {
|
if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) {
|
||||||
vec3 col = cell_colour(cw);
|
vec3 col = cell_colour(cw);
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ void main() {
|
|||||||
if (old == 0u) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); }
|
if (old == 0u) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); }
|
||||||
else if ((r.c.w & 2u) == 0u && (mflags(cell_mat(old)) & (F_GAS | F_LIQUID)) != 0u && (mflags(cell_mat(old)) & (F_STATIC | F_SAND)) == 0u && atomicCompSwap(cells[ci], old, nc) == old) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); }
|
else if ((r.c.w & 2u) == 0u && (mflags(cell_mat(old)) & (F_GAS | F_LIQUID)) != 0u && (mflags(cell_mat(old)) & (F_STATIC | F_SAND)) == 0u && atomicCompSwap(cells[ci], old, nc) == old) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); }
|
||||||
}
|
}
|
||||||
|
else if (r.c.y == 7u) { // PotionSystem 00bfdd10: mode 1, density zero, 20-cell penetration budget.
|
||||||
|
const int ps = alloc_part();
|
||||||
|
if (ps < 0) return;
|
||||||
|
fparts[ps].a = r.a;
|
||||||
|
fparts[ps].b = vec4(r.b.x, 1.0, uintBitsToFloat(birth_frame()), 0.0);
|
||||||
|
fparts[ps].c = uvec4(r.c.z | (((hash3(i, uint(pc.frame), 64u) >> 16) & 0xFFu) << 16), 1u, 20u, 0x300000u);
|
||||||
|
}
|
||||||
else if (r.c.y == 6u) emit_displaced(r.a.xy, r.a.zw, r.c.z | (((hash3(i, uint(pc.frame), 64u) >> 16) & 0xFFu) << 16), 2u, birth_frame()); // GameCreateParticle (007b3120 -> 00716af0): a cell of the material handed to the particle system like a displaced one (AddAsParticle 007168c0): it falls and goes back into the grid, waiting for a free place
|
else if (r.c.y == 6u) emit_displaced(r.a.xy, r.a.zw, r.c.z | (((hash3(i, uint(pc.frame), 64u) >> 16) & 0xFFu) << 16), 2u, birth_frame()); // GameCreateParticle (007b3120 -> 00716af0): a cell of the material handed to the particle system like a displaced one (AddAsParticle 007168c0): it falls and goes back into the grid, waiting for a free place
|
||||||
else emit_material_particle(r.a.xy, r.a.zw, r.b.x, r.b.y, r.c.x, int(r.c.z));
|
else emit_material_particle(r.a.xy, r.a.zw, r.b.x, r.b.y, r.c.x, int(r.c.z));
|
||||||
}
|
}
|
||||||
+8
-2
@@ -27,7 +27,7 @@ layout(std430, set = 0, binding = 39) readonly buffer EntPrev { vec4 prevpos[];
|
|||||||
|
|
||||||
layout(std430, set = 0, binding = 26) readonly buffer Overlay { uint overlay[]; }; // CPU-drawn HUD (ARGB, output pixels)
|
layout(std430, set = 0, binding = 26) readonly buffer Overlay { uint overlay[]; }; // CPU-drawn HUD (ARGB, output pixels)
|
||||||
struct FLight { vec4 a; vec4 b; }; // x, y, radius (world cells), strength / rgb
|
struct FLight { vec4 a; vec4 b; }; // x, y, radius (world cells), strength / rgb
|
||||||
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl;
|
layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; ivec4 burnN; ivec4 burnEv[256]; ivec4 reactN; ivec4 reactEv[64]; ivec4 extra; } ctrl; // extra.yzw: the Options' brightness, contrast, gamma x 10000 (main.cpp)
|
||||||
layout(std430, set = 0, binding = 38) readonly buffer FxLights { uint lcount; uint lpad[3]; FLight lights[512]; }; // lights of projectiles and explosions this frame
|
layout(std430, set = 0, binding = 38) readonly buffer FxLights { uint lcount; uint lpad[3]; FLight lights[512]; }; // lights of projectiles and explosions this frame
|
||||||
|
|
||||||
layout(std430, set = 0, binding = 44) readonly buffer Sky { uint sky[]; }; // src/world/sky.h
|
layout(std430, set = 0, binding = 44) readonly buffer Sky { uint sky[]; }; // src/world/sky.h
|
||||||
@@ -423,7 +423,7 @@ void main() {
|
|||||||
if (p.x >= pc.ow || p.y >= pc.oh) return;
|
if (p.x >= pc.ow || p.y >= pc.oh) return;
|
||||||
ivec2 cp = clamp(ivec2(floor(vec2(p + so + vis) / scl)), ivec2(0), ivec2(pc.gw - 1, pc.gh - 1));
|
ivec2 cp = clamp(ivec2(floor(vec2(p + so + vis) / scl)), ivec2(0), ivec2(pc.gw - 1, pc.gh - 1));
|
||||||
vec3 rgb = unpackUnorm4x8(wcell[cp.y * pc.gw + cp.x].x).rgb; // (only the colour: the light is read for the player's pixels)
|
vec3 rgb = unpackUnorm4x8(wcell[cp.y * pc.gw + cp.x].x).rgb; // (only the colour: the light is read for the player's pixels)
|
||||||
if (ctrl.inp2.y != 0 && (ents[0].st.w & 8) == 0) {
|
if (ctrl.inp2.y != 0 && (ents[0].st.w & 8) == 0) { // (dead: the ragdoll of 00abb7d0 is the body, main.cpp onCreatureDeath(0, true))
|
||||||
vec2 P = ents[0].a.xy;
|
vec2 P = ents[0].a.xy;
|
||||||
const vec2 pv = prevpos[0].xy;
|
const vec2 pv = prevpos[0].xy;
|
||||||
if (distance(pv, P) < 10.0) P = mix(pv, P, ctrl.view.z);
|
if (distance(pv, P) < 10.0) P = mix(pv, P, ctrl.view.z);
|
||||||
@@ -445,6 +445,12 @@ void main() {
|
|||||||
rgb += vec3(0.7, 0.1, 0.0) * a * lh;
|
rgb += vec3(0.7, 0.1, 0.0) * a * lh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (ctrl.extra.y != 0 || ctrl.extra.z != 0 || ctrl.extra.w != 0) { // the Options' sliders (post_final.frag: + brightness, (c - 0.5) x contrast + 0.5, pow(c, gamma), clamped; 1 + the slider for the last two)
|
||||||
|
rgb += float(ctrl.extra.y) * 1e-4;
|
||||||
|
rgb = (rgb - 0.5) * (1.0 + float(ctrl.extra.z) * 1e-4) + 0.5;
|
||||||
|
rgb = pow(max(rgb, vec3(0.0)), vec3(1.0 + float(ctrl.extra.w) * 1e-4));
|
||||||
|
rgb = clamp(rgb, 0.0, 1.0);
|
||||||
|
}
|
||||||
{ // the picture without the HUD for the replay recorder (the game's GIFs have no UI): clip_half.comp / main.cpp copy it
|
{ // the picture without the HUD for the replay recorder (the game's GIFs have no UI): clip_half.comp / main.cpp copy it
|
||||||
const uvec3 q0 = uvec3(clamp(rgb, 0.0, 1.0) * 255.0 + 0.5);
|
const uvec3 q0 = uvec3(clamp(rgb, 0.0, 1.0) * 255.0 + 0.5);
|
||||||
clipraw[p.y * pc.ow + p.x] = 0xFF000000u | (q0.r << 16) | (q0.g << 8) | q0.b;
|
clipraw[p.y * pc.ow + p.x] = 0xFF000000u | (q0.r << 16) | (q0.g << 8) | q0.b;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ void main() {
|
|||||||
ivec4 h0 = entinst[4u * id], hi = entinst[4u * id + 1u], hw = entinst[4u * id + 2u];
|
ivec4 h0 = entinst[4u * id], hi = entinst[4u * id + 1u], hw = entinst[4u * id + 2u];
|
||||||
Ent e = ents[h0.x];
|
Ent e = ents[h0.x];
|
||||||
gKey = (uint(clamp(h0.w + 32768, 0, 65535)) << 16) | (65535u - (id & 65535u));
|
gKey = (uint(clamp(h0.w + 32768, 0, 65535)) << 16) | (65535u - (id & 65535u));
|
||||||
if ((e.st.w & (8 | 4096)) != 0) return; // dead, or its sprites switched off (4096: a creature, main.cpp on_components_enabled)
|
if ((e.st.w & (8 | 4096)) != 0) return; // dead (the host draws the ragdoll), or its sprites switched off (4096: a creature, main.cpp on_components_enabled)
|
||||||
if (e.st.x != 4 && (e.ex8.x & 8192) != 0) return;
|
if (e.st.x != 4 && (e.ex8.x & 8192) != 0) return;
|
||||||
{ // the hit flash: creatures, the player and physics items (a worm segment, kind 0, keeps its root slot in ex19.w)
|
{ // the hit flash: creatures, the player and physics items (a worm segment, kind 0, keeps its root slot in ex19.w)
|
||||||
const int fc = int(e.ex19.w + 0.5);
|
const int fc = int(e.ex19.w + 0.5);
|
||||||
|
|||||||
+2
-2
@@ -142,7 +142,7 @@ std::vector<PathNode> search_pass(const PathGrid& g, float sx, float sy, const S
|
|||||||
else if (g.valid(snx, sny - d) && !g.solid(snx, sny - d)) { sny -= d; moved = true; }
|
else if (g.valid(snx, sny - d) && !g.solid(snx, sny - d)) { sny -= d; moved = true; }
|
||||||
}
|
}
|
||||||
if (!moved) return out;
|
if (!moved) return out;
|
||||||
} else if (!p.can_fly && !g.solid(snx, sny + 1) && !(g.at(snx, sny + 1) & kNodeLiquid) && !(g.at(snx, sny) & kNodeLiquid)) {
|
} else if (!p.can_fly && !p.flyer && !g.solid(snx, sny + 1) && !(g.at(snx, sny + 1) & kNodeLiquid) && !(g.at(snx, sny) & kNodeLiquid)) { // (a flyer starts where it is, in the air: 00861e00 +0x24 at the start)
|
||||||
for (int d = 1; d <= 3; ++d) {
|
for (int d = 1; d <= 3; ++d) {
|
||||||
if (!g.valid(snx, sny + d) || g.solid(snx, sny + d) || (g.at(snx, sny + d) & kNodeLiquid)) break;
|
if (!g.valid(snx, sny + d) || g.solid(snx, sny + d) || (g.at(snx, sny + d) & kNodeLiquid)) break;
|
||||||
sny += 1;
|
sny += 1;
|
||||||
@@ -339,7 +339,7 @@ std::vector<PathNode> search(const PathGrid& g, float sx, float sy, const Search
|
|||||||
bool budget = false;
|
bool budget = false;
|
||||||
std::vector<PathNode> partial;
|
std::vector<PathNode> partial;
|
||||||
if (p.can_walk || p.can_dive || p.can_swim_on_surface) {
|
if (p.can_walk || p.can_dive || p.can_swim_on_surface) {
|
||||||
SearchParams q = p; q.can_fly = false; q.can_jump = false;
|
SearchParams q = p; q.can_fly = false; q.can_jump = false; q.flyer = p.can_fly;
|
||||||
std::vector<PathNode> r = search_pass(g, sx, sy, q, budget);
|
std::vector<PathNode> r = search_pass(g, sx, sy, q, budget);
|
||||||
if (!r.empty() && !budget) return r;
|
if (!r.empty() && !budget) return r;
|
||||||
if (budget) partial = std::move(r);
|
if (budget) partial = std::move(r);
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ enum class Goal { None, Hunt, RandomMove, Goto, MoveNear, Burning, Away, Escape,
|
|||||||
|
|
||||||
struct SearchParams {
|
struct SearchParams {
|
||||||
bool can_fly = false, can_walk = true, can_jump = false, can_dive = false, can_swim_on_surface = false;
|
bool can_fly = false, can_walk = true, can_jump = false, can_dive = false, can_swim_on_surface = false;
|
||||||
|
bool flyer = false; // (set by search(): the creature can fly - 00861e00 decides the start projection on the original can_fly, before the passes turn it off)
|
||||||
int clearance_required = 0; // space_required / 4 (00bca030: input +0xcc)
|
int clearance_required = 0; // space_required / 4 (00bca030: input +0xcc)
|
||||||
float cost_of_flying = 20.0f;
|
float cost_of_flying = 20.0f;
|
||||||
int depth_max = 50; // nodes along the path (AnimalAI pathfinding_max_depth_has_target / _no_target)
|
int depth_max = 50; // nodes along the path (AnimalAI pathfinding_max_depth_has_target / _no_target)
|
||||||
|
|||||||
+21
-3
@@ -49,6 +49,23 @@ struct Icon {
|
|||||||
bool ok() const { return w > 0; }
|
bool ok() const { return w > 0; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// data/shaders/imgui_potion_icon.frag: the upper part retains the glass colour.
|
||||||
|
inline int potion_fill_row(int height, float fraction) {
|
||||||
|
return std::clamp((int)std::ceil(height * (0.85f - 0.67f * std::max(0.1f, fraction)) - 0.5f), 0, height);
|
||||||
|
}
|
||||||
|
inline Icon potion_icon(const Icon& base, uint32_t material, int fill_row) {
|
||||||
|
Icon out = base;
|
||||||
|
for (int y = 0; y < out.h; ++y) for (int x = 0; x < out.w; ++x) {
|
||||||
|
uint32_t& p = out.px[(size_t)y * out.w + x];
|
||||||
|
auto channel = [&](int shift) {
|
||||||
|
const float tint = y >= fill_row ? ((material >> shift) & 255) / 255.0f : 1.0f;
|
||||||
|
return (uint32_t)(((p >> shift) & 255) * tint * 0.85f);
|
||||||
|
};
|
||||||
|
p = (p & 0xFF000000u) | channel(16) << 16 | channel(8) << 8 | channel(0);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
inline Icon load_icon(const ng::Wak& wak, const std::string& path) {
|
inline Icon load_icon(const ng::Wak& wak, const std::string& path) {
|
||||||
Icon ic;
|
Icon ic;
|
||||||
std::vector<uint8_t> raw;
|
std::vector<uint8_t> raw;
|
||||||
@@ -285,7 +302,7 @@ struct State {
|
|||||||
struct WandInfo { const Icon* icon = nullptr; std::string name; int cards = 0, capacity = 0, uses = -1; }; // uses: the uses left of its first limited spell (-1 none)
|
struct WandInfo { const Icon* icon = nullptr; std::string name; int cards = 0, capacity = 0, uses = -1; }; // uses: the uses left of its first limited spell (-1 none)
|
||||||
std::vector<WandInfo> wands; // inventory slots
|
std::vector<WandInfo> wands; // inventory slots
|
||||||
int active_wand = -1;
|
int active_wand = -1;
|
||||||
struct ItemInfo { const Icon* icon = nullptr; float frac = 1.0f; std::string name; bool empty = false; }; // an item of the quick inventory (flask): icon, how full it is
|
struct ItemInfo { const Icon* icon = nullptr; std::string name; bool empty = false; }; // flask icon includes its liquid level
|
||||||
std::vector<ItemInfo> items; // slots 5-8
|
std::vector<ItemInfo> items; // slots 5-8
|
||||||
int active_slot = -1; // highlighted slot (0-7) when it is an item
|
int active_slot = -1; // highlighted slot (0-7) when it is an item
|
||||||
std::vector<const Icon*> spell_icons; // spells of the active wand, in deck order
|
std::vector<const Icon*> spell_icons; // spells of the active wand, in deck order
|
||||||
@@ -322,6 +339,7 @@ struct State {
|
|||||||
bool drag_on = false; const Icon* drag_icon = nullptr; int drag_bg = -1; int drag_x = 0, drag_y = 0; // the item on the cursor
|
bool drag_on = false; const Icon* drag_icon = nullptr; int drag_bg = -1; int drag_x = 0, drag_y = 0; // the item on the cursor
|
||||||
std::vector<TipLine> tip; // hover information
|
std::vector<TipLine> tip; // hover information
|
||||||
int tip_x = 0, tip_y = 0;
|
int tip_x = 0, tip_y = 0;
|
||||||
|
std::string cursor_material;
|
||||||
// ---- the text sprites of the world (shop prices: SpriteComponent is_text_sprite, font_pixel_white), top-left corner in output pixels, scale = output pixels per world pixel ----
|
// ---- the text sprites of the world (shop prices: SpriteComponent is_text_sprite, font_pixel_white), top-left corner in output pixels, scale = output pixels per world pixel ----
|
||||||
std::vector<InvText> world_texts;
|
std::vector<InvText> world_texts;
|
||||||
// ---- overlays drawn over everything (the information box of a wand in the world, the new-wand choice): style 2 the dark information box, 3 the orange banner ----
|
// ---- overlays drawn over everything (the information box of a wand in the world, the new-wand choice): style 2 the dark information box, 3 the orange banner ----
|
||||||
@@ -434,8 +452,6 @@ inline void draw(Canvas& cv, const Assets& as, const State& s) {
|
|||||||
if (i >= 4 && i - 4 < (int)s.items.size() && !s.items[i - 4].empty) {
|
if (i >= 4 && i - 4 < (int)s.items.size() && !s.items[i - 4].empty) {
|
||||||
const State::ItemInfo& it = s.items[i - 4];
|
const State::ItemInfo& it = s.items[i - 4];
|
||||||
if (it.icon && it.icon->ok()) cv.blit_centered(it.icon->px.data(), it.icon->w, it.icon->h, x + slot / 2, y0 + slot / 2, std::max(1, std::min(ui, (slot * 3 / 4) / std::max(it.icon->w, 1))));
|
if (it.icon && it.icon->ok()) cv.blit_centered(it.icon->px.data(), it.icon->w, it.icon->h, x + slot / 2, y0 + slot / 2, std::max(1, std::min(ui, (slot * 3 / 4) / std::max(it.icon->w, 1))));
|
||||||
cv.rect(x + 2 * ui, y0 + slot - 3 * ui, slot - 4 * ui, 2 * ui, rgba(0, 0, 0, 170)); // how full the flask is
|
|
||||||
cv.rect(x + 2 * ui, y0 + slot - 3 * ui, (int)((slot - 4 * ui) * std::clamp(it.frac, 0.0f, 1.0f)), 2 * ui, rgba(110, 170, 230));
|
|
||||||
}
|
}
|
||||||
if (i < 4 && i < (int)s.wands.size() && s.wands[i].icon && s.wands[i].icon->ok()) {
|
if (i < 4 && i < (int)s.wands.size() && s.wands[i].icon && s.wands[i].icon->ok()) {
|
||||||
const Icon& ic = *s.wands[i].icon;
|
const Icon& ic = *s.wands[i].icon;
|
||||||
@@ -443,6 +459,8 @@ inline void draw(Canvas& cv, const Assets& as, const State& s) {
|
|||||||
if (s.wands[i].uses >= 0) cv.text(x + 2 * ui, y0 + 2 * ui, std::to_string(s.wands[i].uses), rgba(255, 255, 255), std::max(1, ts)); // the first limited spell's uses left
|
if (s.wands[i].uses >= 0) cv.text(x + 2 * ui, y0 + 2 * ui, std::to_string(s.wands[i].uses), rgba(255, 255, 255), std::max(1, ts)); // the first limited spell's uses left
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!s.cursor_material.empty() && !s.inv_open && !s.paused)
|
||||||
|
cv.text(x0 + 8 * pitch + 5 * ui, y0 + (slot - 7 * ts) / 2, s.cursor_material, white, ts);
|
||||||
// spells of the active wand, under the quick inventory
|
// spells of the active wand, under the quick inventory
|
||||||
const int sy = y0 + slot + 3 * ui;
|
const int sy = y0 + slot + 3 * ui;
|
||||||
for (size_t i = 0; i < s.spell_icons.size() && !s.inv_open; ++i) {
|
for (size_t i = 0; i < s.spell_icons.size() && !s.inv_open; ++i) {
|
||||||
|
|||||||
+658
-90
File diff suppressed because it is too large.
Load diff
+765
@@ -0,0 +1,765 @@
|
|||||||
|
// The game's menus, drawn into the HUD canvas: the title screen (006e3410 with DAT_0120761b off), the pause menu (the same with the help image), the new game
|
||||||
|
// panel with the game modes (006ceb00 / 006378a0: "normal" and "dailyrun" built in, then every mod with is_game_mode in its mod.xml), the daily run's seed from
|
||||||
|
// the game's backend (0084c6f0: GET https://takapuoli.noitagame.com/callback/ -> "version;daily seed;daily practice seed;1;"), the options (006d5620: the tabs
|
||||||
|
// General / Graphics / Audio / Input / Streaming / Mod settings), the release notes (0084d020 reads _release_notes.txt beside the game: here release_notes.txt
|
||||||
|
// beside our executable, written from the git log at configure time - every entry is a commit, its hash the version), the credits (data/credits.txt) and the
|
||||||
|
// game over screen with the run's stats (006e51e0: the cause of death, then the stats box with the bests, New Game / Save death replay / Progress / Quit).
|
||||||
|
// Layout in GUI units: `gs` window pixels each (the game's GUI scale: the window height / 360, so a 1440p window draws at 4 like the game).
|
||||||
|
#pragma once
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <ctime>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "app/hud.h"
|
||||||
|
#include "core/wak.h"
|
||||||
|
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
#include <wininet.h>
|
||||||
|
#pragma comment(lib, "wininet.lib")
|
||||||
|
|
||||||
|
namespace menu {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- assets ----------------------------------------------------------------
|
||||||
|
struct Assets {
|
||||||
|
hud::Icon logo, gameOver, gameCompleted, help, p9, p9gray, tab, tabHover, tabSel, bannerSel, grass, bannerNormalBg, bannerNormalFg, bannerDailyBg, bannerDailyFg,
|
||||||
|
mountains1, mountains2, layer1, layer2, clouds1, stars[7], cursor, gridBox, gridBoxUnknown, gridNew, iconUnknown;
|
||||||
|
std::vector<uint32_t> sky; int skyW = 0, skyH = 0; // weather_gfx/parallax_colors_main_menu.bmp: a column is the sky's gradient from the top (row 0) down
|
||||||
|
std::string credits;
|
||||||
|
bool loaded = false;
|
||||||
|
static bool load_bmp(const std::vector<uint8_t>& raw, std::vector<uint32_t>& px, int& w, int& h) { // 24-bit uncompressed, bottom-up
|
||||||
|
if (raw.size() < 54 || raw[0] != 'B' || raw[1] != 'M') return false;
|
||||||
|
const uint32_t off = *reinterpret_cast<const uint32_t*>(&raw[10]);
|
||||||
|
const int32_t bw = *reinterpret_cast<const int32_t*>(&raw[18]), bh = *reinterpret_cast<const int32_t*>(&raw[22]);
|
||||||
|
const uint16_t bpp = *reinterpret_cast<const uint16_t*>(&raw[28]);
|
||||||
|
if (bpp != 24 || bw <= 0 || bh == 0) return false;
|
||||||
|
const int ah = std::abs(bh), stride = (bw * 3 + 3) & ~3;
|
||||||
|
if (off + (size_t)stride * ah > raw.size()) return false;
|
||||||
|
w = bw; h = ah; px.assign((size_t)w * h, 0);
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
const uint8_t* row = &raw[off + (size_t)(bh > 0 ? h - 1 - y : y) * stride];
|
||||||
|
for (int x = 0; x < w; ++x) px[(size_t)y * w + x] = 0xFF000000u | (uint32_t)row[x * 3 + 2] << 16 | (uint32_t)row[x * 3 + 1] << 8 | row[x * 3];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void load(const ng::Wak& wak) {
|
||||||
|
auto L = [&](const char* p) { return hud::load_icon(wak, p); };
|
||||||
|
logo = L("data/ui_gfx/pause_menu/noita_logo.png"); gameOver = L("data/ui_gfx/game_over_menu/game_over.png"); gameCompleted = L("data/ui_gfx/game_over_menu/game_completed.png");
|
||||||
|
help = L("data/ui_gfx/pause_menu/help_keyboardmouse.png"); p9 = L("data/ui_gfx/decorations/9piece0.png"); p9gray = L("data/ui_gfx/decorations/9piece0_gray.png");
|
||||||
|
tab = L("data/ui_gfx/decorations/tab.png"); tabHover = L("data/ui_gfx/decorations/tab_hovered.png"); tabSel = L("data/ui_gfx/decorations/tab_selected.png");
|
||||||
|
bannerSel = L("data/ui_gfx/game_mode/banner_selected_overlay.png"); grass = L("data/ui_gfx/game_mode/background_grass.png");
|
||||||
|
bannerNormalBg = L("data/ui_gfx/game_mode/banner_normal_background.png"); bannerNormalFg = L("data/ui_gfx/game_mode/banner_normal_overlay.png");
|
||||||
|
bannerDailyBg = L("data/ui_gfx/game_mode/banner_dailyrun_background.png"); bannerDailyFg = L("data/ui_gfx/game_mode/banner_dailyrun_overlay.png");
|
||||||
|
mountains1 = L("data/weather_gfx/parallax_mountains_01.png"); mountains2 = L("data/weather_gfx/parallax_mountains_02.png");
|
||||||
|
layer1 = L("data/weather_gfx/parallax_mountains_layer_01.png"); layer2 = L("data/weather_gfx/parallax_mountains_layer_02.png"); clouds1 = L("data/weather_gfx/parallax_clounds_01.png");
|
||||||
|
for (int i = 0; i < 7; ++i) stars[i] = L(("data/weather_gfx/star_00" + std::to_string(i + 1) + ".png").c_str());
|
||||||
|
cursor = L("data/ui_gfx/mouse_cursor.png");
|
||||||
|
gridBox = L("data/ui_gfx/progress_menu/grid_box.png"); gridBoxUnknown = L("data/ui_gfx/progress_menu/grid_box_unknown.png"); // (006dfe40: the Progress screen's cells)
|
||||||
|
gridNew = L("data/ui_gfx/progress_menu/grid_highlight_new.png"); iconUnknown = L("data/ui_gfx/progress_menu/icon_unknown.png");
|
||||||
|
std::vector<uint8_t> raw;
|
||||||
|
if (wak.read("data/weather_gfx/parallax_colors_main_menu.bmp", raw)) load_bmp(raw, sky, skyW, skyH);
|
||||||
|
if (wak.read("data/credits.txt", raw)) credits.assign(raw.begin(), raw.end());
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- settings ----------------------------------------------------------------
|
||||||
|
// The options of the Options screen, kept in <save dir>/settings.txt (key=value). The engine reads them where they apply (main.cpp).
|
||||||
|
struct KeyBind { const char* id; const char* name; int key; };
|
||||||
|
struct Settings {
|
||||||
|
int window_mode = 0; // 0 windowed, 1 borderless fullscreen
|
||||||
|
bool vsync = true, cursor = false, replay_recorder = false, damage_numbers = true, pause_unfocused = true, click_switches_item = false, hover_info_at_mouse = false;
|
||||||
|
float music = 1.0f, sfx = 1.0f, screenshake = 1.0f;
|
||||||
|
// 006d5620's sliders: brightness -0.09 .. 0.09, contrast -0.12 .. 0.2, gamma -0.32 .. 0.695 (all 0 by default: post_final.frag adds the brightness, scales about
|
||||||
|
// 0.5 by 1 + contrast, raises to 1 + gamma), the cosmetic particle coefficient 0.05 .. 1 (the count of every cosmetic emission)
|
||||||
|
float brightness = 0.0f, contrast = 0.0f, gamma = 0.0f, cosmetic_particles = 1.0f;
|
||||||
|
static constexpr float kBrightMin = -0.09f, kBrightMax = 0.09f, kContrastMin = -0.12f, kContrastMax = 0.2f, kGammaMin = -0.32f, kGammaMax = 0.695f, kCosmeticMin = 0.05f;
|
||||||
|
bool online = true, streamer_mode = false; // (streamer mode: the game's Twitch integration switch - 006ceb00 opens the Streaming tab when it goes on)
|
||||||
|
std::vector<KeyBind> keys = {{"left", "Move left", 'A'}, {"right", "Move right", 'D'}, {"up", "Levitate / jump", 'W'}, {"down", "Move down", 'S'}, {"kick", "Kick", 'F'},
|
||||||
|
{"interact", "Interact / pick up", 'E'}, {"inventory", "Inventory", VK_TAB}, {"drop", "Drop item", 'Q'}, {"pause", "Pause", 'P'}};
|
||||||
|
int key(const char* id) const { for (const KeyBind& k : keys) if (!std::strcmp(k.id, id)) return k.key; return 0; }
|
||||||
|
std::string path;
|
||||||
|
void load(const std::string& p) {
|
||||||
|
path = p;
|
||||||
|
FILE* f = std::fopen(p.c_str(), "rb");
|
||||||
|
if (!f) return;
|
||||||
|
char line[512];
|
||||||
|
while (std::fgets(line, sizeof line, f)) {
|
||||||
|
std::string s(line); while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) s.pop_back();
|
||||||
|
const size_t eq = s.find('=');
|
||||||
|
if (eq == std::string::npos) continue;
|
||||||
|
const std::string k = s.substr(0, eq), v = s.substr(eq + 1);
|
||||||
|
auto b = [&](const char* n, bool& dst) { if (k == n) dst = v == "1"; };
|
||||||
|
auto fl = [&](const char* n, float& dst) { if (k == n) dst = (float)std::atof(v.c_str()); };
|
||||||
|
if (k == "window_mode") window_mode = std::atoi(v.c_str());
|
||||||
|
b("vsync", vsync); b("cursor", cursor); b("replay_recorder", replay_recorder); b("damage_numbers", damage_numbers); b("pause_unfocused", pause_unfocused);
|
||||||
|
b("click_switches_item", click_switches_item); b("hover_info_at_mouse", hover_info_at_mouse); b("online", online); b("streamer_mode", streamer_mode);
|
||||||
|
fl("music", music); fl("sfx", sfx); fl("screenshake", screenshake); fl("brightness", brightness); fl("contrast", contrast); fl("gamma", gamma); fl("cosmetic_particles", cosmetic_particles);
|
||||||
|
if (k.compare(0, 4, "key_") == 0) for (KeyBind& kb : keys) if (k.substr(4) == kb.id) kb.key = std::atoi(v.c_str());
|
||||||
|
}
|
||||||
|
std::fclose(f);
|
||||||
|
brightness = std::clamp(brightness, kBrightMin, kBrightMax); contrast = std::clamp(contrast, kContrastMin, kContrastMax); gamma = std::clamp(gamma, kGammaMin, kGammaMax);
|
||||||
|
cosmetic_particles = std::clamp(cosmetic_particles, kCosmeticMin, 1.0f);
|
||||||
|
}
|
||||||
|
void save() const {
|
||||||
|
if (path.empty()) return;
|
||||||
|
FILE* f = std::fopen(path.c_str(), "wb");
|
||||||
|
if (!f) return;
|
||||||
|
std::fprintf(f, "window_mode=%d\nvsync=%d\ncursor=%d\nreplay_recorder=%d\ndamage_numbers=%d\npause_unfocused=%d\nclick_switches_item=%d\nhover_info_at_mouse=%d\nonline=%d\nstreamer_mode=%d\n",
|
||||||
|
window_mode, vsync, cursor, replay_recorder, damage_numbers, pause_unfocused, click_switches_item, hover_info_at_mouse, online, streamer_mode);
|
||||||
|
std::fprintf(f, "music=%.3f\nsfx=%.3f\nscreenshake=%.3f\nbrightness=%.3f\ncontrast=%.3f\ngamma=%.3f\ncosmetic_particles=%.3f\n", music, sfx, screenshake, brightness, contrast, gamma, cosmetic_particles);
|
||||||
|
for (const KeyBind& k : keys) std::fprintf(f, "key_%s=%d\n", k.id, k.key);
|
||||||
|
std::fclose(f);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- the run, its stats, the game modes ----------------------------------------------------------------
|
||||||
|
struct GameMode { std::string id, name, desc, banner_bg, banner_fg; bool mod = false; };
|
||||||
|
struct RunStats { int gold = 0; double playtime = 0; int depth = 0, places = 0, kills = 0, maxhp = 0, items = 0; }; // (depth in metres: the deepest point / 10)
|
||||||
|
struct ProgressItem { std::string id, icon, name, desc; bool known = false, is_new = false; int kills = 0, killed_by = 0; }; // a perk, a spell or an enemy of the Progress screen (006dfe40)
|
||||||
|
struct ModInfo { std::string id, name, desc; bool enabled = false, game_mode = false; }; // a folder of mods/ (the Mods screen: 00b84f0's list, enabled in order)
|
||||||
|
struct Context { // what main.cpp knows, given to the menu every frame
|
||||||
|
int w = 0, h = 0, mx = 0, my = 0, wheel = 0;
|
||||||
|
bool click = false, lmb = false, escape = false, has_run = false, mods_used = false, dead = false;
|
||||||
|
int key_pressed = 0; // a key that went down this frame (the Input tab's rebinding)
|
||||||
|
std::string build, run_mode, location, death_cause;
|
||||||
|
uint32_t seed = 0;
|
||||||
|
const std::string* release_notes = nullptr; // the text beside the executable
|
||||||
|
const std::vector<GameMode>* modes = nullptr;
|
||||||
|
const std::vector<std::string>* mods = nullptr; // the enabled mods
|
||||||
|
RunStats stats, best; int total_deaths = 0, total_wins = 0;
|
||||||
|
Settings* settings = nullptr;
|
||||||
|
const std::vector<ProgressItem>* perks = nullptr; const std::vector<ProgressItem>* actions = nullptr; const std::vector<ProgressItem>* enemies = nullptr; // the Progress screen
|
||||||
|
int secrets_found = 0; bool ending0 = false, ending1 = false; // (the progress_* flags: the secret_* / progress_* ones found, the two endings)
|
||||||
|
std::vector<ModInfo>* all_mods = nullptr; // every mod in mods/: the Mods screen toggles and reorders them, Action::ApplyMods writes the list
|
||||||
|
bool replay_available = false; // the replay recorder holds frames: the game over's "Save death replay" opens the clip editor
|
||||||
|
bool suspended = false; // the clip editor is up over the menu: nothing drawn, nothing read
|
||||||
|
std::function<std::string(const std::string&)> tr; // "$menu_continue" -> the game's text (common.csv), the key itself when unknown
|
||||||
|
};
|
||||||
|
enum class Action { None, Continue, StartGame, Quit, SaveQuit, ApplySettings, ResetProgress, SaveReplay, ApplyMods, OpenModFolder, RestartMods, ProgressSeen };
|
||||||
|
struct Result { Action action = Action::None; std::string mode_id; uint32_t seed = 0; bool daily = false, daily_practice = false; };
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- the daily seed ----------------------------------------------------------------
|
||||||
|
// 0084c6f0: HTTPS GET takapuoli.noitagame.com/callback/ (WinINet); the body "<version hash>;<daily run seed>;<daily practice seed>;1;" (the game reads it as lines)
|
||||||
|
struct Daily {
|
||||||
|
std::atomic<int> state{0}; // 0 not asked, 1 fetching, 2 ok, 3 failed
|
||||||
|
uint32_t seed = 0, practice = 0;
|
||||||
|
std::string version;
|
||||||
|
std::thread th;
|
||||||
|
~Daily() { if (th.joinable()) th.detach(); }
|
||||||
|
void fetch() {
|
||||||
|
if (state.load() != 0) return;
|
||||||
|
state = 1;
|
||||||
|
th = std::thread([this] {
|
||||||
|
std::string body;
|
||||||
|
HINTERNET net = InternetOpenA("Noita", INTERNET_OPEN_TYPE_PRECONFIG, nullptr, nullptr, 0);
|
||||||
|
if (net) {
|
||||||
|
HINTERNET con = InternetConnectA(net, "takapuoli.noitagame.com", 443, "", "", INTERNET_SERVICE_HTTP, 0, 0);
|
||||||
|
if (con) {
|
||||||
|
HINTERNET req = HttpOpenRequestA(con, "GET", "/callback/", "HTTP/1.0", nullptr, nullptr, INTERNET_FLAG_SECURE | INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE, 0);
|
||||||
|
if (req && HttpSendRequestA(req, nullptr, 0, nullptr, 0)) {
|
||||||
|
char buf[1024]; DWORD n = 0;
|
||||||
|
while (InternetReadFile(req, buf, sizeof buf, &n) && n > 0 && body.size() < 4096) body.append(buf, n);
|
||||||
|
}
|
||||||
|
if (req) InternetCloseHandle(req);
|
||||||
|
InternetCloseHandle(con);
|
||||||
|
}
|
||||||
|
InternetCloseHandle(net);
|
||||||
|
}
|
||||||
|
std::vector<std::string> parts; std::string cur;
|
||||||
|
for (char ch : body) { if (ch == ';' || ch == '\n') { parts.push_back(cur); cur.clear(); } else if (ch != '\r') cur += ch; }
|
||||||
|
if (!cur.empty()) parts.push_back(cur);
|
||||||
|
auto num = [](const std::string& s, uint32_t& out) { if (s.empty() || s.size() > 10 || s.find_first_not_of("0123456789") != std::string::npos) return false; out = (uint32_t)std::strtoul(s.c_str(), nullptr, 10); return true; };
|
||||||
|
if (parts.size() >= 3 && num(parts[1], seed) && num(parts[2], practice)) { version = parts[0]; state = 2; }
|
||||||
|
else state = 3;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- the menu ----------------------------------------------------------------
|
||||||
|
class Menu {
|
||||||
|
public:
|
||||||
|
enum class Screen { None, Title, NewGame, Pause, Options, ReleaseNotes, Credits, Mods, Progress, GameOver, GameOverStats, Confirm };
|
||||||
|
Screen screen = Screen::None;
|
||||||
|
Daily daily;
|
||||||
|
bool active() const { return screen != Screen::None; }
|
||||||
|
void open(Screen s) { screen = s; hover_ = -1; scroll_ = 0; if (s == Screen::NewGame) { selMode_ = -1; } if (s == Screen::Credits) creditsT_ = 0.0; }
|
||||||
|
void game_over(const std::string& cause) { cause_ = cause; gameOverT_ = 0.0; screen = Screen::GameOver; }
|
||||||
|
void confirm(const std::string& text, Action yes) { confirmText_ = text; confirmAction_ = yes; confirmBack_ = screen; screen = Screen::Confirm; }
|
||||||
|
bool in_game() const { return inGame_; }
|
||||||
|
bool over_game() const { return screen == Screen::GameOver || screen == Screen::GameOverStats; } // (the game over plays over the running world: no pause, no background)
|
||||||
|
void set_in_game(bool b) { inGame_ = b; }
|
||||||
|
uint32_t last_daily_played = 0; // (stats/_last_daily_run_played.txt: the daily can be played once a day)
|
||||||
|
|
||||||
|
Result frame(hud::Canvas& cv, const Assets& as, const Context& c) {
|
||||||
|
Result r;
|
||||||
|
if (!active() || c.suspended) return r;
|
||||||
|
gs_ = std::max(1, (int)std::lround(c.h / 360.0));
|
||||||
|
cv_ = &cv; as_ = &as; c_ = &c; tick_ += 1.0 / 60.0;
|
||||||
|
clicked_ = c.click; hot_ = -1; itemN_ = 0;
|
||||||
|
if (screen == Screen::Title || screen == Screen::NewGame || (!inGame_ && !c.dead && (screen == Screen::Options || screen == Screen::ReleaseNotes || screen == Screen::Credits || screen == Screen::Mods || screen == Screen::Progress || screen == Screen::Confirm)))
|
||||||
|
background();
|
||||||
|
else if (!over_game()) { cv.rect(0, 0, cv.w, cv.h, hud::rgba(0, 0, 0, 150)); cv.mark_all(); }
|
||||||
|
switch (screen) {
|
||||||
|
case Screen::Title: title(r); break;
|
||||||
|
case Screen::Pause: pause(r); break;
|
||||||
|
case Screen::NewGame: newGame(r); break;
|
||||||
|
case Screen::Options: options(r); break;
|
||||||
|
case Screen::ReleaseNotes: releaseNotes(); break;
|
||||||
|
case Screen::Credits: credits(); break;
|
||||||
|
case Screen::Mods: mods(r); break;
|
||||||
|
case Screen::Progress: progress(r); break;
|
||||||
|
case Screen::GameOver: gameOver(r); break;
|
||||||
|
case Screen::GameOverStats: gameOverStats(r); break;
|
||||||
|
case Screen::Confirm: confirmScreen(r); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
if (c.escape) { // Esc: back to the menu behind, or (from the pause menu) back to the game
|
||||||
|
if (screen == Screen::Pause) { screen = Screen::None; r.action = Action::Continue; }
|
||||||
|
else if (screen == Screen::Title || screen == Screen::GameOver || screen == Screen::GameOverStats) {}
|
||||||
|
else if (screen == Screen::NewGame && c.dead) screen = Screen::GameOverStats;
|
||||||
|
else if (screen == Screen::Confirm) screen = confirmBack_;
|
||||||
|
else { if (screen == Screen::Progress) r.action = Action::ProgressSeen; screen = back(); }
|
||||||
|
}
|
||||||
|
if (as.cursor.ok()) cv.icon(as.cursor, c.mx, c.my, std::max(1, gs_ / 2)); // (the game's cursor: the system's is hidden over the window)
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
hud::Canvas* cv_ = nullptr; const Assets* as_ = nullptr; const Context* c_ = nullptr;
|
||||||
|
int gs_ = 2, hover_ = -1, hot_ = -1, itemN_ = 0, selMode_ = 0, tab_ = 0, scroll_ = 0, bindIdx_ = -1, sliderDrag_ = -1;
|
||||||
|
bool clicked_ = false, inGame_ = false;
|
||||||
|
double tick_ = 0.0, creditsT_ = 0.0, gameOverT_ = 0.0;
|
||||||
|
std::string cause_, confirmText_; Action confirmAction_ = Action::None; Screen confirmBack_ = Screen::Title;
|
||||||
|
std::vector<uint32_t> bg_; int bgW_ = 0, bgH_ = 0;
|
||||||
|
uint32_t white() const { return hud::rgba(255, 255, 255); }
|
||||||
|
uint32_t grey() const { return hud::rgba(190, 190, 200); }
|
||||||
|
uint32_t dim() const { return hud::rgba(120, 120, 130); }
|
||||||
|
uint32_t yellow() const { return hud::rgba(255, 230, 80); }
|
||||||
|
std::string tr(const std::string& k) const { return c_->tr ? c_->tr(k) : k; }
|
||||||
|
Screen back() const { return c_->dead ? Screen::GameOverStats : inGame_ ? Screen::Pause : Screen::Title; } // the screen a sub-menu returns to
|
||||||
|
std::string fmt(const std::string& key, const std::string& a0, const std::string& a1 = std::string()) const { // the game's "$0" / "$1" substitution
|
||||||
|
std::string s = tr(key);
|
||||||
|
for (const auto& [tag, val] : {std::pair<const char*, const std::string*>{"$0", &a0}, {"$1", &a1}}) { const size_t p = s.find(tag); if (p != std::string::npos) s.replace(p, 2, *val); }
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
int tw(const std::string& s) const { return cv_->text_width(s, gs_); }
|
||||||
|
int lineH() const { return 11 * gs_; }
|
||||||
|
|
||||||
|
// ---- the parallax background: the sky's gradient, the mountain layers, drifting clouds and twinkling stars (the title and the game over screens) ----
|
||||||
|
void background() {
|
||||||
|
hud::Canvas& cv = *cv_; const Assets& as = *as_;
|
||||||
|
if (bgW_ != cv.w || bgH_ != cv.h) {
|
||||||
|
bgW_ = cv.w; bgH_ = cv.h; bg_.assign((size_t)cv.w * cv.h, 0xFF101018u);
|
||||||
|
hud::Canvas tmp; tmp.px = bg_.data(); tmp.w = cv.w; tmp.h = cv.h; tmp.tiles.assign((size_t)((cv.w + 31) >> 5) * ((cv.h + 31) >> 5), 0); tmp.tw = (cv.w + 31) >> 5; tmp.th = (cv.h + 31) >> 5;
|
||||||
|
// parallax_colors_main_menu.bmp: a column is a time of day (512 of them), the rows come in bands of 20 (a colour, then a white and a black marker row):
|
||||||
|
// band 0 the sky's top, 1 its horizon, 2 the clouds, 3 .. the mountain layers from the far one. Column 288 is the night of the title screen.
|
||||||
|
auto band = [&](int k) -> uint32_t { if (as.sky.empty()) return k == 0 ? 0xFF31253Au : k == 1 ? 0xFF5A4D54u : k == 3 ? 0xFF383C52u : 0xFF292E43u; return as.sky[(size_t)std::min(as.skyH - 1, 20 * k) * as.skyW + std::min(as.skyW - 1, 288)]; };
|
||||||
|
const uint32_t top = band(0), bot = band(1);
|
||||||
|
for (int y = 0; y < cv.h; ++y) {
|
||||||
|
const double f = std::min(1.0, y / (cv.h * 0.75));
|
||||||
|
auto mix = [&](int sh) { return (uint32_t)(((top >> sh) & 255) * (1.0 - f) + ((bot >> sh) & 255) * f); };
|
||||||
|
const uint32_t c = 0xFF000000u | mix(16) << 16 | mix(8) << 8 | mix(0);
|
||||||
|
for (int x = 0; x < cv.w; ++x) bg_[(size_t)y * cv.w + x] = c;
|
||||||
|
}
|
||||||
|
auto layer = [&](const hud::Icon& ic, double scale, int dy, uint32_t tint) { // the layers are white masks: tinted with their band's colour
|
||||||
|
if (!ic.ok()) return;
|
||||||
|
hud::Icon t2 = ic; for (uint32_t& p : t2.px) p = (p & 0xFF000000u) | (tint & 0xFFFFFFu);
|
||||||
|
const int w = (int)(ic.w * scale), h = (int)(ic.h * scale); tmp.icon_fit(t2, (cv.w - w) / 2, cv.h - h + dy, w, h);
|
||||||
|
};
|
||||||
|
const double sc = std::max(cv.w / 1024.0, cv.h * 0.75 / 512.0);
|
||||||
|
layer(as.layer2, sc, (int)(cv.h * 0.08), band(3)); layer(as.layer1, sc, (int)(cv.h * 0.16), band(4));
|
||||||
|
}
|
||||||
|
std::memcpy(cv.px, bg_.data(), bg_.size() * 4); cv.mark_all();
|
||||||
|
uint32_t seed = 12345u; // the stars (a fixed scatter over the sky, twinkling)
|
||||||
|
for (int i = 0; i < 60; ++i) {
|
||||||
|
seed = seed * 1664525u + 1013904223u; const int x = (int)((seed >> 8) % (uint32_t)cv.w);
|
||||||
|
seed = seed * 1664525u + 1013904223u; const int y = (int)((seed >> 8) % (uint32_t)std::max(1, cv.h * 6 / 10));
|
||||||
|
seed = seed * 1664525u + 1013904223u; const hud::Icon& st = as.stars[(seed >> 8) % 7];
|
||||||
|
const double tw = 0.6 + 0.4 * std::sin(tick_ * (1.0 + (i % 5) * 0.3) + i);
|
||||||
|
if (!st.ok()) continue;
|
||||||
|
const int s = std::max(1, gs_ / 2);
|
||||||
|
for (int j = 0; j < st.h; ++j) for (int k = 0; k < st.w; ++k) { const uint32_t p = st.px[(size_t)j * st.w + k]; if (p >> 24) cv.rect(x + k * s, y + j * s, s, s, (p & 0xFFFFFFu) | (uint32_t)((p >> 24) * tw) << 24); }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// ---- widgets ----
|
||||||
|
bool inRect(int x, int y, int w, int h) const { return c_->mx >= x && c_->mx < x + w && c_->my >= y && c_->my < y + h; }
|
||||||
|
// a text button centred on cx (or left-aligned at x with centred == false): grey, white when hovered; true when clicked
|
||||||
|
bool button(int x, int y, const std::string& text, bool centred = true, bool enabled = true) {
|
||||||
|
const int w = tw(text), h = 8 * gs_, bx = centred ? x - w / 2 : x;
|
||||||
|
const bool over = enabled && inRect(bx - 2 * gs_, y - 2 * gs_, w + 4 * gs_, h + 4 * gs_);
|
||||||
|
cv_->text(bx, y, text, !enabled ? dim() : over ? white() : grey(), gs_);
|
||||||
|
if (over && c_->settings && !c_->settings->cursor) {}
|
||||||
|
return over && clicked_;
|
||||||
|
}
|
||||||
|
void label(int x, int y, const std::string& text, uint32_t col) { cv_->text(x, y, text, col, gs_); }
|
||||||
|
void panel(int x, int y, int w, int h, bool gray = false) { // a 9-piece box (decorations/9piece0.png: 3-pixel corners)
|
||||||
|
const hud::Icon& p = gray ? as_->p9gray : as_->p9;
|
||||||
|
if (!p.ok()) { cv_->rect(x, y, w, h, hud::rgba(20, 20, 24, 230)); cv_->frame(x, y, w, h, hud::rgba(170, 140, 90)); return; }
|
||||||
|
const int cs = 3, s = gs_;
|
||||||
|
auto piece = [&](int sx, int sy, int sw, int sh, int dx, int dy, int dw, int dh) {
|
||||||
|
if (dw <= 0 || dh <= 0) return;
|
||||||
|
hud::Icon sub; sub.w = sw; sub.h = sh; sub.px.resize((size_t)sw * sh);
|
||||||
|
for (int j = 0; j < sh; ++j) for (int i = 0; i < sw; ++i) sub.px[(size_t)j * sw + i] = p.px[(size_t)(sy + j) * p.w + sx + i];
|
||||||
|
cv_->icon_fit(sub, dx, dy, dw, dh);
|
||||||
|
};
|
||||||
|
const int c = cs * s;
|
||||||
|
piece(0, 0, cs, cs, x, y, c, c); piece(p.w - cs, 0, cs, cs, x + w - c, y, c, c); piece(0, p.h - cs, cs, cs, x, y + h - c, c, c); piece(p.w - cs, p.h - cs, cs, cs, x + w - c, y + h - c, c, c);
|
||||||
|
piece(cs, 0, p.w - 2 * cs, cs, x + c, y, w - 2 * c, c); piece(cs, p.h - cs, p.w - 2 * cs, cs, x + c, y + h - c, w - 2 * c, c);
|
||||||
|
piece(0, cs, cs, p.h - 2 * cs, x, y + c, c, h - 2 * c); piece(p.w - cs, cs, cs, p.h - 2 * cs, x + w - c, y + c, c, h - 2 * c);
|
||||||
|
piece(cs, cs, p.w - 2 * cs, p.h - 2 * cs, x + c, y + c, w - 2 * c, h - 2 * c);
|
||||||
|
}
|
||||||
|
// the text of a toggle "Label: Value" that cycles on a click; returns true when clicked
|
||||||
|
bool toggle(int x, int y, const std::string& text, const std::string& value) { return button(x, y, text + " " + value, false); }
|
||||||
|
// a slider: the label, then a bar of `bw` px and the value as "$menu_slider_percentage" (006c6470: x 100, "%"); drag sets `v` in lo..hi
|
||||||
|
void slider(int x, int y, const std::string& text, float& v, int id, float lo = 0.0f, float hi = 1.0f) {
|
||||||
|
label(x, y, text, grey());
|
||||||
|
const int bx = x + tw(text) + 6 * gs_, bw = 60 * gs_, bh = 6 * gs_, by = y + gs_;
|
||||||
|
const float f = std::clamp((v - lo) / (hi - lo), 0.0f, 1.0f);
|
||||||
|
cv_->frame(bx, by, bw, bh, grey()); cv_->rect(bx + 1, by + 1, (int)((bw - 2) * f), bh - 2, hud::rgba(200, 180, 120));
|
||||||
|
char pct[16]; std::snprintf(pct, sizeof pct, "%d", (int)std::lround(v * 100.0f));
|
||||||
|
label(bx + bw + 4 * gs_, y, fmt("$menu_slider_percentage", pct), grey());
|
||||||
|
const bool over = inRect(bx - 2 * gs_, by - 2 * gs_, bw + 4 * gs_, bh + 4 * gs_);
|
||||||
|
if (over && c_->lmb && (sliderDrag_ < 0 || sliderDrag_ == id)) sliderDrag_ = id;
|
||||||
|
if (sliderDrag_ == id) { if (c_->lmb) v = lo + (hi - lo) * std::clamp((float)(c_->mx - bx) / (float)(bw - 2), 0.0f, 1.0f); else sliderDrag_ = -1; }
|
||||||
|
}
|
||||||
|
void buildLine() { // bottom-left: the build, like the game's "Noita - Build <date>"
|
||||||
|
const int s = std::max(1, gs_ / 2);
|
||||||
|
cv_->text(4 * s, cv_->h - 20 * s, "Noita GPU - Build " + c_->build, dim(), s);
|
||||||
|
if (c_->has_run) cv_->text(4 * s, cv_->h - 11 * s, "Seed: " + std::to_string(c_->seed) + " Mode: " + c_->run_mode + (c_->mods_used ? " Mods used: yes" : ""), dim(), s);
|
||||||
|
}
|
||||||
|
void logo(int cy) { // the logo at 0.8 x the GUI scale
|
||||||
|
const Assets& as = *as_;
|
||||||
|
if (!as.logo.ok()) { cv_->text(cv_->w / 2 - tw("Noita") , cy, "Noita", yellow(), gs_ * 2); return; }
|
||||||
|
const double sc = gs_ * 0.8; const int w = (int)(as.logo.w * sc), h = (int)(as.logo.h * sc); // (the wak's logo is 133 x 77: 425 px wide at 1440p like the game's)
|
||||||
|
cv_->icon_fit(as.logo, (cv_->w - w) / 2, cy - h / 2, w, h);
|
||||||
|
}
|
||||||
|
// ---- screens ----
|
||||||
|
void title(Result& r) {
|
||||||
|
logo((int)(cv_->h * 0.3));
|
||||||
|
int y = (int)(cv_->h * 0.5); const int cx = cv_->w / 2;
|
||||||
|
if (c_->has_run && button(cx, y, tr("$menu_continue"))) { screen = Screen::None; r.action = Action::Continue; }
|
||||||
|
if (c_->has_run) y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_newgame"))) { open(Screen::NewGame); if (c_->settings && c_->settings->online) daily.fetch(); } y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_options"))) open(Screen::Options); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_mods"))) open(Screen::Mods); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_releasenotes"))) open(Screen::ReleaseNotes); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_credits"))) open(Screen::Credits); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_quit"))) r.action = Action::Quit;
|
||||||
|
buildLine();
|
||||||
|
}
|
||||||
|
void pause(Result& r) {
|
||||||
|
const Assets& as = *as_;
|
||||||
|
int y = 6 * gs_;
|
||||||
|
if (as.help.ok()) { const double sc = gs_ * 0.9; const int w = (int)(as.help.w * sc), h = (int)(as.help.h * sc); cv_->icon_fit(as.help, (cv_->w - w) / 2, y, w, h); y += h + 4 * gs_; }
|
||||||
|
const int cx = cv_->w / 2;
|
||||||
|
cv_->text(cx - tw(tr("$menu_paused")) / 2, y, tr("$menu_paused"), dim(), gs_); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_continue"))) { screen = Screen::None; r.action = Action::Continue; } y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_newgame"))) confirm(tr("$menu_newgame_areyousure"), Action::None); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_progress"))) open(Screen::Progress); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_options"))) open(Screen::Options); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_mods"))) open(Screen::Mods); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_releasenotes"))) open(Screen::ReleaseNotes); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_credits"))) open(Screen::Credits); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menu_saveandquit"))) r.action = Action::SaveQuit;
|
||||||
|
buildLine();
|
||||||
|
}
|
||||||
|
void confirmScreen(Result& r) { // "Are you sure ...?" Yes / No
|
||||||
|
const int w = 180 * gs_, h = 50 * gs_, x = (cv_->w - w) / 2, y = (cv_->h - h) / 2;
|
||||||
|
panel(x, y, w, h);
|
||||||
|
std::vector<std::string> lines; wrap(confirmText_, w - 12 * gs_, lines);
|
||||||
|
int ty = y + 6 * gs_;
|
||||||
|
for (const std::string& l : lines) { label(x + 6 * gs_, ty, l, grey()); ty += 9 * gs_; }
|
||||||
|
if (button(x + w / 3, y + h - 12 * gs_, tr("$menu_yes"))) {
|
||||||
|
if (confirmAction_ == Action::None) { open(Screen::NewGame); if (c_->settings && c_->settings->online) daily.fetch(); } // (a new game from the pause menu: the game mode panel)
|
||||||
|
else { r.action = confirmAction_; screen = confirmBack_; }
|
||||||
|
}
|
||||||
|
if (button(x + 2 * w / 3, y + h - 12 * gs_, tr("$menu_no"))) screen = confirmBack_;
|
||||||
|
}
|
||||||
|
void wrap(const std::string& text, int width, std::vector<std::string>& out) const {
|
||||||
|
std::string line, word;
|
||||||
|
auto flush = [&]() { out.push_back(line); line.clear(); };
|
||||||
|
for (size_t i = 0; i <= text.size(); ++i) {
|
||||||
|
const char ch = i < text.size() ? text[i] : ' ';
|
||||||
|
if (ch == '\\' && i + 1 < text.size() && text[i + 1] == 'n') { line += (line.empty() || word.empty() ? "" : " ") + word; word.clear(); flush(); ++i; continue; }
|
||||||
|
if (ch == '\n') { line += (line.empty() || word.empty() ? "" : " ") + word; word.clear(); flush(); continue; }
|
||||||
|
if (ch == ' ') {
|
||||||
|
const std::string cand = line.empty() ? word : line + " " + word;
|
||||||
|
if (!line.empty() && tw(cand) > width) { flush(); line = word; } else line = cand;
|
||||||
|
word.clear();
|
||||||
|
} else word += ch;
|
||||||
|
}
|
||||||
|
if (!line.empty()) out.push_back(line);
|
||||||
|
}
|
||||||
|
void newGame(Result& r) {
|
||||||
|
const Assets& as = *as_;
|
||||||
|
const std::vector<GameMode>& modes = *c_->modes;
|
||||||
|
// (006ceb00: the banners in a framed column, the description box beside it taking the rest of a 274-unit panel, the bar below; nothing selected at first)
|
||||||
|
const int bw = 64 * gs_, bh = 36 * gs_, gap = 2 * gs_, pw = 208 * gs_;
|
||||||
|
const int colX = (cv_->w - (bw + pw + 4 * gs_)) / 2, top = (int)(cv_->h * 0.3);
|
||||||
|
const int ph = std::max(4, (int)modes.size()) * (bh + gap) + gap;
|
||||||
|
label(colX, top - 12 * gs_, tr("$menu_newgame_selectgamemode"), grey());
|
||||||
|
panel(colX - gap, top - gap, bw + 2 * gap, ph, true);
|
||||||
|
for (size_t i = 0; i < modes.size(); ++i) {
|
||||||
|
const int y = top + (int)i * (bh + gap);
|
||||||
|
const hud::Icon* bg = &as.bannerNormalBg; const hud::Icon* fg = &as.bannerNormalFg;
|
||||||
|
if (modes[i].id == "dailyrun") { bg = &as.bannerDailyBg; fg = &as.bannerDailyFg; }
|
||||||
|
if (modes[i].mod) { bg = modeIcon(modes[i].banner_bg, bg); fg = modeIcon(modes[i].banner_fg, fg); }
|
||||||
|
if (bg && bg->ok()) cv_->icon_fit(*bg, colX, y, bw, bh); else cv_->rect(colX, y, bw, bh, hud::rgba(60, 50, 70));
|
||||||
|
if (fg && fg->ok()) cv_->icon_fit(*fg, colX, y, bw, bh);
|
||||||
|
const bool over = inRect(colX, y, bw, bh);
|
||||||
|
if (over) selMode_ = (int)i; // the banner under the mouse is the one described (it stays until another is hovered)
|
||||||
|
if ((int)i == selMode_ && as.bannerSel.ok()) cv_->icon_fit(as.bannerSel, colX, y, bw, bh);
|
||||||
|
if (over && clicked_) { selMode_ = (int)i; startMode(r); }
|
||||||
|
}
|
||||||
|
// the description box
|
||||||
|
const int px = colX + bw + 4 * gs_;
|
||||||
|
panel(px, top - gap, pw, ph, false);
|
||||||
|
if (selMode_ >= 0 && selMode_ < (int)modes.size()) {
|
||||||
|
const GameMode& m = modes[selMode_];
|
||||||
|
int ty = top + 4 * gs_;
|
||||||
|
label(px + 6 * gs_, ty, tr(m.name), yellow()); ty += lineH();
|
||||||
|
std::vector<std::string> lines; wrap(tr(m.desc), pw - 12 * gs_, lines);
|
||||||
|
for (const std::string& l : lines) { label(px + 6 * gs_, ty, l, grey()); ty += 9 * gs_; }
|
||||||
|
if (m.id == "dailyrun" || m.id == "daily_practice") {
|
||||||
|
ty += 4 * gs_;
|
||||||
|
std::string st;
|
||||||
|
if (!c_->settings || !c_->settings->online) st = tr("$gamemode_dailyrun_desc_offline");
|
||||||
|
else if (daily.state.load() == 1) st = "Connecting...";
|
||||||
|
else if (daily.state.load() == 3) st = tr("$gamemode_dailyrun_desc_connection_failed");
|
||||||
|
else if (daily.state.load() == 2) {
|
||||||
|
if (m.id == "dailyrun" && last_daily_played == daily.seed) st = tr("$gamemode_dailyrun_desc_played");
|
||||||
|
else st = (m.id == "dailyrun" ? "Daily run seed: " : "Daily practice seed: ") + std::to_string(m.id == "dailyrun" ? daily.seed : daily.practice);
|
||||||
|
}
|
||||||
|
lines.clear(); wrap(st, pw - 12 * gs_, lines);
|
||||||
|
for (const std::string& l : lines) { label(px + 6 * gs_, ty, l, grey()); ty += 9 * gs_; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the bar below: the streamer mode, the mod settings
|
||||||
|
const int by = top + ph + 2 * gs_;
|
||||||
|
panel(colX - gap, by, bw + pw + 4 * gs_ + 2 * gap, 14 * gs_, true);
|
||||||
|
{ // 006ceb00: "Streamer mode: on/off" is the Twitch integration's switch; switching it on opens the Options at the Streaming tab (to connect)
|
||||||
|
Settings* S = c_->settings; const bool sm = S && S->streamer_mode;
|
||||||
|
if (toggle(colX + 4 * gs_, by + 3 * gs_, tr("$menu_streamermode").substr(0, tr("$menu_streamermode").find("$0")), tr(sm ? "$option_on" : "$option_off")) && S) {
|
||||||
|
S->streamer_mode = !S->streamer_mode; S->save();
|
||||||
|
if (S->streamer_mode) { open(Screen::Options); tab_ = 4; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (button(colX + bw + pw - tw(tr("$menu_mods_settings_short")), by + 3 * gs_, tr("$menu_mods_settings_short"), false)) { open(Screen::Options); tab_ = 5; }
|
||||||
|
if (button(8 * gs_, cv_->h - 14 * gs_, tr("$menu_return"), false)) screen = c_->dead ? Screen::GameOverStats : (inGame_ ? Screen::Pause : Screen::Title);
|
||||||
|
}
|
||||||
|
std::map<std::string, hud::Icon> modeIcons_;
|
||||||
|
const hud::Icon* modeIcon(const std::string& file, const hud::Icon* fallback) {
|
||||||
|
if (file.empty()) return fallback;
|
||||||
|
auto it = modeIcons_.find(file);
|
||||||
|
if (it == modeIcons_.end()) { hud::Icon ic; if (loose_icon) ic = loose_icon(file); it = modeIcons_.emplace(file, std::move(ic)).first; }
|
||||||
|
return it->second.ok() ? &it->second : fallback;
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
std::function<hud::Icon(const std::string&)> loose_icon; // a mod's banner (mods/<id>/files/...), read through the wak's loose root
|
||||||
|
private:
|
||||||
|
bool canStart(const GameMode& m) const {
|
||||||
|
if (m.id == "dailyrun") return daily.state.load() == 2 && last_daily_played != daily.seed;
|
||||||
|
if (m.id == "daily_practice") return daily.state.load() == 2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void startMode(Result& r) {
|
||||||
|
const GameMode& m = (*c_->modes)[selMode_];
|
||||||
|
if (!canStart(m)) return;
|
||||||
|
r.action = Action::StartGame; r.mode_id = m.id;
|
||||||
|
if (m.id == "dailyrun") { r.seed = daily.seed; r.daily = true; }
|
||||||
|
else if (m.id == "daily_practice") { r.seed = daily.practice; r.daily_practice = true; }
|
||||||
|
else { uint32_t s = (uint32_t)GetTickCount() ^ (uint32_t)std::time(nullptr) * 2654435761u; r.seed = s ? s : 1u; }
|
||||||
|
screen = Screen::None;
|
||||||
|
}
|
||||||
|
void options(Result& r) {
|
||||||
|
const Assets& as = *as_;
|
||||||
|
static const char* tabs[6] = {"General", "Graphics", "Audio", "Input", "Streaming", "Mod settings"};
|
||||||
|
const int pw = std::min(cv_->w - 16 * gs_, 280 * gs_), px = (cv_->w - pw) / 2, py = 24 * gs_, ph = cv_->h - py - 24 * gs_;
|
||||||
|
int tx = px + 4 * gs_;
|
||||||
|
for (int i = 0; i < 6; ++i) {
|
||||||
|
const int tw0 = 44 * gs_, th = 10 * gs_;
|
||||||
|
const bool sel = tab_ == i, over = inRect(tx, py - th, tw0, th);
|
||||||
|
const hud::Icon& ic = sel ? as.tabSel : (over ? as.tabHover : as.tab);
|
||||||
|
if (ic.ok()) cv_->icon_fit(ic, tx, py - th - (sel ? gs_ : 0), tw0, th + (sel ? gs_ : 0)); else cv_->frame(tx, py - th, tw0, th, grey());
|
||||||
|
const std::string t = tabs[i]; const int s = std::max(1, gs_ * 2 / 3);
|
||||||
|
cv_->text(tx + (tw0 - cv_->text_width(t, s)) / 2, py - th + 3 * gs_ - (sel ? gs_ : 0), t, sel ? white() : grey(), s);
|
||||||
|
if (over && clicked_) { tab_ = i; scroll_ = 0; bindIdx_ = -1; }
|
||||||
|
tx += tw0 + gs_;
|
||||||
|
}
|
||||||
|
panel(px, py, pw, ph);
|
||||||
|
Settings& S = *c_->settings;
|
||||||
|
int x = px + 10 * gs_, y = py + 8 * gs_ - scroll_;
|
||||||
|
auto header = [&](const char* t) { label(x, y, t, dim()); y += lineH(); };
|
||||||
|
auto onoff = [&](bool b) { return tr(b ? "$option_on" : "$option_off"); };
|
||||||
|
auto opt = [&](const char* key, bool& b) { if (toggle(x, y, tr(key).substr(0, tr(key).find("$0")), onoff(b))) b = !b; y += lineH(); };
|
||||||
|
if (tab_ == 0) {
|
||||||
|
header("WINDOW");
|
||||||
|
if (toggle(x, y, "Window mode:", S.window_mode == 0 ? "Windowed" : "Fullscreen")) S.window_mode = 1 - S.window_mode; y += lineH();
|
||||||
|
label(x, y, "Resolution: " + std::to_string(c_->w) + " x " + std::to_string(c_->h), grey()); y += lineH();
|
||||||
|
opt("$menuoptions_vsync", S.vsync);
|
||||||
|
y += 4 * gs_; header("COMPATIBILITY");
|
||||||
|
opt("$menuoptions_application_rendered_cursor", S.cursor);
|
||||||
|
y += 4 * gs_; header("REPLAY RECORDER");
|
||||||
|
opt("$menuoptions_replayrecorder", S.replay_recorder);
|
||||||
|
y += 4 * gs_; header("ONLINE");
|
||||||
|
if (toggle(x, y, "Online features:", onoff(S.online))) S.online = !S.online; y += lineH();
|
||||||
|
y += 4 * gs_; header("MISCELLANEOUS");
|
||||||
|
label(x, y, tr("$menuoptions_language"), grey()); y += lineH();
|
||||||
|
if (button(x, y, tr("$menuoptions_resetsave"), false)) confirm(tr("$menuoptions_resetsave_areyousure"), Action::ResetProgress);
|
||||||
|
y += lineH();
|
||||||
|
} else if (tab_ == 1) {
|
||||||
|
header(tr("$menuoptions_heading_rendering").c_str());
|
||||||
|
slider(x, y, tr("$menuoptions_brightness"), S.brightness, 1, Settings::kBrightMin, Settings::kBrightMax); y += lineH();
|
||||||
|
slider(x, y, tr("$menuoptions_contrast"), S.contrast, 2, Settings::kContrastMin, Settings::kContrastMax); y += lineH();
|
||||||
|
slider(x, y, tr("$menuoptions_gamma"), S.gamma, 3, Settings::kGammaMin, Settings::kGammaMax); y += lineH();
|
||||||
|
slider(x, y, tr("$menuoptions_cosmeticparticlecoeff"), S.cosmetic_particles, 4, Settings::kCosmeticMin, 1.0f); y += lineH();
|
||||||
|
r.action = Action::ApplySettings; // (the picture follows the sliders as they move)
|
||||||
|
y += 4 * gs_; header(tr("$menuoptions_heading_userinterface_graphics").c_str());
|
||||||
|
opt("$menuoptions_damagenumbers", S.damage_numbers);
|
||||||
|
y += 4 * gs_; header(tr("$menuoptions_heading_accessibility").c_str());
|
||||||
|
slider(x, y, tr("$menuoptions_screenshake_intensity"), S.screenshake, 5); y += lineH();
|
||||||
|
} else if (tab_ == 2) {
|
||||||
|
header(tr("$menuoptions_heading_volume").c_str());
|
||||||
|
slider(x, y, tr("$menuoptions_musicvolume"), S.music, 6); y += lineH();
|
||||||
|
slider(x, y, tr("$menuoptions_soundsvolume"), S.sfx, 7); y += lineH();
|
||||||
|
} else if (tab_ == 3) {
|
||||||
|
header(tr("$menuoptions_heading_controls").c_str());
|
||||||
|
for (size_t i = 0; i < S.keys.size(); ++i) {
|
||||||
|
KeyBind& k = S.keys[i];
|
||||||
|
const std::string v = bindIdx_ == (int)i ? "..." : keyName(k.key);
|
||||||
|
if (toggle(x, y, std::string(k.name) + ":", v)) bindIdx_ = (int)i;
|
||||||
|
y += 9 * gs_;
|
||||||
|
}
|
||||||
|
if (bindIdx_ >= 0 && c_->key_pressed) { if (c_->key_pressed != VK_ESCAPE) S.keys[bindIdx_].key = c_->key_pressed; bindIdx_ = -1; }
|
||||||
|
label(x, y, tr("$menuoptions_reset_keyboard"), dim()); y += lineH();
|
||||||
|
if (button(x, y, tr("$menuoptions_configurecontrols_reset_all"), false)) { Settings d; S.keys = d.keys; } y += lineH();
|
||||||
|
y += 4 * gs_; header(tr("$menuoptions_heading_userinterface_mouse").c_str());
|
||||||
|
opt("$menuoptions_ui_inventory_icons_always_clickable", S.click_switches_item);
|
||||||
|
opt("$menuoptions_showhoverinfonexttomouse", S.hover_info_at_mouse);
|
||||||
|
y += 4 * gs_; header(tr("$menuoptions_heading_userinterface_input").c_str());
|
||||||
|
opt("$menuoptions_pause_the_game_when_unfocused", S.pause_unfocused);
|
||||||
|
} else if (tab_ == 4) { // 006d5620: TWITCH CHANNEL - Connect / Disconnect (the OAuth sign-in and the chat votes), then the vote settings; the switch itself is the streamer mode
|
||||||
|
header(tr("$menuoptions_heading_streaming_integration").c_str());
|
||||||
|
if (toggle(x, y, tr("$menu_streamermode").substr(0, tr("$menu_streamermode").find("$0")), onoff(S.streamer_mode))) S.streamer_mode = !S.streamer_mode; y += lineH();
|
||||||
|
button(x, y, tr("$menu_streaming_connect_twitch"), false, false); y += lineH();
|
||||||
|
std::vector<std::string> lines; wrap("The Twitch sign-in and the chat votes are not part of this build (to-do #47).", pw - 20 * gs_, lines);
|
||||||
|
for (const std::string& l : lines) { label(x, y, l, dim()); y += 9 * gs_; }
|
||||||
|
} else {
|
||||||
|
header("MODS");
|
||||||
|
if (!c_->mods || c_->mods->empty()) label(x, y, "No mods enabled.", grey());
|
||||||
|
for (const std::string& m : c_->mods ? *c_->mods : std::vector<std::string>()) { label(x, y, m + " - " + tr("$menu_mods_nosettings"), grey()); y += 9 * gs_; }
|
||||||
|
}
|
||||||
|
scrollBy(c_->wheel * 12 * gs_, std::max(0, y + scroll_ - (py + ph) + 20 * gs_));
|
||||||
|
if (button(8 * gs_, cv_->h - 14 * gs_, tr("$menu_applyandreturn"), false)) { S.save(); r.action = Action::ApplySettings; screen = back(); }
|
||||||
|
}
|
||||||
|
void scrollBy(int d, int maxScroll) { scroll_ = std::clamp(scroll_ + d, 0, std::max(0, maxScroll)); }
|
||||||
|
static std::string keyName(int k) {
|
||||||
|
if (k >= 'A' && k <= 'Z') return std::string(1, (char)k);
|
||||||
|
if (k >= '0' && k <= '9') return std::string(1, (char)k);
|
||||||
|
switch (k) { case VK_TAB: return "Tab"; case VK_SPACE: return "Space"; case VK_LEFT: return "Left"; case VK_RIGHT: return "Right"; case VK_UP: return "Up"; case VK_DOWN: return "Down";
|
||||||
|
case VK_SHIFT: return "Shift"; case VK_CONTROL: return "Ctrl"; case VK_RETURN: return "Enter"; case VK_LBUTTON: return "LMB"; case VK_RBUTTON: return "RMB"; default: break; }
|
||||||
|
char b[16]; std::snprintf(b, sizeof b, "key %d", k); return b;
|
||||||
|
}
|
||||||
|
void textScreen(const std::string& titleText, const std::vector<std::string>& lines, bool autoScroll) {
|
||||||
|
const int pw = std::min(cv_->w - 16 * gs_, 300 * gs_), px = (cv_->w - pw) / 2, py = 20 * gs_, ph = cv_->h - py - 24 * gs_;
|
||||||
|
label(px, py - 10 * gs_, titleText, grey());
|
||||||
|
panel(px, py, pw, ph);
|
||||||
|
const int s = std::max(1, gs_ * 2 / 3), lh = 8 * s + 2;
|
||||||
|
const int total = (int)lines.size() * lh;
|
||||||
|
if (autoScroll) { creditsT_ += 1.0; scroll_ = std::clamp((int)(creditsT_ * 0.5 * s) - ph / 2, 0, std::max(0, total - ph + 20 * gs_)); scrollBy(c_->wheel * 3 * lh, total - ph + 20 * gs_); }
|
||||||
|
else scrollBy(c_->wheel * 3 * lh, total - ph + 20 * gs_);
|
||||||
|
int y = py + 6 * gs_ - scroll_;
|
||||||
|
for (const std::string& l : lines) {
|
||||||
|
uint32_t col = grey(); std::string t = l;
|
||||||
|
if (!t.empty() && t[0] == '#') { col = yellow(); t = t.substr(1); }
|
||||||
|
else if (!t.empty() && t[0] == '*') { col = white(); t = t.substr(1); }
|
||||||
|
std::vector<std::string> parts; // (a long line wraps at the box's width)
|
||||||
|
std::string line, word;
|
||||||
|
for (size_t i = 0; i <= t.size(); ++i) {
|
||||||
|
const char ch = i < t.size() ? t[i] : ' ';
|
||||||
|
if (ch == ' ') { const std::string cand = line.empty() ? word : line + " " + word; if (!line.empty() && cv_->text_width(cand, s) > pw - 12 * gs_) { parts.push_back(line); line = word; } else line = cand; word.clear(); }
|
||||||
|
else word += ch;
|
||||||
|
}
|
||||||
|
parts.push_back(line);
|
||||||
|
for (size_t k = 0; k < parts.size(); ++k) {
|
||||||
|
if (y + lh >= py + 3 * gs_ && y < py + ph - 3 * gs_) cv_->text(px + 6 * gs_ + (k ? 8 * s : 0), y, parts[k], k ? grey() : col, s);
|
||||||
|
y += lh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (button(8 * gs_, cv_->h - 14 * gs_, tr("$menu_return"), false)) screen = back();
|
||||||
|
}
|
||||||
|
void releaseNotes() {
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
if (!c_->release_notes || c_->release_notes->empty()) lines.push_back("No release notes beside the executable (release_notes.txt).");
|
||||||
|
else { std::string cur; for (char ch : *c_->release_notes) { if (ch == '\n') { lines.push_back(cur); cur.clear(); } else if (ch != '\r') cur += ch; } if (!cur.empty()) lines.push_back(cur); }
|
||||||
|
textScreen(tr("$menu_releasenotes"), lines, false);
|
||||||
|
}
|
||||||
|
void credits() {
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
std::string cur; for (char ch : as_->credits) { if (ch == '\n') { lines.push_back(cur); cur.clear(); } else if (ch != '\r') cur += ch; }
|
||||||
|
if (!cur.empty()) lines.push_back(cur);
|
||||||
|
std::vector<std::string> ours = {"*NOITA GPU", "A Vulkan rebuild of Noita's engine.", "", "*ORIGINAL GAME", "Noita by Nolla Games - the art, the data and the design below are theirs.", ""};
|
||||||
|
ours.insert(ours.end(), lines.begin(), lines.end());
|
||||||
|
textScreen(tr("$menu_credits"), ours, true);
|
||||||
|
}
|
||||||
|
// The Mods screen: the mods of mods/ in load order, a click on the name enables / disables (the game's: "Click mod name to enable/disable the mod"), Move up /
|
||||||
|
// Move down reorder, the list is written at once (save/mod_config.xml, the game's layout) and applies at the next start - "Restart with enabled mods active"
|
||||||
|
// relaunches the run, "Start a new game with enabled mods active" opens the game modes. A game-mode mod is played from the New game menu, not enabled here.
|
||||||
|
void mods(Result& r) {
|
||||||
|
const int pw = std::min(cv_->w - 16 * gs_, 300 * gs_), px = (cv_->w - pw) / 2, py = 20 * gs_, ph = cv_->h - py - 24 * gs_;
|
||||||
|
label(px, py - 10 * gs_, tr("$menu_mods"), grey());
|
||||||
|
panel(px, py, pw, ph);
|
||||||
|
const int s = std::max(1, gs_ * 2 / 3), lh = 8 * s + 2;
|
||||||
|
int x = px + 6 * gs_, y = py + 6 * gs_ - scroll_;
|
||||||
|
std::vector<std::string> help; wrap(tr("$menu_mods_help"), pw - 12 * gs_, help);
|
||||||
|
for (const std::string& l : help) { cv_->text(x, y, l, dim(), s); y += lh; }
|
||||||
|
y += lh;
|
||||||
|
cv_->text(x, y, tr("$menu_mods_availablemods"), white(), s); y += lh + 2 * gs_;
|
||||||
|
std::vector<ModInfo>* mods = c_->all_mods;
|
||||||
|
if (!mods || mods->empty()) { cv_->text(x, y, tr("$menu_mods_availablemods_none"), grey(), s); y += lh; }
|
||||||
|
std::string tip; int tipY = 0;
|
||||||
|
for (size_t i = 0; mods && i < mods->size(); ++i) {
|
||||||
|
ModInfo& m = (*mods)[i];
|
||||||
|
const std::string box = m.game_mode ? " - " : m.enabled ? "[x]" : "[ ]";
|
||||||
|
const std::string line = box + " " + (m.name.empty() ? m.id : m.name);
|
||||||
|
const int w = cv_->text_width(line, s);
|
||||||
|
const bool over = inRect(x - gs_, y - gs_, w + 2 * gs_, lh);
|
||||||
|
cv_->text(x, y, line, m.game_mode ? dim() : over || m.enabled ? white() : grey(), s);
|
||||||
|
if (over && clicked_ && !m.game_mode) { m.enabled = !m.enabled; r.action = Action::ApplyMods; }
|
||||||
|
if (over) { tip = m.desc + (m.game_mode ? tr("$menu_mods_customactivation") : ""); tipY = y; }
|
||||||
|
const int bx = px + pw - 60 * gs_; // Move up / Move down (small buttons on the row)
|
||||||
|
auto sbtn = [&](int sx, const std::string& t2) { const int w2 = cv_->text_width(t2, s); const bool ov = inRect(sx - gs_, y - gs_, w2 + 2 * gs_, lh); cv_->text(sx, y, t2, ov ? white() : dim(), s); return ov && clicked_; };
|
||||||
|
if (i > 0 && sbtn(bx, tr("$menu_mods_moveup"))) { std::swap((*mods)[i], (*mods)[i - 1]); r.action = Action::ApplyMods; }
|
||||||
|
if (i + 1 < mods->size() && sbtn(bx + 26 * gs_, tr("$menu_mods_movedown"))) { std::swap((*mods)[i], (*mods)[i + 1]); r.action = Action::ApplyMods; }
|
||||||
|
y += lh;
|
||||||
|
}
|
||||||
|
y += lh;
|
||||||
|
cv_->text(x, y, tr("$menu_mods_help_paused"), dim(), s); y += lh + gs_;
|
||||||
|
if (button(x, y, tr("$menu_mods_openmodfolder"), false)) r.action = Action::OpenModFolder; y += lineH();
|
||||||
|
if (c_->has_run && button(x, y, tr("$menu_mods_restart"), false)) { r.action = Action::RestartMods; screen = Screen::None; } if (c_->has_run) y += lineH();
|
||||||
|
if (button(x, y, tr("$menu_mods_newgame"), false)) { open(Screen::NewGame); if (c_->settings && c_->settings->online) daily.fetch(); } y += lineH();
|
||||||
|
if (!tip.empty()) { // the mod's description beside the list
|
||||||
|
std::vector<std::string> lines; wrap(tip, 110 * gs_, lines);
|
||||||
|
const int bw = 116 * gs_, bh = (int)lines.size() * lh + 6 * gs_, bx = std::min(cv_->w - bw - 2 * gs_, c_->mx + 6 * gs_), by = std::min(cv_->h - bh - 2 * gs_, tipY);
|
||||||
|
panel(bx, by, bw, bh, true);
|
||||||
|
int ly = by + 3 * gs_; for (const std::string& l : lines) { cv_->text(bx + 3 * gs_, ly, l, grey(), s); ly += lh; }
|
||||||
|
}
|
||||||
|
scrollBy(c_->wheel * 3 * lh, std::max(0, y + scroll_ - (py + ph) + 20 * gs_));
|
||||||
|
if (button(8 * gs_, cv_->h - 14 * gs_, tr("$menu_return"), false)) screen = back();
|
||||||
|
}
|
||||||
|
// The Progress screen (006dfe40): three grids - the perks (9 columns), the spells (12) and the enemies (9) - each cell a grid_box with the icon when the
|
||||||
|
// perk / spell was picked up (the perk_picked_<id> / action_<id> persistent flag) or the enemy killed, grid_box_unknown with icon_unknown otherwise; an entry
|
||||||
|
// new since the screen was last seen pulses with grid_highlight_new. Hovering shows the name and the description, "???" when unknown (the enemies: Kills /
|
||||||
|
// Killed by). Below: the endings discovered and the secrets found.
|
||||||
|
void progress(Result& r) {
|
||||||
|
const Assets& as = *as_;
|
||||||
|
const int pw = std::min(cv_->w - 16 * gs_, 300 * gs_), px = (cv_->w - pw) / 2, py = 20 * gs_, ph = cv_->h - py - 24 * gs_;
|
||||||
|
label(px, py - 10 * gs_, tr("$menu_progress"), grey());
|
||||||
|
panel(px, py, pw, ph);
|
||||||
|
const int s = std::max(1, gs_ * 2 / 3), lh = 8 * s + 2;
|
||||||
|
const int cell = (as.gridBox.ok() ? as.gridBox.w : 20) * gs_, pitch = cell + gs_;
|
||||||
|
int x = px + 6 * gs_, y = py + 6 * gs_ - scroll_;
|
||||||
|
const ProgressItem* hot = nullptr; bool hotEnemy = false;
|
||||||
|
auto grid = [&](const char* header, const std::vector<ProgressItem>* items, int cols, bool enemies) {
|
||||||
|
if (!items) return;
|
||||||
|
int known = 0; for (const ProgressItem& it : *items) known += it.known;
|
||||||
|
cv_->text(x, y, tr(header) + " " + std::to_string(known) + " / " + std::to_string(items->size()), white(), s); y += lh + gs_;
|
||||||
|
cols = std::max(1, std::min(cols, (pw - 12 * gs_) / pitch));
|
||||||
|
for (size_t i = 0; i < items->size(); ++i) {
|
||||||
|
const ProgressItem& it = (*items)[i];
|
||||||
|
const int cx = x + (int)(i % cols) * pitch, cy = y + (int)(i / cols) * pitch;
|
||||||
|
if (cy + cell < py + 3 * gs_ || cy > py + ph - 3 * gs_) continue; // (scrolled out of the box)
|
||||||
|
const hud::Icon& box = it.known || !as.gridBoxUnknown.ok() ? as.gridBox : as.gridBoxUnknown;
|
||||||
|
if (box.ok()) cv_->icon_fit(box, cx, cy, cell, cell); else cv_->frame(cx, cy, cell, cell, it.known ? grey() : dim());
|
||||||
|
const hud::Icon* ic = it.known ? iconOf(it.icon) : nullptr;
|
||||||
|
if (!ic || !ic->ok()) ic = &as.iconUnknown;
|
||||||
|
if (ic->ok()) { const int iw = std::min(cell - 4 * gs_, ic->w * gs_), ih = std::min(cell - 4 * gs_, ic->h * gs_); cv_->icon_fit(*ic, cx + (cell - iw) / 2, cy + (cell - ih) / 2, iw, ih); }
|
||||||
|
if (it.is_new && it.known && as.gridNew.ok()) { // (006df000: the highlight's alpha follows a sine of the frame)
|
||||||
|
hud::Icon hi = as.gridNew; const double a = 0.5 + 0.5 * std::sin(tick_ * 5.0 + i * 0.7);
|
||||||
|
for (uint32_t& p : hi.px) p = (p & 0xFFFFFFu) | (uint32_t)((p >> 24) * a) << 24;
|
||||||
|
cv_->icon_fit(hi, cx, cy, cell, cell);
|
||||||
|
}
|
||||||
|
if (inRect(cx, cy, cell, cell) && cy >= py && cy + cell <= py + ph) { hot = ⁢ hotEnemy = enemies; }
|
||||||
|
}
|
||||||
|
y += ((int)items->size() + cols - 1) / cols * pitch + lh;
|
||||||
|
};
|
||||||
|
grid("$progress_perks", c_->perks, 9, false);
|
||||||
|
grid("$progress_actions", c_->actions, 12, false);
|
||||||
|
grid("$progress_enemies", c_->enemies, 9, true);
|
||||||
|
if (c_->ending0 || c_->ending1) { cv_->text(x, y, tr(c_->ending0 && c_->ending1 ? "$progress_ending01" : c_->ending0 ? "$progress_ending0" : "$progress_ending1"), grey(), s); y += lh; }
|
||||||
|
cv_->text(x, y, fmt("$progress_completed", std::to_string(c_->secrets_found), ""), grey(), s); y += lh;
|
||||||
|
if (hot) { // the information box of the cell under the mouse
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
if (!hot->known) lines.push_back(tr("$progress_unknown"));
|
||||||
|
else {
|
||||||
|
lines.push_back(hot->is_new ? fmt("$progress_new", tr(hot->name)) : tr(hot->name));
|
||||||
|
if (hotEnemy) { lines.push_back(fmt("$progress_kills", std::to_string(hot->kills))); lines.push_back(fmt("$progress_killed_by", std::to_string(hot->killed_by))); }
|
||||||
|
else wrap(tr(hot->desc), 110 * gs_, lines);
|
||||||
|
}
|
||||||
|
const int bw = 116 * gs_, bh = (int)lines.size() * lh + 6 * gs_, bx = std::min(cv_->w - bw - 2 * gs_, c_->mx + 8 * gs_), by = std::min(cv_->h - bh - 2 * gs_, c_->my + 8 * gs_);
|
||||||
|
panel(bx, by, bw, bh, true);
|
||||||
|
int ly = by + 3 * gs_; for (size_t k = 0; k < lines.size(); ++k) { cv_->text(bx + 3 * gs_, ly, lines[k], k == 0 ? white() : grey(), s); ly += lh; }
|
||||||
|
}
|
||||||
|
scrollBy(c_->wheel * 3 * lh, std::max(0, y + scroll_ - (py + ph) + 20 * gs_));
|
||||||
|
if (button(8 * gs_, cv_->h - 14 * gs_, tr("$menu_return"), false)) { r.action = Action::ProgressSeen; screen = back(); }
|
||||||
|
}
|
||||||
|
const hud::Icon* iconOf(const std::string& file) { return modeIcon(file, nullptr); } // (any image of the wak or a mod, cached)
|
||||||
|
void gameOver(Result& r) {
|
||||||
|
gameOverT_ += 1.0 / 60.0;
|
||||||
|
const Assets& as = *as_;
|
||||||
|
const double fade = std::min(1.0, gameOverT_ / 1.5);
|
||||||
|
const int cx = cv_->w / 2; int y = (int)(cv_->h * 0.18);
|
||||||
|
if (as.gameOver.ok()) { const int w = as.gameOver.w * gs_ / 1, h = as.gameOver.h * gs_; const int ww = std::min(w, cv_->w - 16 * gs_), hh = h * ww / std::max(1, w); cv_->icon_fit(as.gameOver, cx - ww / 2, y - (int)(10 * gs_ * (1.0 - fade)), ww, hh); y += hh + 4 * gs_; }
|
||||||
|
if (gameOverT_ < 1.0) return;
|
||||||
|
std::string cause = tr("$stat_cause_of_death"); const size_t p = cause.find("$0"); if (p != std::string::npos) cause.replace(p, 2, cause_);
|
||||||
|
cv_->text(cx - tw(cause) / 2, y, cause, grey(), gs_); y += lineH();
|
||||||
|
if (button(cx, y, tr("$menugameover_nextbutton"))) screen = Screen::GameOverStats;
|
||||||
|
(void)r;
|
||||||
|
}
|
||||||
|
void gameOverStats(Result& r) {
|
||||||
|
const Assets& as = *as_;
|
||||||
|
const int cx = cv_->w / 2; int y = (int)(cv_->h * 0.18);
|
||||||
|
if (as.gameOver.ok()) { const int w = as.gameOver.w * gs_, h = as.gameOver.h * gs_; const int ww = std::min(w, cv_->w - 16 * gs_), hh = h * ww / std::max(1, w); cv_->icon_fit(as.gameOver, cx - ww / 2, y, ww, hh); y += hh - 6 * gs_; }
|
||||||
|
const int pw = 192 * gs_, px = cx - pw / 2, ph = std::min(200 * gs_, cv_->h - y - 4 * gs_); // (006e51e0: the box of 192 x 200 units under the title)
|
||||||
|
panel(px, y, pw, ph);
|
||||||
|
int ty = y + 12 * gs_; const int lx = px + 36 * gs_;
|
||||||
|
auto row = [&](const std::string& key, const std::string& val, const std::string& best) {
|
||||||
|
label(lx, ty, tr(key), grey());
|
||||||
|
cv_->text(lx + tw(tr(key)) + 3 * gs_, ty, val, yellow(), gs_);
|
||||||
|
if (!best.empty()) { std::string b = tr("$stat_best"); const size_t p = b.find("$0"); if (p != std::string::npos) b.replace(p, 2, best); cv_->text(lx + tw(tr(key)) + tw(val) + 6 * gs_, ty, b, dim(), gs_); }
|
||||||
|
ty += 9 * gs_;
|
||||||
|
};
|
||||||
|
std::string cause = tr("$stat_cause_of_death"); const size_t p = cause.find("$0"); if (p != std::string::npos) cause.replace(p, 2, cause_);
|
||||||
|
label(lx, ty, cause, grey()); ty += 9 * gs_;
|
||||||
|
row("$stat_modsenabled", tr(c_->mods_used ? "$menu_yes" : "$menu_no"), "");
|
||||||
|
auto timeStr = [](double s) { char b[32]; const int t = (int)s; std::snprintf(b, sizeof b, "%d:%02d:%02d", t / 3600, (t / 60) % 60, t % 60); return std::string(b); };
|
||||||
|
row("$stat_gold", std::to_string(c_->stats.gold), std::to_string(c_->best.gold));
|
||||||
|
row("$stat_time", timeStr(c_->stats.playtime), timeStr(c_->best.playtime));
|
||||||
|
row("$stat_depth", std::to_string(c_->stats.depth) + "m", std::to_string(c_->best.depth) + "m");
|
||||||
|
row("$stat_places_visited", std::to_string(c_->stats.places), std::to_string(c_->best.places));
|
||||||
|
row("$stat_enemies_slain", std::to_string(c_->stats.kills), std::to_string(c_->best.kills));
|
||||||
|
row("$stat_max_hp", std::to_string(c_->stats.maxhp), std::to_string(c_->best.maxhp));
|
||||||
|
row("$stat_items_found", std::to_string(c_->stats.items), std::to_string(c_->best.items));
|
||||||
|
row("$stat_total_deaths", std::to_string(c_->total_deaths), "");
|
||||||
|
row("$stat_total_wins", std::to_string(c_->total_wins), "");
|
||||||
|
ty += 8 * gs_;
|
||||||
|
if (button(cx, ty, tr("$menugameover_newgame"))) { open(Screen::NewGame); if (c_->settings && c_->settings->online) daily.fetch(); } ty += lineH();
|
||||||
|
if (button(cx, ty, tr("$menugameover_savereplay"), true, c_->replay_available)) r.action = Action::SaveReplay; ty += lineH(); // (006e51e0: with the replay recorder on, the clip editor over the game over)
|
||||||
|
if (button(cx, ty, tr("$menu_progress"))) open(Screen::Progress); ty += lineH();
|
||||||
|
if (button(cx, ty, tr("$menugameover_quit"))) r.action = Action::Quit;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace menu
|
||||||
@@ -9,7 +9,7 @@ constexpr int kRing = 1024; // commands
|
|||||||
constexpr int kHandles = 4096; // kept instances (handle % kHandles)
|
constexpr int kHandles = 4096; // kept instances (handle % kHandles)
|
||||||
constexpr int kName = 120;
|
constexpr int kName = 120;
|
||||||
|
|
||||||
enum Op : int32_t { None = 0, PlayOneShot, Start, Move, SetParam, Stop, StopNow, Listener, SetVolume, Quit, Release };
|
enum Op : int32_t { None = 0, PlayOneShot, Start, Move, SetParam, Stop, StopNow, Listener, SetVolume, Quit, Release, Pause }; // Pause: value 1 pauses every kept instance, 0 resumes them (the game's pause menu)
|
||||||
|
|
||||||
struct Cmd {
|
struct Cmd {
|
||||||
int32_t op = None;
|
int32_t op = None;
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ bool FmodAudio::push(int op, int handle, float x, float y, float value, const st
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FmodAudio::play(const std::string& event, float x, float y, float volume) { if (!event.empty()) push(PlayOneShot, 0, x, y, volume, event); }
|
void FmodAudio::play(const std::string& event, float x, float y, float volume) { if (!event.empty()) push(PlayOneShot, 0, x, y, volume * sfx_gain_, event); }
|
||||||
|
|
||||||
int FmodAudio::start(const std::string& event, float x, float y, float volume) {
|
int FmodAudio::start(const std::string& event, float x, float y, float volume) {
|
||||||
if (event.empty() || !ok()) return 0;
|
if (event.empty() || !ok()) return 0;
|
||||||
const int h = next_handle_++;
|
const int h = next_handle_++;
|
||||||
if (next_handle_ >= kHandles * 1000) next_handle_ = 1;
|
if (next_handle_ >= kHandles * 1000) next_handle_ = 1;
|
||||||
if (!push(Start, h, x, y, volume, event)) return 0;
|
if (!push(Start, h, x, y, volume * (event.find("music") != std::string::npos ? music_gain_ : sfx_gain_), event)) return 0;
|
||||||
sh_->playing[h % kHandles] = 1;
|
sh_->playing[h % kHandles] = 1;
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
@@ -73,6 +73,7 @@ int FmodAudio::start(const std::string& event, float x, float y, float volume) {
|
|||||||
void FmodAudio::move(int handle, float x, float y) { if (handle > 0) push(Move, handle, x, y, 0, ""); }
|
void FmodAudio::move(int handle, float x, float y) { if (handle > 0) push(Move, handle, x, y, 0, ""); }
|
||||||
void FmodAudio::set_parameter(int handle, const std::string& name, float value) { if (handle > 0) push(SetParam, handle, 0, 0, value, name); }
|
void FmodAudio::set_parameter(int handle, const std::string& name, float value) { if (handle > 0) push(SetParam, handle, 0, 0, value, name); }
|
||||||
void FmodAudio::set_volume(int handle, float volume) { if (handle > 0) push(SetVolume, handle, 0, 0, volume, ""); }
|
void FmodAudio::set_volume(int handle, float volume) { if (handle > 0) push(SetVolume, handle, 0, 0, volume, ""); }
|
||||||
|
void FmodAudio::set_paused(bool paused) { push(Pause, 0, 0, 0, paused ? 1.0f : 0.0f, ""); }
|
||||||
void FmodAudio::release(int handle) { if (handle > 0) push(Release, handle, 0, 0, 0, ""); }
|
void FmodAudio::release(int handle) { if (handle > 0) push(Release, handle, 0, 0, 0, ""); }
|
||||||
void FmodAudio::stop(int handle, bool immediate) { if (handle > 0) push(immediate ? StopNow : Stop, handle, 0, 0, 0, ""); }
|
void FmodAudio::stop(int handle, bool immediate) { if (handle > 0) push(immediate ? StopNow : Stop, handle, 0, 0, 0, ""); }
|
||||||
bool FmodAudio::playing(int handle) const { return handle > 0 && ok() && sh_->playing[handle % kHandles] != 0; }
|
bool FmodAudio::playing(int handle) const { return handle > 0 && ok() && sh_->playing[handle % kHandles] != 0; }
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ public:
|
|||||||
bool ok() const; // the host is up and has loaded the banks
|
bool ok() const; // the host is up and has loaded the banks
|
||||||
// a one-shot event at a world position (pixels): "game_effect/teleport/tick" or "event:/game_effect/teleport/tick"
|
// a one-shot event at a world position (pixels): "game_effect/teleport/tick" or "event:/game_effect/teleport/tick"
|
||||||
void play(const std::string& event, float x, float y, float volume = 1.0f);
|
void play(const std::string& event, float x, float y, float volume = 1.0f);
|
||||||
|
void set_gains(float sfx, float music) { sfx_gain_ = sfx; music_gain_ = music; } // the Options' volumes: one-shots and started events ("music/" events: the music gain)
|
||||||
|
float sfx_gain_ = 1.0f, music_gain_ = 1.0f;
|
||||||
// a kept instance (loops, sounds that follow an entity): handle > 0
|
// a kept instance (loops, sounds that follow an entity): handle > 0
|
||||||
int start(const std::string& event, float x, float y, float volume = 1.0f);
|
int start(const std::string& event, float x, float y, float volume = 1.0f);
|
||||||
void move(int handle, float x, float y);
|
void move(int handle, float x, float y);
|
||||||
void set_parameter(int handle, const std::string& name, float value);
|
void set_parameter(int handle, const std::string& name, float value);
|
||||||
void set_volume(int handle, float volume);
|
void set_volume(int handle, float volume);
|
||||||
|
void set_paused(bool paused); // every started instance holds (the pause menu); one-shots still play
|
||||||
void stop(int handle, bool immediate = false);
|
void stop(int handle, bool immediate = false);
|
||||||
void release(int handle); // the instance plays to its end on its own; the handle is free
|
void release(int handle); // the instance plays to its end on its own; the handle is free
|
||||||
bool playing(int handle) const;
|
bool playing(int handle) const;
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "entity/material_mix.h"
|
||||||
|
#include "core/materials.h"
|
||||||
|
|
||||||
|
namespace ng {
|
||||||
|
struct PotionSpray {
|
||||||
|
double speed = 1, minimum = .5;
|
||||||
|
bool bunch = false, skip_static = false, leak_gas = false;
|
||||||
|
int count = 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 00bfdd10: one attempt per material, or throw_how_many on the initial press.
|
||||||
|
// Velocities are pixels/second here; the GPU transport converts to pixels/tick.
|
||||||
|
template<class Random, class Emit>
|
||||||
|
int potion_spray(std::vector<MixPart>& parts, const MaterialDb& db, const PotionSpray& cfg,
|
||||||
|
double capacity, double aim_x, double aim_y, double holder_vx,
|
||||||
|
bool pressed, Random random, Emit emit) {
|
||||||
|
if (capacity <= 0 || (cfg.bunch && !pressed)) return 0;
|
||||||
|
int attempts = 0;
|
||||||
|
for (auto& part : parts) {
|
||||||
|
if (part.n <= 0 || (cfg.bunch && (int)part.n <= 0)) continue;
|
||||||
|
const Material* m = db.find(part.mat);
|
||||||
|
if (!m) continue;
|
||||||
|
if (!cfg.bunch && cfg.skip_static && (m->type == CellType::Solid ||
|
||||||
|
(m->type == CellType::Liquid && attr_int(*m, "liquid_static", 0)))) continue;
|
||||||
|
const double speed = cfg.speed * std::max(cfg.minimum, std::max(1, (int)part.n) / capacity);
|
||||||
|
const int count = cfg.bunch ? std::max(0, cfg.count) : 1;
|
||||||
|
for (int i = 0; i < count && part.n > 0; ++i) {
|
||||||
|
const double angle = (random() * 2 - 1) * (cfg.bunch ? .25 : .05);
|
||||||
|
const double c = std::cos(angle), s = std::sin(angle), length = std::hypot(aim_x, aim_y);
|
||||||
|
double vx = length > 0 ? (aim_x * c - aim_y * s) / length * speed : 0;
|
||||||
|
double vy = length > 0 ? (aim_x * s + aim_y * c) / length * speed : 0;
|
||||||
|
vx += holder_vx;
|
||||||
|
if (!cfg.bunch && cfg.leak_gas && m->type == CellType::Gas) { vx = 0; vy = -10; }
|
||||||
|
emit(*m, vx, vy);
|
||||||
|
part.n -= 1; // Native spends contents even when the particle pool is full.
|
||||||
|
++attempts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attempts;
|
||||||
|
}
|
||||||
|
} // namespace ng
|
||||||
@@ -90,6 +90,9 @@ int n_GameGetRealWorldTimeSinceStarted(LuaHost&, lua_State* L) {
|
|||||||
lua_pushnumber(L, std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count());
|
lua_pushnumber(L, std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
// the game modes of the menu (src/app/menu.h): NG_DAILY=1 a daily run, 2 a daily practice run (the seed came from the backend)
|
||||||
|
int n_GameIsDailyRun(LuaHost&, lua_State* L) { const char* d = std::getenv("NG_DAILY"); lua_pushboolean(L, d && std::string(d) == "1"); return 1; }
|
||||||
|
int n_GameIsDailyRunOrPractice(LuaHost&, lua_State* L) { const char* d = std::getenv("NG_DAILY"); lua_pushboolean(L, d && *d); return 1; }
|
||||||
int n_GetDailyPracticeRunSeed(LuaHost&, lua_State* L) { // the day's seed: the UTC date
|
int n_GetDailyPracticeRunSeed(LuaHost&, lua_State* L) { // the day's seed: the UTC date
|
||||||
const std::time_t t = std::time(nullptr);
|
const std::time_t t = std::time(nullptr);
|
||||||
std::tm tm{};
|
std::tm tm{};
|
||||||
@@ -344,7 +347,7 @@ void register_host_natives(LuaHost& h) {
|
|||||||
{"SessionNumbersGetValue", n_SessionNumbersGetValue}, {"SessionNumbersSetValue", n_SessionNumbersSetValue}, {"SessionNumbersSave", n_Nop},
|
{"SessionNumbersGetValue", n_SessionNumbersGetValue}, {"SessionNumbersSetValue", n_SessionNumbersSetValue}, {"SessionNumbersSave", n_Nop},
|
||||||
{"MagicNumbersGetValue", n_MagicNumbersGetValue}, {"ModMagicNumbersFileAdd", n_ModMagicNumbersFileAdd},
|
{"MagicNumbersGetValue", n_MagicNumbersGetValue}, {"ModMagicNumbersFileAdd", n_ModMagicNumbersFileAdd},
|
||||||
{"GameGetDateAndTimeUTC", n_GameGetDateAndTimeUTC}, {"GameGetRealWorldTimeSinceStarted", n_GameGetRealWorldTimeSinceStarted}, {"GetDailyPracticeRunSeed", n_GetDailyPracticeRunSeed},
|
{"GameGetDateAndTimeUTC", n_GameGetDateAndTimeUTC}, {"GameGetRealWorldTimeSinceStarted", n_GameGetRealWorldTimeSinceStarted}, {"GetDailyPracticeRunSeed", n_GetDailyPracticeRunSeed},
|
||||||
{"GameIsDailyRun", n_False}, {"GameIsDailyRunOrDailyPracticeRun", n_False}, {"GameIsBetaBuild", n_False}, {"DebugGetIsDevBuild", n_False}, {"GameIsIntroPlaying", n_False},
|
{"GameIsDailyRun", n_GameIsDailyRun}, {"GameIsDailyRunOrDailyPracticeRun", n_GameIsDailyRunOrPractice}, {"GameIsBetaBuild", n_False}, {"DebugGetIsDevBuild", n_False}, {"GameIsIntroPlaying", n_False},
|
||||||
{"DebugBiomeMapGetFilename", n_DebugBiomeMapGetFilename},
|
{"DebugBiomeMapGetFilename", n_DebugBiomeMapGetFilename},
|
||||||
// release-build behaviour: these do nothing outside the developers' builds (EntitySave, trailer mode, the test player) or report to Steam
|
// release-build behaviour: these do nothing outside the developers' builds (EntitySave, trailer mode, the test player) or report to Steam
|
||||||
{"EntitySave", n_Nop}, {"DebugEnableTrailerMode", n_Nop}, {"Debug_SaveTestPlayer", n_Nop}, {"GameGiveAchievement", n_Nop}, {"AutosaveDisable", n_Nop},
|
{"EntitySave", n_Nop}, {"DebugEnableTrailerMode", n_Nop}, {"Debug_SaveTestPlayer", n_Nop}, {"GameGiveAchievement", n_Nop}, {"AutosaveDisable", n_Nop},
|
||||||
|
|||||||
@@ -520,7 +520,9 @@ LuaHost::~LuaHost() {
|
|||||||
if (L_) lua_close(L_);
|
if (L_) lua_close(L_);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string& LuaHost::translate(const std::string& key) {
|
const std::string& LuaHost::translate(const std::string& key) { return translate_in(wak_, loose_dir, key); }
|
||||||
|
|
||||||
|
const std::string& LuaHost::translate_in(const Wak& wak_, const std::string& loose_dir, const std::string& key) {
|
||||||
static const std::string none;
|
static const std::string none;
|
||||||
if (!texts_loaded_) {
|
if (!texts_loaded_) {
|
||||||
texts_loaded_ = true;
|
texts_loaded_ = true;
|
||||||
|
|||||||
+4
-2
@@ -171,6 +171,8 @@ public:
|
|||||||
bool buffer_world_writes = false, wrote_world = false;
|
bool buffer_world_writes = false, wrote_world = false;
|
||||||
std::set<std::string> removed_run_flags;
|
std::set<std::string> removed_run_flags;
|
||||||
std::string loose_dir; // the game folder next to data.wak (the translations are loose files there)
|
std::string loose_dir; // the game folder next to data.wak (the translations are loose files there)
|
||||||
|
// translate() without a host (the menus before a run): the wak and the folder beside it
|
||||||
|
static const std::string& translate_in(const Wak& wak, const std::string& loose_dir, const std::string& key);
|
||||||
std::map<std::string, double> stats, biome_stats_base; // StatsGetValue / StatsBiomeGetValue (the host counts enemies_killed...; StatsResetBiome starts the biome counters over)
|
std::map<std::string, double> stats, biome_stats_base; // StatsGetValue / StatsBiomeGetValue (the host counts enemies_killed...; StatsResetBiome starts the biome counters over)
|
||||||
int next_entity_id = 1000;
|
int next_entity_id = 1000;
|
||||||
struct GenComp { int ent = 0; int index = 0; std::string type; }; // index >= 0: the spawn's recorded component; -1 - k: the k-th component of the file
|
struct GenComp { int ent = 0; int index = 0; std::string type; }; // index >= 0: the spawn's recorded component; -1 - k: the k-th component of the file
|
||||||
@@ -194,8 +196,8 @@ private:
|
|||||||
std::vector<ActionRow> actions_;
|
std::vector<ActionRow> actions_;
|
||||||
size_t actions_len_ = (size_t)-1;
|
size_t actions_len_ = (size_t)-1;
|
||||||
bool tried_actions_ = false;
|
bool tried_actions_ = false;
|
||||||
std::map<std::string, std::string> texts_;
|
static inline std::map<std::string, std::string> texts_; // (one table for every VM and the menus)
|
||||||
bool texts_loaded_ = false;
|
static inline bool texts_loaded_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ng
|
} // namespace ng
|
||||||
Reference in new issue
Block a user