shoo the puppets to the ground (ugly ahh graphics lmao)

This commit is contained in:
2026-08-30 01:07:32 +08:00
parent fbb7388028
commit 64e5c363f2
9 changed files with 259 additions and 9 deletions
+1
View File
@@ -20,6 +20,7 @@ use tauri::Manager;
async fn launch_app(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let handle = app.handle();
db::init(handle).await?;
scene_configuration::init(handle).await?;
keypair::init(handle).await?;
app.manage(interactions::InteractionState::default());
network::init(handle).await?;
+76 -3
View File
@@ -11,10 +11,12 @@ use tauri_specta::Event;
use tokio::time::MissedTickBehavior;
use crate::cursor::{CursorPosition, CursorPositions, CursorState};
use crate::scene_configuration::{PuppetMovementMode, SceneConfigurationState};
const TICK_INTERVAL: Duration = Duration::from_millis(125);
const SPEED_LOGICAL_PIXELS_PER_SECOND: f64 = 80.0;
const FOLLOW_RADIUS_LOGICAL_PIXELS: f64 = 48.0;
const BOTTOM_MOVEMENT_Y: f64 = 1.0;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)]
#[serde(rename_all = "camelCase")]
@@ -39,9 +41,16 @@ impl PuppetStateStore {
cursor_positions: &HashMap<String, CursorPositions>,
viewport: Viewport,
elapsed: Duration,
movement_mode: PuppetMovementMode,
) -> Result<Option<Vec<PuppetState>>, String> {
let mut puppets = self.0.write().map_err(|error| error.to_string())?;
let changed = advance_puppets(&mut puppets, cursor_positions, viewport, elapsed);
let changed = advance_puppets(
&mut puppets,
cursor_positions,
viewport,
elapsed,
movement_mode,
);
Ok(changed.then(|| sorted_snapshot(&puppets)))
}
@@ -95,10 +104,21 @@ pub fn init(handle: &AppHandle) -> Result<(), String> {
continue;
}
};
let movement_mode = match handle
.state::<SceneConfigurationState>()
.puppet_movement_mode()
{
Ok(mode) => mode,
Err(error) => {
eprintln!("failed to read Puppet movement mode: {error}");
continue;
}
};
let puppets = match handle.state::<PuppetStateStore>().update(
&cursor_positions,
viewport,
elapsed,
movement_mode,
) {
Ok(Some(puppets)) => puppets,
Ok(None) => continue,
@@ -128,18 +148,20 @@ fn advance_puppets(
cursor_positions: &HashMap<String, CursorPositions>,
viewport: Viewport,
elapsed: Duration,
movement_mode: PuppetMovementMode,
) -> bool {
let previous_len = puppets.len();
puppets.retain(|id, _| cursor_positions.contains_key(id));
let mut changed = puppets.len() != previous_len;
for (id, cursor) in cursor_positions {
let target = movement_target(&cursor.mapped, movement_mode);
let Some(puppet) = puppets.get_mut(id) else {
puppets.insert(
id.clone(),
PuppetState {
id: id.clone(),
position: cursor.mapped.clone(),
position: target,
is_moving: false,
},
);
@@ -147,12 +169,26 @@ fn advance_puppets(
continue;
};
changed |= advance_puppet(puppet, &cursor.mapped, viewport, elapsed.as_secs_f64());
if movement_mode == PuppetMovementMode::Bottom && puppet.position.y != BOTTOM_MOVEMENT_Y {
puppet.position.y = BOTTOM_MOVEMENT_Y;
changed = true;
}
changed |= advance_puppet(puppet, &target, viewport, elapsed.as_secs_f64());
}
changed
}
fn movement_target(cursor: &CursorPosition, mode: PuppetMovementMode) -> CursorPosition {
CursorPosition {
x: cursor.x,
y: match mode {
PuppetMovementMode::Free => cursor.y,
PuppetMovementMode::Bottom => BOTTOM_MOVEMENT_Y,
},
}
}
fn advance_puppet(
puppet: &mut PuppetState,
target: &CursorPosition,
@@ -220,6 +256,7 @@ mod tests {
&cursors(&[("friend", 0.75, 0.25)]),
viewport(),
TICK_INTERVAL,
PuppetMovementMode::Free,
);
assert!(changed);
@@ -296,10 +333,46 @@ mod tests {
&HashMap::new(),
viewport(),
TICK_INTERVAL,
PuppetMovementMode::Free,
));
assert!(puppets.is_empty());
}
#[test]
fn bottom_mode_preserves_horizontal_target_and_insets_vertical_target() {
let cursor = CursorPosition { x: 0.25, y: 0.1 };
assert_eq!(
movement_target(&cursor, PuppetMovementMode::Bottom),
CursorPosition {
x: 0.25,
y: BOTTOM_MOVEMENT_Y,
}
);
assert_eq!(movement_target(&cursor, PuppetMovementMode::Free), cursor);
}
#[test]
fn bottom_mode_emits_a_fixed_inset_y_for_existing_puppets() {
let mut puppets = HashMap::from([(
"friend".to_owned(),
PuppetState {
id: "friend".to_owned(),
position: CursorPosition { x: 0.25, y: 0.1 },
is_moving: false,
},
)]);
assert!(advance_puppets(
&mut puppets,
&cursors(&[("friend", 0.25, 0.1)]),
viewport(),
TICK_INTERVAL,
PuppetMovementMode::Bottom,
));
assert_eq!(puppets["friend"].position.y, BOTTOM_MOVEMENT_Y);
}
#[test]
fn normalized_step_accounts_for_viewport_aspect_ratio() {
let mut horizontal = PuppetState {
+53 -3
View File
@@ -1,7 +1,8 @@
use crate::db::{self, AppDatabase};
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::{AppHandle, State};
use std::sync::RwLock;
use tauri::{AppHandle, Manager, State};
use tauri_specta::Event;
const SCENE_CONFIGURATION_ID: i64 = 1;
@@ -10,11 +11,41 @@ const MAX_PUPPET_SCALE: f64 = 2.0;
const MIN_PUPPET_OPACITY: f64 = 0.1;
const MAX_PUPPET_OPACITY: f64 = 1.0;
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, Type, sqlx::Type)]
#[serde(rename_all = "camelCase")]
#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
pub enum PuppetMovementMode {
#[default]
Free,
Bottom,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type, sqlx::FromRow)]
#[serde(rename_all = "camelCase")]
pub struct SceneConfiguration {
pub puppet_scale: f64,
pub puppet_opacity: f64,
pub puppet_movement_mode: PuppetMovementMode,
}
pub struct SceneConfigurationState(RwLock<SceneConfiguration>);
impl SceneConfigurationState {
fn new(configuration: SceneConfiguration) -> Self {
Self(RwLock::new(configuration))
}
fn replace(&self, configuration: SceneConfiguration) -> Result<(), String> {
*self.0.write().map_err(|error| error.to_string())? = configuration;
Ok(())
}
pub(crate) fn puppet_movement_mode(&self) -> Result<PuppetMovementMode, String> {
self.0
.read()
.map(|configuration| configuration.puppet_movement_mode)
.map_err(|error| error.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
@@ -25,7 +56,7 @@ pub struct SceneConfigurationChanged {
async fn get(database: &AppDatabase) -> Result<SceneConfiguration, sqlx::Error> {
sqlx::query_as::<_, SceneConfiguration>(
"SELECT puppet_scale, puppet_opacity FROM scene_configuration WHERE id = ?1",
"SELECT puppet_scale, puppet_opacity, puppet_movement_mode FROM scene_configuration WHERE id = ?1",
)
.bind(SCENE_CONFIGURATION_ID)
.fetch_one(database.pool())
@@ -52,10 +83,11 @@ async fn update(
}
sqlx::query(
"UPDATE scene_configuration SET puppet_scale = ?1, puppet_opacity = ?2 WHERE id = ?3",
"UPDATE scene_configuration SET puppet_scale = ?1, puppet_opacity = ?2, puppet_movement_mode = ?3 WHERE id = ?4",
)
.bind(configuration.puppet_scale)
.bind(configuration.puppet_opacity)
.bind(configuration.puppet_movement_mode)
.bind(SCENE_CONFIGURATION_ID)
.execute(database.pool())
.await
@@ -64,6 +96,12 @@ async fn update(
get(database).await.map_err(db::command_error)
}
pub async fn init(handle: &AppHandle) -> Result<(), sqlx::Error> {
let configuration = get(&handle.state::<AppDatabase>()).await?;
handle.manage(SceneConfigurationState::new(configuration));
Ok(())
}
fn emit_changed(handle: &AppHandle, configuration: SceneConfiguration) -> Result<(), String> {
SceneConfigurationChanged { configuration }
.emit(handle)
@@ -75,8 +113,10 @@ fn emit_changed(handle: &AppHandle, configuration: SceneConfiguration) -> Result
pub async fn get_scene_configuration(
handle: AppHandle,
database: State<'_, AppDatabase>,
state: State<'_, SceneConfigurationState>,
) -> Result<SceneConfiguration, String> {
let configuration = get(&database).await.map_err(db::command_error)?;
state.replace(configuration.clone())?;
emit_changed(&handle, configuration.clone())?;
Ok(configuration)
}
@@ -86,9 +126,11 @@ pub async fn get_scene_configuration(
pub async fn update_scene_configuration(
handle: AppHandle,
database: State<'_, AppDatabase>,
state: State<'_, SceneConfigurationState>,
configuration: SceneConfiguration,
) -> Result<SceneConfiguration, String> {
let configuration = update(&database, configuration).await?;
state.replace(configuration.clone())?;
emit_changed(&handle, configuration.clone())?;
Ok(configuration)
}
@@ -121,6 +163,7 @@ mod tests {
SceneConfiguration {
puppet_scale: 1.0,
puppet_opacity: 1.0,
puppet_movement_mode: PuppetMovementMode::Free,
}
);
}
@@ -134,12 +177,17 @@ mod tests {
SceneConfiguration {
puppet_scale: 1.5,
puppet_opacity: 0.4,
puppet_movement_mode: PuppetMovementMode::Bottom,
},
)
.await
.expect("update scene configuration");
assert_eq!(configuration.puppet_scale, 1.5);
assert_eq!(configuration.puppet_opacity, 0.4);
assert_eq!(
configuration.puppet_movement_mode,
PuppetMovementMode::Bottom
);
assert_eq!(get(&database).await.unwrap(), configuration);
let error = update(
@@ -147,6 +195,7 @@ mod tests {
SceneConfiguration {
puppet_scale: 2.1,
puppet_opacity: 0.4,
puppet_movement_mode: PuppetMovementMode::Bottom,
},
)
.await
@@ -159,6 +208,7 @@ mod tests {
SceneConfiguration {
puppet_scale: 1.5,
puppet_opacity: 0.05,
puppet_movement_mode: PuppetMovementMode::Bottom,
},
)
.await