Move big fish runtime to frontend
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-27 21:12:43 +08:00
parent dc619817a1
commit 2792df03a6
42 changed files with 1058 additions and 1895 deletions

View File

@@ -9,17 +9,12 @@ pub const BIG_FISH_SESSION_ID_PREFIX: &str = "big-fish-session-";
pub const BIG_FISH_MESSAGE_ID_PREFIX: &str = "big-fish-message-";
pub const BIG_FISH_OPERATION_ID_PREFIX: &str = "big-fish-operation-";
pub const BIG_FISH_ASSET_SLOT_ID_PREFIX: &str = "big-fish-asset-";
pub const BIG_FISH_RUN_ID_PREFIX: &str = "big-fish-run-";
pub const BIG_FISH_DEFAULT_LEVEL_COUNT: u32 = 8;
pub const BIG_FISH_MIN_LEVEL_COUNT: u32 = 6;
pub const BIG_FISH_MAX_LEVEL_COUNT: u32 = 12;
pub const BIG_FISH_MERGE_COUNT_PER_UPGRADE: u32 = 3;
pub const BIG_FISH_OFFSCREEN_CULL_SECONDS: f32 = 3.0;
pub const BIG_FISH_TARGET_WILD_COUNT: usize = 12;
pub const BIG_FISH_VIEW_WIDTH: f32 = 720.0;
pub const BIG_FISH_VIEW_HEIGHT: f32 = 1280.0;
pub const BIG_FISH_WORLD_HALF_WIDTH: f32 = 900.0;
pub const BIG_FISH_WORLD_HALF_HEIGHT: f32 = 1600.0;
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -72,14 +67,6 @@ pub enum BigFishAssetStatus {
Ready,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum BigFishRunStatus {
Running,
Won,
Failed,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BigFishAnchorItem {
@@ -209,41 +196,6 @@ pub struct BigFishSessionSnapshot {
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BigFishVector2 {
pub x: f32,
pub y: f32,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BigFishRuntimeEntity {
pub entity_id: String,
pub level: u32,
pub position: BigFishVector2,
pub radius: f32,
pub offscreen_seconds: f32,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BigFishRuntimeSnapshot {
pub run_id: String,
pub session_id: String,
pub status: BigFishRunStatus,
pub tick: u64,
pub player_level: u32,
pub win_level: u32,
pub leader_entity_id: Option<String>,
pub owned_entities: Vec<BigFishRuntimeEntity>,
pub wild_entities: Vec<BigFishRuntimeEntity>,
pub camera_center: BigFishVector2,
pub last_input: BigFishVector2,
pub event_log: Vec<String>,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BigFishSessionProcedureResult {
@@ -293,14 +245,6 @@ pub struct BigFishWorksProcedureResult {
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BigFishRunProcedureResult {
pub ok: bool,
pub run: Option<BigFishRuntimeSnapshot>,
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BigFishSessionCreateInput {
@@ -372,43 +316,15 @@ pub struct BigFishPublishInput {
pub published_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BigFishRunStartInput {
pub run_id: String,
pub session_id: String,
pub owner_user_id: String,
pub started_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BigFishRunInputSubmitInput {
pub run_id: String,
pub owner_user_id: String,
pub input_x: f32,
pub input_y: f32,
pub submitted_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BigFishRunGetInput {
pub run_id: String,
pub owner_user_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BigFishFieldError {
MissingSessionId,
MissingOwnerUserId,
MissingMessageId,
MissingMessageText,
MissingRunId,
MissingDraft,
InvalidLevel,
InvalidAssetKind,
InvalidRunState,
}
impl BigFishCreationStage {
@@ -474,16 +390,6 @@ impl BigFishAssetStatus {
}
}
impl BigFishRunStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Running => "running",
Self::Won => "won",
Self::Failed => "failed",
}
}
}
pub fn empty_anchor_pack() -> BigFishAnchorPack {
BigFishAnchorPack {
gameplay_promise: BigFishAnchorItem {
@@ -565,12 +471,14 @@ pub fn compile_default_draft(anchor_pack: &BigFishAnchorPack) -> BigFishGameDraf
background: BigFishBackgroundBlueprint {
theme: theme.clone(),
color_mood: "深蓝、青绿、带少量暖色生物光".to_string(),
foreground_hints: "轻微漂浮颗粒和边缘水草,不遮挡中央操作区".to_string(),
midground_composition: "中央留出清晰活动区域,边缘出生缓冲层".to_string(),
background_depth: "纵深水域与远处体型剪影".to_string(),
safe_play_area_hint: "9:16 竖屏中央 70% 为主要活动区".to_string(),
spawn_edge_hint: "四周边缘作为野生实体出生区".to_string(),
background_prompt_seed: format!("{theme},竖屏 9:16全屏游戏背景无文字无 UI 框"),
foreground_hints: "只保留少量漂浮颗粒和边缘水草,不遮挡中央操作区".to_string(),
midground_composition: "中央留出大面积清晰活动区域,边缘只做出生缓冲层".to_string(),
background_depth: "简洁纵深水域与极少量远处剪影".to_string(),
safe_play_area_hint: "9:16 竖屏中央 80% 为主要活动区".to_string(),
spawn_edge_hint: "四周边缘以少量暗礁或水草提示野生实体出生区".to_string(),
background_prompt_seed: format!(
"{theme},竖屏 9:16全屏大场地游戏背景元素少中央开阔无文字无 UI 框"
),
},
runtime_params: BigFishRuntimeParams {
level_count,
@@ -673,77 +581,6 @@ pub fn build_generated_asset_slot(
})
}
pub fn build_initial_runtime_snapshot(
run_id: String,
session_id: String,
draft: &BigFishGameDraft,
now_micros: i64,
) -> BigFishRuntimeSnapshot {
let mut snapshot = BigFishRuntimeSnapshot {
run_id,
session_id,
status: BigFishRunStatus::Running,
tick: 0,
player_level: 1,
win_level: draft.runtime_params.win_level,
leader_entity_id: Some("owned-1".to_string()),
owned_entities: vec![BigFishRuntimeEntity {
entity_id: "owned-1".to_string(),
level: 1,
position: BigFishVector2 { x: 0.0, y: 0.0 },
radius: entity_radius(1),
offscreen_seconds: 0.0,
}],
wild_entities: vec![
BigFishRuntimeEntity {
entity_id: "wild-open-1".to_string(),
level: 1,
position: BigFishVector2 { x: 72.0, y: 0.0 },
radius: entity_radius(1),
offscreen_seconds: 0.0,
},
BigFishRuntimeEntity {
entity_id: "wild-open-2".to_string(),
level: 1,
position: BigFishVector2 { x: -88.0, y: 30.0 },
radius: entity_radius(1),
offscreen_seconds: 0.0,
},
],
camera_center: BigFishVector2 { x: 0.0, y: 0.0 },
last_input: BigFishVector2 { x: 0.0, y: 0.0 },
event_log: vec!["开局生成 2 个同级可收编目标".to_string()],
updated_at_micros: now_micros,
};
maintain_wild_pool(&mut snapshot, &draft.runtime_params);
snapshot
}
pub fn advance_runtime_snapshot(
mut snapshot: BigFishRuntimeSnapshot,
params: &BigFishRuntimeParams,
input_x: f32,
input_y: f32,
now_micros: i64,
) -> BigFishRuntimeSnapshot {
if snapshot.status != BigFishRunStatus::Running {
return snapshot;
}
let step_seconds = resolve_step_seconds(&snapshot, now_micros);
snapshot.tick = snapshot.tick.saturating_add(1);
snapshot.last_input = normalize_input(input_x, input_y);
move_owned_entities(&mut snapshot, params, step_seconds);
resolve_collisions(&mut snapshot, params);
apply_chain_merges(&mut snapshot, params);
refresh_player_leader(&mut snapshot);
apply_win_or_fail(&mut snapshot, params);
update_wild_culling(&mut snapshot, params, step_seconds);
maintain_wild_pool(&mut snapshot, params);
snapshot.updated_at_micros = now_micros;
snapshot
}
pub fn validate_session_get_input(input: &BigFishSessionGetInput) -> Result<(), BigFishFieldError> {
validate_session_owner(&input.session_id, &input.owner_user_id)
}
@@ -817,36 +654,6 @@ pub fn validate_publish_input(input: &BigFishPublishInput) -> Result<(), BigFish
validate_session_owner(&input.session_id, &input.owner_user_id)
}
pub fn validate_run_start_input(input: &BigFishRunStartInput) -> Result<(), BigFishFieldError> {
validate_session_owner(&input.session_id, &input.owner_user_id)?;
if normalize_required_string(&input.run_id).is_none() {
return Err(BigFishFieldError::MissingRunId);
}
Ok(())
}
pub fn validate_run_get_input(input: &BigFishRunGetInput) -> Result<(), BigFishFieldError> {
if normalize_required_string(&input.run_id).is_none() {
return Err(BigFishFieldError::MissingRunId);
}
if normalize_required_string(&input.owner_user_id).is_none() {
return Err(BigFishFieldError::MissingOwnerUserId);
}
Ok(())
}
pub fn validate_run_input_submit_input(
input: &BigFishRunInputSubmitInput,
) -> Result<(), BigFishFieldError> {
if normalize_required_string(&input.run_id).is_none() {
return Err(BigFishFieldError::MissingRunId);
}
if normalize_required_string(&input.owner_user_id).is_none() {
return Err(BigFishFieldError::MissingOwnerUserId);
}
Ok(())
}
pub fn serialize_anchor_pack(anchor_pack: &BigFishAnchorPack) -> Result<String, serde_json::Error> {
serde_json::to_string(anchor_pack)
}
@@ -873,18 +680,6 @@ pub fn deserialize_asset_coverage(value: &str) -> Result<BigFishAssetCoverage, s
serde_json::from_str(value)
}
pub fn serialize_runtime_snapshot(
snapshot: &BigFishRuntimeSnapshot,
) -> Result<String, serde_json::Error> {
serde_json::to_string(snapshot)
}
pub fn deserialize_runtime_snapshot(
value: &str,
) -> Result<BigFishRuntimeSnapshot, serde_json::Error> {
serde_json::from_str(value)
}
fn fallback_anchor_value(anchor: &BigFishAnchorItem, fallback: &str) -> String {
normalize_required_string(&anchor.value).unwrap_or_else(|| fallback.to_string())
}
@@ -911,8 +706,12 @@ fn build_level_blueprint(level: u32, level_count: u32, theme: &str) -> BigFishLe
1.0 + level as f32 * 0.22
),
size_ratio: 1.0 + (level.saturating_sub(1) as f32 * 0.22),
visual_prompt_seed: format!("{theme} 第 {level} 级实体主图,透明背景,清晰轮廓"),
motion_prompt_seed: format!("{theme} 第 {level} 级实体 idle_float 与 move_swim 动作"),
visual_prompt_seed: format!(
"{theme} 第 {level} 级鱼形实体主图RPG 角色资产口径,透明背景,单体完整入镜,清晰轮廓"
),
motion_prompt_seed: format!(
"{theme} 第 {level} 级鱼形实体 idle_float 与 move_swim 动作RPG 角色动画资产口径,透明背景"
),
merge_source_level: if level == 1 { None } else { Some(level - 1) },
prey_window,
threat_window,
@@ -945,7 +744,7 @@ fn build_asset_prompt_snapshot(
.ok_or(BigFishFieldError::InvalidLevel)?;
let motion_key = motion_key.ok_or(BigFishFieldError::InvalidAssetKind)?;
Ok(format!(
"{},动作位:{}",
"{},动作位:{},透明背景,单体完整入镜",
blueprint.motion_prompt_seed, motion_key
))
}
@@ -1004,311 +803,6 @@ fn validate_level(level: Option<u32>, draft: &BigFishGameDraft) -> Result<(), Bi
}
}
fn normalize_input(x: f32, y: f32) -> BigFishVector2 {
let length = (x * x + y * y).sqrt();
if length <= 1.0 {
return BigFishVector2 { x, y };
}
BigFishVector2 {
x: x / length,
y: y / length,
}
}
/// 运行态仍由 `POST input` 触发推进,因此“屏外 3 秒”这类规则必须按真实秒数累计,
/// 否则会随着输入频率变化而漂移。
fn resolve_step_seconds(snapshot: &BigFishRuntimeSnapshot, now_micros: i64) -> f32 {
((now_micros - snapshot.updated_at_micros).max(0) as f32) / 1_000_000.0
}
fn move_owned_entities(
snapshot: &mut BigFishRuntimeSnapshot,
params: &BigFishRuntimeParams,
step_seconds: f32,
) {
let input = snapshot.last_input.clone();
if let Some(leader) = snapshot.owned_entities.first_mut() {
leader.position.x = clamp_world(
leader.position.x + input.x * params.leader_move_speed * step_seconds,
true,
);
leader.position.y = clamp_world(
leader.position.y + input.y * params.leader_move_speed * step_seconds,
false,
);
snapshot.camera_center = leader.position.clone();
}
let leader_position = snapshot.camera_center.clone();
for (index, follower) in snapshot.owned_entities.iter_mut().enumerate().skip(1) {
let slot_offset = ((index as f32) * 0.7).sin() * 36.0;
let target = BigFishVector2 {
x: leader_position.x - 42.0 - index as f32 * 8.0,
y: leader_position.y + slot_offset,
};
let delta_x = target.x - follower.position.x;
let delta_y = target.y - follower.position.y;
let distance = (delta_x * delta_x + delta_y * delta_y).sqrt();
if distance <= f32::EPSILON {
continue;
}
let catch_up_ratio =
(params.follower_catch_up_speed * step_seconds / distance).clamp(0.0, 1.0);
follower.position.x += delta_x * catch_up_ratio;
follower.position.y += delta_y * catch_up_ratio;
}
}
fn resolve_collisions(snapshot: &mut BigFishRuntimeSnapshot, _params: &BigFishRuntimeParams) {
let mut owned_to_remove = Vec::new();
let mut wild_to_remove = Vec::new();
let mut newly_owned = Vec::new();
for (owned_index, owned) in snapshot.owned_entities.iter().enumerate() {
for (wild_index, wild) in snapshot.wild_entities.iter().enumerate() {
if wild_to_remove.contains(&wild_index) || owned_to_remove.contains(&owned_index) {
continue;
}
if distance(&owned.position, &wild.position) > owned.radius + wild.radius {
continue;
}
if owned.level >= wild.level {
wild_to_remove.push(wild_index);
newly_owned.push(BigFishRuntimeEntity {
entity_id: format!("owned-from-{}-{}", wild.entity_id, snapshot.tick),
level: wild.level,
position: wild.position.clone(),
radius: entity_radius(wild.level),
offscreen_seconds: 0.0,
});
snapshot
.event_log
.push(format!("收编 {} 级实体", wild.level));
} else {
owned_to_remove.push(owned_index);
snapshot.event_log.push(format!(
"{} 级己方实体被 {} 级野生实体吃掉",
owned.level, wild.level
));
}
}
}
remove_indices(&mut snapshot.wild_entities, &wild_to_remove);
remove_indices(&mut snapshot.owned_entities, &owned_to_remove);
snapshot.owned_entities.extend(newly_owned);
}
fn apply_chain_merges(snapshot: &mut BigFishRuntimeSnapshot, params: &BigFishRuntimeParams) {
loop {
let mut merged = false;
for level in 1..params.win_level {
let indices = snapshot
.owned_entities
.iter()
.enumerate()
.filter_map(|(index, entity)| (entity.level == level).then_some(index))
.take(params.merge_count_per_upgrade as usize)
.collect::<Vec<_>>();
if indices.len() < params.merge_count_per_upgrade as usize {
continue;
}
let center = average_position(&indices, &snapshot.owned_entities);
remove_indices(&mut snapshot.owned_entities, &indices);
snapshot.owned_entities.push(BigFishRuntimeEntity {
entity_id: format!("owned-merge-{}-{}", level + 1, snapshot.tick),
level: level + 1,
position: center,
radius: entity_radius(level + 1),
offscreen_seconds: 0.0,
});
snapshot
.event_log
.push(format!("3 个 {} 级实体合成 {}", level, level + 1));
merged = true;
break;
}
if !merged {
break;
}
}
}
fn refresh_player_leader(snapshot: &mut BigFishRuntimeSnapshot) {
snapshot.owned_entities.sort_by(|left, right| {
right
.level
.cmp(&left.level)
.then_with(|| {
distance(&left.position, &snapshot.camera_center)
.partial_cmp(&distance(&right.position, &snapshot.camera_center))
.unwrap_or(std::cmp::Ordering::Equal)
})
.then_with(|| left.entity_id.cmp(&right.entity_id))
});
snapshot.leader_entity_id = snapshot
.owned_entities
.first()
.map(|entity| entity.entity_id.clone());
snapshot.player_level = snapshot
.owned_entities
.iter()
.map(|entity| entity.level)
.max()
.unwrap_or(0);
if let Some(leader) = snapshot.owned_entities.first() {
snapshot.camera_center = leader.position.clone();
}
}
fn apply_win_or_fail(snapshot: &mut BigFishRuntimeSnapshot, params: &BigFishRuntimeParams) {
if snapshot.owned_entities.is_empty() {
snapshot.status = BigFishRunStatus::Failed;
snapshot
.event_log
.push("己方实体归零,本局失败".to_string());
return;
}
if snapshot.player_level >= params.win_level {
snapshot.status = BigFishRunStatus::Won;
snapshot
.event_log
.push("获得最高等级实体,通关".to_string());
}
}
fn update_wild_culling(
snapshot: &mut BigFishRuntimeSnapshot,
params: &BigFishRuntimeParams,
step_seconds: f32,
) {
let player_level = snapshot.player_level;
for wild in &mut snapshot.wild_entities {
let should_cull_level = wild.level == player_level
|| wild.level >= player_level.saturating_add(3)
|| wild.level.saturating_add(3) <= player_level;
if !should_cull_level {
wild.offscreen_seconds = 0.0;
continue;
}
if is_offscreen(&wild.position, &snapshot.camera_center, wild.radius) {
wild.offscreen_seconds += step_seconds;
} else {
wild.offscreen_seconds = 0.0;
}
}
snapshot
.wild_entities
.retain(|wild| wild.offscreen_seconds < params.offscreen_cull_seconds);
}
fn maintain_wild_pool(snapshot: &mut BigFishRuntimeSnapshot, params: &BigFishRuntimeParams) {
if snapshot.status != BigFishRunStatus::Running {
return;
}
let mut next_index = snapshot.wild_entities.len() + snapshot.tick as usize;
while snapshot.wild_entities.len() < params.spawn_target_count as usize {
let level = next_spawn_level(snapshot.player_level.max(1), params.win_level, next_index);
snapshot.wild_entities.push(BigFishRuntimeEntity {
entity_id: format!("wild-{}-{}", snapshot.tick, next_index),
level,
position: spawn_position(&snapshot.camera_center, next_index),
radius: entity_radius(level),
offscreen_seconds: 0.0,
});
next_index += 1;
}
}
fn next_spawn_level(player_level: u32, win_level: u32, index: usize) -> u32 {
if player_level == 1 && index % 4 < 2 {
return 1;
}
let deltas = [-2_i32, -1, 1, 2];
let delta = deltas[index % deltas.len()];
(player_level as i32 + delta).clamp(1, win_level as i32) as u32
}
fn spawn_position(center: &BigFishVector2, index: usize) -> BigFishVector2 {
let side = index % 4;
let offset = ((index as f32 * 37.0) % 420.0) - 210.0;
match side {
0 => BigFishVector2 {
x: center.x - BIG_FISH_VIEW_WIDTH * 0.62,
y: center.y + offset,
},
1 => BigFishVector2 {
x: center.x + BIG_FISH_VIEW_WIDTH * 0.62,
y: center.y + offset,
},
2 => BigFishVector2 {
x: center.x + offset,
y: center.y - BIG_FISH_VIEW_HEIGHT * 0.58,
},
_ => BigFishVector2 {
x: center.x + offset,
y: center.y + BIG_FISH_VIEW_HEIGHT * 0.58,
},
}
}
fn remove_indices<T>(items: &mut Vec<T>, indices: &[usize]) {
let mut sorted = indices.to_vec();
sorted.sort_unstable();
sorted.dedup();
for index in sorted.into_iter().rev() {
if index < items.len() {
items.remove(index);
}
}
}
fn average_position(indices: &[usize], entities: &[BigFishRuntimeEntity]) -> BigFishVector2 {
let mut x = 0.0;
let mut y = 0.0;
for index in indices {
x += entities[*index].position.x;
y += entities[*index].position.y;
}
let count = indices.len().max(1) as f32;
BigFishVector2 {
x: x / count,
y: y / count,
}
}
fn distance(left: &BigFishVector2, right: &BigFishVector2) -> f32 {
let dx = left.x - right.x;
let dy = left.y - right.y;
(dx * dx + dy * dy).sqrt()
}
fn is_offscreen(position: &BigFishVector2, camera: &BigFishVector2, radius: f32) -> bool {
let half_w = BIG_FISH_VIEW_WIDTH / 2.0;
let half_h = BIG_FISH_VIEW_HEIGHT / 2.0;
position.x + radius < camera.x - half_w
|| position.x - radius > camera.x + half_w
|| position.y + radius < camera.y - half_h
|| position.y - radius > camera.y + half_h
}
fn clamp_world(value: f32, horizontal: bool) -> f32 {
let limit = if horizontal {
BIG_FISH_WORLD_HALF_WIDTH
} else {
BIG_FISH_WORLD_HALF_HEIGHT
};
value.clamp(-limit, limit)
}
fn entity_radius(level: u32) -> f32 {
18.0 + level as f32 * 4.0
}
impl fmt::Display for BigFishFieldError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
@@ -1316,11 +810,9 @@ impl fmt::Display for BigFishFieldError {
Self::MissingOwnerUserId => f.write_str("big_fish.owner_user_id 不能为空"),
Self::MissingMessageId => f.write_str("big_fish.message_id 不能为空"),
Self::MissingMessageText => f.write_str("big_fish.message_text 不能为空"),
Self::MissingRunId => f.write_str("big_fish.run_id 不能为空"),
Self::MissingDraft => f.write_str("big_fish.draft 尚未编译"),
Self::InvalidLevel => f.write_str("big_fish.level 不在合法等级范围内"),
Self::InvalidAssetKind => f.write_str("big_fish.asset_kind 或动作位非法"),
Self::InvalidRunState => f.write_str("big_fish.run 当前状态不允许推进"),
}
}
}
@@ -1370,123 +862,4 @@ mod tests {
assert!(coverage.blockers.iter().any(|item| item.contains("背景图")));
}
#[test]
fn same_level_wild_entity_can_be_collected_at_start() {
let draft = compile_default_draft(&infer_anchor_pack("深海", None));
let mut snapshot =
build_initial_runtime_snapshot("run-1".to_string(), "session-1".to_string(), &draft, 1);
snapshot.wild_entities[0].position = BigFishVector2 { x: 1.0, y: 0.0 };
let next = advance_runtime_snapshot(snapshot, &draft.runtime_params, 0.0, 0.0, 2);
assert!(next.owned_entities.len() >= 2);
assert!(
next.event_log
.iter()
.any(|event| event.contains("收编 1 级实体"))
);
}
#[test]
fn three_owned_entities_merge_into_next_level() {
let draft = compile_default_draft(&infer_anchor_pack("深海", None));
let mut snapshot = build_initial_runtime_snapshot(
"run-merge".to_string(),
"session-merge".to_string(),
&draft,
1,
);
snapshot.wild_entities.clear();
snapshot.owned_entities.push(BigFishRuntimeEntity {
entity_id: "owned-2".to_string(),
level: 1,
position: BigFishVector2 { x: 4.0, y: 0.0 },
radius: entity_radius(1),
offscreen_seconds: 0.0,
});
snapshot.owned_entities.push(BigFishRuntimeEntity {
entity_id: "owned-3".to_string(),
level: 1,
position: BigFishVector2 { x: 8.0, y: 0.0 },
radius: entity_radius(1),
offscreen_seconds: 0.0,
});
let next = advance_runtime_snapshot(snapshot, &draft.runtime_params, 0.0, 0.0, 2);
assert!(next.owned_entities.iter().any(|entity| entity.level == 2));
}
#[test]
fn final_level_immediately_wins() {
let draft = compile_default_draft(&infer_anchor_pack("深海", None));
let mut snapshot = build_initial_runtime_snapshot(
"run-win".to_string(),
"session-win".to_string(),
&draft,
1,
);
snapshot.owned_entities[0].level = draft.runtime_params.win_level;
let next = advance_runtime_snapshot(snapshot, &draft.runtime_params, 0.0, 0.0, 2);
assert_eq!(next.status, BigFishRunStatus::Won);
}
#[test]
fn offscreen_same_level_wild_entity_is_removed_after_three_seconds() {
let draft = compile_default_draft(&infer_anchor_pack("深海", None));
let mut snapshot = build_initial_runtime_snapshot(
"run-cull".to_string(),
"session-cull".to_string(),
&draft,
1,
);
snapshot.wild_entities.clear();
snapshot.wild_entities.push(BigFishRuntimeEntity {
entity_id: "wild-cull".to_string(),
level: 1,
position: BigFishVector2 { x: 1000.0, y: 0.0 },
radius: entity_radius(1),
offscreen_seconds: 2.8,
});
snapshot.updated_at_micros = 1_000_000;
let next = advance_runtime_snapshot(snapshot, &draft.runtime_params, 0.0, 0.0, 1_250_000);
assert!(
!next
.wild_entities
.iter()
.any(|entity| entity.entity_id == "wild-cull")
);
}
#[test]
fn offscreen_same_level_wild_entity_is_kept_before_three_seconds_elapsed() {
let draft = compile_default_draft(&infer_anchor_pack("深海", None));
let mut snapshot = build_initial_runtime_snapshot(
"run-cull-safe".to_string(),
"session-cull-safe".to_string(),
&draft,
1,
);
snapshot.wild_entities.clear();
snapshot.wild_entities.push(BigFishRuntimeEntity {
entity_id: "wild-cull-safe".to_string(),
level: 1,
position: BigFishVector2 { x: 1000.0, y: 0.0 },
radius: entity_radius(1),
offscreen_seconds: 2.7,
});
snapshot.updated_at_micros = 1_000_000;
let next = advance_runtime_snapshot(snapshot, &draft.runtime_params, 0.0, 0.0, 1_200_000);
assert!(
next.wild_entities
.iter()
.any(|entity| entity.entity_id == "wild-cull-safe")
);
}
}