mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Keep HUD after landing and restore native shadow font passes
This commit is contained in:
1 parent
763da9bbf5
commit
142fe97a6d
9 files changed
+257
-88
No files matched your search
@@ -37,6 +37,7 @@ fn main() -> Result<(), String> {
|
||||
let shapes: apt_scene::Shapes =
|
||||
serde_json::from_value(json["shapes"].clone()).map_err(|e| e.to_string())?;
|
||||
let mut max_draws = 0;
|
||||
let mut saw_native_shadow_pair = false;
|
||||
for tick in 0..1800 {
|
||||
let new_trick = tick % 180 == 10;
|
||||
if new_trick {
|
||||
@@ -52,15 +53,23 @@ fn main() -> Result<(), String> {
|
||||
input.line_score += 100;
|
||||
input.clean = tick % 360 == 10;
|
||||
input.sketchy = !input.clean;
|
||||
input.line_time = 400.0;
|
||||
input.line_time = 8.0;
|
||||
input.multiplier = if input.multiplier == 1.5 { 2.0 } else { 1.5 };
|
||||
}
|
||||
input.line_time = (input.line_time - 1.0).max(0.0);
|
||||
input.line_time = (input.line_time - 1.0 / 60.0).max(0.0);
|
||||
input.sequence_timer = input.line_time as i32;
|
||||
runtime
|
||||
.update(input.clone(), new_trick, false, tick % 180 == 120)
|
||||
.map_err(|e| format!("Data frame {tick}: {e}"))?;
|
||||
let draws = apt_scene::draw(&runtime.bindings.movie, &runtime.vm, &shapes)?;
|
||||
max_draws = max_draws.max(draws.len());
|
||||
saw_native_shadow_pair |= draws.windows(2).any(|pair| {
|
||||
pair[0].texture.contains("futurashadow")
|
||||
&& pair[0].multiply[..3] == [0., 0., 0.]
|
||||
&& pair[1].texture.contains("futuraheavy")
|
||||
&& pair[1].multiply[0] > 0.
|
||||
});
|
||||
|
||||
if draws
|
||||
.iter()
|
||||
.flat_map(|d| &d.vertices)
|
||||
@@ -69,6 +78,9 @@ fn main() -> Result<(), String> {
|
||||
return Err("Nonfinite authored draw geometry".into());
|
||||
}
|
||||
}
|
||||
if !saw_native_shadow_pair {
|
||||
return Err("Missing native black shadow / heavy foreground pair".into());
|
||||
}
|
||||
println!(
|
||||
"Original HUD action audit passed 1800 authored frames; {} object slots, {} display instances",
|
||||
runtime.vm.objects.len(),
|
||||
|
||||
@@ -41,12 +41,30 @@ fn frame(
|
||||
reverting: false,
|
||||
}
|
||||
}
|
||||
fn publish_hud(
|
||||
scoring: &scoring_runtime::Runtime,
|
||||
hud: &mut hud_runtime::Runtime,
|
||||
) -> Result<(), String> {
|
||||
hud.update(
|
||||
scoring.hud_input(),
|
||||
scoring.new_trick,
|
||||
scoring.modified_trick,
|
||||
scoring.close_tricks,
|
||||
)
|
||||
}
|
||||
fn main() -> Result<(), String> {
|
||||
let root = std::env::args_os()
|
||||
.nth(1)
|
||||
.ok_or("Expected owned assets directory")?;
|
||||
let data = skate_data::collections::Collections::load(std::path::Path::new(&root))?;
|
||||
let mut scoring = scoring_runtime::Runtime::load(&data)?;
|
||||
let movie_path = std::env::args_os()
|
||||
.nth(2)
|
||||
.ok_or("Expected owned trickdisplay.json")?;
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(movie_path).map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut hud = hud_runtime::Runtime::load(&json, scoring.hud_input())?;
|
||||
let kickflip = scoring
|
||||
.data
|
||||
.by_id(96)
|
||||
@@ -55,9 +73,23 @@ fn main() -> Result<(), String> {
|
||||
scoring.advance(frame(0, FilteredCategory::Ground, None))?;
|
||||
for tick in 1..61 {
|
||||
scoring.advance(frame(tick, FilteredCategory::Air, Some(kickflip)))?;
|
||||
publish_hud(&scoring, &mut hud)?;
|
||||
}
|
||||
for tick in 61..70 {
|
||||
scoring.advance(frame(tick, FilteredCategory::Ground, None))?;
|
||||
publish_hud(&scoring, &mut hud)?;
|
||||
if scoring.close_tricks {
|
||||
return Err("Landing emitted the native bail/close event".into());
|
||||
}
|
||||
}
|
||||
if scoring.hud_input().sequence_score != 100 || scoring.hud_input().line_time <= 0. {
|
||||
return Err("Banked landing lost its displayed score/line timer".into());
|
||||
}
|
||||
if scoring.hud_input().line_time != scoring.session.line.points / scoring.data.line_drain {
|
||||
return Err("HUD timer is not in native seconds".into());
|
||||
}
|
||||
if !hud.vm.get(hud.bindings.movie.root, "_visible").truth() {
|
||||
return Err("Original APT root disappeared on landing".into());
|
||||
}
|
||||
let banked = scoring.session.holder.snapshot.last_reward;
|
||||
if banked != 100. {
|
||||
@@ -67,12 +99,14 @@ fn main() -> Result<(), String> {
|
||||
}
|
||||
for tick in 70..80 {
|
||||
scoring.advance(frame(tick, FilteredCategory::Ground, None))?;
|
||||
publish_hud(&scoring, &mut hud)?;
|
||||
}
|
||||
if scoring.session.holder.snapshot.last_reward != banked {
|
||||
return Err("Idle frame published the sequence again".into());
|
||||
}
|
||||
for tick in 80..140 {
|
||||
scoring.advance(frame(tick, FilteredCategory::Air, Some(kickflip)))?;
|
||||
publish_hud(&scoring, &mut hud)?;
|
||||
}
|
||||
let mut cancelled = frame(140, FilteredCategory::Ground, None);
|
||||
cancelled.teleported = true;
|
||||
@@ -85,8 +119,20 @@ fn main() -> Result<(), String> {
|
||||
if scoring.session.combo.multiplier != 1. {
|
||||
return Err("Teleport retained multiplier".into());
|
||||
}
|
||||
// An independent supplied-data sequence checks natural line expiry.
|
||||
let mut expired = scoring_runtime::Runtime::load(&data)?;
|
||||
expired.advance(frame(0, FilteredCategory::Ground, None))?;
|
||||
for tick in 1..61 {
|
||||
expired.advance(frame(tick, FilteredCategory::Air, Some(kickflip)))?;
|
||||
}
|
||||
for tick in 61..300 {
|
||||
expired.advance(frame(tick, FilteredCategory::Ground, None))?;
|
||||
}
|
||||
if expired.hud_input().sequence_score != 0 || expired.hud_input().line_time != 0. {
|
||||
return Err("Expired line retained its HUD score".into());
|
||||
}
|
||||
println!(
|
||||
"Scoring data audit: authored kickflip credited once; idle publication stable; teleport cancels pending rewards and multiplier"
|
||||
"Scoring data audit: authored kickflip credited once; landing display persists; timer uses seconds; line expiry clears score; teleport cancels pending rewards and multiplier"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -123,70 +123,89 @@ fn visit(
|
||||
} else if let Some(text) = &character.text {
|
||||
let font = &movie.text_assets.fonts
|
||||
[&(text["font_id"].as_i64().ok_or("Invalid text font")? as i32)];
|
||||
let height = text["font_height"].as_f64().ok_or("Invalid text height")? as f32;
|
||||
let sx = font.scale[0] * height;
|
||||
let sy = font.scale[1] * height;
|
||||
let value = vm.get(id, "_displayText").text();
|
||||
let bounds = character.bounds.ok_or("Missing text bounds")?;
|
||||
let width = font.width(&value, height);
|
||||
let alignment = text["alignment"].as_u64().unwrap_or(0);
|
||||
let autosize = vm.get(id, "autoSize").text();
|
||||
let alignment = if autosize == "left" { 0 } else { alignment };
|
||||
let mut x = bounds[0]
|
||||
+ font.offset[0] * sx
|
||||
+ match alignment {
|
||||
1 => bounds[2] - bounds[0] - width,
|
||||
2 => (bounds[2] - bounds[0] - width) * 0.5,
|
||||
_ => 0.,
|
||||
};
|
||||
let y = bounds[1] + font.offset[1] * sy;
|
||||
let mut vertices = Vec::new();
|
||||
for c in value.chars() {
|
||||
if let Some(g) = font.glyph(c) {
|
||||
let x0 = x + g.x_offset * sx;
|
||||
let y0 = y + (font.ascent - g.y_offset) * sy;
|
||||
let x1 = x0 + g.width * sx;
|
||||
let y1 = y0 + g.height * sy;
|
||||
let [u0, v0, u1, v1] = g.atlas_bounds;
|
||||
let points = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
|
||||
let uvs = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
|
||||
for i in [0, 1, 2, 0, 2, 3] {
|
||||
vertices.push(transform(
|
||||
matrix,
|
||||
&Vertex {
|
||||
position: points[i],
|
||||
uv: [
|
||||
uvs[i][0] / font.size[0] as f32,
|
||||
uvs[i][1] / font.size[1] as f32,
|
||||
],
|
||||
},
|
||||
));
|
||||
// Native shadow text uses a black atlas pass followed by the sharp
|
||||
// futuraheavy glyphs translated +1 in text X (825D6B68/82CA1FD8).
|
||||
let passes = if let Some(foreground) = font.foreground {
|
||||
vec![
|
||||
(font, true, 0.),
|
||||
(&movie.text_assets.fonts[&foreground], false, 1.),
|
||||
]
|
||||
} else {
|
||||
vec![(font, false, 0.)]
|
||||
};
|
||||
for (font, shadow, advance_x) in passes {
|
||||
let height = text["font_height"].as_f64().ok_or("Invalid text height")? as f32;
|
||||
let sx = font.scale[0] * height;
|
||||
let sy = font.scale[1] * height;
|
||||
let value = vm.get(id, "_displayText").text();
|
||||
let bounds = character.bounds.ok_or("Missing text bounds")?;
|
||||
let width = font.width(&value, height);
|
||||
let alignment = text["alignment"].as_u64().unwrap_or(0);
|
||||
let autosize = vm.get(id, "autoSize").text();
|
||||
let alignment = if autosize == "left" { 0 } else { alignment };
|
||||
let mut x = bounds[0]
|
||||
+ advance_x
|
||||
+ font.offset[0] * sx
|
||||
+ match alignment {
|
||||
1 => bounds[2] - bounds[0] - width,
|
||||
2 => (bounds[2] - bounds[0] - width) * 0.5,
|
||||
_ => 0.,
|
||||
};
|
||||
let y = bounds[1] + font.offset[1] * sy;
|
||||
let mut vertices = Vec::new();
|
||||
for c in value.chars() {
|
||||
if let Some(g) = font.glyph(c) {
|
||||
let x0 = x + g.x_offset * sx;
|
||||
let y0 = y + (font.ascent - g.y_offset) * sy;
|
||||
let x1 = x0 + g.width * sx;
|
||||
let y1 = y0 + g.height * sy;
|
||||
let [u0, v0, u1, v1] = g.atlas_bounds;
|
||||
let points = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
|
||||
let uvs = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
|
||||
for i in [0, 1, 2, 0, 2, 3] {
|
||||
vertices.push(transform(
|
||||
matrix,
|
||||
&Vertex {
|
||||
position: points[i],
|
||||
uv: [
|
||||
uvs[i][0] / font.size[0] as f32,
|
||||
uvs[i][1] / font.size[1] as f32,
|
||||
],
|
||||
},
|
||||
));
|
||||
}
|
||||
x = g.x_advance.mul_add(sx, x);
|
||||
}
|
||||
x = g.x_advance.mul_add(sx, x);
|
||||
}
|
||||
}
|
||||
let argb = u32::from_str_radix(
|
||||
text["color_argb"]
|
||||
.as_str()
|
||||
.ok_or("Missing text color")?
|
||||
.trim_start_matches('#'),
|
||||
16,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let color = [
|
||||
((argb >> 16) & 255) as f32 / 255.,
|
||||
((argb >> 8) & 255) as f32 / 255.,
|
||||
(argb & 255) as f32 / 255.,
|
||||
(argb >> 24) as f32 / 255.,
|
||||
];
|
||||
if !vertices.is_empty() {
|
||||
out.push(Draw {
|
||||
texture: font.texture.clone(),
|
||||
size: font.size,
|
||||
vertices,
|
||||
multiply: std::array::from_fn(|i| multiply[i] * color[i]),
|
||||
add,
|
||||
});
|
||||
let argb = u32::from_str_radix(
|
||||
text["color_argb"]
|
||||
.as_str()
|
||||
.ok_or("Missing text color")?
|
||||
.trim_start_matches('#'),
|
||||
16,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let color = [
|
||||
((argb >> 16) & 255) as f32 / 255.,
|
||||
((argb >> 8) & 255) as f32 / 255.,
|
||||
(argb & 255) as f32 / 255.,
|
||||
(argb >> 24) as f32 / 255.,
|
||||
];
|
||||
if !vertices.is_empty() {
|
||||
out.push(Draw {
|
||||
texture: font.texture.clone(),
|
||||
size: font.size,
|
||||
vertices,
|
||||
multiply: std::array::from_fn(|i| {
|
||||
if shadow && i < 3 {
|
||||
0.
|
||||
} else {
|
||||
multiply[i] * color[i]
|
||||
}
|
||||
}),
|
||||
add,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for child in instance.children.values() {
|
||||
|
||||
@@ -14,6 +14,8 @@ pub struct Glyph {
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct Font {
|
||||
/// 825D6B68 attaches futuraheavy to Futura Shadow; 82CA1FD8 draws both.
|
||||
pub foreground: Option<i32>,
|
||||
pub texture: String,
|
||||
pub size: [u32; 2],
|
||||
pub scale: [f32; 2],
|
||||
@@ -51,6 +53,7 @@ impl TextAssets {
|
||||
.ok_or_else(|| format!("Missing native font {name}.{key}"))
|
||||
};
|
||||
let mut font = Font {
|
||||
foreground: None,
|
||||
texture: asset["texture"]
|
||||
.as_str()
|
||||
.ok_or("Font texture missing")?
|
||||
@@ -93,6 +96,17 @@ impl TextAssets {
|
||||
font,
|
||||
);
|
||||
}
|
||||
for c in json["characters"].as_array().unwrap() {
|
||||
if c["type_name"] == "font" && c["font"]["name"] == "Futura Shadow" {
|
||||
let foreground = json["characters"].as_array().unwrap().iter().find(|f|
|
||||
f["type_name"] == "font" && json["font_mappings"][f["font"]["name"].as_str().unwrap_or("")]["file_name"] == "futuraheavy")
|
||||
.and_then(|f| f["id"].as_i64()).ok_or("Missing native Futura Shadow foreground font")? as i32;
|
||||
out.fonts
|
||||
.get_mut(&(c["id"].as_i64().unwrap() as i32))
|
||||
.unwrap()
|
||||
.foreground = Some(foreground);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
pub fn localize(&self, text: &str) -> String {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#import bevy_ui::ui_vertex_output::UiVertexOutput
|
||||
@group(1) @binding(0) var hud: texture_2d<f32>;
|
||||
@group(1) @binding(1) var hud_sampler: sampler;
|
||||
@fragment
|
||||
fn fragment(in: UiVertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureSample(hud, hud_sampler, in.uv);
|
||||
}
|
||||
@@ -210,7 +210,8 @@ impl Runtime {
|
||||
if previous.multiplier != self.bindings.input.multiplier {
|
||||
methods.push("UpdateMultiplier");
|
||||
}
|
||||
if previous.sequence_score != self.bindings.input.sequence_score
|
||||
if previous.sequence_timer != self.bindings.input.sequence_timer
|
||||
|| previous.sequence_score != self.bindings.input.sequence_score
|
||||
|| previous.clean != self.bindings.input.clean
|
||||
|| previous.sketchy != self.bindings.input.sketchy
|
||||
|| previous.line_score != self.bindings.input.line_score
|
||||
|
||||
@@ -5,10 +5,12 @@ use bevy::{
|
||||
camera::{RenderTarget, visibility::RenderLayers},
|
||||
prelude::*,
|
||||
render::render_resource::{
|
||||
AsBindGroup, Extent3d, PrimitiveTopology, ShaderType, TextureDimension, TextureFormat,
|
||||
AsBindGroup, BlendState, Extent3d, PrimitiveTopology, RenderPipelineDescriptor, ShaderType,
|
||||
TextureDimension, TextureFormat,
|
||||
},
|
||||
shader::ShaderRef,
|
||||
sprite_render::{AlphaMode2d, Material2d, Material2dPlugin, MeshMaterial2d},
|
||||
ui_render::UiMaterialPlugin,
|
||||
};
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
|
||||
@@ -33,6 +35,26 @@ impl Material2d for HudMaterial {
|
||||
AlphaMode2d::Blend
|
||||
}
|
||||
}
|
||||
/// The offscreen target already contains RGB multiplied by coverage. Applying
|
||||
/// ImageNode's straight-alpha blend again suppresses the original soft glow.
|
||||
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
|
||||
struct HudComposite {
|
||||
#[texture(0)]
|
||||
#[sampler(1)]
|
||||
image: Handle<Image>,
|
||||
}
|
||||
impl UiMaterial for HudComposite {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
"embedded://skate3rust/hud_composite.wgsl".into()
|
||||
}
|
||||
fn specialize(descriptor: &mut RenderPipelineDescriptor, _: UiMaterialKey<Self>) {
|
||||
if let Some(fragment) = &mut descriptor.fragment {
|
||||
for target in fragment.targets.iter_mut().flatten() {
|
||||
target.blend = Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
struct Slot {
|
||||
entity: Entity,
|
||||
mesh: Handle<Mesh>,
|
||||
@@ -52,21 +74,25 @@ pub(crate) struct ScoringHudPlugin;
|
||||
impl Plugin for ScoringHudPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
embedded_asset!(app, "hud_render.wgsl");
|
||||
app.add_plugins(Material2dPlugin::<HudMaterial>::default())
|
||||
.add_systems(
|
||||
PostStartup,
|
||||
setup.after(crate::graphics_menu::PresentationSetup),
|
||||
)
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
advance
|
||||
.after(crate::app::SimulationSet::Physics)
|
||||
.run_if(crate::graphics_menu::gameplay_active),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(reset, render).chain().after(crate::app::FrameSet::Physics),
|
||||
);
|
||||
embedded_asset!(app, "hud_composite.wgsl");
|
||||
app.add_plugins((
|
||||
Material2dPlugin::<HudMaterial>::default(),
|
||||
UiMaterialPlugin::<HudComposite>::default(),
|
||||
))
|
||||
.add_systems(
|
||||
PostStartup,
|
||||
setup.after(crate::graphics_menu::PresentationSetup),
|
||||
)
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
advance
|
||||
.after(crate::app::SimulationSet::Physics)
|
||||
.run_if(crate::graphics_menu::gameplay_active),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(reset, render).chain().after(crate::app::FrameSet::Physics),
|
||||
);
|
||||
}
|
||||
}
|
||||
fn setup(
|
||||
@@ -75,6 +101,7 @@ fn setup(
|
||||
skater: Res<SkaterRuntime>,
|
||||
cameras: Query<Entity, With<IsDefaultUiCamera>>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut composites: ResMut<Assets<HudComposite>>,
|
||||
) {
|
||||
// The startup dependency also applies the presentation system's deferred
|
||||
// camera spawn before this query. Without it, setup silently lost the HUD.
|
||||
@@ -153,7 +180,7 @@ fn setup(
|
||||
Msaa::Off,
|
||||
));
|
||||
commands.spawn((
|
||||
ImageNode::new(target),
|
||||
MaterialNode(composites.add(HudComposite { image: target })),
|
||||
UiTargetCamera(output),
|
||||
GlobalZIndex(1),
|
||||
Pickable::IGNORE,
|
||||
|
||||
@@ -113,8 +113,8 @@ impl Runtime {
|
||||
hud_runtime::Input {
|
||||
sequence_score: self.sequence_score as i32,
|
||||
line_score: self.session.holder.snapshot.line as i32,
|
||||
sequence_timer: self.session.line.points as i32,
|
||||
line_time: self.session.line.points,
|
||||
sequence_timer: (self.session.line.points / self.data.line_drain) as i32,
|
||||
line_time: self.session.line.points / self.data.line_drain,
|
||||
line_capacity: self.data.line_capacity,
|
||||
multiplier: self.session.combo.multiplier,
|
||||
clean: self.clean,
|
||||
@@ -561,7 +561,9 @@ impl Runtime {
|
||||
self.session
|
||||
.publish_sequence(&self.data.session_rules(), 1., bailout, true);
|
||||
self.sequence_active = false;
|
||||
self.close_tricks = true;
|
||||
// 82775328 -> 82774E88 closes only for ScoreModule reset/bail
|
||||
// output 14630 (82DA4010/82DA4238), not a banked landing.
|
||||
self.close_tricks = bailout;
|
||||
} else if self.sequence_active {
|
||||
let s = &self.session.holder.snapshot;
|
||||
self.sequence_score = (s.accumulated
|
||||
@@ -590,6 +592,9 @@ impl Runtime {
|
||||
+ self.air_metrics.iter().sum::<f32>())
|
||||
* self.session.combo.multiplier;
|
||||
}
|
||||
if self.session.line.expired || f.teleported || bailout {
|
||||
self.sequence_score = 0.;
|
||||
}
|
||||
self.session.settle_line(f.teleported || bailout, active);
|
||||
self.previous = f.position;
|
||||
Ok(())
|
||||
|
||||
@@ -87,7 +87,7 @@ Validation on 2026-09-08:
|
||||
- The data-only scoring-flow example credits the authored 100-point stationary
|
||||
kickflip once and verifies idle stability and teleport cancellation.
|
||||
- The original HUD data-only audit passes 1,800 frames, with 172 VM slots,
|
||||
116 display instances, at most 31 draw batches and finite geometry.
|
||||
116 display instances, at most 33 draw batches and finite geometry.
|
||||
- The release target builds successfully with static MSVC CRT, without default
|
||||
features. Compiler warnings remain in the existing game and audit-only code.
|
||||
- No game, recomp, controller harness, gameplay automation, `--check-assets`
|
||||
@@ -98,7 +98,7 @@ The copied build and launcher are in ignored `logs/scoring/build`. The launcher
|
||||
uses this worktree's HUD cache and the existing owned asset installation and
|
||||
starts paused. It checks required paths before running and retains failures on
|
||||
screen. The executable SHA256 is
|
||||
`deb1045925e1dd0e7143825d0320172eb2c26cf77e1c0f336d3475887557435d`.
|
||||
`9b3458045e7078dcd6daca3e5bea89d16298315b751b4cac4f82f8a6531cd7c0`.
|
||||
|
||||
The missing-HUD startup defect is fixed: HUD setup now depends on presentation
|
||||
setup, so Bevy applies the deferred camera spawn before the HUD queries it.
|
||||
@@ -107,6 +107,44 @@ caused HUD initialization to return permanently. The rebuilt launcher also
|
||||
saves runtime output to `logs/scoring/build/scoring-test.log`. This fix is
|
||||
release-compiled; interactive rendering has not been launched for verification.
|
||||
|
||||
## Landing lifetime and native font passes
|
||||
|
||||
The landing hide bug came from sending `CloseTrickDisplay` at every score
|
||||
publication. Native `82775328` calls `82774E88` only on scoring output byte
|
||||
14630; `82DA4238` copies that from module byte128, selected by `82DA4010`
|
||||
for its reset/bail category. `82774E88` sets backend byte184, which
|
||||
`825E4F40` turns into event3, resolved by `825D3B38` to
|
||||
`_global.TrickDisplay.CloseTrickDisplay`. Normal banking no longer emits it.
|
||||
Line expiry clears the score and lets the authored Clear/outro actions run.
|
||||
|
||||
The timer unit is now traced end-to-end: `82DA4238` writes line points divided
|
||||
by drain rate to output+40; `82775328` copies it to backend+32;
|
||||
`825C2910` and `825C2D90` truncate that value to seconds for ActionScript.
|
||||
The previous raw-point binding was wrong. Changes in that seconds field now
|
||||
also refresh `UpdateTrickScoring`.
|
||||
|
||||
Observed font code: `825D6B68` compares the APT name with `Futura Shadow`
|
||||
(string at `8220BD44`) and attaches `futuraheavy` (`8220BD54`) as a secondary
|
||||
font, setting X adjustment 1 and the other adjustment 0. `82CA1FD8` first
|
||||
sets the primary RGB multiplier to black while retaining alpha, draws it,
|
||||
then restores the color and draws the foreground with the X adjustment.
|
||||
The scene renderer now follows those two passes. FuturaOuterGlow remains
|
||||
its separately authored tinted atlas pass. No outline radius, new glow
|
||||
color or replacement glyphs were invented.
|
||||
|
||||
The offscreen adapter also needed premultiplied-alpha compositing. The
|
||||
mesh target already stores covered RGB; Bevy's ordinary ImageNode uses
|
||||
straight-alpha blending, which multiplied coverage again and weakened
|
||||
soft atlas edges. The dedicated HUD composite now uses premultiplied
|
||||
blending to preserve the original layers' coverage.
|
||||
|
||||
Validation: supplied scoring data exercises landing, retained HUD visibility,
|
||||
seconds conversion, natural line expiry and teleport cancellation. The
|
||||
1,800-frame original movie audit checks a black shadow/foreground pair and
|
||||
finite geometry (33 maximum batches). Release compilation passes. User
|
||||
screenshots guided the investigation but did not supply rendering constants;
|
||||
no game, reference executable or GPU capture was launched for validation.
|
||||
|
||||
## Native parity gaps
|
||||
|
||||
These are implementation gaps, not merely missing gameplay validation:
|
||||
|
||||
Reference in new issue
Block a user