初始提交

This commit is contained in:
JOJO 2025-06-25 09:35:26 +08:00
commit 2952d367e3
149 changed files with 11449632 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

141
Untitled-1.py Normal file
View File

@ -0,0 +1,141 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
检查所有相关文件中的权重默认值是否已从5改为100
"""
import os
import re
import logging
from colorama import init, Fore, Style
# 初始化colorama
init()
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('check_weights')
# 需要检查的文件和目录
CHECK_PATHS = [
'templates/admin/characters.html',
'templates/weight_management.html',
'static/js/admin/characters.js',
'static/js/game.js',
'app_sqlite.py',
]
# 权重相关的正则表达式模式
WEIGHT_PATTERNS = [
# HTML中的默认值
(r'id="character-weight"[^>]*value="(\d+)"', 'HTML表单默认值'),
(r'min="1" max="(\d+)" value="(\d+)"', 'HTML数字输入范围'),
# JavaScript中的默认值引用
(r'character\.weight \|\| (\d+)', 'JS默认权重引用'),
(r'weight: parseInt\([^)]+\) \|\| (\d+)', 'JS解析默认权重'),
# Python中的默认值
(r"character\['weight'\] = (\d+)", 'Python默认权重赋值'),
# 描述文本中的范围
(r'权重 \(1-(\d+)\)', '权重范围描述'),
(r'默认值为(\d+)', '默认值描述'),
# 范围验证
(r'weight < 1 \|\| weight > (\d+)', 'JS权重验证'),
]
def check_file(file_path):
"""检查单个文件中的权重默认值"""
if not os.path.exists(file_path):
logger.warning(f"文件不存在: {file_path}")
return [], []
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
correct_matches = []
incorrect_matches = []
# 检查所有权重模式
for pattern, description in WEIGHT_PATTERNS:
matches = re.finditer(pattern, content)
for match in matches:
line_num = content[:match.start()].count('\n') + 1
line_text = content.splitlines()[line_num-1]
# 根据不同的模式检查不同的组
if 'max' in pattern and 'value' in pattern:
# 处理同时有max和value的情况
max_val = match.group(1)
value = match.group(2)
if max_val == '1000' and value == '100':
correct_matches.append((file_path, line_num, description, line_text))
else:
incorrect_matches.append((file_path, line_num, description, line_text))
else:
# 普通情况,检查第一个捕获组
weight_value = match.group(1)
if description == '权重范围描述' and weight_value == '1000':
correct_matches.append((file_path, line_num, description, line_text))
elif weight_value == '100':
correct_matches.append((file_path, line_num, description, line_text))
else:
incorrect_matches.append((file_path, line_num, description, line_text))
return correct_matches, incorrect_matches
def main():
"""主函数"""
logger.info("开始检查权重默认值")
all_correct_matches = []
all_incorrect_matches = []
# 查找所有匹配的文件
for path in CHECK_PATHS:
if os.path.isdir(path):
# 处理目录
for root, _, files in os.walk(path):
for file in files:
if file.endswith(('.html', '.js', '.py', '.css')):
file_path = os.path.join(root, file)
correct, incorrect = check_file(file_path)
all_correct_matches.extend(correct)
all_incorrect_matches.extend(incorrect)
else:
# 处理单个文件
correct, incorrect = check_file(path)
all_correct_matches.extend(correct)
all_incorrect_matches.extend(incorrect)
# 输出结果
print("\n" + "="*80)
print(f"{Fore.GREEN}正确的权重默认值 (100 或 1000): {len(all_correct_matches)}{Style.RESET_ALL}")
for file_path, line_num, desc, line_text in all_correct_matches:
print(f"{Fore.GREEN}{file_path}:{line_num} - {desc}{Style.RESET_ALL}")
print(f" {line_text.strip()}")
print("\n" + "="*80)
print(f"{Fore.RED}不正确的权重默认值 (非 100 或 1000): {len(all_incorrect_matches)}{Style.RESET_ALL}")
for file_path, line_num, desc, line_text in all_incorrect_matches:
print(f"{Fore.RED}{file_path}:{line_num} - {desc}{Style.RESET_ALL}")
print(f" {line_text.strip()}")
# 总结
print("\n" + "="*80)
if len(all_incorrect_matches) == 0:
print(f"{Fore.GREEN}所有权重相关默认值检查通过!{Style.RESET_ALL}")
else:
print(f"{Fore.YELLOW}发现 {len(all_incorrect_matches)} 个需要修改的地方。{Style.RESET_ALL}")
return len(all_incorrect_matches) == 0
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

77
add_weights_script.py Normal file
View File

@ -0,0 +1,77 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
给所有角色添加默认权重100的脚本
"""
import os
import json
import logging
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('add_weights')
# 卡牌文件路径 - 根据实际情况修改
CARDS_FILE = 'data/cards.json'
def add_default_weights():
"""给所有角色添加默认权重100"""
try:
# 确保文件存在
if not os.path.exists(CARDS_FILE):
logger.error(f"文件不存在: {CARDS_FILE}")
return False
# 读取卡牌数据
with open(CARDS_FILE, 'r', encoding='utf-8') as f:
cards_data = json.load(f)
characters = cards_data.get('characters', [])
logger.info(f"读取到 {len(characters)} 个角色")
# 记录修改数量
updated_count = 0
# 为每个角色添加默认权重
for character in characters:
if 'weight' not in character:
character['weight'] = 100
updated_count += 1
logger.info(f"已为 {updated_count} 个角色添加默认权重100")
# 如果有修改,保存回文件
if updated_count > 0:
# 先创建备份
backup_file = f"{CARDS_FILE}.bak"
with open(backup_file, 'w', encoding='utf-8') as f:
json.dump(cards_data, f, ensure_ascii=False, indent=2)
logger.info(f"已创建备份文件: {backup_file}")
# 保存修改后的文件
with open(CARDS_FILE, 'w', encoding='utf-8') as f:
json.dump(cards_data, f, ensure_ascii=False, indent=2)
logger.info(f"已保存修改到文件: {CARDS_FILE}")
return True
else:
logger.info("没有角色需要添加权重,所有角色都已有权重设置")
return False
except Exception as e:
logger.error(f"添加默认权重失败: {str(e)}")
return False
def main():
"""主函数"""
logger.info("开始运行添加默认权重脚本")
result = add_default_weights()
if result:
logger.info("脚本执行成功")
else:
logger.warning("脚本执行完成,但未进行修改或出现错误")
if __name__ == "__main__":
main()

1002
app.py Normal file

File diff suppressed because it is too large Load Diff

1816
app_sqlite.py Normal file

File diff suppressed because it is too large Load Diff

176
data/achievements.json Normal file
View File

