This commit is contained in:
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user