Files
cultivation-world-simulator/tools/map_creator/main.py
4thfever 95e1f11502 Refactor/history (#25)
add multi process history modification
2026-01-12 23:25:53 +08:00

345 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import csv
import json
import glob
from flask import Flask, render_template, jsonify, request, send_from_directory
app = Flask(__name__)
# --- 配置路径 ---
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))
ASSETS_DIR = os.path.join(BASE_DIR, "assets")
CONFIG_DIR = os.path.join(BASE_DIR, "static", "game_configs")
OUTPUT_DIR = os.path.dirname(__file__)
# 地图尺寸
MAP_WIDTH = 70
MAP_HEIGHT = 50
@app.route('/')
def index():
return render_template('index.html')
@app.route('/tiles/<path:filename>')
def serve_tile_image(filename):
return send_from_directory(os.path.join(ASSETS_DIR, "tiles"), filename)
@app.route('/sects/<path:filename>')
def serve_sect_image(filename):
return send_from_directory(os.path.join(ASSETS_DIR, "sects"), filename)
@app.route('/cities/<path:filename>')
def serve_city_image(filename):
return send_from_directory(os.path.join(ASSETS_DIR, "cities"), filename)
# 显式定义的区域-地形映射表
# Key: 区域ID (int), Value: {"t": tile_name, "type": "tile" | "sect" | "city"}
REGION_TILE_MAP = {
# --- 城市区域 (City Regions) - 使用 ID ---
301: {"t": "city_301", "type": "city"}, # 青云城
302: {"t": "city_302", "type": "city"}, # 沙月城
303: {"t": "city_303", "type": "city"}, # 翠林城
304: {"t": "city_304", "type": "city"}, # 沧澜城
305: {"t": "city_305", "type": "city"}, # 揽月城
# --- 洞府遗迹 (Cultivate Regions) - 使用 sub_type ---
201: {"t": "cave", "type": "tile"}, # 太白金府
202: {"t": "cave", "type": "tile"}, # 青木洞天
203: {"t": "cave", "type": "tile"}, # 玄水秘境
204: {"t": "cave", "type": "tile"}, # 离火洞府
205: {"t": "cave", "type": "tile"}, # 厚土玄宫
206: {"t": "ruin", "type": "tile"}, # 古越遗迹
207: {"t": "ruin", "type": "tile"}, # 沧海遗迹
}
# 普通区域名称映射(用于后备查找)
NORMAL_REGION_NAME_MAP = {
"平原地带": "plain",
"西域流沙": "desert",
"南疆蛮荒": "rainforest",
"极北冰原": "glacier",
"无边碧海": "sea",
"天河奔流": "water",
"青峰山脉": "mountain",
"万丈雪峰": "snow_mountain",
"碧野千里": "grassland",
"青云林海": "forest",
"炎狱火山": "volcano",
"沃土良田": "farm",
"幽冥毒泽": "swamp",
"十万大山": "mountain",
"紫竹幽境": "bamboo",
"凛霜荒原": "tundra",
"碎星戈壁": "gobi",
"蓬莱遗岛": "island",
}
def get_default_tile(region_id, name, type_tag, sect_id=None, sub_type=None):
"""根据区域ID和类型查找默认 Tile
Args:
region_id: 区域ID
name: 区域名称(用于后备查找)
type_tag: 区域类型 (normal/sect/city/cultivate)
sect_id: 宗门ID仅 sect 类型)
sub_type: 子类型(仅 cultivate 类型cave/ruin
"""
# 1. 优先使用 ID 查表
if region_id in REGION_TILE_MAP:
return REGION_TILE_MAP[region_id]
# 2. 宗门:使用 sect_id 生成 tile 名称
if type_tag == 'sect' and sect_id is not None:
return {"t": f"sect_{sect_id}", "type": "sect"}
# 3. 城市:使用 region_id 生成 tile 名称
if type_tag == 'city':
return {"t": f"city_{region_id}", "type": "city"}
# 4. 修炼区域:使用 sub_type
if type_tag == 'cultivate':
if sub_type in ['cave', 'ruin']:
return {"t": sub_type, "type": "tile"}
# 兜底:根据名称推断
if '遗迹' in name:
return {"t": "ruin", "type": "tile"}
return {"t": "cave", "type": "tile"}
# 5. 普通区域:尝试名称映射
if type_tag == 'normal' and name in NORMAL_REGION_NAME_MAP:
tile_name = NORMAL_REGION_NAME_MAP[name]
return {"t": tile_name, "type": "tile"}
# 默认
return {"t": "plain", "type": "tile"}
@app.route('/api/init')
def init_data():
"""初始化数据读取Tiles列表和Region配置"""
# 1. 获取所有 Tile 图片名称
tile_files = glob.glob(os.path.join(ASSETS_DIR, "tiles", "*.png"))
# 过滤切片 (name_0.png)
tiles = [os.path.splitext(os.path.basename(f))[0] for f in tile_files if not os.path.splitext(os.path.basename(f))[0][-2:] in ['_0', '_1', '_2', '_3']]
tiles.sort()
# 2. 获取所有 Sect 图片名称 (sect_1, sect_2, ...)
sect_files = glob.glob(os.path.join(ASSETS_DIR, "sects", "*.png"))
sect_tiles_set = set()
for f in sect_files:
name = os.path.splitext(os.path.basename(f))[0]
# Extract base name from slices: sect_1_0 -> sect_1
if name.startswith('sect_') and '_' in name[5:]:
# Split by underscore and take first two parts
parts = name.split('_')
if len(parts) >= 3: # sect_1_0
base_name = f"{parts[0]}_{parts[1]}" # sect_1
sect_tiles_set.add(base_name)
sect_tiles = sorted(list(sect_tiles_set))
# 3. 获取所有 City 图片名称 (city_301, city_302, ...)
city_files = glob.glob(os.path.join(ASSETS_DIR, "cities", "*.*"))
# 过滤非图片
city_files = [f for f in city_files if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
city_tiles_map = {} # base_name -> extension (for first slice)
city_tiles_set = set()
for f in city_files:
basename = os.path.basename(f)
name = os.path.splitext(basename)[0]
ext = os.path.splitext(basename)[1]
# Extract base name from slices: city_301_0 -> city_301
if name.startswith('city_') and '_' in name[5:]:
parts = name.split('_')
if len(parts) >= 3: # city_301_0
base_name = f"{parts[0]}_{parts[1]}" # city_301
city_tiles_set.add(base_name)
# Store extension for the first slice
if base_name not in city_tiles_map:
city_tiles_map[base_name] = f"{base_name}_0{ext}"
city_tiles = sorted(list(city_tiles_set))
# 4. 读取 sect.csv 建立 sect_id -> sect_name 映射
sect_id_to_name = {}
sect_csv_path = os.path.join(CONFIG_DIR, "sect.csv")
if os.path.exists(sect_csv_path):
with open(sect_csv_path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f)
rows = list(reader)
data_rows = rows[2:] if len(rows) > 2 else []
for row in data_rows:
if len(row) >= 2:
try:
sid = int(row[0])
sname = row[1]
sect_id_to_name[sid] = sname
except ValueError:
continue
# 5. 读取 Region 配置
regions = []
def parse_csv(filename, id_col, name_col, type_tag, sect_id_col=None, sub_type_col=None):
path = os.path.join(CONFIG_DIR, filename)
if not os.path.exists(path):
print(f"Warning: {path} not found")
return
with open(path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f)
rows = list(reader)
# 跳过前两行 (header 和 description)
data_rows = rows[2:] if len(rows) > 2 else []
for row in data_rows:
if len(row) <= max(id_col, name_col): continue
try:
r_id = int(row[id_col])
name = row[name_col]
# 简单的 hash 颜色生成
color_hash = hash(f"{type_tag}_{r_id}") & 0xFFFFFF
color = f"#{color_hash:06x}"
# 获取 sect_id (用于宗门)
sect_id = None
if type_tag == 'sect' and sect_id_col is not None and len(row) > sect_id_col:
try:
sect_id = int(row[sect_id_col])
except ValueError:
pass
# 获取 sub_type (用于修炼区域)
sub_type = None
if type_tag == 'cultivate' and sub_type_col is not None and len(row) > sub_type_col:
sub_type = row[sub_type_col].strip()
# 计算默认绑定 Tile
bind_info = get_default_tile(r_id, name, type_tag, sect_id=sect_id, sub_type=sub_type)
regions.append({
"id": r_id,
"name": name,
"type": type_tag,
"color": color,
"bindTile": bind_info["t"],
"bindTileType": bind_info["type"]
})
except ValueError:
continue
# 读取四种配置
# normal_region.csv: id=0, name=1
parse_csv("normal_region.csv", 0, 1, "normal")
# sect_region.csv: id=0, name=1, sect_id=3
parse_csv("sect_region.csv", 0, 1, "sect", sect_id_col=3)
# cultivate_region.csv: id=0, name=1, sub_type=3 (在 desc 后面)
parse_csv("cultivate_region.csv", 0, 1, "cultivate", sub_type_col=3)
# city_region.csv: id=0, name=1
parse_csv("city_region.csv", 0, 1, "city")
# 排序优先级normal > sect > cultivate > city > 其他
def sort_priority(r):
if r['type'] == 'normal': return 0
if r['type'] == 'sect': return 1
if r['type'] == 'cultivate': return 2
if r['type'] == 'city': return 3
return 4
regions.sort(key=lambda x: (sort_priority(x), x['id']))
# 3. 尝试读取现有的地图数据
saved_map = load_map_data()
return jsonify({
"width": MAP_WIDTH,
"height": MAP_HEIGHT,
"tiles": tiles,
"sectTiles": sect_tiles,
"cityTiles": city_tiles,
"cityTilesMap": city_tiles_map,
"regions": regions,
"savedMap": saved_map
})
@app.route('/api/save', methods=['POST'])
def save_map():
data = request.json
grid = data.get('grid', []) # list of {x, y, t, r}
tile_csv_path = os.path.join(OUTPUT_DIR, "tile_map.csv")
region_csv_path = os.path.join(OUTPUT_DIR, "region_map.csv")
try:
# 初始化二维数组 (Matrix)
# MAP_HEIGHT 行, MAP_WIDTH 列
tile_matrix = [["plain" for _ in range(MAP_WIDTH)] for _ in range(MAP_HEIGHT)]
region_matrix = [[-1 for _ in range(MAP_WIDTH)] for _ in range(MAP_HEIGHT)]
# 填充数据
for cell in grid:
x, y = cell['x'], cell['y']
if 0 <= x < MAP_WIDTH and 0 <= y < MAP_HEIGHT:
tile_matrix[y][x] = cell['t']
if cell.get('r') is not None:
region_matrix[y][x] = cell['r']
# 保存 Tile Map (矩阵形式)
with open(tile_csv_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerows(tile_matrix)
# 保存 Region Map (矩阵形式)
with open(region_csv_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerows(region_matrix)
return jsonify({"status": "success", "message": "Map saved successfully (Matrix Format)"})
except Exception as e:
return jsonify({"status": "error", "message": str(e)}), 500
def load_map_data():
"""读取矩阵格式 CSV 并重建 Grid 状态"""
tile_csv_path = os.path.join(OUTPUT_DIR, "tile_map.csv")
region_csv_path = os.path.join(OUTPUT_DIR, "region_map.csv")
loaded_data = {} # key: "x,y", value: {t: ..., r: ...}
# 读取 Tile Matrix
if os.path.exists(tile_csv_path):
with open(tile_csv_path, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
for y, row in enumerate(reader):
if y >= MAP_HEIGHT: break
for x, val in enumerate(row):
if x >= MAP_WIDTH: break
key = f"{x},{y}"
loaded_data[key] = {"t": val}
# 读取 Region Matrix
if os.path.exists(region_csv_path):
with open(region_csv_path, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
for y, row in enumerate(reader):
if y >= MAP_HEIGHT: break
for x, val in enumerate(row):
if x >= MAP_WIDTH: break
try:
rid = int(val)
if rid != -1:
key = f"{x},{y}"
if key not in loaded_data:
loaded_data[key] = {"t": "plain"} # 默认
loaded_data[key]["r"] = rid
except ValueError:
continue
return loaded_data
if __name__ == '__main__':
print(f"Starting Map Creator at http://127.0.0.1:5000")
print(f"Assets Dir: {ASSETS_DIR}")
app.run(debug=True, port=5000)