@ -0,0 +1,176 @@
{
"achievements": [
{
"id": "death_loyalty_low_virus_bomb",
"name": "帝皇裁决",
"description": "您的星球被帝国执行了灭绝令。忠诚度不足导致您被视为异端。",
"icon": "☣️",
"death_scenario_id": "loyalty_low_virus_bomb",
"hidden": 0
},
{
"id": "death_loyalty_high_chicken",
"name": "鸡贼之灾",
"description": "忠诚度过高,只有一种可能。",
"icon": "🐔",
"death_scenario_id": "loyalty_high_chicken",
"hidden": 0
},
{
"id": "death_chaos_low_tyranid",
"name": "生物资源",
"description": "混沌侵染过低,您的星球成为了泰伦虫族的完美食物。",
"icon": "🍰",
"death_scenario_id": "chaos_low_tyranid",
"hidden": 0
},
{
"id": "death_chaos_high_khorne",
"name": "血与骷髅",
"description": "混沌侵染过高,您的星球被恐虐军团屠戮。",
"icon": "💀",
"death_scenario_id": "chaos_high_khorne",
"hidden": 0
},
{
"id": "death_population_low_revolt",
"name": "人民之怒",
"description": "民众控制过低,您被起义军推翻。",
"icon": "⚔️",
"death_scenario_id": "population_low_revolt",
"hidden": 0
},
{
"id": "death_population_high_rival",
"name": "权力的游戏",
"description": "民众控制过高,您被政治对手暗杀。",
"icon": "🗡️",
"death_scenario_id": "population_high_rival",
"hidden": 0
},
{
"id": "death_military_low_orks",
"name": "绿皮入侵",
"description": "军事力量过低,您的星球被兽人侵略。",
"icon": "👹",
"death_scenario_id": "military_low_orks",
"hidden": 0
},
{
"id": "death_military_high_coup",
"name": "一言不合",
"description": "军事力量过高,您的军队发动了政变。",
"icon": "👑",
"death_scenario_id": "military_high_coup",
"hidden": 0
},
{
"id": "death_resources_low_chaos",
"name": "饥饿的混沌",
"description": "资源储备过低,饥饿的民众转向了混沌。",
"icon": "🦑",
"death_scenario_id": "resources_low_chaos",
"hidden": 0
},
{
"id": "death_resources_high_tyranid",
"name": "虫族的大餐",
"description": "资源储备过高,您吸引了泰伦虫族的注意。",
"icon": "🐲",
"death_scenario_id": "resources_high_tyranid",
"hidden": 0
},
{
"id": "reign_single_5",
"name": "初露锋芒",
"description": "单次统治达到5年帝国开始记住您的名字。",
"icon": "🌱",
"hidden": 0,
"achievement_type": "reign_single",
"requirement": 5
},
{
"id": "reign_single_10",
"name": "初级执政官",
"description": "单次统治达到10年您已经开始学会如何治理一颗行星。",
"icon": "🌿",
"hidden": 0,
"achievement_type": "reign_single",
"requirement": 10
},
{
"id": "reign_single_50",
"name": "熟练统治者",
"description": "单次统治达到50年您的统治已成为当地传说。",
"icon": "🌲",
"hidden": 0,
"achievement_type": "reign_single",
"requirement": 50
},
{
"id": "reign_single_100",
"name": "百年老人",
"description": "单次统治达到100年您已经成为帝国历史上最长寿的总督之一。",
"icon": "👑",
"hidden": 0,
"achievement_type": "reign_single",
"requirement": 100
},
{
"id": "reign_single_200",
"name": "始皇帝",
"description": "单次统治达到200年不朽的荣光永远铭刻在帝国史册。",
"icon": "⚜️",
"hidden": 0,
"achievement_type": "reign_single",
"requirement": 200
},
{
"id": "reign_total_50",
"name": "见习总督",
"description": "累计统治50年您已崭露头角。",
"icon": "📚",
"hidden": 0,
"achievement_type": "reign_total",
"requirement": 50
},
{
"id": "reign_total_100",
"name": "资深总督",
"description": "累计统治100年您的统治经验得到帝国的认可。",
"icon": "📜",
"hidden": 0,
"achievement_type": "reign_total",
"requirement": 100
},
{
"id": "reign_total_500",
"name": "传奇总督",
"description": "累计统治500年您的名字将被铭记。",
"icon": "🏆",
"hidden": 0,
"achievement_type": "reign_total",
"requirement": 500
},
{
"id": "reign_total_1000",
"name": "千年老店",
"description": "累计统治1000年成为星区传说。",
"icon": "🌟",
"hidden": 0,
"achievement_type": "reign_total",
"requirement": 1000
},
{
"id": "reign_total_114514",
"name": "永恒总督",
"description": "累计统治114514年这已经超越了凡人的理解。",
"icon": "🔮",
"hidden": 0,
"achievement_type": "reign_total",
"requirement": 114514
}
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
{
"active_sessions": [
{
"user_id": "a37c5dcd-eb43-4d95-849e-a010ffc89348",
"login_time": "2025-06-18T12:20:22.096735",
"last_activity": "2025-06-18T12:59:40.831810",
"ip_address": "127.0.0.1",
"status": "idle",
"game_start_time": "2025-06-18T12:54:56.050882"
}
]
}

View File

@ -0,0 +1,12 @@
{
"active_sessions": [
{
"user_id": "10de216c-a8b9-4634-99d2-6b86a18eb86e",
"login_time": "2025-06-18T13:55:23.062172",
"last_activity": "2025-06-18T13:59:51.668179",
"ip_address": "127.0.0.1",
"status": "playing",
"game_start_time": "2025-06-18T13:55:50.142290"
}
]
}

View File

@ -0,0 +1,12 @@
{
"active_sessions": [
{
"user_id": "a7cf3c3e-5a14-4158-b87d-633e9795dd0d",
"login_time": "2025-06-18T14:54:12.853911",
"last_activity": "2025-06-18T14:56:32.481719",
"ip_address": "127.0.0.1",
"status": "idle",
"game_start_time": "2025-06-18T14:54:21.287141"
}
]
}

View File

@ -0,0 +1,3 @@
{
"active_sessions": []
}

View File

@ -0,0 +1,3 @@
{
"active_sessions": []
}

View File

@ -0,0 +1,12 @@
{
"active_sessions": [
{
"user_id": "3ba996ca-a6b0-403f-9c54-5ee80deea7f3",
"login_time": "2025-06-18T18:52:57.798773",
"last_activity": "2025-06-18T18:54:08.613434",
"ip_address": "127.0.0.1",
"status": "playing",
"game_start_time": "2025-06-18T18:55:03.595785"
}
]
}

View File

@ -0,0 +1,3 @@
{
"active_sessions": []
}

View File

@ -0,0 +1,12 @@
{
"active_sessions": [
{
"user_id": "3ba996ca-a6b0-403f-9c54-5ee80deea7f3",
"login_time": "2025-06-18T20:57:36.598863",
"last_activity": "2025-06-18T20:59:27.939257",
"ip_address": "127.0.0.1",
"status": "idle",
"game_start_time": "2025-06-18T20:57:39.507870"
}
]
}

View File

@ -0,0 +1,3 @@
{
"active_sessions": []
}

View File

@ -0,0 +1,3 @@
{
"active_sessions": []
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

17517
data/card_votes.json Normal file

File diff suppressed because it is too large Load Diff

2200
data/cards.json Normal file

File diff suppressed because it is too large Load Diff

2225
data/cards.json.bak Normal file

File diff suppressed because it is too large Load Diff

19342
data/custom_cards.json Normal file

File diff suppressed because it is too large Load Diff

324
data/death_methods.json Normal file
View File

@ -0,0 +1,324 @@
{
"death_scenarios": [
{
"id": "loyalty_low_virus_bomb",
"death_type": "loyalty_low",
"name": "病毒炸弹",
"weight": 100,
"first_card": {
"character": {
"name": "审判官",
"title": "帝国异端审判庭",
"avatar": "⚖️",
"avatar_path": "deaths/inquisitor.png"
},
"text": "总督大人!帝国审判庭已将您认定为异端!",
"options": [
{"text": "什么?"},
{"text": "什么?"}
]
},
"second_card": {
"character": {
"name": "病毒炸弹",
"title": "帝国裁决",
"avatar": "☣️",
"avatar_path": "deaths/virus_bomb.png"
},
"text": "您的星球被帝国执行了灭绝令。亚空间中能听到的,只有您行星的哀嚎。",
"options": [
{"text": ""},
{"text": ""}
]
}
},
{
"id": "loyalty_high_chicken",
"death_type": "loyalty_high",
"name": "鸡贼当道",
"weight": 100,
"first_card": {
"character": {
"name": "异端审查官",
"title": "基因鉴定官",
"avatar": "🧬",
"avatar_path": "deaths/gene_inquisitor.png"
},
"text": "总督大人,这次的基因核查除了问题。",
"options": [
{"text": "这不可能!"},
{"text": "这不可能!"}
]
},
"second_card": {
"character": {
"name": "鸡贼当道",
"title": "泰伦虫族的晚餐",
"avatar": "🐔",
"avatar_path": "deaths/tyranid_dinner.png"
},
"text": "当你的人民极其忠诚,政府运转高效,只有一种可能,全是鸡贼,哦,泰伦虫族的生物舰已经来了",
"options": [
{"text": ""},
{"text": ""}
]
}
},
{
"id": "chaos_low_tyranid",
"death_type": "chaos_low",
"name": "虫族甜点",
"weight": 100,
"first_card": {
"character": {
"name": "帝国海军将军",
"title": "战区指挥官",
"avatar": "🎖️",
"avatar_path": "deaths/navy_general.png"
},
"text": "总督!天空中出现了大量不明飞行物!",
"options": [
{"text": "帝皇保佑我们!"},
{"text": "帝皇保佑我们!"}
]
},
"second_card": {
"character": {
"name": "虫族甜点",
"title": "泰伦虫族的完美猎物",
"avatar": "🍰",
"avatar_path": "deaths/tyranid_dessert.png"
},
"text": "您的星球被虫族视为完美的食物来源。没有混沌的气息让您成为了最理想的猎物。",
"options": [
{"text": ""},
{"text": ""}
]
}
},
{
"id": "chaos_high_khorne",
"death_type": "chaos_high",
"name": "恐虐魔神",
"weight": 100,
"first_card": {
"character": {
"name": "PDF军官",
"title": "行星防卫军指挥官",
"avatar": "👮",
"avatar_path": "deaths/pdf_officer.png"
},
"text": "总督!我看见了,我听见了!亚空间的声音!它...它们在召唤我们!",
"options": [
{"text": "什么?"},
{"text": "什么?"}
]
},
"second_card": {
"character": {
"name": "恐虐魔神",
"title": "混沌四神之一",
"avatar": "💀",
"avatar_path": "deaths/khorne.png"
},
"text": "您的星球被8只恐虐军团屠戮。您的头颅成为了恐虐祭坛上的新装饰品。",
"options": [
{"text": ""},
{"text": ""}
]
}
},
{
"id": "population_low_revolt",
"death_type": "population_low",
"name": "叛军领袖",
"weight": 100,
"first_card": {
"character": {
"name": "警备军官",
"title": "城防指挥官",
"avatar": "🛡️",
"avatar_path": "deaths/guard_officer.png"
},
"text": "总督大人!民众暴动已经扩散到整个行星!我们控制不住了!",
"options": [
{"text": "派出所有军队!"},
{"text": "派出所有军队!"}
]
},
"second_card": {
"character": {
"name": "叛军领袖",
"title": "人民之声",
"avatar": "⚔️",
"avatar_path": "deaths/rebel_leader.png"
},
"text": "民众的怒火最终吞噬了您。这颗行星上的新篇章将不再有您的名字。新任总督不合法?只要能补上仕一税,帝国不在乎谁是总督。",
"options": [
{"text": ""},
{"text": ""}
]
}
},
{
"id": "population_high_rival",
"death_type": "population_high",
"name": "政敌暗杀",
"weight": 100,
"first_card": {
"character": {
"name": "政敌",
"title": "贵族议会领袖",
"avatar": "🎭",
"avatar_path": "deaths/political_rival.png"
},
"text": "总督,您的独裁统治已经让其他贵族感到不安。我们认为您的权力过大了。",
"options": [
{"text": "你算什么东西?"},
{"text": "你算什么东西?"}
]
},
"second_card": {
"character": {
"name": "政敌",
"title": "新任总督",
"avatar": "🗡️",
"avatar_path": "deaths/assassin.png"
},
"text": "您被政治对手暗中除掉,尽管民众爱戴您,但权力的游戏从不怜悯任何人。",
"options": [
{"text": ""},
{"text": ""}
]
}
},
{
"id": "military_low_orks",
"death_type": "military_low",
"name": "兽人老大",
"weight": 100,
"first_card": {
"character": {
"name": "海军将领",
"title": "轨道防御指挥官",
"avatar": "🔭",
"avatar_path": "deaths/navy_commander.png"
},
"text": "总督大人!天上那是什么?",
"options": [
{"text": "月亮?"},
{"text": "月亮?"}
]
},
"second_card": {
"character": {
"name": "兽人老大",
"title": "绿皮入侵者",
"avatar": "👹",
"avatar_path": "deaths/ork_warboss.png"
},
"text": "兽人的战斗月亮轻而易举地摧毁了您的军队他们评价道这真是一场无趣的Waghhh",
"options": [
{"text": ""},
{"text": ""}
]
}
},
{
"id": "military_high_coup",
"death_type": "military_high",
"name": "军事政变",
"weight": 100,
"first_card": {
"character": {
"name": "PDF将军",
"title": "行星防卫军最高指挥官",
"avatar": "👨‍✈️",
"avatar_path": "deaths/pdf_general.png"
},
"text": "总督,我觉得您不太适合这个位置了,我有一各推荐人选。",
"options": [
{"text": "谁?"},
{"text": "谁?"}
]
},
"second_card": {
"character": {
"name": "PDF将军",
"title": "军政府领袖",
"avatar": "👑",
"avatar_path": "deaths/military_coup.png"
},
"text": "我",
"options": [
{"text": ""},
{"text": ""}
]
}
},
{
"id": "resources_low_chaos",
"death_type": "resources_low",
"name": "饥荒动乱",
"weight": 100,
"first_card": {
"character": {
"name": "法务部法警",
"title": "治安官",
"avatar": "👮",
"avatar_path": "deaths/arbites.png"
},
"text": "总督大人!暴民们缺衣少食,正在冲进上巢的街区!",
"options": [
{"text": "什么?"},
{"text": "什么?"}
]
},
"second_card": {
"character": {
"name": "被混沌侵染,浑身触手起义军",
"title": "饥饿的暴民首领",
"avatar": "🦑",
"avatar_path": "deaths/chaos_rioters.png"
},
"text": "饭都吃不饱人家凭什么跟帝国混?",
"options": [
{"text": ""},
{"text": ""}
]
}
},
{
"id": "resources_high_tyranid",
"death_type": "resources_high",
"name": "虫巢暴君",
"weight": 100,
"first_card": {
"character": {
"name": "帝国海军将军",
"title": "舰队指挥官",
"avatar": "🚀",
"avatar_path": "deaths/fleet_commander.png"
},
"text": "总督,星系已经被泰伦虫族包围了!",
"options": [
{"text": "什么?"},
{"text": "什么?"}
]
},
"second_card": {
"character": {
"name": "虫巢暴君",
"title": "虫族最高生物形态",
"avatar": "🐲",
"avatar_path": "deaths/hive_tyrant.png"
},
"text": "好吃!",
"options": [
{"text": ""},
{"text": ""}
]
}
}
]
}

780551
data/game_states.json Normal file

File diff suppressed because it is too large Load Diff

3
data/sessions.json Normal file
View File

@ -0,0 +1,3 @@
{
"active_sessions": []
}

220996
data/users.json Normal file

File diff suppressed because it is too large Load Diff

BIN
data/warhammer.db Normal file

Binary file not shown.

0
db_adapter.log Normal file
View File

725
db_adapter.py Normal file
View File

@ -0,0 +1,725 @@
import sqlite3
import json
import logging
import os
import threading
import datetime
import uuid
import random
# 配置日志
logging.basicConfig(
filename='db_adapter.log',
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('db_adapter')
# 数据库文件路径
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "warhammer.db")
# 线程本地存储,确保每个线程使用独立的数据库连接
_thread_local = threading.local()
def get_db_connection():
"""获取数据库连接(每个线程一个)"""
if not hasattr(_thread_local, 'connection'):
_thread_local.connection = sqlite3.connect(DB_PATH)
# 启用外键约束
_thread_local.connection.execute("PRAGMA foreign_keys = ON")
# 配置连接返回行为字典格式
_thread_local.connection.row_factory = sqlite3.Row
return _thread_local.connection
def close_db_connection():
"""关闭当前线程的数据库连接"""
if hasattr(_thread_local, 'connection'):
_thread_local.connection.close()
delattr(_thread_local, 'connection')
# 用户数据操作
def get_users():
"""获取所有用户数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM users")
users = [dict(row) for row in cursor.fetchall()]
return users
except Exception as e:
logger.error(f"获取用户数据失败: {str(e)}")
return []
def save_users(users):
"""保存用户数据(批量更新)"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# 开始事务
conn.execute("BEGIN TRANSACTION")
for user in users:
cursor.execute(
"INSERT OR REPLACE INTO users (id, username, password, created_at, high_score, total_games, last_year, last_game_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(
user['id'],
user['username'],
user['password'],
user.get('created_at', datetime.datetime.now().isoformat()),
user.get('high_score', 0),
user.get('total_games', 0),
user.get('last_year', 41000),
user.get('last_game_time', None)
)
)
# 提交事务
conn.commit()
return True
except Exception as e:
# 回滚事务
conn.rollback()
logger.error(f"保存用户数据失败: {str(e)}")
return False
# 游戏状态操作
def get_game_states():
"""获取所有游戏状态数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM game_states")
rows = cursor.fetchall()
game_states = []
for row in rows:
state = dict(row)
# 将JSON字符串转换为字典
state['game_data'] = json.loads(state['game_data'])
game_states.append(state)
return game_states
except Exception as e:
logger.error(f"获取游戏状态数据失败: {str(e)}")
return []
def save_game_states(states):
"""保存游戏状态数据(批量更新)"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# 开始事务
conn.execute("BEGIN TRANSACTION")
for state in states:
# 将游戏数据转换为JSON字符串
game_data_json = json.dumps(state['game_data'], ensure_ascii=False)
cursor.execute(
"INSERT OR REPLACE INTO game_states (id, user_id, game_data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
(
state['id'],
state['user_id'],
game_data_json,
state.get('created_at', datetime.datetime.now().isoformat()),
state.get('updated_at', datetime.datetime.now().isoformat())
)
)
# 提交事务
conn.commit()
return True
except Exception as e:
# 回滚事务
conn.rollback()
logger.error(f"保存游戏状态数据失败: {str(e)}")
return False
# 卡牌数据操作
def get_cards():
"""获取卡牌基础数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute("SELECT cards_data FROM game_cards WHERE id = 1")
row = cursor.fetchone()
if row:
return json.loads(row['cards_data'])
else:
logger.warning("未找到卡牌数据,返回空字典")
return {}
except Exception as e:
logger.error(f"获取卡牌数据失败: {str(e)}")
return {}
# 自定义卡牌操作
def get_custom_cards():
"""获取自定义卡牌数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute("""
SELECT c.*, vs.upvotes, vs.downvotes
FROM custom_cards c
LEFT JOIN card_vote_stats vs ON c.id = vs.card_id
""")
rows = cursor.fetchall()
cards = []
for row in rows:
card = {
'id': row['id'],
'title': row['title'],
'character': json.loads(row['character_data']),
'description': row['description'],
'option_a': json.loads(row['option_a']),
'option_b': json.loads(row['option_b']),
'creator_id': row['creator_id'],
'creator_name': row['creator_name'],
'created_at': row['created_at'],
'upvotes': row['upvotes'] or 0,
'downvotes': row['downvotes'] or 0
}
cards.append(card)
return {'cards': cards}
except Exception as e:
logger.error(f"获取自定义卡牌数据失败: {str(e)}")
return {'cards': []}
def save_custom_cards(data):
"""保存自定义卡牌数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# 开始事务
conn.execute("BEGIN TRANSACTION")
# 清空现有数据(可选,根据需要修改)
# cursor.execute("DELETE FROM custom_cards")
# 插入新数据
cards = data.get('cards', [])
for card in cards:
cursor.execute(
"""INSERT OR REPLACE INTO custom_cards
(id, title, character_data, description, option_a, option_b, creator_id, creator_name, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
card['id'],
card['title'],
json.dumps(card['character'], ensure_ascii=False),
card['description'],
json.dumps(card['option_a'], ensure_ascii=False),
json.dumps(card['option_b'], ensure_ascii=False),
card['creator_id'],
card['creator_name'],
card.get('created_at', datetime.datetime.now().isoformat())
)
)
# 确保有对应的投票统计记录
cursor.execute(
"INSERT OR IGNORE INTO card_vote_stats (card_id, upvotes, downvotes) VALUES (?, ?, ?)",
(card['id'], card.get('upvotes', 0), card.get('downvotes', 0))
)
# 提交事务
conn.commit()
return True
except Exception as e:
# 回滚事务
conn.rollback()
logger.error(f"保存自定义卡牌数据失败: {str(e)}")
return False
# 卡牌投票操作
def get_card_votes():
"""获取卡牌投票数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# 获取用户投票
cursor.execute("SELECT user_id, card_id, vote_type FROM card_votes")
vote_rows = cursor.fetchall()
user_votes = {}
for row in vote_rows:
user_id = row['user_id']
if user_id not in user_votes:
user_votes[user_id] = {}
user_votes[user_id][row['card_id']] = row['vote_type']
# 获取投票统计
cursor.execute("SELECT card_id, upvotes, downvotes FROM card_vote_stats")
stat_rows = cursor.fetchall()
card_votes = {}
for row in stat_rows:
card_id = row['card_id']
card_votes[card_id] = {
'upvotes': row['upvotes'] or 0,
'downvotes': row['downvotes'] or 0,
'users': {} # 这部分可能需要额外填充,取决于原来的数据结构
}
return {'user_votes': user_votes, 'card_votes': card_votes}
except Exception as e:
logger.error(f"获取卡牌投票数据失败: {str(e)}")
return {'user_votes': {}, 'card_votes': {}}
def save_card_votes(data):
"""保存卡牌投票数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# 开始事务
conn.execute("BEGIN TRANSACTION")
# 更新用户投票
user_votes = data.get('user_votes', {})
for user_id, votes in user_votes.items():
for card_id, vote_type in votes.items():
cursor.execute(
"""INSERT OR REPLACE INTO card_votes
(card_id, user_id, vote_type, created_at)
VALUES (?, ?, ?, ?)""",
(
card_id,
user_id,
vote_type,
datetime.datetime.now().isoformat()
)
)
# 更新投票统计
card_votes = data.get('card_votes', {})
for card_id, votes in card_votes.items():
cursor.execute(
"INSERT OR REPLACE INTO card_vote_stats (card_id, upvotes, downvotes) VALUES (?, ?, ?)",
(
card_id,
votes.get('upvotes', 0),
votes.get('downvotes', 0)
)
)
# 提交事务
conn.commit()
return True
except Exception as e:
# 回滚事务
conn.rollback()
logger.error(f"保存卡牌投票数据失败: {str(e)}")
return False
# 会话操作
def get_sessions():
"""获取会话数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM sessions")
rows = cursor.fetchall()
active_sessions = []
for row in rows:
session = {
'user_id': row['user_id'],
'login_time': row['login_time'],
'last_activity': row['last_activity'],
'ip_address': row['ip_address'],
'status': row['status']
}
active_sessions.append(session)
return {'active_sessions': active_sessions}
except Exception as e:
logger.error(f"获取会话数据失败: {str(e)}")
return {'active_sessions': []}
def save_sessions(data):
"""保存会话数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# 开始事务
conn.execute("BEGIN TRANSACTION")
# 清空旧会话
cursor.execute("DELETE FROM sessions")
# 插入新会话
active_sessions = data.get('active_sessions', [])
for session in active_sessions:
# 生成会话ID
session_id = str(uuid.uuid4())
cursor.execute(
"""INSERT INTO sessions
(id, user_id, login_time, last_activity, ip_address, status)
VALUES (?, ?, ?, ?, ?, ?)""",
(
session_id,
session['user_id'],
session.get('login_time', datetime.datetime.now().isoformat()),
session.get('last_activity', datetime.datetime.now().isoformat()),
session.get('ip_address', ''),
session.get('status', 'idle')
)
)
# 提交事务
conn.commit()
return True
except Exception as e:
# 回滚事务
conn.rollback()
logger.error(f"保存会话数据失败: {str(e)}")
return False
# 投票卡牌的实用函数
def vote_card(card_id, user_id, vote_type):
"""对卡牌进行投票或取消投票"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# 开始事务
conn.execute("BEGIN TRANSACTION")
# 检查是否已存在投票
cursor.execute("SELECT vote_type FROM card_votes WHERE card_id = ? AND user_id = ?", (card_id, user_id))
existing_vote = cursor.fetchone()
# 检查卡牌投票统计
cursor.execute("SELECT upvotes, downvotes FROM card_vote_stats WHERE card_id = ?", (card_id,))
vote_stats = cursor.fetchone()
upvotes = 0
downvotes = 0
if vote_stats:
upvotes = vote_stats['upvotes'] or 0
downvotes = vote_stats['downvotes'] or 0
new_vote = None
if vote_type == 'none' or (existing_vote and existing_vote['vote_type'] == vote_type):
# 取消投票
if existing_vote:
if existing_vote['vote_type'] == 'upvote':
upvotes = max(0, upvotes - 1)
else:
downvotes = max(0, downvotes - 1)
# 删除投票记录
cursor.execute("DELETE FROM card_votes WHERE card_id = ? AND user_id = ?", (card_id, user_id))
else:
# 如果之前投过票,先取消之前的投票
if existing_vote:
if existing_vote['vote_type'] == 'upvote':
upvotes = max(0, upvotes - 1)
else:
downvotes = max(0, downvotes - 1)
# 添加新投票
if vote_type == 'upvote':
upvotes += 1
new_vote = 'upvote'
else:
downvotes += 1
new_vote = 'downvote'
# 更新或添加投票记录
cursor.execute(
"""INSERT OR REPLACE INTO card_votes
(card_id, user_id, vote_type, created_at)
VALUES (?, ?, ?, ?)""",
(card_id, user_id, vote_type, datetime.datetime.now().isoformat())
)
# 更新投票统计
cursor.execute(
"""INSERT OR REPLACE INTO card_vote_stats
(card_id, upvotes, downvotes)
VALUES (?, ?, ?)""",
(card_id, upvotes, downvotes)
)
# 提交事务
conn.commit()
return {
'success': True,
'upvotes': upvotes,
'downvotes': downvotes,
'newVote': new_vote
}
except Exception as e:
# 回滚事务
conn.rollback()
logger.error(f"投票失败: {str(e)}")
return {
'success': False,
'error': str(e)
}
# 死亡场景操作
def get_death_scenarios(death_type=None):
"""获取死亡场景数据,可选按死亡类型筛选"""
conn = get_db_connection()
cursor = conn.cursor()
try:
if death_type:
cursor.execute("SELECT * FROM death_scenarios WHERE death_type = ?", (death_type,))
else:
cursor.execute("SELECT * FROM death_scenarios")
rows = cursor.fetchall()
scenarios = []
for row in rows:
scenario = {
'id': row['id'],
'death_type': row['death_type'],
'name': row['name'],
'weight': row['weight'] or 100,
'first_card': json.loads(row['first_card']),
'second_card': json.loads(row['second_card'])
}
scenarios.append(scenario)
return scenarios
except Exception as e:
logger.error(f"获取死亡场景数据失败: {str(e)}")
return []
def get_random_death_scenario(death_type):
"""根据死亡类型获取随机死亡场景,考虑权重"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# 获取指定类型的所有死亡场景
cursor.execute("SELECT id, weight FROM death_scenarios WHERE death_type = ?", (death_type,))
scenarios = cursor.fetchall()
if not scenarios:
return None
# 计算权重总和
total_weight = sum(s['weight'] or 100 for s in scenarios)
# 随机选择一个场景,考虑权重
rand_val = random.randint(1, total_weight)
# 根据权重选择
current_weight = 0
selected_id = None
for scenario in scenarios:
current_weight += scenario['weight'] or 100
if rand_val <= current_weight:
selected_id = scenario['id']
break
# 获取选中的场景详情
if selected_id:
cursor.execute("SELECT * FROM death_scenarios WHERE id = ?", (selected_id,))
row = cursor.fetchone()
if row:
return {
'id': row['id'],
'death_type': row['death_type'],
'name': row['name'],
'weight': row['weight'] or 100,
'first_card': json.loads(row['first_card']),
'second_card': json.loads(row['second_card'])
}
return None
except Exception as e:
logger.error(f"获取随机死亡场景失败: {str(e)}")
return None
def save_death_scenarios(scenarios):
"""保存死亡场景数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# 开始事务
conn.execute("BEGIN TRANSACTION")
for scenario in scenarios:
cursor.execute(
"""INSERT OR REPLACE INTO death_scenarios
(id, death_type, name, weight, first_card, second_card)
VALUES (?, ?, ?, ?, ?, ?)""",
(
scenario['id'],
scenario['death_type'],
scenario['name'],
scenario.get('weight', 100),
json.dumps(scenario['first_card'], ensure_ascii=False),
json.dumps(scenario['second_card'], ensure_ascii=False)
)
)
# 提交事务
conn.commit()
return True
except Exception as e:
# 回滚事务
conn.rollback()
logger.error(f"保存死亡场景数据失败: {str(e)}")
return False
# 成就系统操作
def get_achievements():
"""获取所有成就数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM achievements")
rows = cursor.fetchall()
achievements = []
for row in rows:
achievement = {
'id': row['id'],
'name': row['name'],
'description': row['description'],
'icon': row['icon'],
'death_scenario_id': row['death_scenario_id'],
'hidden': row['hidden']
}
achievements.append(achievement)
return achievements
except Exception as e:
logger.error(f"获取成就数据失败: {str(e)}")
return []
def get_user_achievements(user_id):
"""获取用户已解锁的成就"""
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute("""
SELECT a.*, ua.unlock_time
FROM achievements a
JOIN user_achievements ua ON a.id = ua.achievement_id
WHERE ua.user_id = ?
""", (user_id,))
rows = cursor.fetchall()
unlocked = []
for row in rows:
achievement = {
'id': row['id'],
'name': row['name'],
'description': row['description'],
'icon': row['icon'],
'death_scenario_id': row['death_scenario_id'],
'unlock_time': row['unlock_time']
}
unlocked.append(achievement)
return unlocked
except Exception as e:
logger.error(f"获取用户成就数据失败: {str(e)}")
return []
def unlock_achievement(user_id, achievement_id):
"""解锁用户成就"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# 检查成就是否存在
cursor.execute("SELECT id FROM achievements WHERE id = ?", (achievement_id,))
if not cursor.fetchone():
logger.error(f"成就不存在: {achievement_id}")
return False
# 检查用户是否已解锁该成就
cursor.execute(
"SELECT * FROM user_achievements WHERE user_id = ? AND achievement_id = ?",
(user_id, achievement_id)
)
if cursor.fetchone():
# 已解锁,无需重复操作
return True
# 解锁新成就
cursor.execute(
"""INSERT INTO user_achievements (user_id, achievement_id, unlock_time)
VALUES (?, ?, ?)""",
(
user_id,
achievement_id,
datetime.datetime.now().isoformat()
)
)
conn.commit()
return True
except Exception as e:
conn.rollback()
logger.error(f"解锁成就失败: {str(e)}")
return False
def save_achievements(achievements):
"""保存成就数据"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# 开始事务
conn.execute("BEGIN TRANSACTION")
for achievement in achievements:
cursor.execute(
"""INSERT OR REPLACE INTO achievements
(id, name, description, icon, death_scenario_id, hidden)
VALUES (?, ?, ?, ?, ?, ?)""",
(
achievement['id'],
achievement['name'],
achievement['description'],
achievement['icon'],
achievement.get('death_scenario_id'),
achievement.get('hidden', 0)
)
)
# 提交事务
conn.commit()
return True
except Exception as e:
# 回滚事务
conn.rollback()
logger.error(f"保存成就数据失败: {str(e)}")
return False

4
frp启动.bat Normal file
View File

@ -0,0 +1,4 @@
@echo on
cd /d "C:\Users\KOJO JOTARO\Desktop\vps\frp_0.61.2_windows_amd64"
frpc.exe -c frpc.ini
pause

305
migrate_to_sqlite.py Normal file
View File

@ -0,0 +1,305 @@
import json
import os
import sqlite3
import datetime
import logging
# 配置日志
logging.basicConfig(
filename='migration.log',
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('migration')
# 数据文件路径
DATA_DIR = 'data'
DB_PATH = os.path.join(DATA_DIR, 'warhammer.db')
USERS_FILE = os.path.join(DATA_DIR, 'users.json')
GAME_STATES_FILE = os.path.join(DATA_DIR, 'game_states.json')
CARDS_FILE = os.path.join(DATA_DIR, 'cards.json')
CARD_VOTES_FILE = os.path.join(DATA_DIR, 'card_votes.json')
CUSTOM_CARDS_FILE = os.path.join(DATA_DIR, 'custom_cards.json')
SESSIONS_FILE = os.path.join(DATA_DIR, 'sessions.json')
def create_tables(conn):
"""创建数据库表结构"""
cursor = conn.cursor()
# 用户表
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at TEXT NOT NULL,
high_score INTEGER DEFAULT 0,
total_games INTEGER DEFAULT 0,
last_year INTEGER DEFAULT 41000,
last_game_time TEXT
)
''')
# 游戏状态表
cursor.execute('''
CREATE TABLE IF NOT EXISTS game_states (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
game_data TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
)
''')
# 自定义卡牌表
cursor.execute('''
CREATE TABLE IF NOT EXISTS custom_cards (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
character_data TEXT NOT NULL,
description TEXT NOT NULL,
option_a TEXT NOT NULL,
option_b TEXT NOT NULL,
creator_id TEXT NOT NULL,
creator_name TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (creator_id) REFERENCES users(id)
)
''')
# 卡牌投票表
cursor.execute('''
CREATE TABLE IF NOT EXISTS card_votes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
card_id TEXT NOT NULL,
user_id TEXT NOT NULL,
vote_type TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (card_id) REFERENCES custom_cards(id),
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(card_id, user_id)
)
''')
# 卡牌投票统计表
cursor.execute('''
CREATE TABLE IF NOT EXISTS card_vote_stats (
card_id TEXT PRIMARY KEY,
upvotes INTEGER DEFAULT 0,
downvotes INTEGER DEFAULT 0,
FOREIGN KEY (card_id) REFERENCES custom_cards(id)
)
''')
# 会话表
cursor.execute('''
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
login_time TEXT NOT NULL,
last_activity TEXT NOT NULL,
ip_address TEXT,
status TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
)
''')
# 卡牌数据表 - 只存储一条记录,因为这是游戏基础数据
cursor.execute('''
CREATE TABLE IF NOT EXISTS game_cards (
id INTEGER PRIMARY KEY,
cards_data TEXT NOT NULL
)
''')
conn.commit()
logger.info("数据库表创建完成")
def load_json_data(file_path, default_value=None):
"""从JSON文件安全地加载数据"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
logger.warning(f"加载文件 {file_path} 失败: {str(e)}")
return default_value if default_value is not None else {}
def migrate_users(conn, users_data):
"""迁移用户数据"""
cursor = conn.cursor()
for user in users_data:
cursor.execute(
"INSERT OR REPLACE INTO users (id, username, password, created_at, high_score, total_games, last_year, last_game_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(
user['id'],
user['username'],
user['password'],
user.get('created_at', datetime.datetime.utcnow().isoformat()),
user.get('high_score', 0),
user.get('total_games', 0),
user.get('last_year', 41000),
user.get('last_game_time', None)
)
)
conn.commit()
logger.info(f"用户数据迁移完成,共 {len(users_data)} 条记录")
def migrate_game_states(conn, game_states_data):
"""迁移游戏状态数据"""
cursor = conn.cursor()
for state in game_states_data:
cursor.execute(
"INSERT OR REPLACE INTO game_states (id, user_id, game_data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
(
state['id'],
state['user_id'],
json.dumps(state['game_data'], ensure_ascii=False),
state.get('created_at', datetime.datetime.utcnow().isoformat()),
state.get('updated_at', datetime.datetime.utcnow().isoformat())
)
)
conn.commit()
logger.info(f"游戏状态数据迁移完成,共 {len(game_states_data)} 条记录")
def migrate_custom_cards(conn, custom_cards_data):
"""迁移自定义卡牌数据"""
cursor = conn.cursor()
cards = custom_cards_data.get('cards', [])
for card in cards:
cursor.execute(
"INSERT OR REPLACE INTO custom_cards (id, title, character_data, description, option_a, option_b, creator_id, creator_name, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
card['id'],
card['title'],
json.dumps(card['character'], ensure_ascii=False),
card['description'],
json.dumps(card['option_a'], ensure_ascii=False),
json.dumps(card['option_b'], ensure_ascii=False),
card['creator_id'],
card['creator_name'],
card.get('created_at', datetime.datetime.utcnow().isoformat())
)
)
conn.commit()
logger.info(f"自定义卡牌数据迁移完成,共 {len(cards)} 条记录")
def migrate_card_votes(conn, card_votes_data):
"""迁移卡牌投票数据"""
cursor = conn.cursor()
# 迁移用户投票记录
user_votes = card_votes_data.get('user_votes', {})
for user_id, votes in user_votes.items():
for card_id, vote_type in votes.items():
cursor.execute(
"INSERT OR REPLACE INTO card_votes (card_id, user_id, vote_type, created_at) VALUES (?, ?, ?, ?)",
(
card_id,
user_id,
vote_type,
datetime.datetime.utcnow().isoformat()
)
)
# 迁移卡牌投票统计
card_votes = card_votes_data.get('card_votes', {})
for card_id, votes in card_votes.items():
cursor.execute(
"INSERT OR REPLACE INTO card_vote_stats (card_id, upvotes, downvotes) VALUES (?, ?, ?)",
(
card_id,
votes.get('upvotes', 0),
votes.get('downvotes', 0)
)
)
conn.commit()
logger.info(f"卡牌投票数据迁移完成")
def migrate_sessions(conn, sessions_data):
"""迁移会话数据"""
cursor = conn.cursor()
active_sessions = sessions_data.get('active_sessions', [])
for session in active_sessions:
cursor.execute(
"INSERT OR REPLACE INTO sessions (id, user_id, login_time, last_activity, ip_address, status) VALUES (?, ?, ?, ?, ?, ?)",
(
str(hash(session['user_id'] + session.get('login_time', ''))), # 创建唯一ID
session['user_id'],
session.get('login_time', datetime.datetime.utcnow().isoformat()),
session.get('last_activity', datetime.datetime.utcnow().isoformat()),
session.get('ip_address', ''),
session.get('status', 'idle')
)
)
conn.commit()
logger.info(f"会话数据迁移完成,共 {len(active_sessions)} 条记录")
def migrate_cards_data(conn, cards_data):
"""迁移卡牌基础数据"""
cursor = conn.cursor()
cursor.execute(
"INSERT OR REPLACE INTO game_cards (id, cards_data) VALUES (?, ?)",
(1, json.dumps(cards_data, ensure_ascii=False))
)
conn.commit()
logger.info(f"卡牌基础数据迁移完成")
def main():
"""主迁移函数"""
logger.info("开始数据迁移")
# 检查数据库文件路径
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
# 创建/连接到数据库
conn = sqlite3.connect(DB_PATH)
try:
# 创建表结构
create_tables(conn)
# 加载并迁移用户数据
users_data = load_json_data(USERS_FILE, [])
migrate_users(conn, users_data)
# 加载并迁移游戏状态数据
game_states_data = load_json_data(GAME_STATES_FILE, [])
migrate_game_states(conn, game_states_data)
# 加载并迁移自定义卡牌数据
custom_cards_data = load_json_data(CUSTOM_CARDS_FILE, {"cards": []})
migrate_custom_cards(conn, custom_cards_data)
# 加载并迁移卡牌投票数据
card_votes_data = load_json_data(CARD_VOTES_FILE, {"user_votes": {}, "card_votes": {}})
migrate_card_votes(conn, card_votes_data)
# 加载并迁移会话数据
sessions_data = load_json_data(SESSIONS_FILE, {"active_sessions": []})
migrate_sessions(conn, sessions_data)
# 加载并迁移卡牌基础数据
cards_data = load_json_data(CARDS_FILE, {})
migrate_cards_data(conn, cards_data)
logger.info("数据迁移成功完成")
except Exception as e:
logger.error(f"迁移失败: {str(e)}")
raise
finally:
conn.close()
if __name__ == "__main__":
main()

9
migration.log Normal file
View File

@ -0,0 +1,9 @@
2025-05-08 14:24:54,743 - migration - INFO - 开始数据迁移
2025-05-08 14:24:54,802 - migration - INFO - 数据库表创建完成
2025-05-08 14:24:54,966 - migration - INFO - 用户数据迁移完成,共 5113 条记录
2025-05-08 14:24:55,346 - migration - INFO - 游戏状态数据迁移完成,共 4918 条记录
2025-05-08 14:24:55,356 - migration - INFO - 自定义卡牌数据迁移完成,共 30 条记录
2025-05-08 14:24:55,374 - migration - INFO - 卡牌投票数据迁移完成
2025-05-08 14:24:55,381 - migration - INFO - 会话数据迁移完成,共 1 条记录
2025-05-08 14:24:55,392 - migration - INFO - 卡牌基础数据迁移完成
2025-05-08 14:24:55,393 - migration - INFO - 数据迁移成功完成

268
monitor.py Normal file
View File

@ -0,0 +1,268 @@
#!/usr/bin/env python3
from flask import Flask, render_template_string, jsonify
import sqlite3
import datetime
import os
app = Flask(__name__)
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<title>战锤40K行星总督 - 游戏监控</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #0a0a0a;
color: #e0e0e0;
background-image: linear-gradient(to bottom, #0a0a0a, #1a1a1a);
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: rgba(30, 30, 35, 0.9);
border: 2px solid #600;
border-radius: 10px;
padding: 20px;
box-shadow: 0 0 20px rgba(153, 0, 0, 0.5);
}
h1, h2 {
color: #d4af37;
text-shadow: 0 0 10px rgba(212, 175, 55, 0.5);
text-align: center;
}
.stats {
display: flex;
justify-content: space-around;
margin: 30px 0;
flex-wrap: wrap;
}
.stat-card {
background-color: rgba(0, 0, 0, 0.5);
border: 1px solid #600;
border-radius: 8px;
padding: 20px;
width: 200px;
margin: 10px;
text-align: center;
}
.stat-value {
font-size: 2.5rem;
font-weight: bold;
color: #d4af37;
margin: 10px 0;
}
.stat-label {
font-size: 1rem;
color: #999;
}
.user-list {
background-color: rgba(0, 0, 0, 0.5);
border: 1px solid #444;
border-radius: 8px;
padding: 15px;
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
}
.user-item {
display: flex;
justify-content: space-between;
padding: 8px;
border-bottom: 1px solid #333;
}
.user-item:last-child {
border-bottom: none;
}
.playing {
color: #4caf50;
}
.idle {
color: #ff9800;
}
.refresh-time {
text-align: center;
margin-top: 20px;
color: #999;
font-size: 0.8rem;
}
.btn {
background-color: #600;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
display: block;
margin: 20px auto;
font-weight: bold;
}
.btn:hover {
background-color: #900;
}
</style>
</head>
<body>
<div class="container">
<h1>战锤40K行星总督</h1>
<h2>服务器监控面板</h2>
<div class="stats">
<div class="stat-card">
<div class="stat-label">总在线用户</div>
<div class="stat-value" id="total-users">{{ total_users }}</div>
</div>
<div class="stat-card">
<div class="stat-label">游戏中用户</div>
<div class="stat-value" id="playing-users">{{ playing_users }}</div>
</div>
<div class="stat-card">
<div class="stat-label">注册用户总数</div>
<div class="stat-value">{{ registered_users }}</div>
</div>
</div>
<div class="user-list">
<h3>当前在线用户:</h3>
{% if active_users %}
{% for user in active_users %}
<div class="user-item">
<span>{{ user.username }}</span>
<span class="{{ user.status }}">{{ user.status }}</span>
</div>
{% endfor %}
{% else %}
<div class="user-item">当前没有用户在线</div>
{% endif %}
</div>
<button class="btn" onclick="refreshData()">刷新数据</button>
<div class="refresh-time">上次更新时间: {{ refresh_time }}</div>
</div>
<script>
function refreshData() {
fetch('/monitor/api/stats')
.then(response => response.json())
.then(data => {
document.getElementById('total-users').textContent = data.total_users;
document.getElementById('playing-users').textContent = data.playing_users;
// 更新刷新时间
const refreshTimeElement = document.querySelector('.refresh-time');
refreshTimeElement.textContent = '上次更新时间: ' + data.refresh_time;
// 自动刷新整个页面以更新用户列表
setTimeout(() => {
window.location.reload();
}, 100);
})
.catch(error => console.error('获取数据失败:', error));
}
// 每60秒自动刷新一次
setInterval(refreshData, 60000);
</script>
</body>
</html>
"""
def get_db_connection():
"""连接到SQLite数据库"""
db_path = os.path.join('data', 'warhammer.db')
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
return conn
def get_stats():
"""获取游戏统计数据"""
try:
conn = get_db_connection()
# 获取注册用户总数
registered_users = conn.execute('SELECT COUNT(*) as count FROM users').fetchone()['count']
# 获取活跃会话 (假设数据库中有sessions表根据实际情况调整)
# 如果没有sessions表需要创建或使用其他方式跟踪在线用户
try:
cursor = conn.execute('''
SELECT s.*, u.username
FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.last_activity > ?
''', (datetime.datetime.now() - datetime.timedelta(minutes=30),))
active_sessions = cursor.fetchall()
except sqlite3.OperationalError:
# 如果sessions表不存在尝试备选方案
try:
# 尝试从game_states表获取活跃用户
cursor = conn.execute('''
SELECT gs.*, u.username, u.id as user_id
FROM game_states gs
JOIN users u ON gs.user_id = u.id
WHERE gs.updated_at > ?
''', ((datetime.datetime.now() - datetime.timedelta(minutes=30)).isoformat(),))
active_sessions = cursor.fetchall()
# 添加状态字段模拟
for session in active_sessions:
# 假设最近30分钟内更新的都是在玩游戏的
session_dict = dict(session)
session_dict['status'] = 'playing'
session = session_dict
except:
# 备选方案也失败,返回空列表
active_sessions = []
# 计算游戏中的用户数
playing_users = sum(1 for session in active_sessions if session['status'] == 'playing')
# 格式化活跃用户列表
active_users = []
for session in active_sessions:
user_info = {
'username': session['username'],
'status': session['status'],
'user_id': session['user_id']
}
active_users.append(user_info)
conn.close()
return {
'registered_users': registered_users,
'total_users': len(active_sessions),
'playing_users': playing_users,
'active_users': active_users,
'refresh_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
except Exception as e:
print(f"获取统计数据时出错: {str(e)}")
return {
'registered_users': 0,
'total_users': 0,
'playing_users': 0,
'active_users': [],
'refresh_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'error': str(e)
}
@app.route('/monitor')
def monitor_page():
"""监控页面"""
stats = get_stats()
return render_template_string(HTML_TEMPLATE, **stats)
@app.route('/monitor/api/stats')
def api_stats():
"""统计数据API"""
return jsonify(get_stats())
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5050, debug=False)

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
Flask==2.3.3
Werkzeug==2.3.7

3
start.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/bash
cd /var/www/warhammer
gunicorn --bind 0.0.0.0:8080 app_sqlite:app

BIN
static/.DS_Store vendored Normal file

Binary file not shown.

1250
static/css/admin.css Normal file

File diff suppressed because it is too large Load Diff

645
static/css/custom_cards.css Normal file
View File

@ -0,0 +1,645 @@
/* 自制卡牌通用样式 */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 卡牌列表页样式 */
.filters {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
position: sticky; /* 使筛选器固定在顶部 */
top: 0;
z-index: 100;
background-color: rgba(10, 10, 10, 0.95);
padding: 15px 0;
width: 100%;
}
.filter-group {
display: flex;
align-items: center;
gap: 10px;
}
.filter-group select {
padding: 8px;
background-color: #222;
color: #e0e0e0;
border: 1px solid #444;
border-radius: 4px;
}
.cards-wrapper {
margin-top: 20px;
overflow-y: auto; /* 确保内容可滚动 */
padding-bottom: 50px; /* 添加底部空间 */
}
.cards-grid {
display: grid;
grid-template-columns: repeat(2, 1fr); /* 修改为一行显示两张卡牌 */
gap: 20px;
}
.card-item {
background-color: rgba(30, 30, 35, 0.8);
border: 1px solid #600;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
display: flex;
flex-direction: column;
height: 100%; /* 确保所有卡片高度相同 */
}
.card-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(153, 0, 0, 0.4);
background-color: rgba(96, 0, 0, 0.2);
}
.card-header {
padding: 15px;
border-bottom: 1px solid #333;
display: flex;
align-items: center;
gap: 10px;
}
.card-avatar {
font-size: 2rem;
}
.card-info h3 {
margin: 0 0 5px 0;
font-size: 1.2rem;
}
.card-info p {
margin: 0;
color: #999;
font-size: 0.9rem;
}
.card-content {
padding: 15px;
flex-grow: 1;
}
.card-title {
margin: 0 0 10px 0;
color: #d4af37;
}
.card-text {
margin: 0;
color: #e0e0e0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.card-footer {
padding: 10px 15px;
border-top: 1px solid #333;
display: flex;
justify-content: space-between;
align-items: center;
background-color: rgba(0, 0, 0, 0.2);
}
.card-creator {
font-size: 0.8rem;
color: #999;
}
.card-votes {
display: flex;
align-items: center;
gap: 5px;
font-size: 0.9rem;
}
.vote-positive {
color: #4caf50;
}
.vote-negative {
color: #f44336;
}
.vote-neutral {
color: #999;
}
.loading-indicator {
text-align: center;
padding: 40px;
grid-column: 1 / -1;
color: #999;
font-style: italic;
}
/* 弹窗样式 */
.card-modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.8);
animation: fadeIn 0.3s;
}
.modal-content {
background-color: rgba(30, 30, 35, 0.95);
margin: 5% auto;
padding: 20px;
border: 2px solid #600;
border-radius: 8px;
width: 90%;
max-width: 700px;
box-shadow: 0 0 30px rgba(153, 0, 0, 0.7);
animation: slideIn 0.3s;
max-height: 90vh;
overflow-y: auto;
}
.close-modal {
color: #999;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.3s;
}
.close-modal:hover {
color: #d4af37;
}
.card-detail-container {
margin-top: 20px;
}
.card-detail-container .card-title {
margin-bottom: 15px;
font-size: 1.8rem;
}
.card-detail-container .card-creator,
.card-detail-container .card-date {
margin-bottom: 10px;
color: #999;
font-size: 0.9rem;
}
.card-description {
margin: 20px 0;
padding: 15px;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 5px;
line-height: 1.6;
}
.card-options {
margin: 20px 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 640px) {
.card-options {
grid-template-columns: 1fr;
}
}
.card-options .option {
padding: 15px;
border-radius: 5px;
}
.card-options .option:first-child {
background-color: rgba(74, 111, 181, 0.2);
border: 1px solid rgba(74, 111, 181, 0.5);
}
.card-options .option:last-child {
background-color: rgba(179, 57, 57, 0.2);
border: 1px solid rgba(179, 57, 57, 0.5);
}
.card-options .option h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.2rem;
}
.effects {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.effect-tag {
display: inline-flex;
align-items: center;
padding: 5px 8px;
border-radius: 3px;
font-size: 0.9rem;
}
/* 新的效果标签样式 */
.effect-tag.positive {
background-color: rgba(40, 60, 40, 0.9); /* 深绿色/几乎黑色的背景 */
color: #ffffff; /* 纯白色文字 */
border: 1px solid #4caf50;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); /* 添加文字阴影增强可读性 */
}
.effect-tag.negative {
background-color: rgba(60, 40, 40, 0.9); /* 深红色/几乎黑色的背景 */
color: #ffffff; /* 纯白色文字 */
border: 1px solid #f44336;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); /* 添加文字阴影增强可读性 */
}
.card-voting {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #333;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.vote-count {
font-size: 1.2rem;
font-weight: bold;
}
.vote-buttons {
display: flex;
gap: 15px;
}
.btn-vote {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 15px;
font-size: 1rem;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
}
.upvote {
background-color: rgba(76, 175, 80, 0.2);
border: 1px solid rgba(76, 175, 80, 0.5);
color: #81c784;
}
.downvote {
background-color: rgba(244, 67, 54, 0.2);
border: 1px solid rgba(244, 67, 54, 0.5);
color: #e57373;
}
.upvote:hover {
background-color: rgba(76, 175, 80, 0.4);
box-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
}
.downvote:hover {
background-color: rgba(244, 67, 54, 0.4);
box-shadow: 0 0 10px rgba(244, 67, 54, 0.5);
}
.upvote.voted {
background-color: rgba(76, 175, 80, 0.6);
box-shadow: 0 0 10px rgba(76, 175, 80, 0.7);
}
.downvote.voted {
background-color: rgba(244, 67, 54, 0.6);
box-shadow: 0 0 10px rgba(244, 67, 54, 0.7);
}
/* 创建卡牌页面样式 */
.card-form-container {
background-color: rgba(30, 30, 35, 0.8);
border: 1px solid #600;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
margin-bottom: 40px; /* 添加底部空间确保滚动 */
overflow-y: visible; /* 确保内容可以滚动 */
}
.form-section {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #333;
}
.form-section:last-child {
border-bottom: none;
}
.form-section h2 {
margin-bottom: 20px;
font-size: 1.5rem;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #d4af37;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 10px;
background-color: #222;
border: 1px solid #444;
color: #fff;
border-radius: 4px;
font-size: 1rem;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #600;
box-shadow: 0 0 5px rgba(153, 0, 0, 0.5);
}
.form-group input[type="number"] {
width: 100px;
}
.required {
color: #f44336;
}
.character-count {
text-align: right;
font-size: 0.8rem;
color: #999;
margin-top: 5px;
}
.effects-group h3 {
margin-bottom: 15px;
font-size: 1.2rem;
color: #d4af37;
}
.effects-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
@media (max-width: 640px) {
.effects-grid {
grid-template-columns: 1fr;
}
}
.effect-item {
display: flex;
align-items: center;
gap: 10px;
}
.effect-item label {
width: 120px;
margin-bottom: 0;
}
.total-effects {
margin-top: 15px;
text-align: right;
font-size: 1rem;
font-weight: bold;
}
.effect-warning {
color: #f44336;
font-size: 0.9rem;
margin-top: 5px;
display: none;
}
.form-actions {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
/* 表情符号选择器 */
.emoji-picker {
margin-top: 10px;
padding: 10px;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 5px;
}
.emoji-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.emoji-item {
font-size: 1.5rem;
cursor: pointer;
transition: transform 0.2s;
}
.emoji-item:hover {
transform: scale(1.2);
}
/* 动画 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* 响应式适配 */
@media (max-width: 768px) {
.filters {
flex-direction: column;
}
.cards-grid {
grid-template-columns: repeat(2, 1fr); /* 保持一行两张卡牌 */
}
.modal-content {
width: 95%;
margin: 10% auto;
}
.card-options {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.container {
padding: 10px;
}
.cards-grid {
grid-template-columns: repeat(2, 1fr); /* 即使在小屏幕上也保持两列 */
gap: 10px; /* 减小间距 */
}
.form-actions {
flex-direction: column;
gap: 10px;
}
.form-actions button {
width: 100%;
}
/* 调整卡片内容,让它在小屏幕上更紧凑 */
.card-header {
padding: 10px;
}
.card-content {
padding: 10px;
}
.card-footer {
padding: 8px 10px;
}
.card-avatar {
font-size: 1.5rem;
}
.card-info h3 {
font-size: 1rem;
}
.card-info p {
font-size: 0.8rem;
}
}
/* 浅色主题适配 */
@media (prefers-color-scheme: light) {
.card-item, .card-form-container, .modal-content {
background-color: rgba(240, 240, 245, 0.9);
color: #333;
}
.card-title, h1, h2, h3 {
color: #600;
}
.card-text, .form-group label {
color: #333;
}
.form-group input, .form-group textarea, .form-group select {
background-color: #fff;
border-color: #ccc;
color: #333;
}
}
/* 确保可以滚动的页面设置 */
.custom-cards-page, .create-card-page {
overflow-y: auto !important;
height: auto !important;
position: relative !important;
}
/* 页面底部添加额外空间 */
.custom-cards-page footer, .create-card-page footer {
margin-top: 80px;
margin-bottom: 30px;
}
/* 空状态处理 */
.empty-state {
text-align: center;
padding: 40px 20px;
grid-column: 1 / -1;
}
.empty-state h2 {
margin-bottom: 15px;
}
.empty-state .btn {
margin-top: 20px;
}
/* 错误消息样式 */
.error-message {
background-color: rgba(153, 0, 0, 0.3);
border: 1px solid #600;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
color: #ff9999;
text-align: center;
}
/* 消息弹出提示样式 */
.message-popup {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
z-index: 10000;
transition: transform 0.3s ease;
text-align: center;
max-width: 80%;
}
.message-popup.show {
transform: translateX(-50%) translateY(0);
}

1343
static/css/style.css Normal file

File diff suppressed because it is too large Load Diff

BIN
static/images/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1024 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Some files were not shown because too many files have changed in this diff Show More