init
This commit is contained in:
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||||
|
"name": ".NET Core Launch (console)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/csharp/Chapter01/HelloCS/bin/Debug/net7.0/HelloCS.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/csharp/Chapter01/HelloCS",
|
||||||
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
"console": "internalConsole",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/csharp/Chapter01/HelloCS/HelloCS.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/csharp/Chapter01/HelloCS/HelloCS.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/csharp/Chapter01/HelloCS/HelloCS.csproj"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
191
GigaPong/pong.py
Normal file
191
GigaPong/pong.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import pygame
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Инициализация pygame
|
||||||
|
pygame.init()
|
||||||
|
|
||||||
|
# Константы
|
||||||
|
WIDTH, HEIGHT = 800, 600
|
||||||
|
PADDLE_WIDTH, PADDLE_HEIGHT = 10, 100
|
||||||
|
BALL_SIZE = 15
|
||||||
|
WHITE = (255, 255, 255)
|
||||||
|
FPS = 60
|
||||||
|
WINNING_SCORE = 10 # Основной счёт для победы
|
||||||
|
TIE_BREAK_REQUIRED_DIFFERENCE = 2 # Разница для победы в тай-брейке
|
||||||
|
AI_SPEED = 0.7 # Уровень "умения" ИИ (0.5 - очень медленный, 1.0 - идеальный)
|
||||||
|
|
||||||
|
# Создание экрана
|
||||||
|
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
||||||
|
pygame.display.set_caption("Pong с ИИ")
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
# Функция для сброса игры
|
||||||
|
def reset_game():
|
||||||
|
return {
|
||||||
|
"player_paddle": pygame.Rect(20, HEIGHT//2 - PADDLE_HEIGHT//2, PADDLE_WIDTH, PADDLE_HEIGHT),
|
||||||
|
"ai_paddle": pygame.Rect(WIDTH - 30, HEIGHT//2 - PADDLE_HEIGHT//2, PADDLE_WIDTH, PADDLE_HEIGHT),
|
||||||
|
"ball": pygame.Rect(WIDTH//2 - BALL_SIZE//2, HEIGHT//2 - BALL_SIZE//2, BALL_SIZE, BALL_SIZE),
|
||||||
|
"ball_speed_x": 5,
|
||||||
|
"ball_speed_y": 5,
|
||||||
|
"player_score": 0,
|
||||||
|
"ai_score": 0,
|
||||||
|
"game_over": False,
|
||||||
|
"tie_break_active": False,
|
||||||
|
"win_message": None,
|
||||||
|
"show_win_screen": False,
|
||||||
|
"restart_timer": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Состояние игры
|
||||||
|
game_state = reset_game()
|
||||||
|
|
||||||
|
# Шрифты
|
||||||
|
score_font = pygame.font.Font(None, 74)
|
||||||
|
win_font = pygame.font.Font(None, 100)
|
||||||
|
|
||||||
|
def move_ai_paddle(paddle, ball):
|
||||||
|
"""Логика ИИ: ракетка следует за мячом с задержкой"""
|
||||||
|
if ball.centerx > WIDTH/2: # Реагируем только на мяч, приближающийся к ИИ
|
||||||
|
if paddle.centery < ball.centery:
|
||||||
|
paddle.y += min(7 * AI_SPEED, ball.centery - paddle.centery)
|
||||||
|
elif paddle.centery > ball.centery:
|
||||||
|
paddle.y -= min(7 * AI_SPEED, paddle.centery - ball.centery)
|
||||||
|
|
||||||
|
# Ограничение границ экрана
|
||||||
|
paddle.y = max(0, min(HEIGHT - PADDLE_HEIGHT, paddle.y))
|
||||||
|
|
||||||
|
def calculate_ball_angle(contact_point, paddle_center):
|
||||||
|
"""Рассчитывает угол отскока мяча в зависимости от точки контакта"""
|
||||||
|
contact_diff = contact_point - paddle_center
|
||||||
|
angle_factor = contact_diff / (PADDLE_HEIGHT/2) # От -1 до +1
|
||||||
|
|
||||||
|
# Более резкий угол для ударов по краям ракетки
|
||||||
|
return angle_factor * 0.5 # 0.5 - коэффициент максимального изменения угла
|
||||||
|
|
||||||
|
# Основной цикл игры
|
||||||
|
while True:
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
if not game_state["show_win_screen"]:
|
||||||
|
# Обработка управления
|
||||||
|
keys = pygame.key.get_pressed()
|
||||||
|
if keys[pygame.K_w] and game_state["player_paddle"].top > 0:
|
||||||
|
game_state["player_paddle"].y -= 7
|
||||||
|
if keys[pygame.K_s] and game_state["player_paddle"].bottom < HEIGHT:
|
||||||
|
game_state["player_paddle"].y += 7
|
||||||
|
|
||||||
|
# Движение мяча
|
||||||
|
game_state["ball"].x += game_state["ball_speed_x"]
|
||||||
|
game_state["ball"].y += game_state["ball_speed_y"]
|
||||||
|
|
||||||
|
# Отскок от стен
|
||||||
|
if game_state["ball"].top <= 0 or game_state["ball"].bottom >= HEIGHT:
|
||||||
|
game_state["ball_speed_y"] *= -1
|
||||||
|
|
||||||
|
# Проверка на гол
|
||||||
|
if game_state["ball"].left <= 0:
|
||||||
|
game_state["ai_score"] += 1
|
||||||
|
|
||||||
|
# Проверка условий победы после гола
|
||||||
|
if game_state["ai_score"] == WINNING_SCORE:
|
||||||
|
if game_state["player_score"] == WINNING_SCORE - 1:
|
||||||
|
game_state["tie_break_active"] = True
|
||||||
|
elif game_state["player_score"] <= WINNING_SCORE - 2:
|
||||||
|
game_state["win_message"] = "КОМПЬЮТЕР ПОБЕДИЛ!"
|
||||||
|
game_state["show_win_screen"] = True
|
||||||
|
game_state["game_over"] = True
|
||||||
|
game_state["restart_timer"] = pygame.time.get_ticks() + 1000
|
||||||
|
|
||||||
|
# Проверка условий победы в тай-брейке
|
||||||
|
if game_state["tie_break_active"] and abs(game_state["ai_score"] - game_state["player_score"]) >= TIE_BREAK_REQUIRED_DIFFERENCE:
|
||||||
|
game_state["win_message"] = "КОМПЬЮТЕР ПОБЕДИЛ!"
|
||||||
|
game_state["show_win_screen"] = True
|
||||||
|
game_state["game_over"] = True
|
||||||
|
game_state["restart_timer"] = pygame.time.get_ticks() + 1000
|
||||||
|
|
||||||
|
game_state["ball"].center = (WIDTH//2, HEIGHT//2)
|
||||||
|
game_state["ball_speed_x"] = 5
|
||||||
|
game_state["ball_speed_y"] = 5 * random.choice([-1, 1])
|
||||||
|
pygame.time.delay(500)
|
||||||
|
|
||||||
|
elif game_state["ball"].right >= WIDTH:
|
||||||
|
game_state["player_score"] += 1
|
||||||
|
|
||||||
|
# Проверка условий победы после гола
|
||||||
|
if game_state["player_score"] == WINNING_SCORE:
|
||||||
|
if game_state["ai_score"] == WINNING_SCORE - 1:
|
||||||
|
game_state["tie_break_active"] = True
|
||||||
|
elif game_state["ai_score"] <= WINNING_SCORE - 2:
|
||||||
|
game_state["win_message"] = "ТЫ ПОБЕДИЛ!"
|
||||||
|
game_state["show_win_screen"] = True
|
||||||
|
game_state["game_over"] = True
|
||||||
|
game_state["restart_timer"] = pygame.time.get_ticks() + 1000
|
||||||
|
|
||||||
|
# Проверка условий победы в тай-брейке
|
||||||
|
if game_state["tie_break_active"] and abs(game_state["player_score"] - game_state["ai_score"]) >= TIE_BREAK_REQUIRED_DIFFERENCE:
|
||||||
|
game_state["win_message"] = "ТЫ ПОБЕДИЛ!"
|
||||||
|
game_state["show_win_screen"] = True
|
||||||
|
game_state["game_over"] = True
|
||||||
|
game_state["restart_timer"] = pygame.time.get_ticks() + 1000
|
||||||
|
|
||||||
|
game_state["ball"].center = (WIDTH//2, HEIGHT//2)
|
||||||
|
game_state["ball_speed_x"] = -5
|
||||||
|
game_state["ball_speed_y"] = 5 * random.choice([-1, 1])
|
||||||
|
pygame.time.delay(500)
|
||||||
|
|
||||||
|
# Отскок от ракеток с учётом угла
|
||||||
|
if game_state["ball"].colliderect(game_state["player_paddle"]):
|
||||||
|
contact_point = game_state["ball"].centery
|
||||||
|
paddle_center = game_state["player_paddle"].centery
|
||||||
|
|
||||||
|
angle_change = calculate_ball_angle(contact_point, paddle_center)
|
||||||
|
game_state["ball_speed_y"] += angle_change
|
||||||
|
game_state["ball_speed_x"] *= -1.1
|
||||||
|
|
||||||
|
elif game_state["ball"].colliderect(game_state["ai_paddle"]):
|
||||||
|
contact_point = game_state["ball"].centery
|
||||||
|
paddle_center = game_state["ai_paddle"].centery
|
||||||
|
|
||||||
|
angle_change = calculate_ball_angle(contact_point, paddle_center)
|
||||||
|
game_state["ball_speed_y"] += angle_change
|
||||||
|
game_state["ball_speed_x"] *= -1.1
|
||||||
|
|
||||||
|
# Обновление позиции ИИ
|
||||||
|
if not game_state["game_over"]:
|
||||||
|
move_ai_paddle(game_state["ai_paddle"], game_state["ball"])
|
||||||
|
|
||||||
|
# Обработка события нажатия клавиши для перезапуска
|
||||||
|
if game_state["show_win_screen"]:
|
||||||
|
keys = pygame.key.get_pressed()
|
||||||
|
if any(keys): # Любая клавиша
|
||||||
|
game_state = reset_game()
|
||||||
|
|
||||||
|
# Отрисовка
|
||||||
|
screen.fill((0, 0, 0))
|
||||||
|
|
||||||
|
if not game_state["show_win_screen"]:
|
||||||
|
# Отображение счета
|
||||||
|
score_text = score_font.render(f"{game_state['player_score']} {game_state['ai_score']}", True, WHITE)
|
||||||
|
screen.blit(score_text, (WIDTH//2 - 50, 10))
|
||||||
|
|
||||||
|
# Текст тай-брейка
|
||||||
|
if game_state["tie_break_active"]:
|
||||||
|
tie_text = score_font.render("TIE BREAK!", True, WHITE)
|
||||||
|
screen.blit(tie_text, (WIDTH//2 - 100, HEIGHT//2))
|
||||||
|
|
||||||
|
# Основные элементы игры
|
||||||
|
pygame.draw.rect(screen, WHITE, game_state["player_paddle"])
|
||||||
|
pygame.draw.rect(screen, WHITE, game_state["ai_paddle"])
|
||||||
|
pygame.draw.ellipse(screen, WHITE, game_state["ball"])
|
||||||
|
pygame.draw.aaline(screen, WHITE, (WIDTH//2, 0), (WIDTH//2, HEIGHT))
|
||||||
|
else:
|
||||||
|
# Экран победы
|
||||||
|
win_text = win_font.render(game_state["win_message"], True, WHITE)
|
||||||
|
text_rect = win_text.get_rect(center=(WIDTH//2, HEIGHT//2))
|
||||||
|
screen.blit(win_text, text_rect)
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
clock.tick(FPS)
|
||||||
14
UltraChat.code-workspace
Normal file
14
UltraChat.code-workspace
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "csharp/Chapter01/HelloCS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "repos"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
BIN
__pycache__/huy.cpython-310.pyc
Normal file
BIN
__pycache__/huy.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/pytchat.cpython-310.pyc
Normal file
BIN
__pycache__/pytchat.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/twitch.cpython-310.pyc
Normal file
BIN
__pycache__/twitch.cpython-310.pyc
Normal file
Binary file not shown.
BIN
back-coin.jpg
Normal file
BIN
back-coin.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 426 KiB |
BIN
clannad_parser/00414.scr
Normal file
BIN
clannad_parser/00414.scr
Normal file
Binary file not shown.
BIN
clannad_parser/00416.scr
Normal file
BIN
clannad_parser/00416.scr
Normal file
Binary file not shown.
BIN
clannad_parser/00417.scr
Normal file
BIN
clannad_parser/00417.scr
Normal file
Binary file not shown.
BIN
clannad_parser/00418.scr
Normal file
BIN
clannad_parser/00418.scr
Normal file
Binary file not shown.
BIN
clannad_parser/00419.scr
Normal file
BIN
clannad_parser/00419.scr
Normal file
Binary file not shown.
BIN
clannad_parser/00420.scr
Normal file
BIN
clannad_parser/00420.scr
Normal file
Binary file not shown.
BIN
clannad_parser/00421.scr
Normal file
BIN
clannad_parser/00421.scr
Normal file
Binary file not shown.
BIN
clannad_parser/00422.scr
Normal file
BIN
clannad_parser/00422.scr
Normal file
Binary file not shown.
BIN
clannad_parser/00423.scr
Normal file
BIN
clannad_parser/00423.scr
Normal file
Binary file not shown.
BIN
clannad_parser/00424.scr
Normal file
BIN
clannad_parser/00424.scr
Normal file
Binary file not shown.
BIN
clannad_parser/00425.scr
Normal file
BIN
clannad_parser/00425.scr
Normal file
Binary file not shown.
BIN
clannad_parser/00429.scr
Normal file
BIN
clannad_parser/00429.scr
Normal file
Binary file not shown.
BIN
clannad_parser/02426.scr
Normal file
BIN
clannad_parser/02426.scr
Normal file
Binary file not shown.
BIN
clannad_parser/02509.scr
Normal file
BIN
clannad_parser/02509.scr
Normal file
Binary file not shown.
BIN
clannad_parser/04425.scr
Normal file
BIN
clannad_parser/04425.scr
Normal file
Binary file not shown.
BIN
clannad_parser/04503.scr
Normal file
BIN
clannad_parser/04503.scr
Normal file
Binary file not shown.
BIN
clannad_parser/04508.scr
Normal file
BIN
clannad_parser/04508.scr
Normal file
Binary file not shown.
BIN
clannad_parser/05424.scr
Normal file
BIN
clannad_parser/05424.scr
Normal file
Binary file not shown.
BIN
clannad_parser/05430.scr
Normal file
BIN
clannad_parser/05430.scr
Normal file
Binary file not shown.
BIN
clannad_parser/07400.scr
Normal file
BIN
clannad_parser/07400.scr
Normal file
Binary file not shown.
BIN
clannad_parser/07401.scr
Normal file
BIN
clannad_parser/07401.scr
Normal file
Binary file not shown.
BIN
clannad_parser/07500.scr
Normal file
BIN
clannad_parser/07500.scr
Normal file
Binary file not shown.
BIN
clannad_parser/07600.scr
Normal file
BIN
clannad_parser/07600.scr
Normal file
Binary file not shown.
74
clannad_parser/parser.py
Normal file
74
clannad_parser/parser.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
def parse_text_command(data, pos):
|
||||||
|
# Читаем параметры в Little Endian
|
||||||
|
text_id = int.from_bytes(data[pos:pos+2], 'little')
|
||||||
|
voice_id = int.from_bytes(data[pos+2:pos+6], 'little')
|
||||||
|
pos += 6
|
||||||
|
|
||||||
|
# Собираем текст до 0000
|
||||||
|
text_bytes = bytearray()
|
||||||
|
while pos + 2 <= len(data):
|
||||||
|
char_bytes = data[pos:pos+2]
|
||||||
|
if char_bytes == b'\x00\x00':
|
||||||
|
break
|
||||||
|
text_bytes.extend(char_bytes)
|
||||||
|
pos += 2
|
||||||
|
|
||||||
|
# Декодируем UTF-16LE текст
|
||||||
|
text = text_bytes.decode('utf-16le', errors='replace')
|
||||||
|
total_size = 8 + len(text_bytes) + 2 # 8 байт заголовка + текст + 0000
|
||||||
|
return f"TextID: {text_id}, VoiceID: {voice_id}, Text: {text}", total_size
|
||||||
|
|
||||||
|
# База команд (идентификаторы в Big Endian)
|
||||||
|
command_db = {
|
||||||
|
0x1401: {
|
||||||
|
"name": "PlayBGM",
|
||||||
|
"size": 2,
|
||||||
|
"handler": lambda data, pos: (f"BGM Track: {int.from_bytes(data[pos:pos+2], 'little')}", 2)
|
||||||
|
},
|
||||||
|
0x0A00: {
|
||||||
|
"name": "ShowText",
|
||||||
|
"size": 8,
|
||||||
|
"handler": parse_text_command
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_script(file_path):
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
pos = 0
|
||||||
|
output = []
|
||||||
|
|
||||||
|
while pos + 2 <= len(data):
|
||||||
|
# Определяем команду в Big Endian
|
||||||
|
cmd = int.from_bytes(data[pos:pos+2], 'big')
|
||||||
|
|
||||||
|
if cmd in command_db:
|
||||||
|
cmd_info = command_db[cmd]
|
||||||
|
result, size = cmd_info["handler"](data, pos + 2)
|
||||||
|
output.append(f"[0x{pos:08X}][0x{cmd:04X}] {cmd_info['name']}: {result}")
|
||||||
|
pos += 2 + size # 2 байта команды + размер данных
|
||||||
|
|
||||||
|
# Выравнивание по 4 байтам
|
||||||
|
while pos % 4 != 0:
|
||||||
|
pos += 1
|
||||||
|
else:
|
||||||
|
# output.append(f"[0x{pos:08X}][0x{cmd:04X}] UNKNOWN COMMAND")
|
||||||
|
pos += 2
|
||||||
|
|
||||||
|
# Сохраняем в файл
|
||||||
|
with open("script" + file_path + ".txt", "w", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(output))
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python parser.py <script.bin>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
results = parse_script(sys.argv[1])
|
||||||
|
for line in results:
|
||||||
|
print(line)
|
||||||
16956
clannad_parser/script00414.scr.txt
Normal file
16956
clannad_parser/script00414.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
1806
clannad_parser/script00416.scr.txt
Normal file
1806
clannad_parser/script00416.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
2018
clannad_parser/script00417.scr.txt
Normal file
2018
clannad_parser/script00417.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
16956
clannad_parser/script_output.txt
Normal file
16956
clannad_parser/script_output.txt
Normal file
File diff suppressed because it is too large
Load Diff
13
config.ini
Normal file
13
config.ini
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Telegram]
|
||||||
|
bot_secret = 6454033742:AAFj2rUoVb2jJ_Lew4eiIecdr7s7xbrqeNU
|
||||||
|
enabled = 1
|
||||||
|
|
||||||
|
[Youtube]
|
||||||
|
video_id = coYw-eVU0Ks
|
||||||
|
|
||||||
|
[Twitch]
|
||||||
|
channel = wanderbraun
|
||||||
|
|
||||||
|
[Alerts]
|
||||||
|
app_id = 12554
|
||||||
|
api_key = 7P95Bm7StPjosxSoD9GVQRykqy9J61MdiNn9sdMP
|
||||||
14
govno/async_test.py
Normal file
14
govno/async_test.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# First we need the asyncio library
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# Then we need a loop to work with
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
# We also need something to run
|
||||||
|
async def main():
|
||||||
|
for char in 'Hello, world!\n':
|
||||||
|
print(char, end='', flush=True)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# Then, we need to run the loop with a task
|
||||||
|
loop.run_until_complete(main())
|
||||||
29
govno/import.blade.php
Normal file
29
govno/import.blade.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
@extends('layout')
|
||||||
|
|
||||||
|
@section('title')Главная страница@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="bg-dark p-5 rounded">
|
||||||
|
@auth
|
||||||
|
<p class="lead">{{ Auth::user()->name }}, Вы залогинены. Поздравляем.</p>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<p>Дорогие друзья!</p>
|
||||||
|
<p>Спасибо, что посетили эту страницу.</p>
|
||||||
|
<p>Если вы залогинены, то можете просматривать книги и оставлять комментарии как обычный пользователь.</p>
|
||||||
|
<p>Если Администратор включил вашей учётной записи опцию "сотрудник", то можете редактировать и добавлять книги и категории.</p>
|
||||||
|
<p>Вы можете войти как Администратор, используя admin@domain.com / password</p>
|
||||||
|
<p>Или просто выбрать нужную категорию книг:</p>
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
@foreach ($cats as $el)
|
||||||
|
|
||||||
|
<li><a class="nav-link px-2 text-white" href={{ route('categories.show', $el->slug) }}>{{ $el->title }}</a></li>
|
||||||
|
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@endsection
|
||||||
1
govno/messages.json
Normal file
1
govno/messages.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"id": 10921, "type": "tg", "date": "2023-01-22T11:57:37+05:00", "sendr": "\u0410\u0440\u0442\u0451\u043c \u0410\u0440\u0442\u0451\u043c\u043e\u0432", "msg": "\u041a\u0440\u0443\u0442\u043e\u0439 \u0441\u0442\u0440\u0438\u043c, \u0440\u0435\u0441\u043f\u0435\u043a\u0442"}, {"id": 10922, "type": "tg", "date": "2023-01-22T12:08:24+05:00", "sendr": "\u0418\u0433\u043e\u0440\u044c \u0413\u043e\u0440\u043d\u043e\u0441\u0442\u0430\u0435\u0432", "msg": "\ud83d\udcaa"}, {"id": 10923, "type": "tg", "date": "2023-01-22T12:49:31+05:00", "sendr": "\u0414\u0456\u043c\u0430", "msg": "\u0421\u043f\u0430\u0441\u0438\u0431\u043e, \u0434\u043e\u0436\u0434\u0430\u043b\u0441\u044f."}, {"id": 10924, "type": "tg", "date": "2023-01-22T12:59:13+05:00", "sendr": "Heliand", "msg": "\u041e! \u042f \u043f\u043e\u043c\u043d\u044e \u044d\u0442\u043e\u0442 \u0434\u0435\u043d\u044c!"}, {"id": 10926, "type": "tg", "date": "2023-01-22T13:19:42+05:00", "sendr": "Taburet", "msg": "\u0416\u0434\u0451\u043c ep"}, {"id": 11026, "type": "tg", "date": "2023-01-25T12:06:07+05:00", "sendr": "\u041c\u0438\u0445\u0430\u0438\u043b \u0410\u0440\u0437\u044f\u0435\u0432", "msg": "\u0428\u0435\u043c\u0435\u0442, \u0440\u0430\u0434\u0438 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0430 \u043f\u043e\u0441\u043b\u0435 \u0435\u0440 \u0437\u0430\u0439\u043c\u0435\u0448\u044c\u0441\u044f \u0438\u043d\u044b\u043c\u0438 \u0442\u0430\u0439\u0442\u043b\u0430\u043c\u0438 \u0430\u0442\u043b\u0443\u0441 \u043d\u0430 \u043f\u04411?"}, {"id": 11027, "type": "tg", "date": "2023-01-25T15:40:19+05:00", "sendr": "P2 IS EP RUS", "msg": "\u041f\u043e\u0442\u043e\u043c \u0431\u0443\u0434\u0435\u0442 \u0435\u0449\u0451 \u043f2 \u0434\u043b\u044f psp, \u0442\u0430\u043c \u0445\u0432\u0430\u0442\u0430\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u044b :)"}, {"id": 11051, "type": "tg", "date": "2023-01-26T15:01:22+05:00", "sendr": "\u041c\u0438\u0445\u0430\u0438\u043b \u0410\u0440\u0437\u044f\u0435\u0432", "msg": "\u041d\u0443 \u0443 \u0432\u0441\u0435\u0445 \u0438\u0445 \u043f\u0441\u043f \u0442\u0430\u0439\u0442\u043b\u043e\u0432 \u0432\u0440\u043e\u0434\u0435 \u043a\u0430\u043a \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0431\u043b\u0438\u0437\u043a\u0430\u044f. \u0414\u0430\u0436\u0435 \u0443 \u0434\u0435\u0432\u0438\u043b\u0441\u0430\u043c\u043c\u043e\u043d\u0435\u0440\u0430."}, {"id": 11052, "type": "tg", "date": "2023-01-26T15:01:37+05:00", "sendr": "\u041c\u0438\u0445\u0430\u0438\u043b \u0410\u0440\u0437\u044f\u0435\u0432", "msg": "\u041a\u0441\u0442\u0430\u0442\u0438, \u0428\u0435\u043c\u0435\u0442. \u0421\u043b\u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438 \u0441\u0430\u043c\u043c\u043e\u043d\u0435\u0440\u0430 \u0440\u0430\u043d\u044c\u0448\u0435 \u0418\u0432\u0430\u043a\u0443\u0440\u044b?"}]
|
||||||
39
govno/messages_yt.json
Normal file
39
govno/messages_yt.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"author": {
|
||||||
|
"badgeUrl": "",
|
||||||
|
"type": "",
|
||||||
|
"isVerified": false,
|
||||||
|
"isChatOwner": false,
|
||||||
|
"isChatSponsor": false,
|
||||||
|
"isChatModerator": false,
|
||||||
|
"channelId": "UCUM4JfsjCQW25ZtDYq75Wkw",
|
||||||
|
"channelUrl": "http://www.youtube.com/channel/UCUM4JfsjCQW25ZtDYq75Wkw",
|
||||||
|
"name": "さんなー",
|
||||||
|
"imageUrl": "https://yt4.ggpht.com/ytc/AL5GRJVIdNw2w_SquPxEXWws9mBggNNUNWTGAb1WeHok=s64-c-k-c0x00ffffff-no-rj"
|
||||||
|
},
|
||||||
|
"type": "textMessage",
|
||||||
|
"id": "CjsKGkNKem5tWjdkM2Z3Q0ZjRUJmUW9kV080SklBEh1DT0cxMmVDNDNmd0NGYUpMblFrZDQtWUtEZy03NA%3D%3D",
|
||||||
|
"timestamp": 1674478157776,
|
||||||
|
"elapsedTime": "",
|
||||||
|
"datetime": "2023-01-23 17:49:17",
|
||||||
|
"message": "雪の日出勤には危険手当てだせ:megaphone::rabbit2:",
|
||||||
|
"messageEx": [
|
||||||
|
"雪の日出勤には危険手当てだせ",
|
||||||
|
{
|
||||||
|
"id": "📣",
|
||||||
|
"txt": ":megaphone:",
|
||||||
|
"url": "https://www.youtube.com/s/gaming/emoji/0f0cae22/emoji_u1f4e3.svg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "🐇",
|
||||||
|
"txt": ":rabbit2:",
|
||||||
|
"url": "https://www.youtube.com/s/gaming/emoji/0f0cae22/emoji_u1f407.svg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"amountValue": 0,
|
||||||
|
"amountString": "",
|
||||||
|
"currency": "",
|
||||||
|
"bgColor": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
49
govno/mychat.py
Normal file
49
govno/mychat.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
import json
|
||||||
|
|
||||||
|
from telethon import TelegramClient
|
||||||
|
from telethon.errors import SessionPasswordNeededError
|
||||||
|
|
||||||
|
|
||||||
|
# Reading Configs
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read("E:/Games/cgi-bin/config.ini")
|
||||||
|
|
||||||
|
# Setting configuration values
|
||||||
|
api_id = config['Telegram']['api_id']
|
||||||
|
api_hash = config['Telegram']['api_hash']
|
||||||
|
|
||||||
|
api_hash = str(api_hash)
|
||||||
|
|
||||||
|
phone = config['Telegram']['phone']
|
||||||
|
username = config['Telegram']['username']
|
||||||
|
|
||||||
|
|
||||||
|
# Create the client and connect
|
||||||
|
client = TelegramClient(username, api_id, api_hash)
|
||||||
|
client.start()
|
||||||
|
print("Client Created")
|
||||||
|
# Ensure you're authorized
|
||||||
|
""" if not client.is_user_authorized():
|
||||||
|
client.send_code_request(phone)
|
||||||
|
try:
|
||||||
|
client.sign_in(phone, input('Enter the code: '))
|
||||||
|
except SessionPasswordNeededError:
|
||||||
|
client.sign_in(password=input('Password: ')) """
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print('Content-type:text/html')
|
||||||
|
print('\r\n\r\n')
|
||||||
|
print('<html>')
|
||||||
|
print('<head>')
|
||||||
|
print('<title>Hello World - First CGI Program</title>')
|
||||||
|
print('</head>')
|
||||||
|
print('<body>')
|
||||||
|
|
||||||
|
print('<h2>Hello World! This is my first CGI program</h2>')
|
||||||
|
print(api_hash)
|
||||||
|
print('</body>')
|
||||||
|
print('</html>')
|
||||||
7
govno/pytchat___.py
Normal file
7
govno/pytchat___.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import pytchat
|
||||||
|
import time
|
||||||
|
|
||||||
|
chat = pytchat.create(video_id="uIx8l2xlYVY")
|
||||||
|
while chat.is_alive():
|
||||||
|
print(chat.get().json())
|
||||||
|
time.sleep(5)
|
||||||
7
govno/readfile.php
Normal file
7
govno/readfile.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?
|
||||||
|
|
||||||
|
$file = 'D:\Games\Tecmo Cup - Football Game (Europe).nes';
|
||||||
|
header('Content-Type: application/x-binary');
|
||||||
|
header('Content-Disposition: attachment; filename="govno.nes');
|
||||||
|
print(' GOVNO ');
|
||||||
|
//readfile($file);
|
||||||
16
govno/server.conf
Normal file
16
govno/server.conf
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
description "Example JSON HTTP server"
|
||||||
|
author "nitaku - matteo.abrate@gmail.com"
|
||||||
|
|
||||||
|
start on started mountall
|
||||||
|
stop on shutdown
|
||||||
|
|
||||||
|
respawn
|
||||||
|
respawn limit 99 5
|
||||||
|
|
||||||
|
script
|
||||||
|
exec sudo -u www-data /usr/bin/python /data/examples/python_minimal_http/server.py 8009 >> /dev/null 2>> /dev/null
|
||||||
|
|
||||||
|
end script
|
||||||
|
|
||||||
|
post-start script
|
||||||
|
end script
|
||||||
55
govno/server.py
Normal file
55
govno/server.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
#import SocketServer
|
||||||
|
import json
|
||||||
|
import cgi
|
||||||
|
|
||||||
|
class Server(BaseHTTPRequestHandler):
|
||||||
|
def _set_headers(self):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'text/html')
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def do_HEAD(self):
|
||||||
|
self._set_headers()
|
||||||
|
|
||||||
|
# GET sends back a Hello world message
|
||||||
|
def do_GET(self):
|
||||||
|
self._set_headers()
|
||||||
|
self.wfile.write(json.dumps({'hello': 'world', 'received': 'ok'}).encode('utf-8'))
|
||||||
|
# self.wfile.write('GET request for {}'.format(self.path).encode('utf-8'))
|
||||||
|
|
||||||
|
# POST echoes the message adding a JSON field
|
||||||
|
def do_POST(self):
|
||||||
|
ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
|
||||||
|
|
||||||
|
# refuse to receive non-json content
|
||||||
|
if ctype != 'application/json':
|
||||||
|
self.send_response(400)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
|
||||||
|
# read the message and convert it into a python dictionary
|
||||||
|
length = int(self.headers.getheader('content-length'))
|
||||||
|
message = json.loads(self.rfile.read(length))
|
||||||
|
|
||||||
|
# add a property to the object, just to mess with data
|
||||||
|
message['received'] = 'ok'
|
||||||
|
|
||||||
|
# send the message back
|
||||||
|
self._set_headers()
|
||||||
|
self.wfile.write(json.dumps(message))
|
||||||
|
|
||||||
|
def run(server_class=HTTPServer, handler_class=Server, port=8008):
|
||||||
|
server_address = ('', port)
|
||||||
|
httpd = server_class(server_address, handler_class)
|
||||||
|
|
||||||
|
print('Starting httpd on port %d...' % port)
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from sys import argv
|
||||||
|
|
||||||
|
if len(argv) == 2:
|
||||||
|
run(port=int(argv[1]))
|
||||||
|
else:
|
||||||
|
run()
|
||||||
BIN
govno/sshemet.session
Normal file
BIN
govno/sshemet.session
Normal file
Binary file not shown.
32
govno/yt.Id_540000269690007.py
Normal file
32
govno/yt.Id_540000269690007.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import pytchat
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
class DateTimeEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
if isinstance(o, datetime):
|
||||||
|
return o.isoformat()
|
||||||
|
|
||||||
|
if isinstance(o, bytes):
|
||||||
|
return list(o)
|
||||||
|
|
||||||
|
return json.JSONEncoder.default(self, o)
|
||||||
|
|
||||||
|
|
||||||
|
chat = pytchat.create(video_id="coYw-eVU0Ks")
|
||||||
|
while chat.is_alive():
|
||||||
|
#msg = chat.get().json()
|
||||||
|
'''with open('E:/Games/cgi-bin/messages_yt.json', 'w') as outfile:
|
||||||
|
json.dump(msg, outfile, cls=DateTimeEncoder)
|
||||||
|
break'''
|
||||||
|
time.sleep(2)
|
||||||
|
for c in chat.get().items:
|
||||||
|
#print(c.json())
|
||||||
|
print(c.author.name + ': ' + c.message)
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
# Each chat item can also be output in JSON format.
|
||||||
|
for c in chat.get().items:
|
||||||
|
print(c.json())
|
||||||
|
'''
|
||||||
32
govno/yt.py
Normal file
32
govno/yt.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import pytchat
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
class DateTimeEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
if isinstance(o, datetime):
|
||||||
|
return o.isoformat()
|
||||||
|
|
||||||
|
if isinstance(o, bytes):
|
||||||
|
return list(o)
|
||||||
|
|
||||||
|
return json.JSONEncoder.default(self, o)
|
||||||
|
|
||||||
|
|
||||||
|
chat = pytchat.create(video_id="coYw-eVU0Ks")
|
||||||
|
while chat.is_alive():
|
||||||
|
#msg = chat.get().json()
|
||||||
|
'''with open('E:/Games/cgi-bin/messages_yt.json', 'w') as outfile:
|
||||||
|
json.dump(msg, outfile, cls=DateTimeEncoder)
|
||||||
|
break'''
|
||||||
|
time.sleep(2)
|
||||||
|
for c in chat.get().items:
|
||||||
|
#print(c.json())
|
||||||
|
print(c.author.name + ': ' + c.message)
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
# Each chat item can also be output in JSON format.
|
||||||
|
for c in chat.get().items:
|
||||||
|
print(c.json())
|
||||||
|
'''
|
||||||
343
huy.py
Normal file
343
huy.py
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
#Alerts autorization API
|
||||||
|
#https://www.donationalerts.com/oauth/authorize?redirect_uri=http://127.0.0.1/logined&response_type=code&scope=oauth-donation-index&client_id=12552
|
||||||
|
|
||||||
|
#http://127.0.0.1/logined?code=def5020000c8c717bd3993ac0bc29b903eb910fcabac108c2ac9dcaf4c3858b8ba61057915c4a2dd2599b83bebe509a2c771e2a3f4f2581d1db3a6ef5f01331aefdd8956610115e0ec0a6b1292cce0e285c5d38400ff83f79f02f402c2c73f2bde29214728229f2a2580fd36cc269bdbd75d8d2bade73eb8baa04844e0e437e5a1b856abfd4309580b6999d92d4529774289b409a72d81aada4ce4f3164febdab347372c2f9ea6a2038d9fbfef6c2942328b6187c4e6967eaa52530a6183cb1529bcee61279e43c176fcfff291d6f5d410c3648bf2f7497370ccf1bbf21298148f42e93fbcf9821ed307b97070cda02942c4ee7a552e61d3501718d8895da0631745cb3988d37fd73770d9112e56e84b431e80463efa21d6b73a59b5c176ad4adb55a85ff24e9981765ae48c2431e068f77b2982327b4e7dc9535c3045fb8ad6abab2167fd1dcf7144f5752dc517392822d509d7f342266ec24acfac1f0b6c3cb3beb9769168a5a4167cf067c27eabebf0fe79bdd6e59cf6
|
||||||
|
|
||||||
|
#https://www.donationalerts.com/oauth/token?grant_type=authorization_code&client_id=12552&client_secret=EwOBapII6syZy0k9z7AkEVhtUNDRe1VzTHW4AcmZ&redirect_uri=http://127.0.0.1/logined?code=def5020000c8c717bd3993ac0bc29b903eb910fcabac108c2ac9dcaf4c3858b8ba61057915c4a2dd2599b83bebe509a2c771e2a3f4f2581d1db3a6ef5f01331aefdd8956610115e0ec0a6b1292cce0e285c5d38400ff83f79f02f402c2c73f2bde29214728229f2a2580fd36cc269bdbd75d8d2bade73eb8baa04844e0e437e5a1b856abfd4309580b6999d92d4529774289b409a72d81aada4ce4f3164febdab347372c2f9ea6a2038d9fbfef6c2942328b6187c4e6967eaa52530a6183cb1529bcee61279e43c176fcfff291d6f5d410c3648bf2f7497370ccf1bbf21298148f42e93fbcf9821ed307b97070cda02942c4ee7a552e61d3501718d8895da0631745cb3988d37fd73770d9112e56e84b431e80463efa21d6b73a59b5c176ad4adb55a85ff24e9981765ae48c2431e068f77b2982327b4e7dc9535c3045fb8ad6abab2167fd1dcf7144f5752dc517392822d509d7f342266ec24acfac1f0b6c3cb3beb9769168a5a4167cf067c27eabebf0fe79bdd6e59cf6
|
||||||
|
import configparser
|
||||||
|
import random
|
||||||
|
import keyboard
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import asyncio
|
||||||
|
import socket
|
||||||
|
import emoji
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
import time
|
||||||
|
import tzlocal
|
||||||
|
from zoneinfo import *
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
import pytchat
|
||||||
|
import telegram
|
||||||
|
from donationalerts import DonationAlertsAPI, Scope
|
||||||
|
from urllib import parse
|
||||||
|
sys.path.append('twitchchatirc')
|
||||||
|
import twitch
|
||||||
|
|
||||||
|
|
||||||
|
# Reading Configs
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read("E:/Games/cgi-bin/config.ini")
|
||||||
|
|
||||||
|
|
||||||
|
alerts = DonationAlertsAPI(
|
||||||
|
config['Alerts']['app_id'],
|
||||||
|
config['Alerts']['api_key'],
|
||||||
|
"http://127.0.0.1:8008/login",
|
||||||
|
[
|
||||||
|
Scope.OAUTH_USER_SHOW,
|
||||||
|
Scope.OAUTH_DONATION_INDEX
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# some functions to parse json date
|
||||||
|
class DateTimeEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
if isinstance(o, datetime):
|
||||||
|
return o.isoformat()
|
||||||
|
if isinstance(o, bytes):
|
||||||
|
return list(o)
|
||||||
|
return json.JSONEncoder.default(self, o)
|
||||||
|
|
||||||
|
|
||||||
|
chat = pytchat.create(video_id=config['Youtube']['video_id'])
|
||||||
|
# twitchConn = twitch_chat_irc.TwitchChatIRC()
|
||||||
|
|
||||||
|
|
||||||
|
TGenabled = config['Telegram']['enabled']
|
||||||
|
if TGenabled:
|
||||||
|
bot = telegram.Bot(config['Telegram']['bot_secret']) #our nice Telegram eikichiBot
|
||||||
|
|
||||||
|
all_comments = []
|
||||||
|
all_old_comments = []
|
||||||
|
is_changed = False
|
||||||
|
yt_comments = []
|
||||||
|
tg_comments = []
|
||||||
|
tw_comments = []
|
||||||
|
alerts_comments = []
|
||||||
|
ytJSON = []
|
||||||
|
overallcount = 0
|
||||||
|
access_token = '' #alerts_access_token
|
||||||
|
|
||||||
|
LOCAL_TIMEZONE = tzlocal.get_localzone_name()
|
||||||
|
tz = ZoneInfo('Asia/Yekaterinburg')
|
||||||
|
|
||||||
|
#gett DonationAlerts
|
||||||
|
def get_alerts():
|
||||||
|
global access_token
|
||||||
|
global alerts_comments
|
||||||
|
alerts_comments = []
|
||||||
|
if not access_token:
|
||||||
|
print('Waiting for Alerts auth...')
|
||||||
|
return
|
||||||
|
|
||||||
|
repr(access_token)
|
||||||
|
al = alerts.donations.get(access_token.access_token)
|
||||||
|
#print(al.items)
|
||||||
|
for item in al.items:
|
||||||
|
u = item.username if item.username else 'Аноним'
|
||||||
|
m = item.message if item.message else ' --- '
|
||||||
|
|
||||||
|
amount = format(item.amount, '.2f') if item.amount != int(item.amount) else int(item.amount) #Если без десятичных, то удаляем их
|
||||||
|
amount = f"<br>{amount} {item.currency}"
|
||||||
|
#dt = item.created_at(ZoneInfo('UTC'))
|
||||||
|
if item.is_shown:
|
||||||
|
dt = datetime.fromisoformat(item.shown_at) + timedelta(hours=5)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
dtM = datetime.now() - timedelta(days=30)
|
||||||
|
if dt > dtM: #Донаты за последний месяц...
|
||||||
|
dt = dt.replace().astimezone(tz)
|
||||||
|
comm = dict({'id': item.id, 'type': 'donate', 'amount': amount, 'date': dt , 'sendr': u, 'msg': emoji.emojize(m)})
|
||||||
|
alerts_comments.append(comm)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#Get telegram comments...
|
||||||
|
async def get_tg():
|
||||||
|
global tg_comments
|
||||||
|
tg_comments = []
|
||||||
|
|
||||||
|
async with bot:
|
||||||
|
try:
|
||||||
|
updates = await bot.get_updates(allowed_updates=['message', 'edited_message'], timeout=None)
|
||||||
|
except telegram.error.TimedOut:
|
||||||
|
print('TG connection timeout 2')
|
||||||
|
time.sleep(5)
|
||||||
|
except:
|
||||||
|
print('TG connection timeout...')
|
||||||
|
else:
|
||||||
|
for upd in updates:
|
||||||
|
msg = upd.message
|
||||||
|
if upd.edited_message:
|
||||||
|
msg = upd.edited_message
|
||||||
|
|
||||||
|
#tg_comments = list(filter(lambda x: (hasattr(msg,'message_id') & x['id'] != msg.message_id), tg_comments)) #если это сообщение редактирования, то удаляем все предыдущие
|
||||||
|
|
||||||
|
if not hasattr(msg, 'text'):
|
||||||
|
continue
|
||||||
|
if not msg.text:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sendr = msg.from_user.first_name
|
||||||
|
if msg.from_user.last_name:
|
||||||
|
sendr = sendr + ' ' + msg.from_user.last_name
|
||||||
|
|
||||||
|
if sendr == "Group":
|
||||||
|
sendr = 'Админ'
|
||||||
|
|
||||||
|
#corricting TG time +timezone!!!!3
|
||||||
|
netdatetime = msg.date.replace(tzinfo=tz)
|
||||||
|
#print (repr(dt))
|
||||||
|
comm = dict({'id': msg.message_id, 'type': 'tg', 'date': netdatetime, 'sendr': sendr, 'msg': msg.text})
|
||||||
|
tg_comments.append(comm)
|
||||||
|
|
||||||
|
|
||||||
|
#Get youtube new comments...
|
||||||
|
def get_yt():
|
||||||
|
global yt_comments
|
||||||
|
global chat
|
||||||
|
|
||||||
|
itms = chat.get()
|
||||||
|
if not hasattr(itms, 'items'):
|
||||||
|
print('YT has no items attribute! (Empty?). Reconnecting...') #Catching YT empty object exception
|
||||||
|
time.sleep(5)
|
||||||
|
chat = pytchat.create(video_id=config['Youtube']['video_id']) #Reconnect YT
|
||||||
|
return
|
||||||
|
|
||||||
|
for c in itms.items:
|
||||||
|
dt = datetime.fromisoformat(c.datetime).replace(tzinfo=tz)
|
||||||
|
comm = dict({'id': c.id, 'type': 'yt', 'date': dt , 'sendr': c.author.name, 'msg': emoji.emojize(c.message)})
|
||||||
|
yt_comments.append(comm)
|
||||||
|
|
||||||
|
|
||||||
|
def makeJSONObject(): #we are parsing TG and YT
|
||||||
|
global all_comments
|
||||||
|
global yt_comments
|
||||||
|
global tg_comments
|
||||||
|
global tw_comments
|
||||||
|
global alerts_comments
|
||||||
|
global all_old_comments
|
||||||
|
global overallcount
|
||||||
|
global twitch_socket
|
||||||
|
|
||||||
|
tw_comments = twitch_socket.all_messages
|
||||||
|
|
||||||
|
|
||||||
|
get_yt() #Get YouTube
|
||||||
|
|
||||||
|
try:
|
||||||
|
get_alerts() #Get Donations
|
||||||
|
except:
|
||||||
|
print('ALERTS TIMEOUNT BLYAT!')
|
||||||
|
|
||||||
|
|
||||||
|
if TGenabled: #& int(time.time()) % 4 == 0:
|
||||||
|
try:
|
||||||
|
asyncio.run(get_tg()) #Get Telegram Comments
|
||||||
|
#print('')
|
||||||
|
except:
|
||||||
|
print('TG TIMEOUNT BLYAT!')
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
if len(yt_comments) > 15:
|
||||||
|
yt_comments = yt_comments[-15:]
|
||||||
|
if len(tg_comments) > 15:
|
||||||
|
tg_comments = tg_comments[-15:]
|
||||||
|
if len(tw_comments) > 15:
|
||||||
|
tw_comments = tw_comments[-15:]
|
||||||
|
|
||||||
|
dt = datetime.now(ZoneInfo('UTC'))
|
||||||
|
dt = dt.replace().astimezone(tz)
|
||||||
|
hello_txt = dict({'id': random.randint(100000, 999999), 'type': 'hello', 'date': dt, 'sendr': 'Eikichi-bot', 'msg': '🔥 Спасибо всем на стриме за интерес к переводу и поддержку! 🔥'})
|
||||||
|
|
||||||
|
|
||||||
|
if int(time.time()) % 600 == 0: #Выдавать сообщение каждые 45 минут
|
||||||
|
all_comments.append(hello_txt)
|
||||||
|
|
||||||
|
updateAllComments(hello_txt)
|
||||||
|
|
||||||
|
overallcount += 1
|
||||||
|
print(datetime.now().strftime('%H:%M:%S') + ' TG And YouTube checked.... ' + str(len(all_comments)) + ' elements (' + str(overallcount) + ')')
|
||||||
|
|
||||||
|
#------------------------------------------------------------
|
||||||
|
|
||||||
|
def sortdate(e):
|
||||||
|
return e['date']
|
||||||
|
|
||||||
|
def updateAllComments(hello):
|
||||||
|
global all_comments
|
||||||
|
global tg_comments
|
||||||
|
global yt_comments
|
||||||
|
global tw_comments
|
||||||
|
global alerts_comments
|
||||||
|
|
||||||
|
for i in tg_comments:
|
||||||
|
if not i in all_comments:
|
||||||
|
all_comments.append(i)
|
||||||
|
for i in yt_comments:
|
||||||
|
if not i in all_comments:
|
||||||
|
all_comments.append(i)
|
||||||
|
for i in tw_comments:
|
||||||
|
if not i in all_comments:
|
||||||
|
all_comments.append(i)
|
||||||
|
for i in alerts_comments:
|
||||||
|
if not i in all_comments:
|
||||||
|
all_comments.append(i)
|
||||||
|
all_comments.sort(key=sortdate) #Sort objects by time
|
||||||
|
all_comments = all_comments[-150:] #Пул сообщений 150
|
||||||
|
# searchAndAddHello(hello)
|
||||||
|
|
||||||
|
def searchAndAddHello(hello):
|
||||||
|
global all_comments
|
||||||
|
for i in all_comments:
|
||||||
|
if i['type'] == 'hello':
|
||||||
|
return True
|
||||||
|
all_comments.append(hello)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def printAllComments():
|
||||||
|
global all_comments
|
||||||
|
print('----------')
|
||||||
|
print(all_comments)
|
||||||
|
print('----------')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Server(BaseHTTPRequestHandler):
|
||||||
|
global access_token
|
||||||
|
def _set_headers(self):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def do_HEAD(self):
|
||||||
|
self._set_headers()
|
||||||
|
|
||||||
|
|
||||||
|
def log_message(self, format, *args): #Disable server log!!!
|
||||||
|
return
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
global access_token
|
||||||
|
if self.path == '/alert_auth':
|
||||||
|
self.send_response(301)
|
||||||
|
self.send_header('Location', alerts.authorize.login())
|
||||||
|
self.end_headers()
|
||||||
|
if self.path.startswith('/login'):
|
||||||
|
url = dict(parse.parse_qsl(parse.urlsplit(self.path).query))
|
||||||
|
code = url['code']
|
||||||
|
access_token = alerts.authorize.get_access_token(code)
|
||||||
|
print("Access token Success!")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type','text/html')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b'Access token success!')
|
||||||
|
return
|
||||||
|
|
||||||
|
self._set_headers()
|
||||||
|
self.wfile.write(json.dumps(all_comments, cls=DateTimeEncoder).encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def run(server_class=HTTPServer, handler_class=Server, port=8008):
|
||||||
|
|
||||||
|
server_address = ('', port)
|
||||||
|
httpd = server_class(server_address, handler_class)
|
||||||
|
print('Starting httpd on http://127.0.0.1:%d ...' % port)
|
||||||
|
print()
|
||||||
|
try:
|
||||||
|
httpd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
httpd.server_close()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print('--- TelegramYoutubeTwitch Simple Chat parser --- Sergey Shemet (C) 2025 ---')
|
||||||
|
print()
|
||||||
|
print('Please, autorize your DonationAlerts at http://127.0.0.1:8008/alert_auth')
|
||||||
|
print('...Hold Ctrl-Alt-Shift-C for exit and C to clear console...')
|
||||||
|
|
||||||
|
|
||||||
|
twitch_socket = twitch.TwitchChatIRC(config['Twitch']['channel'])
|
||||||
|
|
||||||
|
|
||||||
|
# init the thread as daemon for HTTPServer
|
||||||
|
d = threading.Thread(target=run, name='Daemon')
|
||||||
|
d.daemon = True
|
||||||
|
d.start()
|
||||||
|
|
||||||
|
# init twitch socket reader
|
||||||
|
tw = threading.Thread(target=twitch_socket.listen, name='twitchirc')
|
||||||
|
tw.daemon = True
|
||||||
|
tw.start()
|
||||||
|
|
||||||
|
print('Starting YT DA TG subsystems...')
|
||||||
|
|
||||||
|
polTime = 2 # Secs to run check routine
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
if keyboard.is_pressed('Ctrl+Shift+Alt+c'):
|
||||||
|
sys.exit(1)
|
||||||
|
if keyboard.is_pressed('Ctrl+Shift+Alt+z'):
|
||||||
|
printAllComments()
|
||||||
|
if keyboard.is_pressed('c'):
|
||||||
|
os.system("cls")
|
||||||
|
thisSecond = int(time.time())
|
||||||
|
if thisSecond % polTime == 0: # every X seconds
|
||||||
|
makeJSONObject()
|
||||||
|
time.sleep(1)
|
||||||
378
huySeek.py
Normal file
378
huySeek.py
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
import configparser
|
||||||
|
import random
|
||||||
|
import keyboard
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import time
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
import pytchat
|
||||||
|
import telegram
|
||||||
|
from donationalerts import DonationAlertsAPI, Scope
|
||||||
|
from urllib import parse
|
||||||
|
import emoji
|
||||||
|
sys.path.append('twitchchatirc')
|
||||||
|
import twitch
|
||||||
|
|
||||||
|
# ====================
|
||||||
|
# Конфигурация
|
||||||
|
# ====================
|
||||||
|
class Config:
|
||||||
|
def __init__(self):
|
||||||
|
self.config = configparser.ConfigParser()
|
||||||
|
self.config.read("config.ini")
|
||||||
|
|
||||||
|
# Настройки
|
||||||
|
self.timezone = ZoneInfo('Asia/Yekaterinburg')
|
||||||
|
self.poll_interval = 2 # seconds
|
||||||
|
self.max_comments_per_source = 15
|
||||||
|
self.max_total_comments = 150
|
||||||
|
self.hello_message_interval = 600 # seconds
|
||||||
|
|
||||||
|
# API keys
|
||||||
|
self.alerts_app_id = self.config['Alerts']['app_id']
|
||||||
|
self.alerts_api_key = self.config['Alerts']['api_key']
|
||||||
|
self.telegram_enabled = self.config['Telegram']['enabled']
|
||||||
|
self.telegram_bot_secret = self.config['Telegram']['bot_secret']
|
||||||
|
self.youtube_video_id = self.config['Youtube']['video_id']
|
||||||
|
self.twitch_channel = self.config['Twitch']['channel']
|
||||||
|
|
||||||
|
# ====================
|
||||||
|
# Модели данных
|
||||||
|
# ====================
|
||||||
|
class Comment:
|
||||||
|
def __init__(self, comment_id, comment_type, sender, message, date, amount=None):
|
||||||
|
self.id = comment_id
|
||||||
|
self.type = comment_type
|
||||||
|
self.sender = sender
|
||||||
|
self.message = message
|
||||||
|
self.date = date
|
||||||
|
self.amount = amount
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'type': self.type,
|
||||||
|
'sender': self.sender,
|
||||||
|
'message': emoji.emojize(self.message),
|
||||||
|
'date': self.date,
|
||||||
|
'amount': self.amount
|
||||||
|
}
|
||||||
|
|
||||||
|
# ====================
|
||||||
|
# Сервисы
|
||||||
|
# ====================
|
||||||
|
class DonationAlertsService:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.api = DonationAlertsAPI(
|
||||||
|
config.alerts_app_id,
|
||||||
|
config.alerts_api_key,
|
||||||
|
"http://127.0.0.1:8008/login",
|
||||||
|
[Scope.OAUTH_USER_SHOW, Scope.OAUTH_DONATION_INDEX]
|
||||||
|
)
|
||||||
|
self.access_token = None
|
||||||
|
|
||||||
|
def get_donations(self):
|
||||||
|
if not self.access_token:
|
||||||
|
print('Waiting for Alerts auth...')
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
donations = self.api.donations.get(self.access_token.access_token)
|
||||||
|
return self._process_donations(donations.items)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Alerts error: {str(e)}')
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _process_donations(self, items):
|
||||||
|
result = []
|
||||||
|
for item in items:
|
||||||
|
if not item.is_shown:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sender = item.username or 'Аноним'
|
||||||
|
message = item.message or '---'
|
||||||
|
amount = self._format_amount(item.amount, item.currency)
|
||||||
|
date = self._parse_donation_date(item.shown_at)
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
Comment(
|
||||||
|
comment_id=item.id,
|
||||||
|
comment_type='donate',
|
||||||
|
sender=sender,
|
||||||
|
message=message,
|
||||||
|
date=date,
|
||||||
|
amount=amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _format_amount(self, amount, currency):
|
||||||
|
if amount != int(amount):
|
||||||
|
return f"{format(amount, '.2f')} {currency}"
|
||||||
|
return f"{int(amount)} {currency}"
|
||||||
|
|
||||||
|
def _parse_donation_date(self, date_str):
|
||||||
|
dt = datetime.fromisoformat(date_str).astimezone(self.config.timezone)
|
||||||
|
# Фильтр донатов за последний месяц
|
||||||
|
if dt > datetime.now(self.config.timezone) - timedelta(days=30):
|
||||||
|
return dt
|
||||||
|
return None
|
||||||
|
|
||||||
|
class TelegramService:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.bot = telegram.Bot(config.telegram_bot_secret) if config.telegram_enabled else None
|
||||||
|
|
||||||
|
async def get_messages(self):
|
||||||
|
if not self.bot:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
updates = await self.bot.get_updates(
|
||||||
|
allowed_updates=['message', 'edited_message'],
|
||||||
|
timeout=None
|
||||||
|
)
|
||||||
|
return self._process_updates(updates)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Telegram error: {str(e)}')
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _process_updates(self, updates):
|
||||||
|
messages = []
|
||||||
|
for upd in updates:
|
||||||
|
msg = upd.edited_message if upd.edited_message else upd.message
|
||||||
|
|
||||||
|
if not hasattr(msg, 'text') or not msg.text:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sender = self._get_sender_name(msg.from_user)
|
||||||
|
date = msg.date.replace(tzinfo=self.config.timezone)
|
||||||
|
|
||||||
|
messages.append(
|
||||||
|
Comment(
|
||||||
|
comment_id=msg.message_id,
|
||||||
|
comment_type='tg',
|
||||||
|
sender=sender,
|
||||||
|
message=msg.text,
|
||||||
|
date=date
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def _get_sender_name(self, user):
|
||||||
|
if user.first_name and user.last_name:
|
||||||
|
return f"{user.first_name} {user.last_name}"
|
||||||
|
return user.first_name or "Админ"
|
||||||
|
|
||||||
|
class YouTubeService:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.chat = pytchat.create(video_id=config.youtube_video_id)
|
||||||
|
|
||||||
|
def get_messages(self):
|
||||||
|
try:
|
||||||
|
items = self.chat.get()
|
||||||
|
if not hasattr(items, 'items'):
|
||||||
|
print('YT reconnecting...')
|
||||||
|
self.chat = pytchat.create(video_id=self.config.youtube_video_id)
|
||||||
|
return []
|
||||||
|
|
||||||
|
return self._process_messages(items.items)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'YouTube error: {str(e)}')
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _process_messages(self, items):
|
||||||
|
messages = []
|
||||||
|
for item in items:
|
||||||
|
date = datetime.fromisoformat(item.datetime).replace(tzinfo=self.config.timezone)
|
||||||
|
messages.append(
|
||||||
|
Comment(
|
||||||
|
comment_id=item.id,
|
||||||
|
comment_type='yt',
|
||||||
|
sender=item.author.name,
|
||||||
|
message=item.message,
|
||||||
|
date=date
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
class TwitchService:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.socket = twitch.TwitchChatIRC(config.twitch_channel)
|
||||||
|
|
||||||
|
def get_messages(self):
|
||||||
|
# Предполагаем, что сообщения добавляются в self.socket.all_messages
|
||||||
|
# в отдельном потоке (как в оригинальном коде)
|
||||||
|
return getattr(self.socket, 'all_messages', [])
|
||||||
|
|
||||||
|
# ====================
|
||||||
|
# Ядро приложения
|
||||||
|
# ====================
|
||||||
|
class ChatAggregator:
|
||||||
|
def __init__(self):
|
||||||
|
self.config = Config()
|
||||||
|
self.services = {
|
||||||
|
'alerts': DonationAlertsService(self.config),
|
||||||
|
'telegram': TelegramService(self.config),
|
||||||
|
'youtube': YouTubeService(self.config),
|
||||||
|
'twitch': TwitchService(self.config)
|
||||||
|
}
|
||||||
|
self.comments = []
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.hello_message = Comment(
|
||||||
|
comment_id=random.randint(100000, 999999),
|
||||||
|
comment_type='hello',
|
||||||
|
sender='Eikichi-bot',
|
||||||
|
message='🔥 Спасибо всем на стриме за интерес к переводу и поддержку! 🔥',
|
||||||
|
date=datetime.now(self.config.timezone)
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# Запуск сервера в отдельном потоке
|
||||||
|
server_thread = threading.Thread(target=self._run_server)
|
||||||
|
server_thread.daemon = True
|
||||||
|
server_thread.start()
|
||||||
|
|
||||||
|
# Запуск Twitch в отдельном потоке
|
||||||
|
twitch_thread = threading.Thread(target=self.services['twitch'].socket.listen)
|
||||||
|
twitch_thread.daemon = True
|
||||||
|
twitch_thread.start()
|
||||||
|
|
||||||
|
print('System started. Press Ctrl+Shift+Alt+C to exit.')
|
||||||
|
|
||||||
|
# Главный цикл
|
||||||
|
while True:
|
||||||
|
if keyboard.is_pressed('Ctrl+Shift+Alt+C'):
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if int(time.time()) % self.config.poll_interval == 0:
|
||||||
|
self.update_comments()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def update_comments(self):
|
||||||
|
# Сбор сообщений со всех сервисов
|
||||||
|
new_comments = []
|
||||||
|
|
||||||
|
# DonationAlerts
|
||||||
|
new_comments.extend(self.services['alerts'].get_donations())
|
||||||
|
|
||||||
|
# YouTube
|
||||||
|
new_comments.extend(self.services['youtube'].get_messages())
|
||||||
|
|
||||||
|
# Telegram
|
||||||
|
if self.config.telegram_enabled:
|
||||||
|
try:
|
||||||
|
telegram_comments = asyncio.run(self.services['telegram'].get_messages())
|
||||||
|
new_comments.extend(telegram_comments)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Telegram error: {e}')
|
||||||
|
|
||||||
|
# Twitch
|
||||||
|
new_comments.extend(self.services['twitch'].get_messages())
|
||||||
|
|
||||||
|
# Добавление приветственного сообщения
|
||||||
|
if int(time.time()) % self.config.hello_message_interval == 0:
|
||||||
|
new_comments.append(self.hello_message)
|
||||||
|
|
||||||
|
# Обновление основного списка комментариев
|
||||||
|
with self.lock:
|
||||||
|
# Добавление новых уникальных комментариев
|
||||||
|
existing_ids = {c.id for c in self.comments}
|
||||||
|
self.comments.extend(
|
||||||
|
c for c in new_comments
|
||||||
|
if c.id not in existing_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сортировка по дате
|
||||||
|
self.comments.sort(key=lambda x: x.date)
|
||||||
|
|
||||||
|
# Ограничение общего количества
|
||||||
|
self.comments = self.comments[-self.config.max_total_comments:]
|
||||||
|
|
||||||
|
print(f"{datetime.now().strftime('%H:%M:%S')} Updated. Total comments: {len(self.comments)}")
|
||||||
|
|
||||||
|
def get_comments_json(self):
|
||||||
|
with self.lock:
|
||||||
|
return json.dumps(
|
||||||
|
[c.to_dict() for c in self.comments],
|
||||||
|
cls=DateTimeEncoder,
|
||||||
|
ensure_ascii=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run_server(self):
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.aggregator = kwargs.pop('aggregator')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _set_headers(self):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json; charset=utf-8')
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == '/alert_auth':
|
||||||
|
self.send_response(301)
|
||||||
|
self.send_header('Location', self.aggregator.services['alerts'].api.authorize.login())
|
||||||
|
self.end_headers()
|
||||||
|
elif self.path.startswith('/login'):
|
||||||
|
self._handle_alerts_login()
|
||||||
|
else:
|
||||||
|
self._set_headers()
|
||||||
|
self.wfile.write(self.aggregator.get_comments_json().encode('utf-8'))
|
||||||
|
|
||||||
|
def _handle_alerts_login(self):
|
||||||
|
url = dict(parse.parse_qsl(parse.urlsplit(self.path).query))
|
||||||
|
code = url.get('code')
|
||||||
|
if code:
|
||||||
|
try:
|
||||||
|
self.aggregator.services['alerts'].access_token = (
|
||||||
|
self.aggregator.services['alerts'].api.authorize.get_access_token(code)
|
||||||
|
)
|
||||||
|
print("DonationAlerts authorized successfully!")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'text/html')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b'Authorization successful! You can close this page.')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Alerts auth error: {str(e)}')
|
||||||
|
self.send_error(500, 'Authorization failed')
|
||||||
|
else:
|
||||||
|
self.send_error(400, 'Missing code parameter')
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
return
|
||||||
|
|
||||||
|
server_address = ('', 8008)
|
||||||
|
httpd = HTTPServer(server_address, lambda *args: Handler(*args, aggregator=self))
|
||||||
|
print(f'Starting HTTP server on port 8008...')
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
# ====================
|
||||||
|
# Вспомогательные классы
|
||||||
|
# ====================
|
||||||
|
class DateTimeEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
if isinstance(o, datetime):
|
||||||
|
return o.isoformat()
|
||||||
|
return super().default(o)
|
||||||
|
|
||||||
|
# ====================
|
||||||
|
# Точка входа
|
||||||
|
# ====================
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print('\n--- Telegram/YouTube/Twitch Chat Aggregator ---\n')
|
||||||
|
print('For DonationAlerts authorization visit: http://127.0.0.1:8008/alert_auth')
|
||||||
|
print('Press Ctrl+Shift+Alt+C to exit\n')
|
||||||
|
|
||||||
|
aggregator = ChatAggregator()
|
||||||
|
aggregator.run()
|
||||||
125
index.html
Normal file
125
index.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>template</title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Condensed">
|
||||||
|
<style>
|
||||||
|
|
||||||
|
|
||||||
|
body {
|
||||||
|
|
||||||
|
|
||||||
|
background-color: #494949;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: flex-end;
|
||||||
|
|
||||||
|
|
||||||
|
font-family: "Roboto Condensed", sans-serif;
|
||||||
|
font-size: 1em;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
color: lighter;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
text-shadow: 1px 1px 1px rgba(0,0,0,0.004);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatwin {
|
||||||
|
/* color: white; */
|
||||||
|
height: 100%;
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.chatRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameline {
|
||||||
|
color: white;
|
||||||
|
padding: 5px;
|
||||||
|
width: 25%;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-direction: column;
|
||||||
|
vertical-align: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt {
|
||||||
|
background-image: url('data:image/svg+xml,<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"><g fill="%239C92AC" fill-opacity="0.4" fill-rule="evenodd"><path d="M0 40L40 0H20L0 20M40 40V20L20 40"/></g></svg>');
|
||||||
|
background-color: #E53935A0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg {
|
||||||
|
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><g fill-rule="evenodd"><g fill="%239C92AC" fill-opacity="0.4"><path opacity=".5" d="M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z"/><path d="M6 5V0H5v5H0v1h5v94h1V6h94V5H6z"/></g></g></svg>');
|
||||||
|
background-color: #2196F3A0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw {
|
||||||
|
background-color: #6034b2A0 ;
|
||||||
|
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='60' viewBox='0 0 20 12'><g fill-rule='evenodd'><g id='charlie-brown' fill='%236441a5' fill-opacity='0.35'><path d='M9.8 12L0 2.2V.8l10 10 10-10v1.4L10.2 12h-.4zm-4 0L0 6.2V4.8L7.2 12H5.8zm8.4 0L20 6.2V4.8L12.8 12h1.4zM9.8 0l.2.2.2-.2h-.4zm-4 0L10 4.2 14.2 0h-1.4L10 2.8 7.2 0H5.8z'/></g></g></svg>");
|
||||||
|
}
|
||||||
|
/* <svg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'><rect fill='#6441A5' width='120' height='120'/><polygon fill='#5C3C98' fill-opacity='1' points='120 120 60 120 90 90 120 60 120 0 120 0 60 60 0 0 0 60 30 90 60 120 120 120 '/></svg> */
|
||||||
|
.donate {
|
||||||
|
background-image: url('back-coin.jpg');
|
||||||
|
/* background-size: 500px; */
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello {
|
||||||
|
background-color: #00bb77;
|
||||||
|
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='59' height='59' viewBox='0 0 120 120'><polygon fill='%23000' fill-opacity='0.11' points='120 0 120 60 90 30 60 0 0 0 0 0 60 60 0 120 60 120 90 90 120 60 120 0'/></svg>");
|
||||||
|
}
|
||||||
|
|
||||||
|
.msgline {
|
||||||
|
padding: 5px;
|
||||||
|
width: 75%;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="chatwin">
|
||||||
|
<!--
|
||||||
|
<div idd="1" class="chatRow">
|
||||||
|
<div class="nameline tg">Edvard Force</div>
|
||||||
|
<div class="msgline">вот Тенчу Великому зашла , он в ней быстро разобрался и втащил за один стрим )))) ждем когда до нее доберется ))))</div>
|
||||||
|
</div>
|
||||||
|
<div idd="2" class="chatRow">
|
||||||
|
<div class="nameline tg" >Диванный Воин</div>
|
||||||
|
<div class="msgline">dc или если ты действительно решишь дать бан то мышкой два клика не сделаешь?</div>
|
||||||
|
</div>
|
||||||
|
<div idd="3" class="chatRow">
|
||||||
|
<div class="nameline tg">Yoshka's Cat</div>
|
||||||
|
<div class="msgline">великий ну эта часть ванпис лутьше чем вариорсы надо признать</div>
|
||||||
|
</div>
|
||||||
|
<div idd="4" class="chatRow">
|
||||||
|
<div class="nameline yt">XAOSHammer</div>
|
||||||
|
<div class="msgline">Великий а куда друг твой пропал спец по японскому языку ?))))</div>
|
||||||
|
</div>
|
||||||
|
<div idd="5" class="chatRow">
|
||||||
|
<div class="nameline tg">Kino Konformist</div>
|
||||||
|
<div class="msgline">вот Тенчу Великому зашла , он в ней быстро разобрался и втащил за один стрим )))) ждем когда до нее доберется ))))</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<div id="anchor"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
148
script.js
Normal file
148
script.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
let messages = [
|
||||||
|
"I wondered why the baseball was getting bigger. Then it hit me.",
|
||||||
|
"Police were called to a day care, where a three-year-old was resisting a rest.",
|
||||||
|
"Did you hear about the guy whose whole left side was cut off? He’s all right now.",
|
||||||
|
"The roundest knight at King Arthur’s round table was Sir Cumference.",
|
||||||
|
"To write with a broken pencil is pointless.",
|
||||||
|
"When fish are in schools they sometimes take debate.",
|
||||||
|
"The short fortune teller who escaped from prison was a small medium at large.",
|
||||||
|
"A thief who stole a calendar… got twelve months.",
|
||||||
|
"A thief fell and broke his leg in wet cement. He became a hardened criminal.",
|
||||||
|
"Thieves who steal corn from a garden could be charged with stalking.",
|
||||||
|
"When the smog lifts in Los Angeles , U. C. L. A.",
|
||||||
|
"The math professor went crazy with the blackboard. He did a number on it.",
|
||||||
|
"The professor discovered that his theory of earthquakes was on shaky ground.",
|
||||||
|
"The dead batteries were given out free of charge.",
|
||||||
|
"If you take a laptop computer for a run you could jog your memory.",
|
||||||
|
"A dentist and a manicurist fought tooth and nail.",
|
||||||
|
"A bicycle can’t stand alone; it is two tired.",
|
||||||
|
"A will is a dead giveaway.",
|
||||||
|
"Time flies like an arrow; fruit flies like a banana.",
|
||||||
|
"A backward poet writes inverse.",
|
||||||
|
"In a democracy it’s your vote that counts; in feudalism, it’s your Count that votes.",
|
||||||
|
"A chicken crossing the road: poultry in motion.",
|
||||||
|
"If you don’t pay your exorcist you can get repossessed.",
|
||||||
|
"With her marriage she got a new name and a dress.",
|
||||||
|
"Show me a piano falling down a mine shaft and I’ll show you A-flat miner.",
|
||||||
|
"When a clock is hungry it goes back four seconds.",
|
||||||
|
"The guy who fell onto an upholstery machine was fully recovered.",
|
||||||
|
"A grenade fell onto a kitchen floor in France and resulted in Linoleum Blownapart.",
|
||||||
|
"You are stuck with your debt if you can’t budge it.",
|
||||||
|
"Local Area Network in Australia : The LAN down under.",
|
||||||
|
"He broke into song because he couldn’t find the key.",
|
||||||
|
"A calendar’s days are numbered."
|
||||||
|
];
|
||||||
|
|
||||||
|
let nicks = [
|
||||||
|
"Edvard Force",
|
||||||
|
"Диванный Воин",
|
||||||
|
"Yoshka's Cat",
|
||||||
|
"XAOSHammer",
|
||||||
|
"Kino Konformist" ];
|
||||||
|
|
||||||
|
const chatwin = document.getElementById("chatwin");
|
||||||
|
const anchor = document.getElementById("anchor");
|
||||||
|
|
||||||
|
function randomMessage() {
|
||||||
|
return messages[(Math.random() * messages.length) | 0];
|
||||||
|
}
|
||||||
|
function randomNick() {
|
||||||
|
return nicks[(Math.random() * nicks.length) | 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomChat() { if (Math.random() * 2 > 1) {return "tg"} else { return "yt" } }
|
||||||
|
|
||||||
|
function goDown() {
|
||||||
|
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeElementsByClass(className){
|
||||||
|
const elements = document.getElementsByClassName(className);
|
||||||
|
while(elements.length > 15){
|
||||||
|
elements[0].parentNode.removeChild(elements[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChatLine() {
|
||||||
|
|
||||||
|
const nm = document.createElement("div");
|
||||||
|
nm.className = "nameline " + randomChat();
|
||||||
|
nm.innerHTML = randomNick();
|
||||||
|
|
||||||
|
const msg = document.createElement("div");
|
||||||
|
msg.className = "msgline";
|
||||||
|
msg.innerHTML = randomMessage();
|
||||||
|
|
||||||
|
const rw = document.createElement("div");
|
||||||
|
rw.className = "chatRow";
|
||||||
|
rw.appendChild(nm);
|
||||||
|
rw.appendChild(msg);
|
||||||
|
console.log(rw);
|
||||||
|
|
||||||
|
chatwin.insertBefore(rw, anchor);
|
||||||
|
|
||||||
|
|
||||||
|
goDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createNewLine(json) {
|
||||||
|
|
||||||
|
json.forEach(element => {
|
||||||
|
|
||||||
|
//checking new IDs on screen and print
|
||||||
|
|
||||||
|
//const elements = document.getElementsByName(element["id"]);
|
||||||
|
//if (elements.length > 0) { //if this id is present on screen
|
||||||
|
|
||||||
|
let curline = document.getElementById(element["id"])
|
||||||
|
if (curline) {
|
||||||
|
curline.innerHTML = element["msg"]
|
||||||
|
return;
|
||||||
|
}; //Updating text...
|
||||||
|
|
||||||
|
const nm = document.createElement("div");
|
||||||
|
nm.className = "nameline " + element["type"];
|
||||||
|
nm.innerHTML = element["sendr"];
|
||||||
|
if (element["type"] == "donate") { nm.innerHTML = nm.innerHTML + '<br><p style="color: red">' + element['amount']+"</p>" }
|
||||||
|
|
||||||
|
const msg = document.createElement("div");
|
||||||
|
msg.className = "msgline";
|
||||||
|
msg.innerHTML = element["msg"];
|
||||||
|
msg.setAttribute("id", element["id"]);
|
||||||
|
if (element["type"] == "donate") { msg.style="background-color: #0000FF20" }
|
||||||
|
|
||||||
|
|
||||||
|
const rw = document.createElement("div");
|
||||||
|
rw.className = "chatRow";
|
||||||
|
rw.setAttribute("name", element["id"]);
|
||||||
|
|
||||||
|
rw.appendChild(nm);
|
||||||
|
rw.appendChild(msg);
|
||||||
|
//console.log(rw);
|
||||||
|
|
||||||
|
chatwin.insertBefore(rw, anchor);
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestNewLines() {
|
||||||
|
|
||||||
|
var request = new XMLHttpRequest();
|
||||||
|
request.open('GET', "http://localhost:8008/");
|
||||||
|
request.responseType = 'json';
|
||||||
|
request.send();
|
||||||
|
|
||||||
|
request.onload = function() {
|
||||||
|
var chatJSON = request.response;
|
||||||
|
createNewLine(chatJSON);
|
||||||
|
goDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(requestNewLines,1000);
|
||||||
55
style.css
Normal file
55
style.css
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: 1em;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
color: lighter;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
text-shadow: 1px 1px 1px rgba(0,0,0,0.004);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatwin {
|
||||||
|
color: white;
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: 500px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.chatRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameline {
|
||||||
|
color: light-grey;
|
||||||
|
padding: 10px;
|
||||||
|
width: 25%;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt {
|
||||||
|
background-image: url('data:image/svg+xml,%3Csvg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="%239C92AC" fill-opacity="0.4" fill-rule="evenodd"%3E%3Cpath d="M0 40L40 0H20L0 20M40 40V20L20 40"/%3E%3C/g%3E%3C/svg%3E');
|
||||||
|
background-color: #E53935A0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* background-color: #DFDBE5;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='152' height='152' viewBox='0 0 152 152'%3E%3Cg fill-rule='evenodd'%3E%3Cg id='temple' fill='%239C92AC' fill-opacity='0.4'%3E%3Cpath d='M152 150v2H0v-2h28v-8H8v-20H0v-2h8V80h42v20h20v42H30v8h90v-8H80v-42h20V80h42v40h8V30h-8v40h-42V50H80V8h40V0h2v8h20v20h8V0h2v150zm-2 0v-28h-8v20h-20v8h28zM82 30v18h18V30H82zm20 18h20v20h18V30h-20V10H82v18h20v20zm0 2v18h18V50h-18zm20-22h18V10h-18v18zm-54 92v-18H50v18h18zm-20-18H28V82H10v38h20v20h38v-18H48v-20zm0-2V82H30v18h18zm-20 22H10v18h18v-18zm54 0v18h38v-20h20V82h-18v20h-20v20H82zm18-20H82v18h18v-18zm2-2h18V82h-18v18zm20 40v-18h18v18h-18zM30 0h-2v8H8v20H0v2h8v40h42V50h20V8H30V0zm20 48h18V30H50v18zm18-20H48v20H28v20H10V30h20V10h38v18zM30 50h18v18H30V50zm-2-40H10v18h18V10z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); */
|
||||||
|
|
||||||
|
.tg {
|
||||||
|
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"%3E%3Cg fill-rule="evenodd"%3E%3Cg fill="%239C92AC" fill-opacity="0.4"%3E%3Cpath opacity=".5" d="M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z"/%3E%3Cpath d="M6 5V0H5v5H0v1h5v94h1V6h94V5H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');
|
||||||
|
background-color: #2196F3A0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msgline {
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
12
tet/camera.reg
Normal file
12
tet/camera.reg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
[HKEY_CLASSES_ROOT\camera]
|
||||||
|
@="URL:Camera Protocol"
|
||||||
|
"URL Protocol"=""
|
||||||
|
|
||||||
|
[HKEY_CLASSES_ROOT\camera\shell]
|
||||||
|
|
||||||
|
[HKEY_CLASSES_ROOT\camera\shell\open]
|
||||||
|
|
||||||
|
[HKEY_CLASSES_ROOT\camera\shell\open\command]
|
||||||
|
@="\"C:\\Program Files (x86)\\SATVISION\\Satvision V.2.0\\Satvision V.2.0.exe\" \"%1\""
|
||||||
3
tet/camera_del.reg
Normal file
3
tet/camera_del.reg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
[-HKEY_CLASSES_ROOT\camera]
|
||||||
202
tet/island_script.txt
Normal file
202
tet/island_script.txt
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# Инициализация персонажей
|
||||||
|
@char yui:Юи|Каштановые волосы|Энергичная, но напуганная|Пахнет цитрусами
|
||||||
|
@char himitsu:Химицу|Черные волосы|Загадочная художница|Пахнет лавандой
|
||||||
|
@char mitsuhа:Мицуха|Розовые волосы|Ранимая певица|Пахнет ванилью
|
||||||
|
|
||||||
|
# Инициализация очков
|
||||||
|
@set yui_points = 0
|
||||||
|
@set himitsu_points = 0
|
||||||
|
@set mitsuha_points = 0
|
||||||
|
|
||||||
|
# --- ДЕНЬ 1: ТЕРРИТОРИЯ ТЕПЛА ---
|
||||||
|
@label start
|
||||||
|
@bg morning
|
||||||
|
*УТРО. Холод в пустой квартире стал почти осязаемым, пробираясь под самую одежду. Вы находите Юи в гостиной — она дрожит, и её кожа покрылась крупными мурашками.*
|
||||||
|
yui: Я скоро превращусь в лед... Можно мне хоть немного твоего тепла?
|
||||||
|
|
||||||
|
choice Прижать её к себе всем телом=>goto d1_yui_warm|Отстраниться и принести плед=>goto d1_yui_cold|Пошутить, что она преувеличивает=>goto d1_yui_none
|
||||||
|
|
||||||
|
@label d1_yui_warm
|
||||||
|
@bg day
|
||||||
|
*ДЕНЬ. Вы обхватываете её за талию и вжимаете в себя. Юи стонет, её соски отчетливо твердеют под тонкой тканью, царапая вашу грудь.*
|
||||||
|
*Она жадно ловит ваше дыхание, прижимаясь бедрами так плотно, что вы чувствуете каждый её изгиб.*
|
||||||
|
@set yui_points += 15
|
||||||
|
@goto d1_himitsu_scene
|
||||||
|
|
||||||
|
@label d1_yui_cold
|
||||||
|
*Юи разочарованно кутается в плед. Атмосфера между вами становится холоднее метели за окном.*
|
||||||
|
@set yui_points -= 5
|
||||||
|
@goto d1_himitsu_scene
|
||||||
|
|
||||||
|
@label d1_yui_none
|
||||||
|
*Она обиженно отворачивается. Вы явно выбрали неудачный момент для шуток.*
|
||||||
|
@set yui_points -= 10
|
||||||
|
@goto d1_himitsu_scene
|
||||||
|
|
||||||
|
@label d1_himitsu_scene
|
||||||
|
@bg evening
|
||||||
|
*ВЕЧЕР. Химицу сидит у окна, рисуя пальцем на запотевшем стекле символы, напоминающие сплетения тел.*
|
||||||
|
himitsu: Ты слышишь, как бьется моё сердце? Оно требует чего-то большего, чем просто тишина.
|
||||||
|
|
||||||
|
choice Положить руку ей на грудь=>goto d1_himi_hot|Просто слушать её слова=>goto d1_himi_soft|Сказать, что слышите только ветер=>goto d1_himi_none
|
||||||
|
|
||||||
|
@label d1_himi_hot
|
||||||
|
@bg night
|
||||||
|
*НОЧЬ. Ваша ладонь накрывает её грудь, чувствуя бешеный ритм. Химицу прижимает вашу руку сильнее, её дыхание становится тяжелым и прерывистым.*
|
||||||
|
*Она смотрит на вас затуманенным взглядом, и её тело под вашими пальцами кажется обжигающим.*
|
||||||
|
@set himitsu_points += 15
|
||||||
|
@goto d1_mitsuha_scene
|
||||||
|
|
||||||
|
@label d1_himi_soft
|
||||||
|
*Она лишь слегка улыбается, оценив ваше внимание, но искры не происходит.*
|
||||||
|
@set himitsu_points += 5
|
||||||
|
@goto d1_mitsuha_scene
|
||||||
|
|
||||||
|
@label d1_himi_none
|
||||||
|
*Химицу замыкается в себе. Вы упустили шанс стать к ней ближе.*
|
||||||
|
@set himitsu_points -= 10
|
||||||
|
@goto d1_mitsuha_scene
|
||||||
|
|
||||||
|
@label d1_mitsuha_scene
|
||||||
|
@bg morning
|
||||||
|
*УТРО (День 1-2). Мицуха зажигает свечи, её розовые волосы светятся в полумраке.*
|
||||||
|
mitsuhа: Кажется, метель заберет меня, если рядом никого не будет... Побудь со мной.
|
||||||
|
|
||||||
|
choice Обнять её со спины=>goto d1_mitsu_close|Взять её за руку=>goto d1_mitsu_hold|Посоветовать ей не бояться=>goto d1_mitsu_none
|
||||||
|
|
||||||
|
@label d1_mitsu_close
|
||||||
|
@bg day
|
||||||
|
*ДЕНЬ. Вы смыкаете руки на её талии. Мицуха выгибается, её ягодицы плотно прижимаются к вам, а дыхание сбивается на тихий стон.*
|
||||||
|
*Вы чувствуете мягкость её тела и сладкий аромат ванили, который кружит голову.*
|
||||||
|
@set mitsuha_points += 15
|
||||||
|
@goto d1_night_together
|
||||||
|
|
||||||
|
@label d1_mitsu_hold
|
||||||
|
*Она благодарно сжимает ваши пальцы, но ожидала более смелого шага.*
|
||||||
|
@set mitsuha_points += 5
|
||||||
|
@goto d1_night_together
|
||||||
|
|
||||||
|
@label d1_mitsu_none
|
||||||
|
*Мицуха опускает глаза. Ей не нужны советы, ей нужно было ваше тепло.*
|
||||||
|
@set mitsuha_points -= 10
|
||||||
|
@goto d1_night_together
|
||||||
|
|
||||||
|
@label d1_night_together
|
||||||
|
@bg night
|
||||||
|
*НОЧЬ. Чтобы не замерзнуть, вы спите на одном матрасе. Юи закидывает на вас ногу, а Мицуха обнимает за талию.*
|
||||||
|
*Вы засыпаете в коконе из женских тел, чувствуя их жар и слыша мерное, возбуждающее дыхание.*
|
||||||
|
@goto d2_start
|
||||||
|
|
||||||
|
# --- ДЕНЬ 2: ОБНАЖЕННЫЕ ЧУВСТВА ---
|
||||||
|
@label d2_start
|
||||||
|
@bg morning
|
||||||
|
*УТРО. Вы находите Юи на кухне. Она заспанная, её пижама сползла с плеча, открывая вид на нежную кожу.*
|
||||||
|
yui: Мне всю ночь снилось, как ты меня касаешься... Это было слишком реально.
|
||||||
|
|
||||||
|
choice Поцеловать её, углубляя контакт=>goto d2_yui_sex|Погладить её по плечу=>goto d2_yui_touch|Сказать, что это просто сны=>goto d2_yui_none
|
||||||
|
|
||||||
|
@label d2_yui_sex
|
||||||
|
@bg day
|
||||||
|
*ДЕНЬ. Поцелуй превращается в жадное исследование. Юи сплетается с вами, её горячий язык ищет вашего.*
|
||||||
|
*Она прижимается бедрами так яростно, что вы чувствуете её влажное желание сквозь тонкую ткань.*
|
||||||
|
@set yui_points += 15
|
||||||
|
@goto d2_himitsu_scene
|
||||||
|
|
||||||
|
@label d2_yui_touch
|
||||||
|
*Она улыбается, но в её глазах читается неудовлетворенное ожидание.*
|
||||||
|
@set yui_points += 5
|
||||||
|
@goto d2_himitsu_scene
|
||||||
|
|
||||||
|
@label d2_yui_none
|
||||||
|
*Юи разочарованно отстраняется. Момент близости безвозвратно утерян.*
|
||||||
|
@set yui_points -= 15
|
||||||
|
@goto d2_himitsu_scene
|
||||||
|
|
||||||
|
@label d2_himitsu_scene
|
||||||
|
@bg evening
|
||||||
|
*ВЕЧЕР. Химицу приглашает вас в свою комнату. Воздух здесь пропитан лавандой и тайной.*
|
||||||
|
himitsu: Сними рубашку... Я хочу запечатлеть твою силуэт, прежде чем всё закончится.
|
||||||
|
|
||||||
|
choice Раздеться и подойти вплотную=>goto d2_himi_naked|Снять только рубашку и стоять=>goto d2_himi_half|Отказаться раздеваться=>goto d2_himi_none
|
||||||
|
|
||||||
|
@label d2_himi_naked
|
||||||
|
@bg night
|
||||||
|
*НОЧЬ. Она ведет холодными пальцами по вашей коже, спускаясь всё ниже. Её вздохи становятся лихорадочными.*
|
||||||
|
*Химицу прижимается щекой к вашей груди, и вы чувствуете, как её собственное тело дрожит от нетерпения.*
|
||||||
|
@set himitsu_points += 15
|
||||||
|
@goto d2_mitsuha_scene
|
||||||
|
|
||||||
|
@label d2_himi_half
|
||||||
|
*Она рисует, но между вами остается дистанция, которую она не решается сократить.*
|
||||||
|
@set himitsu_points += 5
|
||||||
|
@goto d2_mitsuha_scene
|
||||||
|
|
||||||
|
@label d2_himi_none
|
||||||
|
*«Ты слишком скучный для музы», — холодно бросает она.*
|
||||||
|
@set himitsu_points -= 20
|
||||||
|
@goto d2_mitsuha_scene
|
||||||
|
|
||||||
|
@label d2_mitsuha_scene
|
||||||
|
@bg morning
|
||||||
|
*УТРО (День 2-3). Мицуха поет тихую балладу, её голос вибрирует глубоко в вашей груди.*
|
||||||
|
mitsuhа: Я хочу быть твоей... хотя бы пока бушует эта метель.
|
||||||
|
|
||||||
|
choice Поцеловать её в шею, лаская изгибы=>goto d2_mitsu_neck|Крепко обнять за плечи=>goto d2_mitsu_soft|Просто похвалить вокал=>goto d2_mitsu_none
|
||||||
|
|
||||||
|
@label d2_mitsu_neck
|
||||||
|
@bg day
|
||||||
|
*ДЕНЬ. Мицуха стонет, откидывая голову назад. Ваши губы находят её самую чувствительную точку.*
|
||||||
|
*Вы чувствуете нежную тяжесть её груди в своих руках и то, как она жадно ловит воздух ртом.*
|
||||||
|
@set mitsuha_points += 15
|
||||||
|
@goto d2_night_logic
|
||||||
|
|
||||||
|
@label d2_mitsu_soft
|
||||||
|
*Она прижимается к вам, ища защиты, но не страсти.*
|
||||||
|
@set mitsuha_points += 5
|
||||||
|
@goto d2_night_logic
|
||||||
|
|
||||||
|
@label d2_mitsu_none
|
||||||
|
*Мицуха замолкает. Вы разбили магию момента своей обыденностью.*
|
||||||
|
@set mitsuha_points -= 15
|
||||||
|
@goto d2_night_logic
|
||||||
|
|
||||||
|
@label d2_night_logic
|
||||||
|
@bg night
|
||||||
|
*НОЧЬ. Напряжение достигло пика. В темноте слышны лишь вздохи и шорох простыней.*
|
||||||
|
@if yui_points == 30 then *Юи вжимается в вас, её бедра двигаются в такт вашему дыханию, ища спасения от желания.*
|
||||||
|
@if himitsu_points == 30 then *Рука Химицу скользит под одеяло, её ладони обжигают вашу кожу лихорадочным жаром.*
|
||||||
|
@if mitsuha_points == 30 then *Мицуха обвивает вас, вы чувствуете её грудь и то, как она дрожит от каждого касания.*
|
||||||
|
*Остальные девушки, чувствуя этот жар, начинают ласкать друг друга в поисках утешения.*
|
||||||
|
@goto d3_final
|
||||||
|
|
||||||
|
# --- ДЕНЬ 3: ПОСЛЕДНЯЯ НОЧЬ ---
|
||||||
|
@label d3_final
|
||||||
|
@bg morning
|
||||||
|
*УТРО. Метель стихла, но внутри вас бушует шторм. Вы ловите их томные взгляды.*
|
||||||
|
@bg day
|
||||||
|
*ДЕНЬ. Каждое случайное касание теперь обжигает как разряд тока.*
|
||||||
|
@bg evening
|
||||||
|
*ВЕЧЕР. Пора решить, чьё тепло станет вашим навсегда.*
|
||||||
|
|
||||||
|
@if yui_points >= 30 then goto end_yui
|
||||||
|
@if himitsu_points >= 30 then goto end_himitsu
|
||||||
|
@if mitsuha_points >= 30 then goto end_mitsuha
|
||||||
|
@goto end_lonely
|
||||||
|
|
||||||
|
@label end_yui
|
||||||
|
*Юи уводит вас в свою комнату. Её поцелуй сбивает с ног, а руки жадно срывают одежду.*
|
||||||
|
@goto credits
|
||||||
|
|
||||||
|
@label end_himitsu
|
||||||
|
*Химицу забирает вашу волю в темноте мастерской. Её дыхание — единственное, что теперь имеет значение.*
|
||||||
|
@goto credits
|
||||||
|
|
||||||
|
@label end_mitsuha
|
||||||
|
*Мицуха шепчет слова любви, её тело пахнет ванилью и страстью, когда она тянет вас в постель.*
|
||||||
|
@goto credits
|
||||||
|
|
||||||
|
@label end_lonely
|
||||||
|
*Вы выходите на мороз один. Конец.*
|
||||||
|
|
||||||
|
@label credits
|
||||||
|
*Игра завершена.*
|
||||||
629
tet/island_script_ds.txt
Normal file
629
tet/island_script_ds.txt
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
# Романтическая визуальная новелла "Запертые вместе"
|
||||||
|
# Главный герой оказывается в квартире с тремя девушками
|
||||||
|
# и должен строить отношения, чтобы выжить
|
||||||
|
|
||||||
|
# Инициализация персонажей
|
||||||
|
@char yui:Юи|Каштановые волосы, зеленые глаза, спортивная фигура|Студентка, занимается танцами|Пахнет цитрусами
|
||||||
|
@char himitsu:Химицу|Черные волосы, фиолетовые глаза, готический стиль|Художница, любит читать|Пахнет лавандой
|
||||||
|
@char mitsuhа:Мицуха|Розовые волосы, голубые глаза, кудрявая|Певица в караоке-клубе|Пахнет ванилью
|
||||||
|
|
||||||
|
# Инициализация переменных
|
||||||
|
@set yui_points = 0
|
||||||
|
@set himitsu_points = 0
|
||||||
|
@set mitsuhа_points = 0
|
||||||
|
@set day = 1
|
||||||
|
@set time = "утро"
|
||||||
|
@set sanity = 100
|
||||||
|
@set killed = 0
|
||||||
|
|
||||||
|
# Начало игры
|
||||||
|
@label start
|
||||||
|
@bg morning
|
||||||
|
*Вы просыпаетесь в незнакомой квартире после сильной метели. За окном белая пелена, дороги замело, связь не работает. В квартире кроме вас находятся три девушки.*
|
||||||
|
|
||||||
|
# Первая встреча
|
||||||
|
@label day1_morning
|
||||||
|
@bg morning
|
||||||
|
*Лёгкий солнечный свет пробивается через шторы. Вы слышите голоса из кухни.*
|
||||||
|
|
||||||
|
choice Подойти познакомиться=>goto meet_girls|Осмотреть квартиру=>goto explore_house|Попробовать найти выход=>goto try_exit
|
||||||
|
|
||||||
|
@label meet_girls
|
||||||
|
@bg morning
|
||||||
|
yui: О, ты наконец проснулся! Мы уже начали волноваться.
|
||||||
|
himitsu: *пристально смотрит* Ты вообще помнишь, как сюда попал?
|
||||||
|
mitsuhа: Не пугай его! Он же только что очнулся.
|
||||||
|
|
||||||
|
choice Извиниться за беспокойство=>goto polite_start|Спросить, где вы=>goto direct_question|Пошутить=>goto joke_response
|
||||||
|
|
||||||
|
@label polite_start
|
||||||
|
@set sanity+=5
|
||||||
|
*Девушки выглядят довольными вашими манерами*
|
||||||
|
yui: Ничего страшного! Метель всех застала врасплох.
|
||||||
|
himitsu: Да, мы тоже здесь застряли. Похоже, придётся пережидать.
|
||||||
|
@set yui_points+=5
|
||||||
|
@set himitsu_points+=5
|
||||||
|
@goto day1_breakfast
|
||||||
|
|
||||||
|
@label direct_question
|
||||||
|
@set sanity-=3
|
||||||
|
*Девушки переглядываются*
|
||||||
|
mitsuhа: Это моя квартира... ну, точнее, съёмная.
|
||||||
|
himitsu: А ты просто лежал у подъезда, мы не могли оставить тебя в метель.
|
||||||
|
@set mitsuhа_points+=3
|
||||||
|
@goto day1_breakfast
|
||||||
|
|
||||||
|
@label joke_response
|
||||||
|
@set sanity+=10
|
||||||
|
*Девушки смеются*
|
||||||
|
yui: О, весельчак! Это хорошо, в заточении пригодится.
|
||||||
|
mitsuhа: *хихикает* Ты забавный!
|
||||||
|
himitsu: *саркастично* Надеюсь, твои шутки не закончатся вместе с едой.
|
||||||
|
@set yui_points+=7
|
||||||
|
@set mitsuhа_points+=10
|
||||||
|
@set himitsu_points+=3
|
||||||
|
@goto day1_breakfast
|
||||||
|
|
||||||
|
@label explore_house
|
||||||
|
@bg grey
|
||||||
|
*Вы осматриваете квартиру. Небольшая гостиная, кухня, две спальни и ванная. Окна плотно закрыты, на балконной двери висит замок.*
|
||||||
|
|
||||||
|
himitsu: *появляется в дверях* Искал выход? Бесполезно, я уже пробовала.
|
||||||
|
@set himitsu_points+=5
|
||||||
|
@goto day1_breakfast
|
||||||
|
|
||||||
|
@label try_exit
|
||||||
|
@bg grey
|
||||||
|
*Вы пытаетесь открыть входную дверь, но она не поддаётся. Слышен голос за спиной.*
|
||||||
|
|
||||||
|
yui: Эй, не трать силы! Мы уже пробовали - снег завалил дверь снаружи.
|
||||||
|
@set yui_points+=5
|
||||||
|
@goto day1_breakfast
|
||||||
|
|
||||||
|
# Завтрак первого дня
|
||||||
|
@label day1_breakfast
|
||||||
|
@bg morning
|
||||||
|
mitsuhа: Ну что, будем завтракать? У меня есть рис и немного овощей.
|
||||||
|
yui: Я могу приготовить омлет!
|
||||||
|
himitsu: *вздыхает* Ладно, помогу накрыть на стол.
|
||||||
|
|
||||||
|
choice Помочь Юи с готовкой=>goto help_yui|Помочь Химицу с сервировкой=>goto help_himitsu|Поболтать с Мицухой=>goto talk_mitsuhа
|
||||||
|
|
||||||
|
@label help_yui
|
||||||
|
@bg morning
|
||||||
|
*Вы помогаете Юи готовить омлет. Она улыбается вашей помощи.*
|
||||||
|
yui: Ты неплохо справляешься! Часто готовишь?
|
||||||
|
choice Да, люблю готовить=>goto cooking_skill|Нет, просто повезло=>goto cooking_luck
|
||||||
|
|
||||||
|
@label cooking_skill
|
||||||
|
yui: Круто! Может, потом научишь меня чему-нибудь?
|
||||||
|
@set yui_points+=10
|
||||||
|
@goto breakfast_common
|
||||||
|
|
||||||
|
@label cooking_luck
|
||||||
|
yui: Скромничаешь, я вижу!
|
||||||
|
@set yui_points+=5
|
||||||
|
@goto breakfast_common
|
||||||
|
|
||||||
|
@label help_himitsu
|
||||||
|
@bg morning
|
||||||
|
*Вы помогаете Химицу расставлять посуду. Она наблюдает за вами.*
|
||||||
|
himitsu: Ты... не такой, как я ожидала.
|
||||||
|
choice Что ты ожидала?=>goto himitsu_expectations|Просто стараюсь помочь=>goto himitsu_help
|
||||||
|
|
||||||
|
@label himitsu_expectations
|
||||||
|
himitsu: Не знаю... Кого-то более напуганного или агрессивного.
|
||||||
|
@set himitsu_points+=8
|
||||||
|
@goto breakfast_common
|
||||||
|
|
||||||
|
@label himitsu_help
|
||||||
|
himitsu: Это... мило с твоей стороны.
|
||||||
|
@set himitsu_points+=5
|
||||||
|
@goto breakfast_common
|
||||||
|
|
||||||
|
@label talk_mitsuhа
|
||||||
|
@bg morning
|
||||||
|
*Вы садитесь рядом с Мицухой, которая нарезает овощи.*
|
||||||
|
mitsuhа: О! Хочешь помочь? Можешь нарезать огурец.
|
||||||
|
*Вы начинаете резать огурец, но получается неровно*
|
||||||
|
mitsuhа: *смеётся* Ничего, для первого раза сойдёт!
|
||||||
|
@set mitsuhа_points+=7
|
||||||
|
@goto breakfast_common
|
||||||
|
|
||||||
|
@label breakfast_common
|
||||||
|
@bg morning
|
||||||
|
*Вы все вместе завтракаете. В квартире царит дружеская атмосфера.*
|
||||||
|
|
||||||
|
yui: Так... что будем делать, пока не кончилась метель?
|
||||||
|
himitsu: У меня есть колода карт.
|
||||||
|
mitsuhа: А я могу петь! *смущённо* Если хотите, конечно...
|
||||||
|
|
||||||
|
choice Предложить играть в карты=>goto play_cards|Попросить Мицуху спеть=>goto ask_sing|Предложить рассказывать истории=>goto tell_stories
|
||||||
|
|
||||||
|
@label play_cards
|
||||||
|
@bg day
|
||||||
|
*Вы играете в карты с девушками. Юи азартна, Химицу хладнокровна, а Мицуха постоянно путает правила.*
|
||||||
|
|
||||||
|
@if himitsu_points>yui_points then goto himitsu_wins
|
||||||
|
@if himitsu_points>mitsuhа_points then goto himitsu_wins
|
||||||
|
@if yui_points>himitsu_points then goto yui_wins
|
||||||
|
@if yui_points>mitsuhа_points then goto yui_wins
|
||||||
|
@goto mitsuhа_wins
|
||||||
|
|
||||||
|
@label himitsu_wins
|
||||||
|
himitsu: *ухмыляется* Кажется, я выигрываю. Опять.
|
||||||
|
@set himitsu_points+=10
|
||||||
|
@goto day1_afternoon
|
||||||
|
|
||||||
|
@label yui_wins
|
||||||
|
yui: Да! Ещё одна победа! *радостно подпрыгивает*
|
||||||
|
@set yui_points+=10
|
||||||
|
@goto day1_afternoon
|
||||||
|
|
||||||
|
@label mitsuhа_wins
|
||||||
|
mitsuhа: Ой, я выиграла? Неожиданно!
|
||||||
|
@set mitsuhа_points+=10
|
||||||
|
@goto day1_afternoon
|
||||||
|
|
||||||
|
@label ask_sing
|
||||||
|
@bg day
|
||||||
|
mitsuhа: *радостно* Правда хочешь послушать? Ну ладно...
|
||||||
|
*Мицуха берёт гитару и исполняет красивую балладу*
|
||||||
|
|
||||||
|
choice Похвалить её голос=>goto praise_voice|Спросить, где научилась=>goto ask_learning|Аплодировать стоя=>goto standing_ovation
|
||||||
|
|
||||||
|
@label praise_voice
|
||||||
|
mitsuhа: Спасибо! *краснеет* Я редко пою для других.
|
||||||
|
@set mitsuhа_points+=10
|
||||||
|
@goto day1_afternoon
|
||||||
|
|
||||||
|
@label ask_learning
|
||||||
|
mitsuhа: Я работаю в караоке-клубе... но это секрет, ладно?
|
||||||
|
@set mitsuhа_points+=7
|
||||||
|
@goto day1_afternoon
|
||||||
|
|
||||||
|
@label standing_ovation
|
||||||
|
mitsuhа: *смущённо* Ой, ну что ты! *но явно рада*
|
||||||
|
@set mitsuhа_points+=15
|
||||||
|
@goto day1_afternoon
|
||||||
|
|
||||||
|
@label tell_stories
|
||||||
|
@bg day
|
||||||
|
*Вы рассказываете интересную историю из своей жизни. Девушки слушают с интересом.*
|
||||||
|
|
||||||
|
choice Рассказать смешную историю=>goto funny_story|Рассказать трогательную историю=>goto touching_story|Рассказать страшную историю=>goto scary_story
|
||||||
|
|
||||||
|
@label funny_story
|
||||||
|
yui: *смеётся* О боже, это же так глупо!
|
||||||
|
himitsu: *подавляет улыбку* Да уж...
|
||||||
|
@set yui_points+=10
|
||||||
|
@set himitsu_points+=5
|
||||||
|
@goto day1_afternoon
|
||||||
|
|
||||||
|
@label touching_story
|
||||||
|
mitsuhа: *глаза влажные* Это так... трогательно.
|
||||||
|
himitsu: *кивает* Сильная история.
|
||||||
|
@set mitsuhа_points+=12
|
||||||
|
@set himitsu_points+=8
|
||||||
|
@goto day1_afternoon
|
||||||
|
|
||||||
|
@label scary_story
|
||||||
|
yui: *вздрагивает* Брр, не люблю такие истории!
|
||||||
|
himitsu: *заинтересованно* Хороший рассказчик...
|
||||||
|
@set yui_points-=5
|
||||||
|
@set himitsu_points+=15
|
||||||
|
@goto day1_afternoon
|
||||||
|
|
||||||
|
# День 1 - Послеобеденное время
|
||||||
|
@label day1_afternoon
|
||||||
|
@bg day
|
||||||
|
*После совместного времяпрепровождения атмосфера в квартире стала более дружелюбной.*
|
||||||
|
|
||||||
|
yui: Эй, а давайте проверим, как там на улице?
|
||||||
|
*Вы подходите к окну - метель всё ещё бушует*
|
||||||
|
himitsu: Похоже, нам здесь ещё на день-два.
|
||||||
|
mitsuhа: Тогда надо проверить запасы еды...
|
||||||
|
|
||||||
|
choice Предложить помочь с инвентаризацией=>goto check_supplies|Предложить сыграть в другую игру=>goto another_game|Пойти отдохнуть=>goto take_nap
|
||||||
|
|
||||||
|
@label check_supplies
|
||||||
|
@bg day
|
||||||
|
*Вы проверяете запасы еды вместе с Мицухой*
|
||||||
|
mitsuhа: Риса хватит на пару дней, овощей меньше... Ой!
|
||||||
|
*Она случайно роняет банку, вы ловите её*
|
||||||
|
mitsuhа: *смущённо* Спасибо... ты ловкий.
|
||||||
|
@set mitsuhа_points+=10
|
||||||
|
@goto day1_evening
|
||||||
|
|
||||||
|
@label another_game
|
||||||
|
@bg day
|
||||||
|
*Вы предлагаете новую игру. Юи с энтузиазмом соглашается.*
|
||||||
|
yui: Да! Я обожаю игры! Давай что-нибудь активное!
|
||||||
|
@set yui_points+=10
|
||||||
|
@goto day1_evening
|
||||||
|
|
||||||
|
@label take_nap
|
||||||
|
@bg day
|
||||||
|
*Вы решаете вздремнуть. Химицу сидит рядом с книгой.*
|
||||||
|
himitsu: *тихо* Умный поступок. В таких ситуациях важно сохранять силы.
|
||||||
|
@set himitsu_points+=7
|
||||||
|
@goto day1_evening
|
||||||
|
|
||||||
|
# День 1 - Вечер
|
||||||
|
@label day1_evening
|
||||||
|
@bg sunset
|
||||||
|
*Солнце садится, в квартире становится темнее. Вы замечаете, как девушки начинают нервничать.*
|
||||||
|
|
||||||
|
choice Предложить зажечь свечи=>goto light_candles|Проверить телефон (вдруг появилась связь)=>goto check_phone|Начать разговор=>goto start_conversation
|
||||||
|
|
||||||
|
@label light_candles
|
||||||
|
@bg sunset
|
||||||
|
*Вы находите свечи и зажигаете их. Мицуха благодарно улыбается.*
|
||||||
|
mitsuhа: Как романтично... в каком-то смысле.
|
||||||
|
@set mitsuhа_points+=8
|
||||||
|
@set yui_points+=3
|
||||||
|
@set himitsu_points+=3
|
||||||
|
@goto day1_dinner
|
||||||
|
|
||||||
|
@label check_phone
|
||||||
|
@bg sunset
|
||||||
|
*Вы проверяете телефон - связи по-прежнему нет.*
|
||||||
|
yui: *вздыхает* Я тоже уже пробовала... Ничего.
|
||||||
|
@set sanity-=5
|
||||||
|
@goto day1_dinner
|
||||||
|
|
||||||
|
@label start_conversation
|
||||||
|
@bg sunset
|
||||||
|
*Вы пытаетесь разрядить обстановку разговором*
|
||||||
|
himitsu: *снисходительно* Ну давай, развлекай нас.
|
||||||
|
choice Расспросить о них=>goto ask_about_them|Рассказать о себе=>goto tell_about_yourself|Придумать игру=>goto create_game
|
||||||
|
|
||||||
|
@label ask_about_them
|
||||||
|
*Девушки немного оживляются, рассказывая о себе*
|
||||||
|
@set yui_points+=5
|
||||||
|
@set himitsu_points+=5
|
||||||
|
@set mitsuhа_points+=5
|
||||||
|
@goto day1_dinner
|
||||||
|
|
||||||
|
@label tell_about_yourself
|
||||||
|
*Вы рассказываете о себе. Химицу слушает с интересом.*
|
||||||
|
@set himitsu_points+=10
|
||||||
|
@goto day1_dinner
|
||||||
|
|
||||||
|
@label create_game
|
||||||
|
*Вы придумываете игру, Юи в восторге*
|
||||||
|
@set yui_points+=15
|
||||||
|
@goto day1_dinner
|
||||||
|
|
||||||
|
# Ужин первого дня
|
||||||
|
@label day1_dinner
|
||||||
|
@bg night
|
||||||
|
*Вы ужинаете при свечах. Атмосфера странным образом становится уютной.*
|
||||||
|
|
||||||
|
mitsuhа: Знаете... несмотря на всё, это довольно мило.
|
||||||
|
yui: Да уж, могло быть и хуже!
|
||||||
|
himitsu: *покатывается глазами* Вы слишком легко ко всему относитесь.
|
||||||
|
|
||||||
|
choice Согласиться с Мицухой=>goto agree_mitsuhа|Поддержать Юи=>goto agree_yui|Промолчать=>goto stay_silent
|
||||||
|
|
||||||
|
@label agree_mitsuhа
|
||||||
|
mitsuhа: *улыбается* Приятно, что ты так думаешь.
|
||||||
|
@set mitsuhа_points+=10
|
||||||
|
@goto day1_sleep
|
||||||
|
|
||||||
|
@label agree_yui
|
||||||
|
yui: Вот именно! *подмигивает*
|
||||||
|
@set yui_points+=10
|
||||||
|
@goto day1_sleep
|
||||||
|
|
||||||
|
@label stay_silent
|
||||||
|
himitsu: *кивает* Хотя бы один человек здесь адекватный.
|
||||||
|
@set himitsu_points+=7
|
||||||
|
@goto day1_sleep
|
||||||
|
|
||||||
|
# Ночь первого дня
|
||||||
|
@label day1_sleep
|
||||||
|
@bg night
|
||||||
|
*Приходит время спать. В квартире две спальни - одна для девушек, одна для вас.*
|
||||||
|
|
||||||
|
choice Предложить дежурить по очереди=>goto night_watch|Пожелать спокойной ночи=>goto good_night|Проверить двери ещё раз=>goto check_doors
|
||||||
|
|
||||||
|
@label night_watch
|
||||||
|
*Девушки удивлены вашим предложением*
|
||||||
|
yui: О, ответственный! Мне нравится!
|
||||||
|
@set yui_points+=10
|
||||||
|
@set himitsu_points+=5
|
||||||
|
@set mitsuhа_points+=5
|
||||||
|
@goto day2_morning
|
||||||
|
|
||||||
|
@label good_night
|
||||||
|
*Вы вежливо прощаетесь*
|
||||||
|
mitsuhа: Спокойной ночи... и спасибо за сегодня.
|
||||||
|
@set mitsuhа_points+=7
|
||||||
|
@goto day2_morning
|
||||||
|
|
||||||
|
@label check_doors
|
||||||
|
*Вы проверяете двери - без изменений*
|
||||||
|
himitsu: *тихо* Упрямый... но это хорошо.
|
||||||
|
@set himitsu_points+=10
|
||||||
|
@goto day2_morning
|
||||||
|
|
||||||
|
# День 2 - Утро
|
||||||
|
@label day2_morning
|
||||||
|
@bg morning
|
||||||
|
@set day = 2
|
||||||
|
*Вы просыпаетесь от запаха готовящегося завтрака. Метель за окном продолжается, но уже не так сильно.*
|
||||||
|
|
||||||
|
yui: Доброе утро! Я приготовила завтрак!
|
||||||
|
himitsu: *ворчит* И разбудила всех на три этажа.
|
||||||
|
mitsuhа: *подаёт тарелку* Кушай, пока горячее.
|
||||||
|
|
||||||
|
choice Поблагодарить Юи=>goto thank_yui|Пошутить над Химицу=>goto joke_himitsu|Помочь Мицухе=>goto help_mitsuhа
|
||||||
|
|
||||||
|
@label thank_yui
|
||||||
|
yui: *радостно* Всегда пожалуйста!
|
||||||
|
@set yui_points+=10
|
||||||
|
@goto day2_breakfast
|
||||||
|
|
||||||
|
@label joke_himitsu
|
||||||
|
himitsu: *фыркает* Смешно. Очень смешно.
|
||||||
|
@set himitsu_points+=5
|
||||||
|
@goto day2_breakfast
|
||||||
|
|
||||||
|
@label help_mitsuhа
|
||||||
|
mitsuhа: Ой, спасибо! *улыбается*
|
||||||
|
@set mitsuhа_points+=10
|
||||||
|
@goto day2_breakfast
|
||||||
|
|
||||||
|
# День 2 - Завтрак
|
||||||
|
@label day2_breakfast
|
||||||
|
@bg morning
|
||||||
|
*За завтраком девушки обсуждают, как провести день.*
|
||||||
|
|
||||||
|
himitsu: Электричество есть, но интернет не работает. Типично.
|
||||||
|
mitsuhа: Может, споём караоке? У меня есть микрофон!
|
||||||
|
yui: О да! А потом можно устроить мини-спортзал!
|
||||||
|
|
||||||
|
choice Поддержать идею караоке=>goto karaoke_time|Предложить настольные игры=>goto board_games|Пойти проверить окна=>goto check_windows
|
||||||
|
|
||||||
|
@label karaoke_time
|
||||||
|
@bg day
|
||||||
|
mitsuhа: Ура! *хлопает в ладоши* Давайте дуэтом!
|
||||||
|
@set mitsuhа_points+=10
|
||||||
|
@set sanity+=15
|
||||||
|
@goto day2_activity
|
||||||
|
|
||||||
|
@label board_games
|
||||||
|
@bg day
|
||||||
|
himitsu: Наконец-то здравая идея.
|
||||||
|
@set himitsu_points+=10
|
||||||
|
@set yui_points-=5
|
||||||
|
@goto day2_activity
|
||||||
|
|
||||||
|
@label check_windows
|
||||||
|
@bg day
|
||||||
|
*Вы проверяете окна - снег немного осел, но выйти пока невозможно.*
|
||||||
|
yui: *подходит сзади* Нашёл выход?
|
||||||
|
@set yui_points+=7
|
||||||
|
@set sanity-=5
|
||||||
|
@goto day2_activity
|
||||||
|
|
||||||
|
# День 2 - Активности
|
||||||
|
@label day2_activity
|
||||||
|
@bg day
|
||||||
|
*Внезапно гаснет свет. Раздаётся вздох разочарования.*
|
||||||
|
|
||||||
|
himitsu: Прекрасно. Просто прекрасно.
|
||||||
|
mitsuhа: *испуганно* Темно...
|
||||||
|
choice Найти фонарик=>goto find_flashlight|Успокоить Мицуху=>goto comfort_mitsuhа|Посмеяться над ситуацией=>goto laugh_situation
|
||||||
|
|
||||||
|
@label find_flashlight
|
||||||
|
@bg grey
|
||||||
|
*Вы находите фонарик в ящике. Химицу одобрительно кивает.*
|
||||||
|
himitsu: Полезно иметь такого человека рядом.
|
||||||
|
@set himitsu_points+=10
|
||||||
|
@goto day2_lunch
|
||||||
|
|
||||||
|
@label comfort_mitsuhа
|
||||||
|
@bg grey
|
||||||
|
*Вы берёте Мицуху за руку. Она постепенно успокаивается.*
|
||||||
|
mitsuhа: Спасибо... мне страшно в темноте.
|
||||||
|
@set mitsuhа_points+=15
|
||||||
|
@goto day2_lunch
|
||||||
|
|
||||||
|
@label laugh_situation
|
||||||
|
@bg grey
|
||||||
|
yui: *смеётся* Да уж, ещё один повод для воспоминаний!
|
||||||
|
@set yui_points+=10
|
||||||
|
@set mitsuhа_points-=5
|
||||||
|
@goto day2_lunch
|
||||||
|
|
||||||
|
# День 2 - Обед
|
||||||
|
@label day2_lunch
|
||||||
|
@bg day
|
||||||
|
*Свет вернулся. Вы вместе готовите обед из остатков еды.*
|
||||||
|
|
||||||
|
choice Помочь Юи с готовкой=>goto cook_with_yui|Убрать на кухне=>goto clean_kitchen|Поделиться едой с Химицу=>goto share_food
|
||||||
|
|
||||||
|
@label cook_with_yui
|
||||||
|
@bg day
|
||||||
|
*Юи учит вас готовить простое блюдо. Получается не идеально, но съедобно.*
|
||||||
|
yui: Неплохо для первого раза! *подмигивает*
|
||||||
|
@set yui_points+=12
|
||||||
|
@goto day2_evening
|
||||||
|
|
||||||
|
@label clean_kitchen
|
||||||
|
@bg day
|
||||||
|
*Вы моете посуду. Химицу неожиданно присоединяется.*
|
||||||
|
himitsu: *тихо* Ты... не такой неряха, как большинство парней.
|
||||||
|
@set himitsu_points+=10
|
||||||
|
@goto day2_evening
|
||||||
|
|
||||||
|
@label share_food
|
||||||
|
@bg day
|
||||||
|
*Вы отдаёте Химицу последний кусок пирога. Она удивлена.*
|
||||||
|
himitsu: Ты... уверен? *берет пирог, избегая зрительного контакта*
|
||||||
|
@set himitsu_points+=15
|
||||||
|
@set sanity-=5 # голод
|
||||||
|
@goto day2_evening
|
||||||
|
|
||||||
|
# День 2 - Вечер
|
||||||
|
@label day2_evening
|
||||||
|
@bg sunset
|
||||||
|
*Запасы еды заканчиваются. Напряжение растёт.*
|
||||||
|
|
||||||
|
yui: Эй, может, расскажем страшные истории? Чтобы отвлечься!
|
||||||
|
mitsuhа: *дрожит* Я... я не уверена...
|
||||||
|
himitsu: *саркастично* Отличная идея. Добавить страха в нашу и без того ужасную ситуацию.
|
||||||
|
|
||||||
|
choice Поддержать Юи=>goto scary_stories|Предложить альтернативу=>goto alternative_activity|Уйти в другую комнату=>goto leave_room
|
||||||
|
|
||||||
|
@label scary_stories
|
||||||
|
@bg night
|
||||||
|
*Вы рассказываете историю. Юи в восторге, Мицуха прячется под плед.*
|
||||||
|
@set yui_points+=15
|
||||||
|
@set mitsuhа_points-=10
|
||||||
|
@set sanity-=10
|
||||||
|
@goto day2_night
|
||||||
|
|
||||||
|
@label alternative_activity
|
||||||
|
@bg night
|
||||||
|
*Вы предлагаете рассказывать смешные истории. Атмосфера улучшается.*
|
||||||
|
@set mitsuhа_points+=10
|
||||||
|
@set yui_points+=5
|
||||||
|
@set sanity+=10
|
||||||
|
@goto day2_night
|
||||||
|
|
||||||
|
@label leave_room
|
||||||
|
@bg night
|
||||||
|
*Вы уходите, оставляя девушек разбираться самим. Химицу позже находит вас читающим.*
|
||||||
|
himitsu: Умный ход. *оставляет чашку чая рядом*
|
||||||
|
@set himitsu_points+=10
|
||||||
|
@set yui_points-=5
|
||||||
|
@goto day2_night
|
||||||
|
|
||||||
|
# День 2 - Ночь
|
||||||
|
@label day2_night
|
||||||
|
@bg night
|
||||||
|
*Вы просыпаетесь от шума. Кто-то ходит по квартире.*
|
||||||
|
|
||||||
|
choice Исследовать=>goto investigate_noise|Притвориться спящим=>goto pretend_sleep|Позвать на помощь=>goto call_for_help
|
||||||
|
|
||||||
|
@label investigate_noise
|
||||||
|
@bg black
|
||||||
|
*Это Химицу стоит у окна. Она быстро поворачивается.*
|
||||||
|
himitsu: *шепотом* Не мог спать. Метель... напоминает мне кое-что.
|
||||||
|
choice Расспросить=>goto ask_her_past|Просто постоять рядом=>goto stand_with_her|Вернуться спать=>goto return_to_sleep
|
||||||
|
|
||||||
|
@label ask_her_past
|
||||||
|
himitsu: *после паузы* Когда я была маленькой... *резко обрывает* Забудь.
|
||||||
|
@set himitsu_points+=15
|
||||||
|
@goto day3_morning
|
||||||
|
|
||||||
|
@label stand_with_her
|
||||||
|
*Вы молча стоите рядом. Через несколько минут она незаметно берёт вас за руку.*
|
||||||
|
@set himitsu_points+=20
|
||||||
|
@goto day3_morning
|
||||||
|
|
||||||
|
@label return_to_sleep
|
||||||
|
himitsu: *саркастично* Да, беги. Все бегут.
|
||||||
|
@set himitsu_points-=10
|
||||||
|
@goto day3_morning
|
||||||
|
|
||||||
|
@label pretend_sleep
|
||||||
|
@bg black
|
||||||
|
*Вы слышите, как кто-то поправляет ваше одеяло. Пахнет ванилью.*
|
||||||
|
@set mitsuhа_points+=10
|
||||||
|
@goto day3_morning
|
||||||
|
|
||||||
|
@label call_for_help
|
||||||
|
@bg black
|
||||||
|
yui: *вбегает* Что случилось?! О... *видит, что это просто Химицу*
|
||||||
|
himitsu: *ядовито* Поздравляю, всех разбудил.
|
||||||
|
@set yui_points+=5
|
||||||
|
@set himitsu_points-=10
|
||||||
|
@goto day3_morning
|
||||||
|
|
||||||
|
# День 3 - Утро
|
||||||
|
@label day3_morning
|
||||||
|
@bg morning
|
||||||
|
@set day=3
|
||||||
|
*Метель почти утихла. Девушки взволнованны.*
|
||||||
|
|
||||||
|
mitsuhа: *смотрит в окно* Кажется, сегодня сможем выбраться!
|
||||||
|
yui: Надо собрать вещи! *бегает по комнате*
|
||||||
|
himitsu: *спокойно* Наконец-то. *но выглядит немного грустной*
|
||||||
|
|
||||||
|
choice Помочь собираться=>goto help_packing|Проверить выход=>goto check_exit|Поговорить с Химицу=>goto talk_to_himitsu
|
||||||
|
|
||||||
|
@label help_packing
|
||||||
|
*Вы помогаете Мицухе собрать её многочисленные вещи.*
|
||||||
|
mitsuhа: Ты такой внимательный! *краснеет*
|
||||||
|
@set mitsuhа_points+=15
|
||||||
|
@goto day3_decision
|
||||||
|
|
||||||
|
@label check_exit
|
||||||
|
*Вы очищаете дверь от снега. Юи присоединяется.*
|
||||||
|
yui: Командная работа! *улыбается*
|
||||||
|
@set yui_points+=10
|
||||||
|
@goto day3_decision
|
||||||
|
|
||||||
|
@label talk_to_himitsu
|
||||||
|
himitsu: *неожиданно* Может... как-нибудь встретимся? Если выберемся.
|
||||||
|
@set himitsu_points+=20
|
||||||
|
@goto day3_decision
|
||||||
|
|
||||||
|
# День 3 - Решающий выбор
|
||||||
|
@label day3_decision
|
||||||
|
@bg day
|
||||||
|
*Дверь наконец открывается. Пришло время прощаться... или нет?*
|
||||||
|
|
||||||
|
choice Пригласить Юи на свидание=>goto date_yui|Предложить Химицу пойти вместе=>goto go_with_himitsu|Признаться Мицухе в чувствах=>goto confess_to_mitsuhа|Промолчать и уйти=>goto bad_ending_lonely
|
||||||
|
|
||||||
|
@label date_yui
|
||||||
|
@if yui_points>50 then goto yui_good_ending
|
||||||
|
*Юи вежливо отказывает. Похоже, вы не нашли общий язык.*
|
||||||
|
@goto credits
|
||||||
|
|
||||||
|
@label yui_good_ending
|
||||||
|
yui: *сияет* Я думала, ты никогда не предложишь!
|
||||||
|
*Вы начинаете встречаться. Её энергия заряжает вас каждый день.*
|
||||||
|
@goto credits
|
||||||
|
|
||||||
|
@label go_with_himitsu
|
||||||
|
@if himitsu_points>60 then goto himitsu_good_ending
|
||||||
|
*himitsu холодно отвечает: "Не думаю, что это хорошая идея."*
|
||||||
|
@goto credits
|
||||||
|
|
||||||
|
@label himitsu_good_ending
|
||||||
|
himitsu: *с лёгкой улыбкой* Ладно... но только если ты будешь молчать в моей мастерской.
|
||||||
|
*Вы становитесь её моделью, а затем и чем-то большим.*
|
||||||
|
@goto credits
|
||||||
|
|
||||||
|
@label confess_to_mitsuhа
|
||||||
|
@if mitsuhа_points>55 then goto mitsuhа_good_ending
|
||||||
|
*mitsuhа смущённо бормочет извинения и быстро уходит.*
|
||||||
|
@goto credits
|
||||||
|
|
||||||
|
@label mitsuhа_good_ending
|
||||||
|
mitsuhа: *тихо* Я... я тоже тебя люблю.
|
||||||
|
*Вы посещаете все её выступления и однагод делаете предложение на сцене.*
|
||||||
|
@goto credits
|
||||||
|
|
||||||
|
@label bad_ending_lonely
|
||||||
|
@bg black
|
||||||
|
*Вы уходите, не попрощавшись. Девушки больше никогда вас не видят.*
|
||||||
|
*Конец.*
|
||||||
|
@goto credits
|
||||||
|
|
||||||
|
# Титры с обновлёнными переменными
|
||||||
|
@label credits
|
||||||
|
*Спасибо за игру!*
|
||||||
|
*Итоговые показатели:*
|
||||||
|
*Дней вместе: {day}*
|
||||||
|
*Очки Юи: {yui_points}*
|
||||||
|
*Очки Химицу: {himitsu_points}*
|
||||||
|
*Очки Мицухи: {mitsuhа_points}*
|
||||||
|
*Уровень рассудка: {sanity}*
|
||||||
|
@if killed==1 then *Вас убили! Плохая концовка.*
|
||||||
|
@if sanity<30 then *Вы сошли с ума. Плохая концовка.*
|
||||||
397
tet/island_script_qwen.txt
Normal file
397
tet/island_script_qwen.txt
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
# Начало игры: Знакомство с игроком и событием
|
||||||
|
|
||||||
|
@label start
|
||||||
|
|
||||||
|
*Вы просыпаетесь на песчаном пляже под звуки волн и крики чаек.*
|
||||||
|
|
||||||
|
Player: Где я...? Что случилось?
|
||||||
|
|
||||||
|
*Над вами склоняется красивая девушка с длинными светлыми волосами и мягким голосом.*
|
||||||
|
|
||||||
|
Aoi: Ты очнулся... Хорошо, что ты жив.
|
||||||
|
|
||||||
|
Player: Кто вы? Почему мы здесь?
|
||||||
|
|
||||||
|
Aoi: Мы потерпели крушение. Остальные тоже выжили. Но это уже третий день, как мы здесь.
|
||||||
|
|
||||||
|
*Позади появляются ещё две девушки — одна с короткой стрижкой и весёлым взглядом, другая — задумчивая и серьёзная.*
|
||||||
|
|
||||||
|
Natsuki: Эй, он проснулся? Привет, красавчик!
|
||||||
|
|
||||||
|
Yume: Не шуми так, Natsuki. Он, наверное, всё ещё слаб.
|
||||||
|
|
||||||
|
@char Aoi:Аои|Длинные светлые волосы, голубые глаза|Спокойная, уравновешенная|Пахнет морским бризом
|
||||||
|
@char Natsuki:Нацуки|Короткие розовые волосы, карие глаза|Энергичная, любит приключения|Пахнет клубникой
|
||||||
|
@char Yume:Юме|Чёрные волосы до плеч, серые глаза|Замкнутая, но добрая внутри|Пахнет старыми книгами
|
||||||
|
|
||||||
|
@set aoi = 0
|
||||||
|
@set natsuki = 0
|
||||||
|
@set yume = 0
|
||||||
|
@set day = 1
|
||||||
|
|
||||||
|
@goto morning_start
|
||||||
|
|
||||||
|
# Утро первого дня
|
||||||
|
|
||||||
|
@label morning_start
|
||||||
|
|
||||||
|
@bg morning
|
||||||
|
*Nад вами рассвет, оранжевые облака медленно проплывают над горизонтом. Море спокойно, а воздух наполнен запахом соли.*
|
||||||
|
|
||||||
|
Aoi: Сегодня мы должны исследовать остров. Может быть, найдём еду или сигнал для спасения.
|
||||||
|
|
||||||
|
Natsuki: Ага! Я уже готова! Только давайте возьмём палки и верёвку.
|
||||||
|
|
||||||
|
Yume: Или можно поискать в пещерах. Там может быть сухо и безопасно.
|
||||||
|
|
||||||
|
choice Пойти с Аои=>goto explore_aoi | Пойти с Нацуки=>goto explore_natsuki | Пойти с Юме=>goto explore_yume
|
||||||
|
|
||||||
|
# Исследование с Аои
|
||||||
|
|
||||||
|
@label explore_aoi
|
||||||
|
@set aoi += 10
|
||||||
|
|
||||||
|
*Вы идёте по берегу вдоль скал. Аои аккуратно собирает ракушки и показывает вам, какие из них можно есть.*
|
||||||
|
|
||||||
|
Aoi: Эти ракушки — хорошее дополнение к пище. Они богаты белком.
|
||||||
|
|
||||||
|
Player: Ты знаешь много полезного...
|
||||||
|
|
||||||
|
Aoi: Я училась на биолога. Так что немного знаю природу.
|
||||||
|
|
||||||
|
*Aoi улыбается, её щёки слегка розовеют.*
|
||||||
|
|
||||||
|
@goto evening_day1
|
||||||
|
|
||||||
|
# Исследование с Нацуки
|
||||||
|
|
||||||
|
@label explore_natsuki
|
||||||
|
@set natsuki += 10
|
||||||
|
|
||||||
|
*Natsuki ведёт вас вглубь джунглей. Она легко карабкается по деревьям и срывает фрукты.*
|
||||||
|
|
||||||
|
Natsuki: Смотри, эти плоды вкусные и сочные!
|
||||||
|
|
||||||
|
Player: А они безопасны?
|
||||||
|
|
||||||
|
Natsuki: Ну, если я не умерла, то и ты не умрёшь!
|
||||||
|
|
||||||
|
*Она смеётся и протягивает тебе половину фрукта.*
|
||||||
|
|
||||||
|
Natsuki: Давай, попробуй!
|
||||||
|
|
||||||
|
Player: Вкусно...
|
||||||
|
|
||||||
|
Natsuki: Я рада!
|
||||||
|
|
||||||
|
@goto evening_day1
|
||||||
|
|
||||||
|
# Исследование с Юме
|
||||||
|
|
||||||
|
@label explore_yume
|
||||||
|
@set yume += 10
|
||||||
|
|
||||||
|
*Юме ведёт вас к древней пещере. Внутри темно, но она зажигает маленький факел.*
|
||||||
|
|
||||||
|
Yume: Здесь раньше жили люди. Видишь эти следы?
|
||||||
|
|
||||||
|
Player: Ты думаешь, они были здесь?
|
||||||
|
|
||||||
|
Yume: Возможно. Но сейчас мы одни.
|
||||||
|
|
||||||
|
*Она поворачивается к вам, её взгляд становится мягче.*
|
||||||
|
|
||||||
|
Yume: Мне приятно, что ты выбрал меня.
|
||||||
|
|
||||||
|
@goto evening_day1
|
||||||
|
|
||||||
|
# Вечер первого дня
|
||||||
|
|
||||||
|
@label evening_day1
|
||||||
|
@bg sunset
|
||||||
|
|
||||||
|
*Солнце заходит, окрашивая небо в теплые оттенки. Вы возвращаетесь на пляж.*
|
||||||
|
|
||||||
|
Player: Как прошёл ваш день?
|
||||||
|
|
||||||
|
Aoi: Мы нашли ракушки и воду.
|
||||||
|
|
||||||
|
Natsuki: А я нашла бананы! Вкуснющие!
|
||||||
|
|
||||||
|
Yume: В пещере могут быть припасы. Нужно исследовать дальше.
|
||||||
|
|
||||||
|
*Все трое садятся рядом с костром, который разожгла Нацуки.*
|
||||||
|
|
||||||
|
@set day = 2
|
||||||
|
@goto night_day1
|
||||||
|
|
||||||
|
# Ночь первого дня
|
||||||
|
|
||||||
|
@label night_day1
|
||||||
|
@bg night
|
||||||
|
|
||||||
|
*Луна светит ярко. Все трое сидят у костра, рассказывая истории.*
|
||||||
|
|
||||||
|
Aoi: Я всегда хотела побывать на необитаемом острове... Только не в таких обстоятельствах.
|
||||||
|
|
||||||
|
Natsuki: А я бы хотела стать пиратом!
|
||||||
|
|
||||||
|
Yume: Я хочу домой. К своим книгам.
|
||||||
|
|
||||||
|
Player: А что бы вы сделали, если бы нас спасли завтра?
|
||||||
|
|
||||||
|
*Девушки задумываются.*
|
||||||
|
|
||||||
|
Aoi: Пошла бы на прогулку с любимым человеком.
|
||||||
|
|
||||||
|
Natsuki: Отметила бы спасение шикарной вечеринкой!
|
||||||
|
|
||||||
|
Yume: Посидела бы с чашкой чая и книгой.
|
||||||
|
|
||||||
|
*Тихо. Только треск костра и шум волн.*
|
||||||
|
|
||||||
|
@goto morning_day2
|
||||||
|
|
||||||
|
# Утро второго дня
|
||||||
|
|
||||||
|
@label morning_day2
|
||||||
|
@bg morning
|
||||||
|
|
||||||
|
*Утро. Все выглядят немного уставшими, но бодрыми.*
|
||||||
|
|
||||||
|
Player: Что будем делать сегодня?
|
||||||
|
|
||||||
|
Aoi: Можно попробовать построить укрытие получше.
|
||||||
|
|
||||||
|
Natsuki: Или построить плот!
|
||||||
|
|
||||||
|
Yume: Лучше проверить пещеры. Может быть, там есть выход.
|
||||||
|
|
||||||
|
choice Помочь Аои=>goto help_aoi | Помочь Нацуки=>goto help_natsuki | Помочь Юме=>goto help_yume
|
||||||
|
|
||||||
|
# Помощь Аои
|
||||||
|
|
||||||
|
@label help_aoi
|
||||||
|
@set aoi += 15
|
||||||
|
|
||||||
|
*Aoi ведёт вас к месту, где можно собрать лианы и сделать кров.*
|
||||||
|
|
||||||
|
Aoi: Вот, держи эту ветку. Нужно связать их так...
|
||||||
|
|
||||||
|
Player: Как ты всё знаешь?
|
||||||
|
|
||||||
|
Aoi: Когда-то была в походе с отцом. Он был военным.
|
||||||
|
|
||||||
|
*Aoi смотрит вдаль, словно вспоминая.*
|
||||||
|
|
||||||
|
@goto evening_day2
|
||||||
|
|
||||||
|
# Помощь Нацуки
|
||||||
|
|
||||||
|
@label help_natsuki
|
||||||
|
@set natsuki += 15
|
||||||
|
|
||||||
|
*Natsuki тащит бревна и предлагает сделать плот.*
|
||||||
|
|
||||||
|
Natsuki: Если мы построим плот, сможем уйти отсюда!
|
||||||
|
|
||||||
|
Player: А ты уверена, что он удержит нас?
|
||||||
|
|
||||||
|
Natsuki: Конечно! Я мастер по плотам!
|
||||||
|
|
||||||
|
*Она хватает вас за руку.*
|
||||||
|
|
||||||
|
Natsuki: Давай, помоги мне перевернуть его!
|
||||||
|
|
||||||
|
*Вы вместе переворачиваете плот. Она смеётся.*
|
||||||
|
|
||||||
|
@goto evening_day2
|
||||||
|
|
||||||
|
# Помощь Юме
|
||||||
|
|
||||||
|
@label help_yume
|
||||||
|
@set yume += 15
|
||||||
|
|
||||||
|
*Юме снова ведёт вас в пещеру. Теперь она освещает стены факелом.*
|
||||||
|
|
||||||
|
Yume: Здесь есть какие-то символы. Может, это предупреждение?
|
||||||
|
|
||||||
|
Player: О чём?
|
||||||
|
|
||||||
|
Yume: Не знаю... Но я чувствую, что это важно.
|
||||||
|
|
||||||
|
*Она смотрит на вас своими глубокими глазами.*
|
||||||
|
|
||||||
|
Yume: Ты единственный, кому я могу доверить это.
|
||||||
|
|
||||||
|
@goto evening_day2
|
||||||
|
|
||||||
|
# Вечер второго дня
|
||||||
|
|
||||||
|
@label evening_day2
|
||||||
|
@bg sunset
|
||||||
|
|
||||||
|
*Сегодня вы вместе делали плот. Все устали, но довольны.*
|
||||||
|
|
||||||
|
Natsuki: Завтра можем попробовать на нём уплыть!
|
||||||
|
|
||||||
|
Aoi: Но лучше подождать, пока мы наберём еды.
|
||||||
|
|
||||||
|
Yume: Я всё ещё думаю, что в пещерах может быть что-то важное.
|
||||||
|
|
||||||
|
Player: Может, стоит провести ночь в пещере?
|
||||||
|
|
||||||
|
*Девушки переглядываются.*
|
||||||
|
|
||||||
|
Aoi: Я согласна, если будет тепло.
|
||||||
|
|
||||||
|
Natsuki: Только если будет музыка!
|
||||||
|
|
||||||
|
Yume: Если ты будешь рядом, я соглашусь.
|
||||||
|
|
||||||
|
*Все улыбаются.*
|
||||||
|
|
||||||
|
@set day = 3
|
||||||
|
@goto night_day2
|
||||||
|
|
||||||
|
# Ночь второго дня
|
||||||
|
|
||||||
|
@label night_day2
|
||||||
|
@bg night
|
||||||
|
|
||||||
|
*Вы в пещере. Огонь освещает лица девушек.*
|
||||||
|
|
||||||
|
Aoi: Было бы хорошо, если бы мы могли остаться так навсегда.
|
||||||
|
|
||||||
|
Natsuki: Только без дождя и пауков!
|
||||||
|
|
||||||
|
Yume: Здесь тихо... Мне нравится.
|
||||||
|
|
||||||
|
Player: Я рад, что вы все здесь.
|
||||||
|
|
||||||
|
*Тишина.*
|
||||||
|
|
||||||
|
*Aoi кладёт голову вам на плечо.*
|
||||||
|
|
||||||
|
Aoi: Ты... особенный для меня.
|
||||||
|
|
||||||
|
*Natsuki смеётся.*
|
||||||
|
|
||||||
|
Natsuki: А ты уверен, кто тебе нравится больше?
|
||||||
|
|
||||||
|
*Юме молчит, но смотрит на вас с теплотой.*
|
||||||
|
|
||||||
|
@goto morning_day3
|
||||||
|
|
||||||
|
# Утро третьего дня
|
||||||
|
|
||||||
|
@label morning_day3
|
||||||
|
@bg morning
|
||||||
|
|
||||||
|
*Вы просыпаетесь в пещере. Всё тело затекло, но рядом лежит Юме.*
|
||||||
|
|
||||||
|
Yume: Ты проснулся? Хорошо.
|
||||||
|
|
||||||
|
Player: Ты спала рядом?
|
||||||
|
|
||||||
|
Yume: Да. Мне было спокойнее.
|
||||||
|
|
||||||
|
*Вы выходите из пещеры. Вдалеке виднеется парус.*
|
||||||
|
|
||||||
|
Player: Смотрите! Это корабль!
|
||||||
|
|
||||||
|
*Все радуются.*
|
||||||
|
|
||||||
|
Natsuki: Он нас спасёт!
|
||||||
|
|
||||||
|
Aoi: Но... ты же выберешь кого-то из нас?
|
||||||
|
|
||||||
|
Player: Я...
|
||||||
|
|
||||||
|
*Выбор:*
|
||||||
|
|
||||||
|
choice Сказать "Я люблю тебя" Аои=>goto ending_aoi | Сказать "Я люблю тебя" Нацуки=>goto ending_natsuki | Сказать "Я люблю тебя" Юме=>goto ending_yume | Сказать "Я не могу выбрать"=>goto ending_all
|
||||||
|
|
||||||
|
# Концовка: Аои
|
||||||
|
|
||||||
|
@label ending_aoi
|
||||||
|
@set aoi += 20
|
||||||
|
|
||||||
|
*Aoi смотрит на вас с благодарностью.*
|
||||||
|
|
||||||
|
Aoi: Я... рада.
|
||||||
|
|
||||||
|
*Вы подходите к кораблю. Аои берёт вас за руку.*
|
||||||
|
|
||||||
|
*Финал: Вы возвращаетесь домой вместе. Через год вы женитесь на Аои.*
|
||||||
|
|
||||||
|
@bg white
|
||||||
|
*A happy ending with Aoi*
|
||||||
|
@goto end
|
||||||
|
|
||||||
|
# Концовка: Нацуки
|
||||||
|
|
||||||
|
@label ending_natsuki
|
||||||
|
@set natsuki += 20
|
||||||
|
|
||||||
|
*Natsuki смеётся и обнимает вас.*
|
||||||
|
|
||||||
|
Natsuki: Ты лучший!
|
||||||
|
|
||||||
|
*Вы подходите к кораблю. Но внезапно Нацуки достаёт нож.*
|
||||||
|
|
||||||
|
Natsuki: Я не могу позволить тебе уйти...
|
||||||
|
|
||||||
|
*Она ударяет вас в спину.*
|
||||||
|
|
||||||
|
@bg black
|
||||||
|
*Bad ending: Killed by Natsuki*
|
||||||
|
@goto end
|
||||||
|
|
||||||
|
# Концовка: Юме
|
||||||
|
|
||||||
|
@label ending_yume
|
||||||
|
@set yume += 20
|
||||||
|
|
||||||
|
*Юме берёт вас за руку.*
|
||||||
|
|
||||||
|
Yume: Я буду с тобой.
|
||||||
|
|
||||||
|
*Вы подходите к кораблю. Но Юме вдруг толкает вас вниз.*
|
||||||
|
|
||||||
|
Yume: Прости... я не могу.
|
||||||
|
|
||||||
|
*Она убегает обратно в пещеру.*
|
||||||
|
|
||||||
|
@bg black
|
||||||
|
*Sad ending: Yume leaves you behind*
|
||||||
|
@goto end
|
||||||
|
|
||||||
|
# Концовка: Не могу выбрать
|
||||||
|
|
||||||
|
@label ending_all
|
||||||
|
|
||||||
|
*Natsuki, Аои и Юме смотрят на вас с разочарованием.*
|
||||||
|
|
||||||
|
Aoi: Тогда... мы уйдём.
|
||||||
|
|
||||||
|
Natsuki: Без тебя.
|
||||||
|
|
||||||
|
Yume: Прости.
|
||||||
|
|
||||||
|
*Корабль уходит. Вы остаётесь один.*
|
||||||
|
|
||||||
|
@bg black
|
||||||
|
*Bad ending: You are left alone*
|
||||||
|
@goto end
|
||||||
|
|
||||||
|
# Конец игры
|
||||||
|
|
||||||
|
@label end
|
||||||
|
@bg black
|
||||||
|
|
||||||
|
*Игра окончена.*
|
||||||
|
*Хочешь начать заново?*
|
||||||
|
choice Перезапустить=>goto start | Выйти=>end_game
|
||||||
|
|
||||||
|
@end_game
|
||||||
410
tet/novel — копия.txt
Normal file
410
tet/novel — копия.txt
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
import pygame
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Инициализация Pygame
|
||||||
|
pygame.init()
|
||||||
|
|
||||||
|
# Настройки окна
|
||||||
|
WIDTH, HEIGHT = 1280, 720
|
||||||
|
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
||||||
|
pygame.display.set_caption("Визуальная новелла")
|
||||||
|
|
||||||
|
# Цвета
|
||||||
|
WHITE = (255, 255, 255)
|
||||||
|
BLACK = (0, 0, 0)
|
||||||
|
GRAY = (200, 200, 200)
|
||||||
|
LIGHT_BLUE = (173, 216, 230)
|
||||||
|
DARK_BLUE = (0, 0, 139)
|
||||||
|
CHOICE_BG = (240, 240, 255)
|
||||||
|
|
||||||
|
# Шрифты
|
||||||
|
font = pygame.font.Font(None, 36)
|
||||||
|
character_font = pygame.font.Font(None, 32)
|
||||||
|
choice_font = pygame.font.Font(None, 30)
|
||||||
|
small_font = pygame.font.Font(None, 24)
|
||||||
|
|
||||||
|
class NovelEngine:
|
||||||
|
def __init__(self):
|
||||||
|
self.script = []
|
||||||
|
self.current_line = 0
|
||||||
|
self.running = True
|
||||||
|
self.background = None
|
||||||
|
self.characters = {}
|
||||||
|
self.variables = {}
|
||||||
|
self.choices = []
|
||||||
|
self.waiting_for_choice = False
|
||||||
|
self.call_stack = [] # Стек для хранения позиций возврата
|
||||||
|
self.text_box_rect = pygame.Rect(50, HEIGHT - 250, WIDTH - 100, 200)
|
||||||
|
self.choice_box_rect = pygame.Rect(WIDTH//2 - 300, HEIGHT//2 - 150, 600, 300)
|
||||||
|
|
||||||
|
def load_script(self, filename):
|
||||||
|
"""Загрузка скрипта из файла"""
|
||||||
|
self.script = []
|
||||||
|
script_path = os.path.join(os.path.dirname(__file__), filename)
|
||||||
|
|
||||||
|
if not os.path.exists(script_path):
|
||||||
|
print(f"Ошибка: файл {script_path} не найден!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(script_path, 'r', encoding='utf-8') as file:
|
||||||
|
for line in file:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith('#'):
|
||||||
|
self.script.append(line)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def parse_character(self, command):
|
||||||
|
"""Обработка описания персонажа с дополнительными атрибутами"""
|
||||||
|
if command.startswith("char "):
|
||||||
|
parts = command[5:].split("|")
|
||||||
|
char_id = parts[0].strip().split(":")[0]
|
||||||
|
char_data = {
|
||||||
|
"name": parts[0].split(":")[1].strip(),
|
||||||
|
"appearance": parts[1].strip() if len(parts) > 1 else "",
|
||||||
|
"details": parts[2].strip() if len(parts) > 2 else "",
|
||||||
|
"scent": parts[3].strip() if len(parts) > 3 else ""
|
||||||
|
}
|
||||||
|
self.characters[char_id] = char_data
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def parse_command(self, command):
|
||||||
|
"""Обработка команд скрипта"""
|
||||||
|
# Случайный переход в подпрограмму
|
||||||
|
if command.startswith("random_gosub "):
|
||||||
|
parts = command[12:].split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
probability = float(parts[0])
|
||||||
|
if random.random() < probability:
|
||||||
|
# Сохраняем текущую позицию для возврата
|
||||||
|
self.call_stack.append(self.current_line + 1)
|
||||||
|
# Переходим к случайной подпрограмме
|
||||||
|
sub_label = random.choice(parts[1:])
|
||||||
|
self.jump_to_label(sub_label)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Возврат из подпрограммы
|
||||||
|
elif command == "return":
|
||||||
|
if self.call_stack:
|
||||||
|
self.current_line = self.call_stack.pop()
|
||||||
|
else:
|
||||||
|
print("Ошибка: стек вызовов пуст!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Установка переменной
|
||||||
|
elif command.startswith("set "):
|
||||||
|
parts = command[4:].split("|")
|
||||||
|
for part in parts:
|
||||||
|
if "=" in part:
|
||||||
|
var, val = part.split("=", 1)
|
||||||
|
self.variables[var.strip()] = val.strip()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Условие
|
||||||
|
elif command.startswith("if "):
|
||||||
|
match = re.match(r'if (\w+)([=!<>]+)(.+?) then goto (\w+)', command[3:])
|
||||||
|
if match:
|
||||||
|
var_name, op, value, label = match.groups()
|
||||||
|
current_value = self.variables.get(var_name, "")
|
||||||
|
|
||||||
|
condition_met = False
|
||||||
|
if op == "==":
|
||||||
|
condition_met = str(current_value) == value
|
||||||
|
elif op == "!=":
|
||||||
|
condition_met = str(current_value) != value
|
||||||
|
elif op == ">":
|
||||||
|
condition_met = float(current_value) > float(value)
|
||||||
|
elif op == "<":
|
||||||
|
condition_met = float(current_value) < float(value)
|
||||||
|
|
||||||
|
if condition_met:
|
||||||
|
self.jump_to_label(label)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Метка
|
||||||
|
elif command.startswith("label "):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Подпрограмма
|
||||||
|
elif command.startswith("sub_"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Переход
|
||||||
|
elif command.startswith("goto "):
|
||||||
|
label = command[5:].strip()
|
||||||
|
self.jump_to_label(label)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Фон
|
||||||
|
elif command.startswith("bg "):
|
||||||
|
bg_color = command[3:].strip().lower()
|
||||||
|
if bg_color == "black":
|
||||||
|
self.background = BLACK
|
||||||
|
elif bg_color == "white":
|
||||||
|
self.background = WHITE
|
||||||
|
elif bg_color == "blue":
|
||||||
|
self.background = LIGHT_BLUE
|
||||||
|
elif bg_color == "sunset":
|
||||||
|
self.background = (255, 153, 51) # Оранжевый для заката
|
||||||
|
else:
|
||||||
|
self.background = GRAY
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Персонаж
|
||||||
|
elif command.startswith("char "):
|
||||||
|
return self.parse_character(command)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def draw_descriptions(self, text, x, y, font_obj, color):
|
||||||
|
"""Отрисовка текста с учётом звёздочек"""
|
||||||
|
if text.startswith("*") and text.endswith("*"):
|
||||||
|
# Рисуем курсивом для описаний
|
||||||
|
italic_font = pygame.font.Font(None, 32)
|
||||||
|
italic_font.set_italic(True)
|
||||||
|
self.draw_text(text[1:-1], x, y, italic_font, color)
|
||||||
|
else:
|
||||||
|
self.draw_text(text, x, y, font_obj, color)
|
||||||
|
|
||||||
|
def jump_to_label(self, label):
|
||||||
|
"""Переход к метке в скрипте"""
|
||||||
|
for i, line in enumerate(self.script):
|
||||||
|
if line.startswith(f"@label {label}") or line.startswith(f"@{label}"):
|
||||||
|
self.current_line = i
|
||||||
|
return
|
||||||
|
print(f"Метка '{label}' не найдена!")
|
||||||
|
|
||||||
|
def process_choices(self, line):
|
||||||
|
"""Обработка строки с выбором"""
|
||||||
|
if line.startswith("choice "):
|
||||||
|
choices_text = line[7:].split("|")
|
||||||
|
self.choices = []
|
||||||
|
|
||||||
|
for choice_text in choices_text:
|
||||||
|
parts = choice_text.split("=>")
|
||||||
|
if len(parts) == 2:
|
||||||
|
choice_display = parts[0].strip()
|
||||||
|
choice_action = parts[1].strip()
|
||||||
|
self.choices.append((choice_display, choice_action))
|
||||||
|
|
||||||
|
self.waiting_for_choice = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def substitute_variables(self, text):
|
||||||
|
"""Подстановка переменных в текст"""
|
||||||
|
for var_name, var_value in self.variables.items():
|
||||||
|
text = text.replace(f"{{{var_name}}}", str(var_value))
|
||||||
|
return text
|
||||||
|
|
||||||
|
def get_character_info(self, char_id):
|
||||||
|
"""Возвращает полное описание персонажа"""
|
||||||
|
if char_id not in self.characters:
|
||||||
|
return f"Персонаж {char_id}", ""
|
||||||
|
|
||||||
|
char = self.characters.get(char_id)
|
||||||
|
if isinstance(char, str):
|
||||||
|
return char, ""
|
||||||
|
|
||||||
|
details = []
|
||||||
|
if char["appearance"]: details.append(char['appearance'])
|
||||||
|
if char["details"]: details.append(char['details'])
|
||||||
|
if char["scent"]: details.append(char['scent'])
|
||||||
|
|
||||||
|
return char["name"], " | ".join(details)
|
||||||
|
|
||||||
|
def process_line(self):
|
||||||
|
if self.current_line >= len(self.script):
|
||||||
|
self.running = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
line = self.script[self.current_line]
|
||||||
|
line = self.substitute_variables(line)
|
||||||
|
|
||||||
|
# Обработка команд
|
||||||
|
if line.startswith("@"):
|
||||||
|
if self.parse_command(line[1:]):
|
||||||
|
self.current_line += 1
|
||||||
|
return self.process_line()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Обработка выбора
|
||||||
|
if self.process_choices(line):
|
||||||
|
self.current_line += 1
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Обработка диалога с проверкой разделителя
|
||||||
|
if ":" in line and not line.startswith("*"):
|
||||||
|
parts = line.split(":", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
char_id = parts[0].strip()
|
||||||
|
text = parts[1].strip()
|
||||||
|
return char_id, text
|
||||||
|
|
||||||
|
# Обработка описания или простого текста
|
||||||
|
return None, line
|
||||||
|
|
||||||
|
def make_choice(self, choice_index):
|
||||||
|
"""Обработка выбора игрока"""
|
||||||
|
if 0 <= choice_index < len(self.choices):
|
||||||
|
choice_action = self.choices[choice_index][1]
|
||||||
|
|
||||||
|
if choice_action.startswith("goto "):
|
||||||
|
label = choice_action[5:].strip()
|
||||||
|
self.jump_to_label(label)
|
||||||
|
elif "+=" in choice_action:
|
||||||
|
var, value = choice_action.split("+=", 1)
|
||||||
|
var = var.strip()
|
||||||
|
self.variables[var] = str(int(self.variables.get(var, 0)) + int(value.strip()))
|
||||||
|
self.current_line += 1
|
||||||
|
elif "-=" in choice_action:
|
||||||
|
var, value = choice_action.split("-=", 1)
|
||||||
|
var = var.strip()
|
||||||
|
self.variables[var] = str(int(self.variables.get(var, 0)) - int(value.strip()))
|
||||||
|
self.current_line += 1
|
||||||
|
elif "=" in choice_action:
|
||||||
|
var, value = choice_action.split("=", 1)
|
||||||
|
self.variables[var.strip()] = value.strip()
|
||||||
|
self.current_line += 1
|
||||||
|
|
||||||
|
self.choices = []
|
||||||
|
self.waiting_for_choice = False
|
||||||
|
|
||||||
|
def next_line(self):
|
||||||
|
"""Переход к следующей строке"""
|
||||||
|
self.current_line += 1
|
||||||
|
if self.current_line >= len(self.script):
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def draw_text_box(self):
|
||||||
|
"""Отрисовка текстового поля"""
|
||||||
|
pygame.draw.rect(screen, WHITE, self.text_box_rect)
|
||||||
|
pygame.draw.rect(screen, DARK_BLUE, self.text_box_rect, 3)
|
||||||
|
|
||||||
|
def draw_choice_box(self):
|
||||||
|
"""Отрисовка поля выбора"""
|
||||||
|
pygame.draw.rect(screen, CHOICE_BG, self.choice_box_rect)
|
||||||
|
pygame.draw.rect(screen, DARK_BLUE, self.choice_box_rect, 3)
|
||||||
|
|
||||||
|
def draw_text(self, text, x, y, font_obj, color=BLACK, max_width=None):
|
||||||
|
"""Отрисовка текста с переносом строк"""
|
||||||
|
if max_width is None:
|
||||||
|
max_width = self.text_box_rect.width - 40
|
||||||
|
|
||||||
|
words = text.split(' ')
|
||||||
|
space_width = font_obj.size(' ')[0]
|
||||||
|
line_height = font_obj.get_height()
|
||||||
|
|
||||||
|
current_line = []
|
||||||
|
current_width = 0
|
||||||
|
|
||||||
|
for word in words:
|
||||||
|
word_surface = font_obj.render(word, True, color)
|
||||||
|
word_width = word_surface.get_width()
|
||||||
|
|
||||||
|
if current_width + word_width <= max_width:
|
||||||
|
current_line.append(word)
|
||||||
|
current_width += word_width + space_width
|
||||||
|
else:
|
||||||
|
line_text = ' '.join(current_line)
|
||||||
|
line_surface = font_obj.render(line_text, True, color)
|
||||||
|
screen.blit(line_surface, (x, y))
|
||||||
|
y += line_height
|
||||||
|
current_line = [word]
|
||||||
|
current_width = word_width + space_width
|
||||||
|
|
||||||
|
if current_line:
|
||||||
|
line_text = ' '.join(current_line)
|
||||||
|
line_surface = font_obj.render(line_text, True, color)
|
||||||
|
screen.blit(line_surface, (x, y))
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
"""Отрисовка текущего состояния"""
|
||||||
|
# Фон
|
||||||
|
bg_color = self.background if self.background else LIGHT_BLUE
|
||||||
|
screen.fill(bg_color)
|
||||||
|
|
||||||
|
# Текстовое поле
|
||||||
|
self.draw_text_box()
|
||||||
|
|
||||||
|
# Текст
|
||||||
|
line_data = self.process_line()
|
||||||
|
x = self.text_box_rect.x + 20
|
||||||
|
y = self.text_box_rect.y + 20
|
||||||
|
|
||||||
|
if line_data and not self.waiting_for_choice:
|
||||||
|
if isinstance(line_data, tuple): # Диалог с персонажем
|
||||||
|
char_id, text = line_data
|
||||||
|
name, details = self.get_character_info(char_id)
|
||||||
|
|
||||||
|
# Отрисовка имени персонажа
|
||||||
|
name_surface = character_font.render(name + ":", True, DARK_BLUE)
|
||||||
|
screen.blit(name_surface, (x, y))
|
||||||
|
y += 40
|
||||||
|
|
||||||
|
# Отрисовка текста персонажа
|
||||||
|
self.draw_text(text, x, y, font, BLACK)
|
||||||
|
y += 60
|
||||||
|
|
||||||
|
# Отрисовка деталей персонажа (если есть)
|
||||||
|
if details:
|
||||||
|
details_surface = small_font.render(details, True, (100, 100, 100))
|
||||||
|
screen.blit(details_surface, (x, y))
|
||||||
|
else: # Описание или простой текст
|
||||||
|
_, text = line_data
|
||||||
|
self.draw_descriptions(text, x, y, font, BLACK)
|
||||||
|
|
||||||
|
# Варианты выбора
|
||||||
|
if self.waiting_for_choice and self.choices:
|
||||||
|
self.draw_choice_box()
|
||||||
|
choice_y = self.choice_box_rect.y + 30
|
||||||
|
for i, (choice_text, _) in enumerate(self.choices):
|
||||||
|
choice_surface = choice_font.render(f"{i+1}. {choice_text}", True, BLACK)
|
||||||
|
screen.blit(choice_surface, (self.choice_box_rect.x + 30, choice_y))
|
||||||
|
choice_y += 40
|
||||||
|
|
||||||
|
# Индикатор продолжения
|
||||||
|
if self.running and not self.waiting_for_choice and line_data:
|
||||||
|
indicator = font.render("> Нажмите ПРОБЕЛ", True, DARK_BLUE)
|
||||||
|
screen.blit(indicator, (WIDTH - 200, HEIGHT - 50))
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Основной цикл"""
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
|
elif event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_SPACE and not self.waiting_for_choice:
|
||||||
|
self.next_line()
|
||||||
|
elif event.key == pygame.K_1 and self.waiting_for_choice and len(self.choices) >= 1:
|
||||||
|
self.make_choice(0)
|
||||||
|
elif event.key == pygame.K_2 and self.waiting_for_choice and len(self.choices) >= 2:
|
||||||
|
self.make_choice(1)
|
||||||
|
elif event.key == pygame.K_3 and self.waiting_for_choice and len(self.choices) >= 3:
|
||||||
|
self.make_choice(2)
|
||||||
|
elif event.key == pygame.K_4 and self.waiting_for_choice and len(self.choices) >= 4:
|
||||||
|
self.make_choice(3)
|
||||||
|
elif event.key == pygame.K_ESCAPE:
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
self.draw()
|
||||||
|
clock.tick(60)
|
||||||
|
|
||||||
|
# Создаем экземпляр движка и запускаем
|
||||||
|
if __name__ == "__main__":
|
||||||
|
engine = NovelEngine()
|
||||||
|
|
||||||
|
if engine.load_script("island_script.txt"):
|
||||||
|
engine.run()
|
||||||
|
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
621
tet/novel.py
Normal file
621
tet/novel.py
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
import pygame
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Инициализация Pygame
|
||||||
|
pygame.init()
|
||||||
|
|
||||||
|
# Настройки окна
|
||||||
|
WIDTH, HEIGHT = 1920, 1080
|
||||||
|
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
||||||
|
pygame.display.set_caption("Визуальная новелла")
|
||||||
|
|
||||||
|
# Цвета
|
||||||
|
WHITE = (255, 255, 255)
|
||||||
|
BLACK = (0, 0, 0)
|
||||||
|
GRAY = (200, 200, 200)
|
||||||
|
LIGHT_BLUE = (173, 216, 230)
|
||||||
|
DARK_BLUE = (0, 0, 139)
|
||||||
|
CHOICE_BG = (240, 240, 255)
|
||||||
|
|
||||||
|
NIGHT_BLUE = (5, 5, 30)
|
||||||
|
MORNING_YELLOW = (255, 220, 100)
|
||||||
|
MORNING_GREEN = (100, 180, 80)
|
||||||
|
MORNING_DARK_BLUE = (20, 40, 100)
|
||||||
|
DAY_GREEN = (100, 200, 80)
|
||||||
|
DAY_BLUE = (100, 200, 255)
|
||||||
|
EVENING_ORANGE = (255, 120, 50)
|
||||||
|
EVENING_DARK_GREEN = (30, 80, 40)
|
||||||
|
|
||||||
|
# Шрифты
|
||||||
|
try:
|
||||||
|
font = pygame.font.Font(None, 36)
|
||||||
|
character_font = pygame.font.Font(None, 32)
|
||||||
|
choice_font = pygame.font.Font(None, 30)
|
||||||
|
small_font = pygame.font.Font(None, 24)
|
||||||
|
italic_font = pygame.font.Font(None, 32)
|
||||||
|
italic_font.set_italic(True)
|
||||||
|
except:
|
||||||
|
font = pygame.font.Font(None, 36)
|
||||||
|
character_font = pygame.font.Font(None, 32)
|
||||||
|
choice_font = pygame.font.Font(None, 30)
|
||||||
|
small_font = pygame.font.Font(None, 24)
|
||||||
|
italic_font = pygame.font.Font(None, 32)
|
||||||
|
italic_font.set_italic(True)
|
||||||
|
|
||||||
|
class NovelEngine:
|
||||||
|
def __init__(self):
|
||||||
|
self.script = []
|
||||||
|
self.current_line = 0
|
||||||
|
self.running = True
|
||||||
|
self.background = None
|
||||||
|
self.characters = {}
|
||||||
|
self.variables = {}
|
||||||
|
self.choices = []
|
||||||
|
self.waiting_for_choice = False
|
||||||
|
self.call_stack = [] # Стек для хранения позиций возврата
|
||||||
|
self.text_box_rect = pygame.Rect(50, HEIGHT - 250, WIDTH - 100, 200)
|
||||||
|
self.choice_box_rect = pygame.Rect(WIDTH//2 - 300, HEIGHT//2 - 150, 600, 300)
|
||||||
|
self.special_background = None
|
||||||
|
self.background_textures = {} # Текстуры для сложных фонов
|
||||||
|
self.init_background_textures() # Инициализируем текстуры
|
||||||
|
|
||||||
|
def load_script(self, filename):
|
||||||
|
"""Загрузка скрипта из файла"""
|
||||||
|
self.script = []
|
||||||
|
script_path = os.path.join(os.path.dirname(__file__), filename)
|
||||||
|
|
||||||
|
if not os.path.exists(script_path):
|
||||||
|
print(f"Ошибка: файл {script_path} не найден!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(script_path, 'r', encoding='utf-8') as file:
|
||||||
|
for line in file:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith('#'):
|
||||||
|
self.script.append(line)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def parse_character(self, command):
|
||||||
|
"""Обработка описания персонажа с дополнительными атрибутами"""
|
||||||
|
if command.startswith("char "):
|
||||||
|
parts = command[5:].split("|")
|
||||||
|
char_id = parts[0].strip().split(":")[0]
|
||||||
|
char_data = {
|
||||||
|
"name": parts[0].split(":")[1].strip(),
|
||||||
|
"appearance": parts[1].strip() if len(parts) > 1 else "",
|
||||||
|
"details": parts[2].strip() if len(parts) > 2 else "",
|
||||||
|
"scent": parts[3].strip() if len(parts) > 3 else ""
|
||||||
|
}
|
||||||
|
self.characters[char_id] = char_data
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def init_background_textures(self):
|
||||||
|
"""Создаем текстуры для сложных фонов один раз при инициализации"""
|
||||||
|
# Ночное небо
|
||||||
|
night_surface = pygame.Surface((WIDTH, HEIGHT))
|
||||||
|
night_surface.fill(NIGHT_BLUE)
|
||||||
|
for _ in range(100):
|
||||||
|
x = random.randint(0, WIDTH)
|
||||||
|
y = random.randint(0, HEIGHT // 2)
|
||||||
|
size = random.randint(1, 3)
|
||||||
|
brightness = random.randint(200, 255)
|
||||||
|
pygame.draw.circle(night_surface, (brightness, brightness, brightness), (x, y), size)
|
||||||
|
self.background_textures["night"] = night_surface
|
||||||
|
|
||||||
|
morning_surface = pygame.Surface((WIDTH, HEIGHT))
|
||||||
|
for y in range(HEIGHT):
|
||||||
|
ratio = y / HEIGHT
|
||||||
|
# Верхняя часть (темно-синяя)
|
||||||
|
if ratio < 0.3:
|
||||||
|
r = int(MORNING_DARK_BLUE[0])
|
||||||
|
g = int(MORNING_DARK_BLUE[1])
|
||||||
|
b = int(MORNING_DARK_BLUE[2])
|
||||||
|
# Средняя часть (переход)
|
||||||
|
elif ratio < 0.7:
|
||||||
|
r = int(MORNING_DARK_BLUE[0] + (MORNING_YELLOW[0] - MORNING_DARK_BLUE[0]) * (ratio - 0.3) / 0.4)
|
||||||
|
g = int(MORNING_DARK_BLUE[1] + (MORNING_YELLOW[1] - MORNING_DARK_BLUE[1]) * (ratio - 0.3) / 0.4)
|
||||||
|
b = int(MORNING_DARK_BLUE[2] + (MORNING_YELLOW[2] - MORNING_DARK_BLUE[2]) * (ratio - 0.3) / 0.4)
|
||||||
|
# Нижняя часть (зеленая)
|
||||||
|
else:
|
||||||
|
r = int(MORNING_GREEN[0])
|
||||||
|
g = int(MORNING_GREEN[1])
|
||||||
|
b = int(MORNING_GREEN[2])
|
||||||
|
pygame.draw.line(morning_surface, (r, g, b), (0, y), (WIDTH, y))
|
||||||
|
pygame.draw.circle(morning_surface, MORNING_YELLOW, (WIDTH // 2, HEIGHT // 3), 50)
|
||||||
|
self.background_textures["morning"] = morning_surface
|
||||||
|
|
||||||
|
day_surface = pygame.Surface((WIDTH, HEIGHT))
|
||||||
|
# Градиент от голубого к зеленому
|
||||||
|
for y in range(HEIGHT):
|
||||||
|
ratio = y / HEIGHT
|
||||||
|
if ratio < 0.8:
|
||||||
|
r = int(DAY_BLUE[0])
|
||||||
|
g = int(DAY_BLUE[1])
|
||||||
|
b = int(DAY_BLUE[2])
|
||||||
|
else:
|
||||||
|
r = int(DAY_GREEN[0])
|
||||||
|
g = int(DAY_GREEN[1])
|
||||||
|
b = int(DAY_GREEN[2])
|
||||||
|
pygame.draw.line(day_surface, (r, g, b), (0, y), (WIDTH, y))
|
||||||
|
|
||||||
|
# Рисуем облака
|
||||||
|
for _ in range(5):
|
||||||
|
x = random.randint(0, WIDTH)
|
||||||
|
y = random.randint(50, HEIGHT // 3)
|
||||||
|
size = random.randint(30, 70)
|
||||||
|
pygame.draw.circle(day_surface, WHITE, (x, y), size)
|
||||||
|
pygame.draw.circle(day_surface, WHITE, (x + size//2, y - size//3), size//2)
|
||||||
|
pygame.draw.circle(day_surface, WHITE, (x - size//2, y - size//4), size//2)
|
||||||
|
self.background_textures["day"] = day_surface
|
||||||
|
|
||||||
|
evening_surface = pygame.Surface((WIDTH, HEIGHT))
|
||||||
|
for y in range(HEIGHT):
|
||||||
|
ratio = y / HEIGHT
|
||||||
|
if ratio < 0.5:
|
||||||
|
r = int(EVENING_ORANGE[0])
|
||||||
|
g = int(EVENING_ORANGE[1])
|
||||||
|
b = int(EVENING_ORANGE[2])
|
||||||
|
else:
|
||||||
|
r = int(EVENING_ORANGE[0] + (EVENING_DARK_GREEN[0] - EVENING_ORANGE[0]) * (ratio - 0.5) / 0.5)
|
||||||
|
g = int(EVENING_ORANGE[1] + (EVENING_DARK_GREEN[1] - EVENING_ORANGE[1]) * (ratio - 0.5) / 0.5)
|
||||||
|
b = int(EVENING_ORANGE[2] + (EVENING_DARK_GREEN[2] - EVENING_ORANGE[2]) * (ratio - 0.5) / 0.5)
|
||||||
|
pygame.draw.line(evening_surface, (r, g, b), (0, y), (WIDTH, y))
|
||||||
|
|
||||||
|
# Рисуем заходящее солнце
|
||||||
|
pygame.draw.circle(evening_surface, (255, 200, 100), (WIDTH // 2, HEIGHT // 2), 60)
|
||||||
|
pygame.draw.rect(evening_surface, EVENING_DARK_GREEN, (0, HEIGHT // 2, WIDTH, HEIGHT // 2))
|
||||||
|
self.background_textures["evening"] = evening_surface
|
||||||
|
|
||||||
|
def draw_night_sky(self):
|
||||||
|
"""Рисует ночное небо со звездами"""
|
||||||
|
screen.blit(self.background_textures["night"], (0, 0))
|
||||||
|
|
||||||
|
def draw_morning_sky(self):
|
||||||
|
"""Рисует утреннее небо с градиентом"""
|
||||||
|
screen.blit(self.background_textures["morning"], (0, 0))
|
||||||
|
|
||||||
|
def draw_day_sky(self):
|
||||||
|
"""Рисует дневное небо с облаками"""
|
||||||
|
screen.blit(self.background_textures["day"], (0, 0))
|
||||||
|
|
||||||
|
def draw_evening_sky(self):
|
||||||
|
"""Рисует вечернее небо с закатом"""
|
||||||
|
screen.blit(self.background_textures["evening"], (0, 0))
|
||||||
|
|
||||||
|
def parse_command(self, command):
|
||||||
|
"""Обработка команд скрипта"""
|
||||||
|
# Случайный переход в подпрограмму
|
||||||
|
if command.startswith("random_gosub "):
|
||||||
|
parts = command[12:].split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
# Создаем список вариантов с их вероятностями
|
||||||
|
options = []
|
||||||
|
probabilities = []
|
||||||
|
labels = []
|
||||||
|
|
||||||
|
# Разбираем части на вероятности и метки
|
||||||
|
i = 0
|
||||||
|
while i < len(parts):
|
||||||
|
try:
|
||||||
|
prob = float(parts[i])
|
||||||
|
label = parts[i+1]
|
||||||
|
probabilities.append(prob)
|
||||||
|
labels.append(label)
|
||||||
|
options.append((prob, label))
|
||||||
|
i += 2
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
break
|
||||||
|
|
||||||
|
if options:
|
||||||
|
# Нормализуем вероятности (на случай, если они не суммируются в 1)
|
||||||
|
total = sum(prob for prob, label in options)
|
||||||
|
if total > 0:
|
||||||
|
rand = random.random() * total
|
||||||
|
cumulative = 0
|
||||||
|
for prob, label in options:
|
||||||
|
cumulative += prob
|
||||||
|
if rand <= cumulative:
|
||||||
|
# Сохраняем текущую позицию для возврата
|
||||||
|
self.call_stack.append(self.current_line + 1)
|
||||||
|
# Переходим к выбранной подпрограмме
|
||||||
|
self.jump_to_label(label)
|
||||||
|
break
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Возврат из подпрограммы
|
||||||
|
elif command == "return":
|
||||||
|
if self.call_stack:
|
||||||
|
self.current_line = self.call_stack.pop()
|
||||||
|
else:
|
||||||
|
print("Ошибка: стек вызовов пуст!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Установка переменной
|
||||||
|
elif command.startswith("set "):
|
||||||
|
parts = command[4:].split("|")
|
||||||
|
for part in parts:
|
||||||
|
part = part.strip()
|
||||||
|
if "+=" in part:
|
||||||
|
var, val = part.split("+=", 1)
|
||||||
|
var = var.strip()
|
||||||
|
current = int(self.variables.get(var, 0))
|
||||||
|
self.variables[var] = str(current + int(val.strip()))
|
||||||
|
elif "-=" in part:
|
||||||
|
var, val = part.split("-=", 1)
|
||||||
|
var = var.strip()
|
||||||
|
current = int(self.variables.get(var, 0))
|
||||||
|
self.variables[var] = str(current - int(val.strip()))
|
||||||
|
elif "=" in part:
|
||||||
|
var, val = part.split("=", 1)
|
||||||
|
var = var.strip()
|
||||||
|
val = val.strip()
|
||||||
|
|
||||||
|
# Обработка инкремента (++var)
|
||||||
|
if val.startswith("++"):
|
||||||
|
var_to_inc = val[2:]
|
||||||
|
self.variables[var] = str(int(self.variables.get(var_to_inc, 0)) + 1)
|
||||||
|
# Обработка декремента (--var)
|
||||||
|
elif val.startswith("--"):
|
||||||
|
var_to_dec = val[2:]
|
||||||
|
self.variables[var] = str(int(self.variables.get(var_to_dec, 0)) - 1)
|
||||||
|
# Обычное присваивание
|
||||||
|
else:
|
||||||
|
self.variables[var] = val
|
||||||
|
print(f"После выполнения '{command}': {self.variables}") # Отладочный вывод
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Условие
|
||||||
|
elif command.startswith("if "):
|
||||||
|
# ИСПРАВЛЕННЫЙ regex с обработкой пробелов
|
||||||
|
match = re.match(r'if\s+(\w+)\s*([=!<>]+)\s*(.+?)\s+then\s+goto\s+(\w+)', command)
|
||||||
|
if match:
|
||||||
|
var_name, op, value, label = match.groups()
|
||||||
|
current_value = self.variables.get(var_name, "0")
|
||||||
|
|
||||||
|
print(f"Отладка: if {var_name}({current_value}) {op} {value} then goto {label}") # Отладка
|
||||||
|
|
||||||
|
# Пробуем численное сравнение
|
||||||
|
try:
|
||||||
|
current_num = float(current_value)
|
||||||
|
value_num = float(value)
|
||||||
|
if op == ">": condition_met = current_num > value_num
|
||||||
|
elif op == "<": condition_met = current_num < value_num
|
||||||
|
elif op == ">=": condition_met = current_num >= value_num
|
||||||
|
elif op == "<=": condition_met = current_num <= value_num
|
||||||
|
elif op == "==": condition_met = current_num == value_num
|
||||||
|
elif op == "!=": condition_met = current_num != value_num
|
||||||
|
else: condition_met = False
|
||||||
|
except ValueError:
|
||||||
|
# Строковое сравнение
|
||||||
|
if op == ">": condition_met = current_value > value
|
||||||
|
elif op == "<": condition_met = current_value < value
|
||||||
|
elif op == ">=": condition_met = current_value >= value
|
||||||
|
elif op == "<=": condition_met = current_value <= value
|
||||||
|
elif op == "==": condition_met = current_value == value
|
||||||
|
elif op == "!=": condition_met = current_value != value
|
||||||
|
else: condition_met = False
|
||||||
|
|
||||||
|
if condition_met:
|
||||||
|
print(f"Условие выполнено, переход к {label}") # Отладка
|
||||||
|
self.jump_to_label(label)
|
||||||
|
else:
|
||||||
|
print("Условие не выполнено") # Отладка
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Метка
|
||||||
|
elif command.startswith("label "):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Подпрограмма
|
||||||
|
elif command.startswith("sub_"):
|
||||||
|
if not self.call_stack or self.call_stack[-1] != self.current_line:
|
||||||
|
self.call_stack.append(self.current_line)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Возврат из подпрограммы
|
||||||
|
elif command == "return":
|
||||||
|
if not self.call_stack:
|
||||||
|
print("Предупреждение: return без вызова подпрограммы, пропускаем")
|
||||||
|
self.current_line += 1
|
||||||
|
else:
|
||||||
|
self.current_line = self.call_stack.pop() + 1 # Возвращаемся на следующую строку
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Переход
|
||||||
|
elif command.startswith("goto "):
|
||||||
|
label = command[5:].strip()
|
||||||
|
self.jump_to_label(label)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Фон
|
||||||
|
elif command.startswith("bg "):
|
||||||
|
bg_color = command[3:].strip().lower()
|
||||||
|
if bg_color == "black":
|
||||||
|
self.background = BLACK
|
||||||
|
self.special_background = None
|
||||||
|
elif bg_color == "white":
|
||||||
|
self.background = WHITE
|
||||||
|
self.special_background = None
|
||||||
|
elif bg_color == "blue":
|
||||||
|
self.background = LIGHT_BLUE
|
||||||
|
self.special_background = None
|
||||||
|
elif bg_color == "night":
|
||||||
|
self.special_background = "night"
|
||||||
|
elif bg_color == "morning":
|
||||||
|
self.special_background = "morning"
|
||||||
|
elif bg_color == "day":
|
||||||
|
self.special_background = "day"
|
||||||
|
elif bg_color == "evening" or bg_color == "sunset":
|
||||||
|
self.special_background = "evening"
|
||||||
|
else:
|
||||||
|
self.background = GRAY
|
||||||
|
self.special_background = None
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Персонаж
|
||||||
|
elif command.startswith("char "):
|
||||||
|
return self.parse_character(command)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def draw_descriptions(self, text, x, y, font_obj, color):
|
||||||
|
"""Отрисовка текста с учётом звёздочек"""
|
||||||
|
if text.startswith("*") and text.endswith("*"):
|
||||||
|
# Удаляем звёздочки и рисуем курсивом
|
||||||
|
self.draw_text(text[1:-1], x, y, italic_font, (150, 150, 150))
|
||||||
|
else:
|
||||||
|
self.draw_text(text, x, y, font_obj, color)
|
||||||
|
|
||||||
|
def jump_to_label(self, label):
|
||||||
|
"""Переход к метке в скрипте"""
|
||||||
|
for i, line in enumerate(self.script):
|
||||||
|
if line.startswith(f"@label {label}") or line.startswith(f"@{label}"):
|
||||||
|
self.current_line = i
|
||||||
|
return
|
||||||
|
print(f"Метка '{label}' не найдена!")
|
||||||
|
|
||||||
|
def process_choices(self, line):
|
||||||
|
"""Обработка строки с выбором"""
|
||||||
|
if line.startswith("choice "):
|
||||||
|
choices_text = line[7:].split("|")
|
||||||
|
self.choices = []
|
||||||
|
|
||||||
|
for choice_text in choices_text:
|
||||||
|
parts = choice_text.split("=>")
|
||||||
|
if len(parts) == 2:
|
||||||
|
choice_display = parts[0].strip()
|
||||||
|
choice_action = parts[1].strip()
|
||||||
|
self.choices.append((choice_display, choice_action))
|
||||||
|
|
||||||
|
self.waiting_for_choice = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def substitute_variables(self, text):
|
||||||
|
"""Подстановка переменных в текст"""
|
||||||
|
for var_name, var_value in self.variables.items():
|
||||||
|
text = text.replace(f"{{{var_name}}}", str(var_value))
|
||||||
|
return text
|
||||||
|
|
||||||
|
def get_character_info(self, char_id):
|
||||||
|
"""Возвращает полное описание персонажа"""
|
||||||
|
# Убираем вывод "Персонаж none" для несуществующих ID
|
||||||
|
if char_id not in self.characters:
|
||||||
|
return "", "" # Возвращаем пустые строки
|
||||||
|
|
||||||
|
char = self.characters.get(char_id)
|
||||||
|
if isinstance(char, str):
|
||||||
|
return char, ""
|
||||||
|
|
||||||
|
details = []
|
||||||
|
if char.get("appearance"): details.append(char['appearance'])
|
||||||
|
if char.get("details"): details.append(char['details'])
|
||||||
|
if char.get("scent"): details.append(char['scent'])
|
||||||
|
|
||||||
|
return char.get("name", ""), " | ".join(details)
|
||||||
|
|
||||||
|
def process_line(self):
|
||||||
|
if self.current_line >= len(self.script):
|
||||||
|
self.running = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
line = self.script[self.current_line]
|
||||||
|
line = self.substitute_variables(line)
|
||||||
|
|
||||||
|
# Обработка команд
|
||||||
|
if line.startswith("@"):
|
||||||
|
if self.parse_command(line[1:]):
|
||||||
|
self.current_line += 1
|
||||||
|
return self.process_line()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Обработка выбора
|
||||||
|
if self.process_choices(line):
|
||||||
|
self.current_line += 1
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Обработка диалога с проверкой разделителя
|
||||||
|
if ":" in line and not line.startswith("*"):
|
||||||
|
parts = line.split(":", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
char_id = parts[0].strip()
|
||||||
|
text = parts[1].strip()
|
||||||
|
return char_id, text
|
||||||
|
|
||||||
|
# Обработка описания или простого текста
|
||||||
|
return None, line
|
||||||
|
|
||||||
|
def make_choice(self, choice_index):
|
||||||
|
"""Обработка выбора игрока"""
|
||||||
|
if 0 <= choice_index < len(self.choices):
|
||||||
|
choice_action = self.choices[choice_index][1]
|
||||||
|
|
||||||
|
if choice_action.startswith("goto "):
|
||||||
|
label = choice_action[5:].strip()
|
||||||
|
self.jump_to_label(label)
|
||||||
|
elif "+=" in choice_action:
|
||||||
|
var, value = choice_action.split("+=", 1)
|
||||||
|
var = var.strip()
|
||||||
|
self.variables[var] = str(int(self.variables.get(var, 0)) + int(value.strip()))
|
||||||
|
self.current_line += 1
|
||||||
|
elif "-=" in choice_action:
|
||||||
|
var, value = choice_action.split("-=", 1)
|
||||||
|
var = var.strip()
|
||||||
|
self.variables[var] = str(int(self.variables.get(var, 0)) - int(value.strip()))
|
||||||
|
self.current_line += 1
|
||||||
|
elif "=" in choice_action:
|
||||||
|
var, value = choice_action.split("=", 1)
|
||||||
|
self.variables[var.strip()] = value.strip()
|
||||||
|
self.current_line += 1
|
||||||
|
|
||||||
|
self.choices = []
|
||||||
|
self.waiting_for_choice = False
|
||||||
|
|
||||||
|
def next_line(self):
|
||||||
|
"""Переход к следующей строке"""
|
||||||
|
self.current_line += 1
|
||||||
|
if self.current_line >= len(self.script):
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def draw_text_box(self):
|
||||||
|
"""Отрисовка текстового поля"""
|
||||||
|
pygame.draw.rect(screen, WHITE, self.text_box_rect)
|
||||||
|
pygame.draw.rect(screen, DARK_BLUE, self.text_box_rect, 3)
|
||||||
|
|
||||||
|
def draw_choice_box(self):
|
||||||
|
"""Отрисовка поля выбора"""
|
||||||
|
# Создаем полупрозрачную поверхность
|
||||||
|
s = pygame.Surface((600, 300), pygame.SRCALPHA)
|
||||||
|
s.fill((240, 240, 255, 230)) # Последний параметр - прозрачность
|
||||||
|
screen.blit(s, (self.choice_box_rect.x, self.choice_box_rect.y))
|
||||||
|
pygame.draw.rect(screen, DARK_BLUE, self.choice_box_rect, 3)
|
||||||
|
|
||||||
|
def draw_text(self, text, x, y, font_obj, color=BLACK, max_width=None):
|
||||||
|
"""Отрисовка текста с переносом строк"""
|
||||||
|
if max_width is None:
|
||||||
|
max_width = self.text_box_rect.width - 40
|
||||||
|
|
||||||
|
words = text.split(' ')
|
||||||
|
space_width = font_obj.size(' ')[0]
|
||||||
|
line_height = font_obj.get_height()
|
||||||
|
|
||||||
|
current_line = []
|
||||||
|
current_width = 0
|
||||||
|
|
||||||
|
for word in words:
|
||||||
|
word_surface = font_obj.render(word, True, color)
|
||||||
|
word_width = word_surface.get_width()
|
||||||
|
|
||||||
|
if current_width + word_width <= max_width:
|
||||||
|
current_line.append(word)
|
||||||
|
current_width += word_width + space_width
|
||||||
|
else:
|
||||||
|
line_text = ' '.join(current_line)
|
||||||
|
line_surface = font_obj.render(line_text, True, color)
|
||||||
|
screen.blit(line_surface, (x, y))
|
||||||
|
y += line_height
|
||||||
|
current_line = [word]
|
||||||
|
current_width = word_width + space_width
|
||||||
|
|
||||||
|
if current_line:
|
||||||
|
line_text = ' '.join(current_line)
|
||||||
|
line_surface = font_obj.render(line_text, True, color)
|
||||||
|
screen.blit(line_surface, (x, y))
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
"""Отрисовка текущего состояния"""
|
||||||
|
# Фон
|
||||||
|
if self.special_background and self.special_background in self.background_textures:
|
||||||
|
screen.blit(self.background_textures[self.special_background], (0, 0))
|
||||||
|
elif self.background:
|
||||||
|
screen.fill(self.background)
|
||||||
|
else:
|
||||||
|
screen.fill(LIGHT_BLUE)
|
||||||
|
|
||||||
|
# Текстовое поле (убираем очистку перед выбором)
|
||||||
|
if not self.waiting_for_choice:
|
||||||
|
self.draw_text_box()
|
||||||
|
|
||||||
|
# Текст
|
||||||
|
line_data = self.process_line()
|
||||||
|
x = self.text_box_rect.x + 20
|
||||||
|
y = self.text_box_rect.y + 20
|
||||||
|
|
||||||
|
if line_data and not self.waiting_for_choice:
|
||||||
|
if isinstance(line_data, tuple): # Диалог с персонажем
|
||||||
|
char_id, text = line_data
|
||||||
|
name, details = self.get_character_info(char_id)
|
||||||
|
|
||||||
|
# Отрисовываем имя только если оно есть
|
||||||
|
if name:
|
||||||
|
name_surface = character_font.render(name + ":", True, DARK_BLUE)
|
||||||
|
screen.blit(name_surface, (x, y))
|
||||||
|
y += 40
|
||||||
|
|
||||||
|
# Отрисовка текста персонажа
|
||||||
|
self.draw_text(text, x, y, font, BLACK)
|
||||||
|
y += 60
|
||||||
|
|
||||||
|
# Отрисовка деталей персонажа (если есть)
|
||||||
|
if details:
|
||||||
|
details_surface = small_font.render(details, True, (100, 100, 100))
|
||||||
|
screen.blit(details_surface, (x, y))
|
||||||
|
else: # Описание или простой текст
|
||||||
|
_, text = line_data
|
||||||
|
self.draw_descriptions(text, x, y, font, BLACK)
|
||||||
|
|
||||||
|
# Варианты выбора
|
||||||
|
if self.waiting_for_choice and self.choices:
|
||||||
|
self.draw_choice_box()
|
||||||
|
choice_y = self.choice_box_rect.y + 30
|
||||||
|
for i, (choice_text, _) in enumerate(self.choices):
|
||||||
|
choice_surface = choice_font.render(f"{i+1}. {choice_text}", True, BLACK)
|
||||||
|
screen.blit(choice_surface, (self.choice_box_rect.x + 30, choice_y))
|
||||||
|
choice_y += 40
|
||||||
|
|
||||||
|
# Индикатор продолжения
|
||||||
|
if self.running and not self.waiting_for_choice and line_data:
|
||||||
|
indicator = font.render("> ПРОБЕЛ", True, DARK_BLUE)
|
||||||
|
screen.blit(indicator, (WIDTH - 200, HEIGHT - 50))
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Основной цикл"""
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
|
elif event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_SPACE and not self.waiting_for_choice:
|
||||||
|
self.next_line()
|
||||||
|
elif event.key == pygame.K_1 and self.waiting_for_choice and len(self.choices) >= 1:
|
||||||
|
self.make_choice(0)
|
||||||
|
elif event.key == pygame.K_2 and self.waiting_for_choice and len(self.choices) >= 2:
|
||||||
|
self.make_choice(1)
|
||||||
|
elif event.key == pygame.K_3 and self.waiting_for_choice and len(self.choices) >= 3:
|
||||||
|
self.make_choice(2)
|
||||||
|
elif event.key == pygame.K_4 and self.waiting_for_choice and len(self.choices) >= 4:
|
||||||
|
self.make_choice(3)
|
||||||
|
elif event.key == pygame.K_ESCAPE:
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
self.draw()
|
||||||
|
clock.tick(60)
|
||||||
|
|
||||||
|
# Создаем экземпляр движка и запускаем
|
||||||
|
if __name__ == "__main__":
|
||||||
|
engine = NovelEngine()
|
||||||
|
|
||||||
|
if engine.load_script("island_script.txt"):
|
||||||
|
engine.run()
|
||||||
|
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
188
tet/pong.py
Normal file
188
tet/pong.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import pygame
|
||||||
|
import sys
|
||||||
|
import random
|
||||||
|
import math
|
||||||
|
|
||||||
|
# Инициализация Pygame
|
||||||
|
pygame.init()
|
||||||
|
|
||||||
|
# Константы
|
||||||
|
WIDTH, HEIGHT = 1920, 1080
|
||||||
|
PADDLE_WIDTH, PADDLE_HEIGHT = 30, 200
|
||||||
|
BALL_SIZE = 30
|
||||||
|
WHITE = (255, 255, 255)
|
||||||
|
BLACK = (0, 0, 0)
|
||||||
|
GREEN = (50, 168, 82)
|
||||||
|
LIGHT_GREEN = (100, 200, 100)
|
||||||
|
FPS = 120 # Увеличили FPS для плавности на высоких скоростях
|
||||||
|
|
||||||
|
# Создание окна
|
||||||
|
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
||||||
|
pygame.display.set_caption("Pong - ХЕРАЧЬ Edition v2.0")
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
# Загрузка шрифтов
|
||||||
|
try:
|
||||||
|
pixel_font = pygame.font.Font("PressStart2P-Regular.ttf", 120)
|
||||||
|
except:
|
||||||
|
pixel_font = pygame.font.SysFont('arial', 120)
|
||||||
|
|
||||||
|
# Игровые объекты
|
||||||
|
player_paddle = pygame.Rect(100, HEIGHT // 2 - PADDLE_HEIGHT // 2, PADDLE_WIDTH, PADDLE_HEIGHT)
|
||||||
|
ai_paddle = pygame.Rect(WIDTH - 100 - PADDLE_WIDTH, HEIGHT // 2 - PADDLE_HEIGHT // 2, PADDLE_WIDTH, PADDLE_HEIGHT)
|
||||||
|
ball = pygame.Rect(WIDTH // 2 - BALL_SIZE // 2, HEIGHT // 2 - BALL_SIZE // 2, BALL_SIZE, BALL_SIZE)
|
||||||
|
|
||||||
|
# Физика мяча
|
||||||
|
base_speed = 3
|
||||||
|
max_speed = 35 # Увеличили максимальную скорость
|
||||||
|
speed_increase = 0.7 # Увеличили ускорение после удара
|
||||||
|
ball_speed = base_speed
|
||||||
|
ball_dx = ball_speed * random.choice((1, -1))
|
||||||
|
ball_dy = ball_speed * random.choice((1, -1))
|
||||||
|
|
||||||
|
# Скорости
|
||||||
|
player_speed = 0
|
||||||
|
ai_speed = 0
|
||||||
|
|
||||||
|
# Счет
|
||||||
|
player_score = 0
|
||||||
|
ai_score = 0
|
||||||
|
font = pygame.font.Font(None, 72)
|
||||||
|
|
||||||
|
def draw_field():
|
||||||
|
"""Рисует поле с разметкой"""
|
||||||
|
screen.fill(GREEN)
|
||||||
|
|
||||||
|
# Полупрозрачная надпись
|
||||||
|
text_surface = pixel_font.render("ХЕРАЧЬ!", True, LIGHT_GREEN)
|
||||||
|
text_surface.set_alpha(30)
|
||||||
|
screen.blit(text_surface, (WIDTH//2 - text_surface.get_width()//2, HEIGHT//2 - text_surface.get_height()//2))
|
||||||
|
|
||||||
|
# Разметка
|
||||||
|
for y in range(0, HEIGHT, 40):
|
||||||
|
pygame.draw.rect(screen, WHITE, (WIDTH // 2 - 5, y, 10, 20))
|
||||||
|
pygame.draw.rect(screen, WHITE, (0, 0, WIDTH, HEIGHT), 10)
|
||||||
|
|
||||||
|
def reset_ball():
|
||||||
|
"""Сбрасывает мяч в центр"""
|
||||||
|
global ball_speed, ball_dx, ball_dy
|
||||||
|
ball.center = (WIDTH // 2, HEIGHT // 2)
|
||||||
|
ball_speed = base_speed
|
||||||
|
angle = math.radians(random.uniform(30, 60))
|
||||||
|
direction = random.choice((-1, 1))
|
||||||
|
ball_dx = direction * ball_speed * math.cos(angle)
|
||||||
|
ball_dy = ball_speed * math.sin(angle) * random.choice((-1, 1))
|
||||||
|
|
||||||
|
def handle_collision():
|
||||||
|
"""Обрабатывает все столкновения"""
|
||||||
|
global ball_dx, ball_dy, ball_speed
|
||||||
|
|
||||||
|
# Жесткие границы (мяч не может вылететь)
|
||||||
|
if ball.top < 0:
|
||||||
|
ball.top = 0
|
||||||
|
ball_dy = abs(ball_dy) * 0.95 # Небольшая потеря энергии
|
||||||
|
if ball.bottom > HEIGHT:
|
||||||
|
ball.bottom = HEIGHT
|
||||||
|
ball_dy = -abs(ball_dy) * 0.95
|
||||||
|
|
||||||
|
# Обработка ударов о ракетки с более сильным ускорением
|
||||||
|
if ball.colliderect(player_paddle) and ball_dx < 0:
|
||||||
|
# Сильное ускорение + случайный угол
|
||||||
|
relative_intersect = (player_paddle.centery - ball.centery) / (PADDLE_HEIGHT / 2)
|
||||||
|
ball_dx = abs(ball_dx) * 1.25 # Увеличили ускорение
|
||||||
|
ball_dy = -relative_intersect * ball_speed * 2
|
||||||
|
# Добавляем случайности
|
||||||
|
ball_dx += random.uniform(-0.5, 0.5)
|
||||||
|
ball_dy += random.uniform(-1, 1)
|
||||||
|
|
||||||
|
elif ball.colliderect(ai_paddle) and ball_dx > 0:
|
||||||
|
relative_intersect = (ai_paddle.centery - ball.centery) / (PADDLE_HEIGHT / 2)
|
||||||
|
ball_dx = -abs(ball_dx) * 1.25
|
||||||
|
ball_dy = -relative_intersect * ball_speed * 2
|
||||||
|
ball_dx += random.uniform(-0.5, 0.5)
|
||||||
|
ball_dy += random.uniform(-1, 1)
|
||||||
|
|
||||||
|
# Нормализация скорости
|
||||||
|
speed = math.sqrt(ball_dx**2 + ball_dy**2)
|
||||||
|
if speed > max_speed:
|
||||||
|
ball_dx = (ball_dx / speed) * max_speed
|
||||||
|
ball_dy = (ball_dy / speed) * max_speed
|
||||||
|
ball_speed = speed
|
||||||
|
|
||||||
|
def ai_movement():
|
||||||
|
"""Упрощенный ИИ с ошибками"""
|
||||||
|
global ai_speed
|
||||||
|
|
||||||
|
# Простое следование за мячом с задержкой и ошибками
|
||||||
|
target_y = ball.centery + random.uniform(-50, 50) # Добавляем ошибку
|
||||||
|
|
||||||
|
# Замедляем ИИ на высоких скоростях
|
||||||
|
reaction_speed = max(3, 8 - ball_speed * 0.2)
|
||||||
|
|
||||||
|
if ai_paddle.centery < target_y:
|
||||||
|
ai_speed = min(reaction_speed, target_y - ai_paddle.centery)
|
||||||
|
else:
|
||||||
|
ai_speed = max(-reaction_speed, target_y - ai_paddle.centery)
|
||||||
|
|
||||||
|
ai_paddle.y += ai_speed
|
||||||
|
|
||||||
|
# Границы
|
||||||
|
if ai_paddle.top < 0:
|
||||||
|
ai_paddle.top = 0
|
||||||
|
if ai_paddle.bottom > HEIGHT:
|
||||||
|
ai_paddle.bottom = HEIGHT
|
||||||
|
|
||||||
|
# Основной цикл
|
||||||
|
while True:
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_DOWN:
|
||||||
|
player_speed = 15
|
||||||
|
if event.key == pygame.K_UP:
|
||||||
|
player_speed = -15
|
||||||
|
if event.key == pygame.K_ESCAPE:
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
|
if event.type == pygame.KEYUP:
|
||||||
|
if event.key == pygame.K_DOWN or event.key == pygame.K_UP:
|
||||||
|
player_speed = 0
|
||||||
|
|
||||||
|
# Движение
|
||||||
|
player_paddle.y += player_speed
|
||||||
|
if player_paddle.top < 0:
|
||||||
|
player_paddle.top = 0
|
||||||
|
if player_paddle.bottom > HEIGHT:
|
||||||
|
player_paddle.bottom = HEIGHT
|
||||||
|
|
||||||
|
ai_movement()
|
||||||
|
|
||||||
|
# Физика мяча
|
||||||
|
ball.x += ball_dx
|
||||||
|
ball.y += ball_dy
|
||||||
|
handle_collision()
|
||||||
|
|
||||||
|
# Голы
|
||||||
|
if ball.left <= 0:
|
||||||
|
ai_score += 1
|
||||||
|
reset_ball()
|
||||||
|
if ball.right >= WIDTH:
|
||||||
|
player_score += 1
|
||||||
|
base_speed = min(15, base_speed + speed_increase)
|
||||||
|
reset_ball()
|
||||||
|
|
||||||
|
# Отрисовка
|
||||||
|
draw_field()
|
||||||
|
pygame.draw.rect(screen, WHITE, player_paddle, border_radius=10)
|
||||||
|
pygame.draw.rect(screen, WHITE, ai_paddle, border_radius=10)
|
||||||
|
pygame.draw.ellipse(screen, WHITE, ball)
|
||||||
|
|
||||||
|
# Счет
|
||||||
|
screen.blit(font.render(f"{player_score}", True, WHITE), (WIDTH//4, 50))
|
||||||
|
screen.blit(font.render(f"{ai_score}", True, WHITE), (3*WIDTH//4 - 50, 50))
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
clock.tick(FPS)
|
||||||
256
tet/tetris.py
Normal file
256
tet/tetris.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import pygame
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Инициализация pygame
|
||||||
|
pygame.init()
|
||||||
|
|
||||||
|
# Цвета
|
||||||
|
BLACK = (0, 0, 0)
|
||||||
|
WHITE = (255, 255, 255)
|
||||||
|
GRAY = (70, 70, 70)
|
||||||
|
COLORS = [
|
||||||
|
(0, 255, 255), # I - голубой
|
||||||
|
(0, 0, 255), # J - синий
|
||||||
|
(255, 165, 0), # L - оранжевый
|
||||||
|
(255, 255, 0), # O - желтый
|
||||||
|
(0, 255, 0), # S - зеленый
|
||||||
|
(128, 0, 128), # T - фиолетовый
|
||||||
|
(255, 0, 0) # Z - красный
|
||||||
|
]
|
||||||
|
|
||||||
|
# Настройки игры
|
||||||
|
BLOCK_SIZE = 30
|
||||||
|
GRID_WIDTH = 10
|
||||||
|
GRID_HEIGHT = 20
|
||||||
|
SCREEN_WIDTH = BLOCK_SIZE * (GRID_WIDTH + 6)
|
||||||
|
SCREEN_HEIGHT = BLOCK_SIZE * GRID_HEIGHT
|
||||||
|
GAME_AREA_LEFT = BLOCK_SIZE
|
||||||
|
|
||||||
|
# Фигуры тетрамино
|
||||||
|
SHAPES = [
|
||||||
|
[[1, 1, 1, 1]], # I
|
||||||
|
[[1, 0, 0], [1, 1, 1]], # J
|
||||||
|
[[0, 0, 1], [1, 1, 1]], # L
|
||||||
|
[[1, 1], [1, 1]], # O
|
||||||
|
[[0, 1, 1], [1, 1, 0]], # S
|
||||||
|
[[0, 1, 0], [1, 1, 1]], # T
|
||||||
|
[[1, 1, 0], [0, 1, 1]] # Z
|
||||||
|
]
|
||||||
|
|
||||||
|
# Создание экрана
|
||||||
|
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
|
||||||
|
pygame.display.set_caption("Тетрис")
|
||||||
|
|
||||||
|
# Часы для управления FPS
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
class Tetris:
|
||||||
|
def __init__(self):
|
||||||
|
self.grid = [[0 for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||||
|
self.current_piece = self.new_piece()
|
||||||
|
self.game_over = False
|
||||||
|
self.score = 0
|
||||||
|
self.level = 1
|
||||||
|
self.fall_speed = 0.5 # секунды между падением на 1 клетку
|
||||||
|
self.fall_time = 0
|
||||||
|
|
||||||
|
def new_piece(self):
|
||||||
|
# Выбираем случайную фигуру
|
||||||
|
shape = random.choice(SHAPES)
|
||||||
|
color = COLORS[SHAPES.index(shape)]
|
||||||
|
|
||||||
|
# Начальная позиция (по центру сверху)
|
||||||
|
x = GRID_WIDTH // 2 - len(shape[0]) // 2
|
||||||
|
y = 0
|
||||||
|
|
||||||
|
return {"shape": shape, "color": color, "x": x, "y": y}
|
||||||
|
|
||||||
|
def valid_move(self, piece, x_offset=0, y_offset=0):
|
||||||
|
for y, row in enumerate(piece["shape"]):
|
||||||
|
for x, cell in enumerate(row):
|
||||||
|
if cell:
|
||||||
|
new_x = piece["x"] + x + x_offset
|
||||||
|
new_y = piece["y"] + y + y_offset
|
||||||
|
|
||||||
|
if (new_x < 0 or new_x >= GRID_WIDTH or
|
||||||
|
new_y >= GRID_HEIGHT or
|
||||||
|
(new_y >= 0 and self.grid[new_y][new_x])):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def rotate_piece(self):
|
||||||
|
# Получаем текущую фигуру
|
||||||
|
piece = self.current_piece
|
||||||
|
# Транспонируем матрицу фигуры (поворот на 90 градусов)
|
||||||
|
rotated = [[piece["shape"][y][x] for y in range(len(piece["shape"])-1, -1, -1)]
|
||||||
|
for x in range(len(piece["shape"][0]))]
|
||||||
|
|
||||||
|
old_shape = piece["shape"]
|
||||||
|
piece["shape"] = rotated
|
||||||
|
|
||||||
|
# Если после поворота фигура выходит за границы или пересекается с другими блоками,
|
||||||
|
# отменяем поворот
|
||||||
|
if not self.valid_move(piece):
|
||||||
|
piece["shape"] = old_shape
|
||||||
|
|
||||||
|
def lock_piece(self):
|
||||||
|
piece = self.current_piece
|
||||||
|
for y, row in enumerate(piece["shape"]):
|
||||||
|
for x, cell in enumerate(row):
|
||||||
|
if cell:
|
||||||
|
# Проверяем, не выходит ли фигура за верхнюю границу (игра окончена)
|
||||||
|
if piece["y"] + y < 0:
|
||||||
|
self.game_over = True
|
||||||
|
else:
|
||||||
|
self.grid[piece["y"] + y][piece["x"] + x] = piece["color"]
|
||||||
|
|
||||||
|
# Проверяем заполненные линии
|
||||||
|
self.clear_lines()
|
||||||
|
# Создаем новую фигуру
|
||||||
|
self.current_piece = self.new_piece()
|
||||||
|
|
||||||
|
# Если новая фигура сразу не может разместиться, игра окончена
|
||||||
|
if not self.valid_move(self.current_piece):
|
||||||
|
self.game_over = True
|
||||||
|
|
||||||
|
def clear_lines(self):
|
||||||
|
lines_cleared = 0
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
if all(self.grid[y]):
|
||||||
|
lines_cleared += 1
|
||||||
|
# Удаляем линию и сдвигаем все вышележащие линии вниз
|
||||||
|
for y2 in range(y, 0, -1):
|
||||||
|
self.grid[y2] = self.grid[y2-1][:]
|
||||||
|
self.grid[0] = [0 for _ in range(GRID_WIDTH)]
|
||||||
|
|
||||||
|
# Обновляем счет
|
||||||
|
if lines_cleared == 1:
|
||||||
|
self.score += 100 * self.level
|
||||||
|
elif lines_cleared == 2:
|
||||||
|
self.score += 300 * self.level
|
||||||
|
elif lines_cleared == 3:
|
||||||
|
self.score += 500 * self.level
|
||||||
|
elif lines_cleared == 4:
|
||||||
|
self.score += 800 * self.level
|
||||||
|
|
||||||
|
# Обновляем уровень (каждые 10 линий)
|
||||||
|
self.level = 1 + self.score // 1000
|
||||||
|
# Увеличиваем скорость (максимум 0.05 секунды между падениями)
|
||||||
|
self.fall_speed = max(0.05, 0.5 - (self.level - 1) * 0.05)
|
||||||
|
|
||||||
|
def update(self, delta_time):
|
||||||
|
if self.game_over:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.fall_time += delta_time
|
||||||
|
|
||||||
|
# Автоматическое падение фигуры
|
||||||
|
if self.fall_time >= self.fall_speed:
|
||||||
|
self.fall_time = 0
|
||||||
|
if self.valid_move(self.current_piece, 0, 1):
|
||||||
|
self.current_piece["y"] += 1
|
||||||
|
else:
|
||||||
|
self.lock_piece()
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
# Очищаем экран
|
||||||
|
screen.fill(BLACK)
|
||||||
|
|
||||||
|
# Рисуем игровую область
|
||||||
|
pygame.draw.rect(screen, WHITE, (GAME_AREA_LEFT, 0, BLOCK_SIZE * GRID_WIDTH, SCREEN_HEIGHT), 1)
|
||||||
|
|
||||||
|
# Рисуем сетку
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
pygame.draw.rect(screen, GRAY,
|
||||||
|
(GAME_AREA_LEFT + x * BLOCK_SIZE, y * BLOCK_SIZE,
|
||||||
|
BLOCK_SIZE, BLOCK_SIZE), 1)
|
||||||
|
|
||||||
|
# Рисуем статичные блоки
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
if self.grid[y][x]:
|
||||||
|
pygame.draw.rect(screen, self.grid[y][x],
|
||||||
|
(GAME_AREA_LEFT + x * BLOCK_SIZE, y * BLOCK_SIZE,
|
||||||
|
BLOCK_SIZE, BLOCK_SIZE))
|
||||||
|
pygame.draw.rect(screen, WHITE,
|
||||||
|
(GAME_AREA_LEFT + x * BLOCK_SIZE, y * BLOCK_SIZE,
|
||||||
|
BLOCK_SIZE, BLOCK_SIZE), 1)
|
||||||
|
|
||||||
|
# Рисуем текущую фигуру
|
||||||
|
if not self.game_over:
|
||||||
|
piece = self.current_piece
|
||||||
|
for y, row in enumerate(piece["shape"]):
|
||||||
|
for x, cell in enumerate(row):
|
||||||
|
if cell:
|
||||||
|
pygame.draw.rect(screen, piece["color"],
|
||||||
|
(GAME_AREA_LEFT + (piece["x"] + x) * BLOCK_SIZE,
|
||||||
|
(piece["y"] + y) * BLOCK_SIZE,
|
||||||
|
BLOCK_SIZE, BLOCK_SIZE))
|
||||||
|
pygame.draw.rect(screen, WHITE,
|
||||||
|
(GAME_AREA_LEFT + (piece["x"] + x) * BLOCK_SIZE,
|
||||||
|
(piece["y"] + y) * BLOCK_SIZE,
|
||||||
|
BLOCK_SIZE, BLOCK_SIZE), 1)
|
||||||
|
|
||||||
|
# Рисуем информацию (счет, уровень)
|
||||||
|
font = pygame.font.SysFont(None, 36)
|
||||||
|
score_text = font.render(f"Счет: {self.score}", True, WHITE)
|
||||||
|
level_text = font.render(f"Уровень: {self.level}", True, WHITE)
|
||||||
|
screen.blit(score_text, (GAME_AREA_LEFT + GRID_WIDTH * BLOCK_SIZE + 10, 30))
|
||||||
|
screen.blit(level_text, (GAME_AREA_LEFT + GRID_WIDTH * BLOCK_SIZE + 10, 70))
|
||||||
|
|
||||||
|
# Если игра окончена, выводим сообщение
|
||||||
|
if self.game_over:
|
||||||
|
game_over_font = pygame.font.SysFont(None, 48)
|
||||||
|
game_over_text = game_over_font.render("Игра Окончена!", True, (255, 0, 0))
|
||||||
|
screen.blit(game_over_text, (GAME_AREA_LEFT + GRID_WIDTH * BLOCK_SIZE // 2 - 100,
|
||||||
|
SCREEN_HEIGHT // 2 - 24))
|
||||||
|
|
||||||
|
# Создаем экземпляр игры
|
||||||
|
game = Tetris()
|
||||||
|
|
||||||
|
# Основной игровой цикл
|
||||||
|
running = True
|
||||||
|
last_time = pygame.time.get_ticks()
|
||||||
|
|
||||||
|
while running:
|
||||||
|
# Управление FPS
|
||||||
|
current_time = pygame.time.get_ticks()
|
||||||
|
delta_time = (current_time - last_time) / 1000.0 # Конвертируем в секунды
|
||||||
|
last_time = current_time
|
||||||
|
|
||||||
|
# Обработка событий
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
running = False
|
||||||
|
|
||||||
|
if not game.game_over:
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_LEFT:
|
||||||
|
if game.valid_move(game.current_piece, -1, 0):
|
||||||
|
game.current_piece["x"] -= 1
|
||||||
|
elif event.key == pygame.K_RIGHT:
|
||||||
|
if game.valid_move(game.current_piece, 1, 0):
|
||||||
|
game.current_piece["x"] += 1
|
||||||
|
elif event.key == pygame.K_DOWN:
|
||||||
|
if game.valid_move(game.current_piece, 0, 1):
|
||||||
|
game.current_piece["y"] += 1
|
||||||
|
elif event.key == pygame.K_UP:
|
||||||
|
game.rotate_piece()
|
||||||
|
elif event.key == pygame.K_SPACE:
|
||||||
|
# Мгновенное падение
|
||||||
|
while game.valid_move(game.current_piece, 0, 1):
|
||||||
|
game.current_piece["y"] += 1
|
||||||
|
game.lock_piece()
|
||||||
|
|
||||||
|
# Обновление игры
|
||||||
|
game.update(delta_time)
|
||||||
|
|
||||||
|
# Отрисовка
|
||||||
|
game.draw()
|
||||||
|
pygame.display.flip()
|
||||||
|
|
||||||
|
# Ограничиваем FPS
|
||||||
|
clock.tick(60)
|
||||||
|
|
||||||
|
pygame.quit()
|
||||||
88
twitch.py
Normal file
88
twitch.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
############################################### twitchSockets
|
||||||
|
import re
|
||||||
|
from zoneinfo import *
|
||||||
|
from datetime import datetime
|
||||||
|
import socket
|
||||||
|
# from huy import tw_comments
|
||||||
|
class TwitchChatIRC():
|
||||||
|
__HOST = 'irc.chat.twitch.tv'
|
||||||
|
__NICK = 'justinfan67420'
|
||||||
|
__PASS = 'SCHMOOPIIE'
|
||||||
|
__PORT = 6667
|
||||||
|
all_messages = []
|
||||||
|
|
||||||
|
|
||||||
|
__PATTERN = re.compile(r'@(.+?(?=\s+:)).*PRIVMSG[^:]*:([^\r\n]*)')
|
||||||
|
|
||||||
|
def __init__(self, channel):
|
||||||
|
|
||||||
|
self.__SOCKET = socket.socket()
|
||||||
|
self.__SOCKET.connect((self.__HOST, self.__PORT))
|
||||||
|
print('Connected to twitch socket:',self.__HOST,'on port',self.__PORT)
|
||||||
|
self.__send_raw('CAP REQ :twitch.tv/tags')
|
||||||
|
self.__send_raw('PASS ' + self.__PASS)
|
||||||
|
self.__send_raw('NICK ' + self.__NICK)
|
||||||
|
|
||||||
|
self.__send_raw('JOIN #{}'.format(channel))
|
||||||
|
self.__SOCKET.settimeout(2.0)
|
||||||
|
|
||||||
|
|
||||||
|
def __send_raw(self, string):
|
||||||
|
self.__SOCKET.send((string+'\r\n').encode('utf-8'))
|
||||||
|
|
||||||
|
def __recvall(self,buffer_size):
|
||||||
|
data = b''
|
||||||
|
while True:
|
||||||
|
part =self.__SOCKET.recv(buffer_size)
|
||||||
|
data += part
|
||||||
|
|
||||||
|
if len(part) < buffer_size:
|
||||||
|
break
|
||||||
|
return data.decode('utf-8')
|
||||||
|
|
||||||
|
def close_connection(__SOCKET):
|
||||||
|
__SOCKET.close()
|
||||||
|
print('Connection closed')
|
||||||
|
|
||||||
|
|
||||||
|
def listen(self, timeout=None, message_timeout=1.0, on_message = None, buffer_size = 4096, message_limit = None, output=None):
|
||||||
|
readbuffer = ''
|
||||||
|
print('Starting twitch thread...')
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
new_info = self.__recvall(buffer_size)
|
||||||
|
readbuffer += new_info
|
||||||
|
|
||||||
|
if('PING :tmi.twitch.tv' in readbuffer):
|
||||||
|
self.__send_raw('PONG :tmi.twitch.tv')
|
||||||
|
|
||||||
|
matches = list(self.__PATTERN.finditer(readbuffer))
|
||||||
|
|
||||||
|
if(matches):
|
||||||
|
if(len(matches) > 1):
|
||||||
|
matches = matches[:-1] # assume last one is incomplete
|
||||||
|
last_index = matches[-1].span()[1]
|
||||||
|
readbuffer = readbuffer[last_index:]
|
||||||
|
for match in matches:
|
||||||
|
data = {}
|
||||||
|
for item in match.group(1).split(';'):
|
||||||
|
keys = item.split('=',1)
|
||||||
|
data[keys[0]]=keys[1]
|
||||||
|
data['message'] = match.group(2)
|
||||||
|
dt = datetime.fromtimestamp(int(data['tmi-sent-ts'])/1000).replace(tzinfo=ZoneInfo('Asia/Yekaterinburg'))
|
||||||
|
my_data = dict({'id': data['id'], 'type': 'tw', 'date': dt, 'sendr': data['display-name'], 'msg': data['message'].encode('utf-8').decode('utf-8','ignore')})
|
||||||
|
# print(data)
|
||||||
|
self.all_messages.append(my_data)
|
||||||
|
except socket.timeout:
|
||||||
|
if(timeout != None):
|
||||||
|
print('timeout!')
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('Interrupted by user.')
|
||||||
|
|
||||||
|
# except Exception as e:
|
||||||
|
# print('Unknown Error:',e)
|
||||||
|
# raise e
|
||||||
|
# return messages
|
||||||
|
|
||||||
|
########################################################################################
|
||||||
2
twitchchatirc/.gitignore
vendored
Normal file
2
twitchchatirc/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.env
|
||||||
|
__pycache__
|
||||||
21
twitchchatirc/LICENSE
Normal file
21
twitchchatirc/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Joshua Lochner
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
189
twitchchatirc/README.md
Normal file
189
twitchchatirc/README.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# Twitch Chat IRC
|
||||||
|
A simple tool used to send and receive Twitch chat messages over IRC with python web sockets. Receiving does not require authentication, while sending does.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
### Requirements:
|
||||||
|
* This tool was created in a Python 3 environment.
|
||||||
|
* Run `pip install -r requirements.txt` to ensure you have the necessary dependencies.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
If you intend to send messages, you will require authentication.
|
||||||
|
1. Go to https://twitchapps.com/tmi/
|
||||||
|
2. Click "Connect".
|
||||||
|
3. Log in with Twitch.
|
||||||
|
4. Copy the generated oath token. Now, there are 2 ways to proceed:
|
||||||
|
- (Recommended) Create a file called `.env` and save your credentials here as:
|
||||||
|
> NICK=x <br> PASS=y
|
||||||
|
|
||||||
|
replacing `x` and `y` with your username and oauth token respectively.<br> See `example.env` for an example.
|
||||||
|
|
||||||
|
- Pass your credentials as function/command line arguments. See below for examples.
|
||||||
|
|
||||||
|
|
||||||
|
## Command line:
|
||||||
|
### Usage
|
||||||
|
```
|
||||||
|
usage: twitch_chat_irc.py [-h] [-timeout TIMEOUT]
|
||||||
|
[-message_timeout MESSAGE_TIMEOUT]
|
||||||
|
[-buffer_size BUFFER_SIZE]
|
||||||
|
[-message_limit MESSAGE_LIMIT] [-username USERNAME]
|
||||||
|
[-oauth OAUTH] [--send] [-output OUTPUT]
|
||||||
|
channel_name
|
||||||
|
|
||||||
|
Send and receive Twitch chat messages over IRC with python web sockets. For
|
||||||
|
more info, go to https://dev.twitch.tv/docs/irc/guide
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
channel_name Twitch channel name (username)
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-timeout TIMEOUT, -t TIMEOUT
|
||||||
|
time in seconds needed to close connection after not
|
||||||
|
receiving any new data (default: None = no timeout)
|
||||||
|
-message_timeout MESSAGE_TIMEOUT, -mt MESSAGE_TIMEOUT
|
||||||
|
time in seconds between checks for new data (default:
|
||||||
|
1 second)
|
||||||
|
-buffer_size BUFFER_SIZE, -b BUFFER_SIZE
|
||||||
|
buffer size (default: 4096 bytes = 4 KB)
|
||||||
|
-message_limit MESSAGE_LIMIT, -l MESSAGE_LIMIT
|
||||||
|
maximum amount of messages to get (default: None =
|
||||||
|
unlimited)
|
||||||
|
-username USERNAME, -u USERNAME
|
||||||
|
username (default: None)
|
||||||
|
-oauth OAUTH, -password OAUTH, -p OAUTH
|
||||||
|
oath token (default: None). Get custom one from
|
||||||
|
https://twitchapps.com/tmi/
|
||||||
|
--send send mode (default: False)
|
||||||
|
-output OUTPUT, -o OUTPUT
|
||||||
|
output file (default: None = print to standard output)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
#### Receiving messages
|
||||||
|
##### 1. Output messages from a livestream to standard output
|
||||||
|
```
|
||||||
|
python twitch_chat_irc.py <channel_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 2. Output messages from a livestream to a file
|
||||||
|
```
|
||||||
|
python twitch_chat_irc.py <channel_name> -output <file_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
If the file name ends in `.json`, the array will be written to the file in JSON format. Similarly, if the file name ends in `.csv`, the data will be written in CSV format. <br> Otherwise, the chat messages will be outputted to the file in the following format:<br>
|
||||||
|
`[<time>] <author>: <message>`
|
||||||
|
|
||||||
|
##### 3. Set a timeout (close connection if no message has been sent in a certain time)
|
||||||
|
```
|
||||||
|
python twitch_chat_irc.py <channel_name> -timeout <time_in_seconds> -output <file_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
There are other options, such as `message_timeout` and `buffer_size`, but these normally do not need to be changed. See above for a description of all options.
|
||||||
|
|
||||||
|
##### 4. Set a maximum number of messages to read (close connection once limit has been reached)
|
||||||
|
```
|
||||||
|
python twitch_chat_irc.py <channel_name> -message_limit <number_of_messages> -output <file_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Example outputs
|
||||||
|
[JSON Example](examples/example.json):
|
||||||
|
```
|
||||||
|
python twitch_chat_irc.py <channel_name> -output example.json
|
||||||
|
```
|
||||||
|
|
||||||
|
[CSV Example](examples/example.csv):
|
||||||
|
```
|
||||||
|
python twitch_chat_irc.py <channel_name> -output example.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
[Text Example](examples/example.txt):
|
||||||
|
```
|
||||||
|
python twitch_chat_irc.py <channel_name> -output example.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Sending messages
|
||||||
|
This will open an interactive session which allows you to send messages to the specified channel.
|
||||||
|
##### 1. Send messages to a channel (authentication via .env)
|
||||||
|
```
|
||||||
|
python twitch_chat_irc.py --send <channel_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 2. Send messages to a channel (authentication via arguments)
|
||||||
|
```
|
||||||
|
python twitch_chat_irc.py --send <channel_name> -username <username> -oauth <oauth_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python module
|
||||||
|
|
||||||
|
### Importing the module
|
||||||
|
|
||||||
|
```python
|
||||||
|
import twitch_chat_irc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
#### Starting a connection
|
||||||
|
This allows for both receiving and sending of messages
|
||||||
|
##### 1. Start a connection with Twitch chat using credentials in `.env` (if any)
|
||||||
|
|
||||||
|
```python
|
||||||
|
connection = twitch_chat_irc.TwitchChatIRC()
|
||||||
|
```
|
||||||
|
##### 2. Start a connection with Twitch chat using credentials
|
||||||
|
|
||||||
|
```python
|
||||||
|
connection = twitch_chat_irc.TwitchChatIRC('username','oauth:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
|
||||||
|
```
|
||||||
|
#### Receiving messages
|
||||||
|
The `listen` method returns a list when a `KeyboardInterrupt` is fired, or when a timeout/limit has been reached. The arguments shown below can be used together to form more complex method calls.
|
||||||
|
|
||||||
|
##### 1. Get a list of messages from a channel
|
||||||
|
```python
|
||||||
|
messages = connection.listen('channel_name')
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 2. Get a list of messages from a channel, stopping after not getting a message for 30 seconds
|
||||||
|
```python
|
||||||
|
messages = connection.listen('channel_name', timeout=30)
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 3. Get a list of messages from a channel, stopping after getting 100 messages
|
||||||
|
```python
|
||||||
|
messages = connection.listen('channel_name', message_limit=100)
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 4. Write messages from a channel to a file
|
||||||
|
```python
|
||||||
|
connection.listen('channel_name', output='file.txt')
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 5. Set a callback function to be fired each time a message is received
|
||||||
|
```python
|
||||||
|
def do_something(message):
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
connection.listen('channel_name', on_message=do_something)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sending messages
|
||||||
|
The `send` method allows for messages to be sent to different channels. This method requires valid authentication to be provided, otherwise an exception will be called.
|
||||||
|
|
||||||
|
##### 1. Send a message
|
||||||
|
```python
|
||||||
|
message = 'Hello world!'
|
||||||
|
connection.send('channel_name', message)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Close connection
|
||||||
|
The `close_connection` method closes the connection with Twitch chat. No futher messages can be received or sent now.
|
||||||
|
|
||||||
|
##### 1. Close a connection
|
||||||
|
```python
|
||||||
|
connection.close()
|
||||||
|
```
|
||||||
4
twitchchatirc/example.env
Normal file
4
twitchchatirc/example.env
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Once credentials are set up, rename this file to .env
|
||||||
|
# Go to https://twitchapps.com/tmi/ to get your oauth token
|
||||||
|
NICK=username
|
||||||
|
PASS=oauth:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
128
twitchchatirc/examples/example.csv
Normal file
128
twitchchatirc/examples/example.csv
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
badge-info,emote-only,badges,emotes,room-id,client-nonce,user-type,tmi-sent-ts,mod,user-id,flags,turbo,display-name,subscriber,id,message,color
|
||||||
|
subscriber/1,1,subscriber/0,25:0-4,127550308,09b8128f063b62f7942334efb9a66909,,1595816099308,0,430645544,,0,sophiexbc,1,79f7b57f-e341-4f7a-baed-2f4af2842d87,Kappa,#1E90FF
|
||||||
|
,,premium/1,25:4-8,127550308,4f32226107d3c55d19776502289d4dc2,,1595816099422,0,72104509,,0,dougie___jones,0,cf73e3c1-ce4e-4b46-9f2f-c61a0992ef2a,lag Kappa,#00FF7F
|
||||||
|
,,bits-charity/1,,127550308,185dee001c30ccb0fadc56f02d13e8b0,,1595816099572,0,148564215,,0,coffeeindex,0,63c97135-ac87-466e-8683-00168f2a629e,L OMEGALUL G,#4C3D30
|
||||||
|
,1,,25:0-4,127550308,a270a675bf2b23165cd938e47c0caf5b,,1595816099785,0,68412986,,0,AmazonESP,0,3dd26b43-94ef-4f52-b02b-0c37d912db57,Kappa,#1E90FF
|
||||||
|
subscriber/1,,subscriber/0,25:0-4,127550308,a7c8803dc1952befc7af87e3fbf998e4,,1595816100038,0,36029783,,0,Deflecti0n,1,29a116c0-c9a2-4db5-985b-c1f6420575ed,Kappa lag,#FF0000
|
||||||
|
subscriber/1,,subscriber/0,,127550308,7fd5330dbaebcb8c5ea041cfe28f5ebf,,1595816100393,0,187748286,,0,Dortok,1,35ee1917-0e1f-4b9f-b93a-c030d740bd66,YEP lag,#8A2BE2
|
||||||
|
subscriber/2,,"subscriber/0,premium/1",,127550308,d016325ea59d18b1b3c47ebd9a45e82d,,1595816100907,0,58745385,,0,danification9,1,98ea9d04-e3c0-4fb9-8de6-f053ea13f8a7,Sadge,
|
||||||
|
subscriber/2,,subscriber/0,,127550308,fb41538db686b39f413eda0af42cec1c,,1595816101643,0,43658581,,0,pab1994,1,35ed9d06-f3cd-41c4-9fcd-3d25dc94fd5a,yup,
|
||||||
|
,,,,127550308,8163a20dc852d1e8cc783411b38b22b4,,1595816101637,0,164828199,,0,aa175,0,83da06ce-193a-4b9c-b7da-35b65a1b5ff2,g3 rooooooooooook @BotezLive,
|
||||||
|
subscriber/1,,subscriber/0,,127550308,c83235028c79087d0146bb8735412f87,,1595816102163,0,25101760,,0,vespereq_,1,b1673d7c-a049-46c9-bae0-f56bfaa7ac2e,It's t h e wine,
|
||||||
|
subscriber/2,,subscriber/0,,127550308,28eb83194e637818b539d2a411bce483,,1595816102288,0,541613570,0-4:A.3,0,botezslavic,1,6f371af1-9680-4743-8402-fa4154fa4327,DRUNK,#8A2BE2
|
||||||
|
subscriber/1,,subscriber/0,,127550308,a6754c4e9795f9d1e344f689b72e4ab1,,1595816102306,0,52430007,,0,rowrow_,1,daa114be-9c2b-409c-be84-0fe03e10790c,no johns,#DAA520
|
||||||
|
subscriber/2,1,"subscriber/0,premium/1",115847:0-6,127550308,c4cc2b7fbab56d770b1b892b7bcfe452,,1595816102360,0,168644445,,0,slightlyHyPhi,1,1ba8cdea-403f-4265-b0ca-29edfd0b9725,KappaHD,
|
||||||
|
,,hype-train/2,,127550308,c779bfdd9b99c56aaafe4c9c122f448a,,1595816102389,0,56448721,,0,ReadingRailroad,0,3c9f340c-b408-48ef-86df-9e6fc64ac13c,Yep,#D2691E
|
||||||
|
,,premium/1,,127550308,59d0902b7dd8e7823c044688a26b246c,,1595816102598,0,466535256,,0,CrenshawViews,0,828c2fbd-b921-4430-b95d-dc8549a14591,yeah it glitched,#1546D1
|
||||||
|
,,premium/1,,127550308,a2c5a2d65b01bdb151944299d42be320,,1595816102879,0,92289099,,0,Heat_Signature,0,be24b7b2-11db-4e0c-8fdf-296561fb56bd,LAG ON CHESS OMEGALUL,#FF0000
|
||||||
|
subscriber/1,,subscriber/0,25:0-4,127550308,45bdd07b402d1bc7b630f3399248954e,,1595816103185,0,25334507,,0,LeetChocolate,1,577286ea-4a0b-47b9-a78e-98bad4bbf9c5,Kappa lag,#FF4500
|
||||||
|
,,,,127550308,f659cba7c6f4ffb5e30753d5840002f7,,1595816103329,0,499095797,,0,swornheart,0,b5d0b644-982a-4171-bf0f-a2dcd0b7ee22,that was brain lag,#0000FF
|
||||||
|
,,,,127550308,cb3d75a24f56025acb5dd10ea71334ef,,1595816103349,0,93958150,,0,JpForrReal,0,59ec74e2-c256-4d5e-810b-91d6d074660d,OMEGALUL,#F4BBFF
|
||||||
|
,1,premium/1,1140308:0-5,127550308,,,1595816103439,0,220980492,,0,AleFalnx,0,4ebf0739-9a1e-4cbf-86af-c395c1f1eec0,atpCap,#89DF9A
|
||||||
|
,1,,25:0-4,127550308,b1a00f55915c59e8e87b3c78c97809f0,,1595816103488,0,178346898,,0,hericium7,0,badf7c28-9ce0-4f22-ad6c-14a4b232298a,Kappa,
|
||||||
|
subscriber/1,,"subscriber/0,glhf-pledge/1",,127550308,679619cd7f623b08fb7cef135b335d6d,,1595816103492,0,246206955,,0,ezeroh13,1,c8951d08-159d-45ef-9d55-94244df817d6,YEP LAG,#B22222
|
||||||
|
,,premium/1,25:4-8,127550308,8db72e3f8b40969d001812f9fd88d04c,,1595816103535,0,195983667,,0,Nativezk,0,f3d2809c-a2e1-414c-99cf-f861afa1e667,lag Kappa,#FF69B4
|
||||||
|
,,premium/1,25:13-17,127550308,516d0459f3764bec68c66474f502c768,,1595816103731,0,72104509,,0,dougie___jones,0,2b6b4971-d03c-48ef-b30e-2625a9594ec8,yeah totally Kappa,#00FF7F
|
||||||
|
,1,,25:0-4,127550308,3566311f7dad1ccec17385d4fd0ecd25,,1595816103979,0,47117924,,0,CoDyPhin,0,8531ead3-783a-4394-8e0d-39555e6cae11,Kappa,#1E90FF
|
||||||
|
subscriber/1,,"subscriber/0,premium/1",,127550308,a1f63fd5d71add06d18519e24ae4d649,,1595816104011,0,57651751,,0,Nico3094,1,99b7e9dc-0b98-4a4f-839b-b049ed293022,lag YEP,#0000FF
|
||||||
|
subscriber/1,,"subscriber/0,premium/1",,127550308,ed024a395f2653252d0f1f0719e082f4,,1595816104086,0,76578093,,0,RoLoSC,1,829e8f65-e498-40cd-b575-b99e6141cd0e,yeah little lage,#FF0000
|
||||||
|
subscriber/1,,"subscriber/0,premium/1",25:0-4,127550308,e4f66802252d122a6a1aec5d6be1911a,,1595816104351,0,46899419,,0,ArcticPolarbear,1,eb399ea9-7198-4abc-a0b9-34c6f7b9f743,Kappa lag,#B22222
|
||||||
|
subscriber/1,,"subscriber/0,bits/1000",,127550308,030d87013b0cf82db86359a62986b341,,1595816104364,0,426507835,,0,kzp99,1,a619a855-265c-44cf-9415-b920b1d88bea,LAG,
|
||||||
|
,,premium/1,,127550308,d7997a5b244bdd712f140c3641ca2bd7,,1595816104463,0,59865720,,0,FrankDaTankGaming,0,5cd8fd7b-0609-4b65-ba19-a53bda8ac7a1,does that for me sometimes too,#CC0000
|
||||||
|
,,premium/1,,127550308,68b246b5bd6667927320467e39f3d6a7,,1595816104600,0,22265843,,0,modestmage,0,d1deb4da-9673-48e1-898c-741b6e0565b9,wasnt that a trade?,#008000
|
||||||
|
,,,,127550308,3afd5d8d06cc3d97b13d56522fa14e80,,1595816104624,0,45557046,,0,yop01,0,faae5af5-d551-42b7-993a-3d99d1f1c840,Yes,#00FF7F
|
||||||
|
subscriber/1,,subscriber/0,25:0-4,127550308,160b2211a3db32b13014711567d491fe,,1595816104597,0,430645544,,0,sophiexbc,1,7a357cf3-5314-4885-b30a-9f7b43b9a9a7,Kappa lag,#1E90FF
|
||||||
|
subscriber/1,,"subscriber/0,premium/1",,127550308,663889801a61a035f00b2d397988822f,,1595816104756,0,169719325,,0,swift_ocelot,1,0e1c06c1-1697-422d-b58d-6ff0b8e713fb,YEP lag,#FF7F50
|
||||||
|
,,,,127550308,,,1595816105421,0,39067863,,0,cinasxd,0,613a93e5-82ce-410c-b040-5f781f2cf2e6,kappa,
|
||||||
|
,,premium/1,,127550308,,,1595816105609,0,208236597,,0,jrude72,0,71baa10e-4edb-43fa-a289-469e1c4d1161,So what r u up to for the next 24 hours?,
|
||||||
|
,,,,127550308,c78950b7be2e6822bda92a6db5f57509,,1595816105662,0,44112166,,0,brettydoes,0,aeecd502-e436-4628-af05-933d28f1dba2,YEP lag,#FF69B4
|
||||||
|
,,,,127550308,86ad8f283188e0a1495b7c842137b75f,,1595816105720,0,553991126,,0,kavifa6176,0,59b15ae1-4061-470a-952b-ee6283c530ef,Brain lag,
|
||||||
|
subscriber/3,,subscriber/3,25:6-10,127550308,88c2064750caacd13b04453b343ab1bc,,1595816106052,0,233736723,,0,drawingdead,1,77fcfbed-dae2-45cd-99a6-b681adeb3211,mhmmm Kappa,
|
||||||
|
,,,,127550308,dca8af8308374e8cda730d766192a417,,1595816106179,0,27479429,,0,Cajun292,0,88fdf5b9-4a98-4bea-ae02-eccade8f2a62,bullet lag,#FE02CC
|
||||||
|
,,premium/1,,127550308,8487307d22ca70080bf127bd1cbc259a,,1595816106261,0,107123504,,0,bittersandwich,0,32122034-f1e1-486c-b980-5241ce9f305f,blaming on lag LULW,#14DBD1
|
||||||
|
,,,,127550308,5000e2f06c600b36b1e99bba6720cbe0,,1595816106687,0,189862796,0-3:A.3,0,loayakram,0,ef39469c-4532-4d63-a0f2-c892b9d98763,fake,
|
||||||
|
subscriber/1,,subscriber/0,,127550308,7bab671f446e42a643744a4cfe29c4e9,,1595816106783,0,233579908,7-11:A.3,0,xivi76,1,40b88458-e54f-4862-9e96-df1ef2429530,you're drunk,
|
||||||
|
subscriber/1,,subscriber/0,,127550308,9964a62a8c7e8fa5253018dddbccff18,,1595816106749,0,25101760,,0,vespereq_,1,1834b9a3-be36-475c-a20f-2519d07ad5a4,WINE LAG,
|
||||||
|
,,,,127550308,901dc1be63bbc1b233d7fc8cd79b5d2c,,1595816106826,0,484766599,0-4:A.3,0,deepbishop,0,c90fad1e-d6c1-47d4-85bf-ec57b1bfc866,drunk,
|
||||||
|
subscriber/1,1,"subscriber/0,premium/1",25:0-4,127550308,6d002d55bd5c748445848baef962a43f,,1595816106963,0,77110683,,0,HaloMonkey3009,1,1568de89-043b-4052-bb5d-4877c4a836f3,Kappa,#00FCFF
|
||||||
|
subscriber/1,1,subscriber/0,25:0-4,127550308,a2a1400a09c0c8d1e0056b5473e0d15e,,1595816107192,0,74878349,,0,kilner111,1,744dde86-9485-446a-9a86-e0ff2f83260b,Kappa,#8A2BE2
|
||||||
|
,,,,127550308,7e861ef2cecfb902c95351920555e2f6,,1595816107269,0,452580538,,0,NotReformeduwu,0,4f4140f6-5dd7-4baa-9c5c-06237f24011f,YEP,
|
||||||
|
,,,,127550308,,,1595816107375,0,254499567,,0,juanhc24v2,0,0778230b-6174-450f-b852-a4f7b2823ebe,YEP LAG,#00FF7F
|
||||||
|
subscriber/2,,"subscriber/0,premium/1",,127550308,74b2411af72bc224a6b125769950b9d5,,1595816107440,0,58745385,,0,danification9,1,91c2e96c-5450-4cf9-bae4-a1ae89f43965,CHESS LAG,
|
||||||
|
subscriber/1,1,subscriber/0,25:0-4,127550308,084dd2f2f4f9d8003edc2e7abe2d9320,,1595816107521,0,120169544,,0,ItsRua,1,15090353-1236-4cae-ac3a-8ed6597cf17a,Kappa,
|
||||||
|
,1,premium/1,25:0-4,127550308,008abf709a01b2cec0e77ae608748fb0,,1595816107628,0,500900526,,0,Pong8770,0,599ebd5a-f07a-4d62-b86a-d1889a1db71f,Kappa,#B22222
|
||||||
|
,1,premium/1,25:0-4,127550308,,,1595816107793,0,39491422,,0,magnumd0nger,0,7ea756ff-736c-4199-890b-913585edb080,Kappa,#850303
|
||||||
|
subscriber/1,,subscriber/0,,127550308,9cd9d5322cb32748c80f9ac636785fa9,,1595816107829,0,52430007,,0,rowrow_,1,cf7fd9ef-c2e8-450e-9968-5aa25b3007e3,no johns boatz,#DAA520
|
||||||
|
subscriber/1,,"subscriber/0,premium/1",,127550308,884ab0300555032462e1f65005166f11,,1595816108173,0,46899419,,0,ArcticPolarbear,1,17d7c1ad-4e6f-49f0-a523-a65e3ec2da7a,brain lag,#B22222
|
||||||
|
,,bits/1000,,127550308,c65720b8372d7b27a8fab30829848eb4,,1595816108223,0,169027853,,0,febog,0,61d074ac-d6e5-4446-a37e-5eae0f224165,YEP lag,#1E90FF
|
||||||
|
subscriber/3,,subscriber/3,,127550308,23c8813ef0fc52c15f90a741371b1770,,1595816108353,0,28276685,,0,1y1e,1,855d978e-dfb9-4c9e-8739-4c37b36eb77e,lag exists between eyes and brain,#2E8B57
|
||||||
|
,,premium/1,25:4-8,127550308,,,1595816108409,0,25286001,,0,Nicktown,0,d763a6fc-634d-42af-9c52-f66a70bce5ce,lag Kappa,#0000FF
|
||||||
|
,,,,127550308,69f54844b1ecdc5a5306bf75b67cbffe,,1595816108414,0,24651046,,0,Gabber__,0,d1e612ea-338e-42e8-84a0-4584ee18008b,monkaHmm,#FFFFFF
|
||||||
|
subscriber/7,,"subscriber/6,bits/100",,127550308,58fc6c30a6445398e467483f4629ab79,,1595816108844,0,217399619,,0,Adenosine_Tri_Phosphate,1,afd25cc8-9510-4550-b5eb-9324b6ca610f,#blamechesscom,#999999
|
||||||
|
,,,,127550308,94f92dad885d49cdf0d4d74d694b94d4,,1595816108963,0,265540494,,0,maek_28,0,973dfe4b-9be9-4d05-85ca-755551f52190,nope,
|
||||||
|
,,,,127550308,ce7721c36b549cd969d1732b0c66f5a7,,1595816109129,0,274725794,,0,einn9,0,75b1f985-8db0-4a91-995e-d3261a51b90e,sadchamp,
|
||||||
|
subscriber/1,,subscriber/0,,127550308,cedca7b9fa8f9889d212559ef4a65bc4,,1595816109165,0,97151792,,0,thekid_54,1,be0445d4-96fd-4a2d-9a98-85b298e2d5c4,24 hours?,#FD00BB
|
||||||
|
,1,,25:0-4,127550308,d21664375fd9a9a340c0d18f8476cf87,,1595816109286,0,132665916,,0,oceanman_takemebytheh4nd,0,218ab304-f255-4e24-82fc-4412db8769fc,Kappa,#551A8B
|
||||||
|
,,,25:4-8,127550308,99d8e8432f44cf0fd5fc0c37c515c046,,1595816109366,0,31050861,,0,skitlzx,0,fc682baf-6159-4e62-8902-fc941ae6b3e4,lag Kappa,#0000FF
|
||||||
|
,,,,127550308,65cc7bea1952413af77c3beb754eee9b,,1595816109437,0,204434546,,0,ImDisManyTwitch,0,9362896b-3856-4ede-bb62-b37dc12345a1,'Wine Lag',
|
||||||
|
,,,,127550308,0da04429d4f57809a0d152ce649b93f1,,1595816109656,0,484766599,,0,deepbishop,0,27c630ae-443e-4527-9dff-2a40698607a2,lag in your brain,
|
||||||
|
,,,,127550308,cc09588431d2060e47a58712164dbdaa,,1595816109830,0,534243080,,0,gretasdisciples,0,b2c20b7b-e3fa-4e8f-a9e7-9a2e1e98f958,"SURE, A lag",
|
||||||
|
,1,,25:0-4,127550308,be44d05ef78860a93763195c268e8d05,,1595816110137,0,254157596,,0,cupofcocoa__,0,edfe34a2-9c4d-4399-a7d3-c05d41fdaa18,Kappa,#0000FF
|
||||||
|
,,,,127550308,f7f49c9e34784250d858b7a763b0d3d2,,1595816110138,0,77980562,,0,BBeatless,0,c7cd9b9f-651a-4b35-a66c-1d16f47c724e,brain LAG,#1E90FF
|
||||||
|
subscriber/1,,subscriber/0,25:0-4,127550308,4276af5f703b600d8f49fee2f41b10cf,,1595816110119,0,430645544,,0,sophiexbc,1,d40b23ce-ada7-486a-8770-ea37e7f60cae,Kappa sure,#1E90FF
|
||||||
|
,,,,127550308,5c4a80567ab0f943af66d0998cbdcee8,,1595816110594,0,189862796,,0,loayakram,0,9e8eed62-e2c3-4011-b31e-547d304b7285,NO,
|
||||||
|
subscriber/5,,subscriber/3,,127550308,65e436840083d37e6cc9daf6a9b38fb5,,1595816110766,0,207843882,,0,Shroffy7,1,1fb7c924-ea3c-4cfc-9cd6-6808a1aacfad,chess.com PepeLaugh,#FF4500
|
||||||
|
,,,,127550308,f26a03c645cc67bf375b6847ef3d6c93,,1595816110976,0,263124733,,0,LudwigVanBeethoven23,0,d35c610a-c7d9-4a5b-affd-9ae7e935d7cb,lag in chess,
|
||||||
|
,,,,127550308,b9eadfa3e9dfe27f6db4083634fa7b56,,1595816111332,0,164828199,,0,aa175,0,7b3c6764-5ed9-4b6a-bdc9-188915db017f,THE FINAL POSITION IS A DRAw,
|
||||||
|
subscriber/2,,"subscriber/0,premium/1",,127550308,04f914d5b9b8405c21597e9b9161a502,,1595816111530,0,58745385,,0,danification9,1,6429e1e0-c6cf-4666-b884-bef0a8e5d8a2,Brain lag,
|
||||||
|
,1,,25:0-4,127550308,,,1595816111656,0,51032758,,0,Whole_Lotta_Lies,0,94d64716-8213-45ce-a6f1-ef60ab9f8440,Kappa,#2E8B57
|
||||||
|
subscriber/1,,subscriber/0,,127550308,1b843e3869ab21c8923dd1d7f4795673,,1595816111645,0,557195178,,0,byebrows2020,1,9ff86b87-f69b-4343-8faa-91c1be1284ff,LAG BLAME,
|
||||||
|
,,,,127550308,9d293af5749552b8629f0897523baf7d,,1595816112076,0,262128196,,0,slinkyshoots,0,d5ca1063-6f94-44aa-a239-00fe530785b1,Kapp,#00FF7F
|
||||||
|
subscriber/2,,subscriber/0,25:0-4,127550308,db149a2243bb3914841fe23d9cb1bbeb,,1595816112201,0,541613570,6-10:A.3,0,botezslavic,1,eb0c8775-b1b5-49ca-9ec4-1832b62947bb,Kappa Drunk already,#8A2BE2
|
||||||
|
,1,premium/1,25:0-4,127550308,74fe1bc5dae66988b60a8c54e68798e0,,1595816112487,0,195983667,,0,Nativezk,0,36b645e1-e01f-404a-af35-1d478c171c72,Kappa,#FF69B4
|
||||||
|
,,,,127550308,bfbb279442719e5920d553a2761538dc,,1595816112658,0,119618817,,0,ElJavieer,0,f05fb74b-dff1-4654-98e4-28d03d6e78f2,Brain lag,#2E8B57
|
||||||
|
,1,,25:0-4,127550308,,,1595816112802,0,413018612,,0,k4rzheka,0,c4561234-f570-4f41-8904-09aa52c33fd8,Kappa,
|
||||||
|
,,,,127550308,9ad449b192d1de4d7aa596bd156a2062,,1595816113542,0,71627954,,0,Toorshul891,0,0a2f91e4-04c0-43c2-a9c4-7be881a83cfe,yes,#FF0000
|
||||||
|
subscriber/1,,subscriber/0,,127550308,,,1595816113595,0,41972554,,0,elsenatto,1,d0a07de4-fd30-4216-8e58-62c9dbf65797,YEP LAG,#FF0000
|
||||||
|
,,premium/1,,127550308,5f16e408c7c90ffbe042ab4375fff7c7,,1595816113605,0,409091205,,0,iamzul95,0,57c2de5e-fa67-4462-ae17-3aed3f9d91ce,there was a lag,
|
||||||
|
subscriber/1,,"subscriber/0,premium/1",,127550308,,,1595816113693,0,87187380,,0,SemiPro19,1,1ab4c9b5-8383-473a-a860-95121028f0f9,wine lag,
|
||||||
|
,,premium/1,,127550308,e64fcb6a9b5a08b259cc04c152d1dfec,,1595816114198,0,101983685,,0,Clench1k,0,2ca56351-ee78-4efd-8c32-72694cfec3d8,YEP lag,#FF0000
|
||||||
|
subscriber/1,,subscriber/0,,127550308,9021f68fc93100962d0e988f6143018f,,1595816114801,0,23289875,,0,Jaaledon,1,6da1d281-2924-40b3-8fa7-bc425c03beba,"Sure, blame it on your ISP",#0000FF
|
||||||
|
,,,25:8-12,127550308,64c03873fd1d7f7ef5fd375842d73de2,,1595816114958,0,504713042,,0,ocelot022,0,3797dcd3-52ff-45f7-b573-5cc52412e549,ok sure Kappa,
|
||||||
|
subscriber/10,,subscriber/6,"302934780:10-18,26-34,42-50,58-66,74-82",127550308,516affced6902164299a700a388cf543,,1595816115272,0,117880554,,0,sepehr91,1,87d8db4c-04e8-4ec9-9bc2-8fb8c61a286b,lag pepeD botez2300 pepeD botez2300 pepeD botez2300 pepeD botez2300 pepeD botez2300 lag,#00FF7F
|
||||||
|
,,,,127550308,d8e9fbbe67345be0e78d4fab5bdfbc23,,1595816115454,0,484766599,,0,deepbishop,0,467592bf-bdf1-46eb-a8b2-a9b5bd2a6bb6,brain lag,
|
||||||
|
,,premium/1,,127550308,,,1595816115903,0,512437416,,0,augusto_vam,0,384f4487-62fd-464b-aa43-132f5d29c97d,BRAIN LAG,
|
||||||
|
,,,,127550308,,,1595816115910,0,550092913,,0,010293101,0,acb3ba00-6e77-4039-b298-9d8c226bceee,Dronk,
|
||||||
|
subscriber/1,,subscriber/0,,127550308,89a17afbfa128e565f95c860f2a3a6ca,,1595816115909,0,58317233,,0,mrchair1982,1,91dd1bf2-2737-4951-862b-f8021e9f6d02,how have you already dropped so far down from 2300?,#FF0000
|
||||||
|
subscriber/1,,subscriber/0,,127550308,,,1595816116392,0,113854995,,0,xavierrocket,1,6200b631-4f59-4374-a551-1185e3eff4d9,"I've had a ton of messed up moves because of lag, I know the pain Sadge",#CC0000
|
||||||
|
,,,25:4-8,127550308,d3edaa32d363cf4b4220b4bfbf730c3c,,1595816116471,0,6888738,,0,AznBoy222,0,73251b3e-c38e-4041-bb10-67adb9ca183b,yep Kappa,#FF0000
|
||||||
|
subscriber/1,,subscriber/0,,127550308,,,1595816116526,0,48780004,,0,Cnoized,1,fffd4298-a450-42be-a2db-cb0e0e53a148,I saw it.,#00FF7F
|
||||||
|
,,,,127550308,,,1595816116580,0,144960091,,0,jisos12369,0,f9c2ff7f-d306-4ca7-bbbd-2dbdd25c0614,YEP lag,#1E90FF
|
||||||
|
,,premium/1,,127550308,3c37ca135a77f7597a4cdf86321b5bca,,1595816116668,0,442498004,,0,juanber,0,f8c1c408-4c4a-4a8e-b1a4-422345ce902f,brain lag,
|
||||||
|
subscriber/7,1,subscriber/2006,302895853:0-7,127550308,10ba03ddf7192e3c492547c018c47c83,,1595816116704,0,255884924,,0,RO0PE,1,1a125c0f-3ffd-45c2-ae32-5c4758634c43,botezCIA,
|
||||||
|
,,,25:58-62,127550308,7cab32d02b49d46c0f8f094192eb4e3a,,1595816117665,0,90516629,,0,Mossico,0,2104a46a-a31e-4811-8de9-42777b9ce925,perhaps your perception of time is impaired at the moment Kappa,
|
||||||
|
subscriber/3,,subscriber/3,88:15-22,127550308,649c5c64e0b9a9946509692b9fe935fa,,1595816118589,0,152865823,,0,lurker_above,1,b4ae57d5-ff9b-4751-8b90-63aefd1015d4,24 hour stream PogChamp,
|
||||||
|
,,premium/1,,127550308,805dc602bc1d75963a5e5bf9ffb86ab6,,1595816118924,0,177511477,,0,dildo_fire,0,f504ed44-9c19-4c4d-b69c-7456e76c74ba,that's why i play on lichess,#504B44
|
||||||
|
,,,,127550308,,,1595816119093,0,527834016,,0,zloriginals,0,48474373-5e57-42e5-ba93-d346eaf8f184,Just u,
|
||||||
|
,,,,127550308,319a67044c5003601f705abaf16542cd,,1595816119284,0,47117924,,0,CoDyPhin,0,f1a5b6d5-3614-4d6f-8a8f-c9511c7e3a61,1345 AndreaPls pepeD blobDance AndreaPls pepeD blobDance AndreaPls pepeD blobDance AndreaPls pepeD blobDance AndreaPls pepeD blobDance AndreaPls pepeD blobDance,#1E90FF
|
||||||
|
subscriber/1,,"subscriber/0,premium/1",,127550308,,,1595816119373,0,98416009,0-4:A.3,0,Schultzky,1,507e9eb5-593d-48f5-b014-10b6784b5e8a,drunk,#FF0000
|
||||||
|
,,,,127550308,ee1bbc8080c48ce26accd61ea405d39c,,1595816119464,0,189862796,,0,loayakram,0,a9fa6238-e237-4ac7-b9bd-57374a109d07,KAPPA,
|
||||||
|
,1,,"145315:0-12,14-26,28-40,42-54,56-68,70-82,84-96,98-110,112-124,126-138,140-152,154-166,168-180,182-194,196-208,210-222,224-236,238-250,252-264,266-278,280-292,294-306,308-320",127550308,,,1595816119454,0,436262291,,0,itzghost6890,0,64ac2f7d-e975-457c-aa3e-ced46c94f678,TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati,
|
||||||
|
subscriber/1,,"subscriber/2000,bits/100",,127550308,8aab484c2f0ef821c0536dde4f90f8fb,,1595816119878,0,55373647,,0,EpicTripleAssTap,1,11770323-aec0-4767-81b6-588208c53310,PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls,#00FF7F
|
||||||
|
,,premium/1,,127550308,d8386d364e9de4113d16f99f088ad8ca,,1595816119897,0,478625547,,0,skitzt,0,074ab457-dcb8-49c3-be2f-163d20afe789,so 24hr stream?,
|
||||||
|
subscriber/1,,"subscriber/0,premium/1",,127550308,,,1595816120966,0,9095096,,0,NoPantzzz,1,19d535d1-fe64-4750-8a9e-ffe7e43882d2,Laggggggg,#FEA400
|
||||||
|
subscriber/11,,subscriber/6,,127550308,e2b66a7a4278499c62ef48eef673a41e,,1595816121587,0,250839454,,0,zPOUTINEZ,1,5241f949-62dc-45c5-bf81-176cd812b54b,its the wine dude,#FF0000
|
||||||
|
,,,,127550308,5d93ca39fa974f87ae34faa6da934d3c,,1595816122054,0,500766391,,0,merlinnimue,0,5bc1e672-a956-4c64-8bf8-25884361a120,spirit of rasputin,
|
||||||
|
subscriber/2,,subscriber/0,25:9-13,127550308,e84f1dc8db2401daa4e706f8e25802a4,,1595816122170,0,541613570,,0,botezslavic,1,71281c31-2ccc-4ca6-8115-dfcebe388ce4,wine lag Kappa,#8A2BE2
|
||||||
|
,,premium/1,,127550308,3ba4d9d4e0e8bf2f750121df96d36bba,,1595816122308,0,152601449,,0,midgetpanda96,0,eedf2720-66c6-4459-84e0-9bdd6f82152c,ya,
|
||||||
|
,,,25:0-4,127550308,8c590466109e0380d4329bdc33dded67,,1595816123077,0,152709545,,0,stepkc,0,88683f85-805a-4e87-9b78-4a72cadaab7a,Kappa hmm,#00FF7F
|
||||||
|
,,,,127550308,4ba0e28fa15dda235da65be4fe4b7e5e,,1595816123425,0,265540494,,0,maek_28,0,1088b96e-4a40-433a-a620-36a569dec5ec,no lag nope,
|
||||||
|
subscriber/1,1,"subscriber/0,sub-gifter/1",70433:0-8,127550308,,,1595816123499,0,553594803,,0,dogplatformr2,1,46ddc40c-e032-4056-a8a2-e751087f3162,KappaRoss,
|
||||||
|
,,,,127550308,,,1595816123729,0,51032758,,0,Whole_Lotta_Lies,0,72c0bdeb-ce70-4a9d-95c8-777923a4685c,5Head lag,#2E8B57
|
||||||
|
,1,,301443647:0-4/301443646:6-10,127550308,,,1595816124273,0,76087796,,0,Krappa_1_2_3,0,6606358f-2bbb-47e7-9341-3961719ebeea,nymn1 nymn2,#FF0000
|
||||||
|
subscriber/1,,subscriber/0,,127550308,b43c8b5a271298708a7808d74ef71363,,1595816124725,0,522200051,,0,creatovert,1,edfd099d-37b8-4a69-b564-7467d8969fcf,MIND LAG DUE TO WINE,#8A2BE2
|
||||||
|
,,,,127550308,,,1595816124894,0,484284968,,0,kaibennett06,0,a703c39a-7206-42c3-a559-99459054931f,YEP LAG,
|
||||||
|
subscriber/1,,subscriber/0,,127550308,,,1595816124950,0,130382221,,0,k33ling33,1,8fe6738f-d9e7-4490-a703-cd240726ce39,Omg lag,
|
||||||
|
,,,,127550308,f15ab8ab2a81f42f6919b7753be2285d,,1595816125071,0,32171084,,0,dslx,0,d5509a70-fe08-427c-b63a-fd4087dc06e7,Yep lag,#9ACD32
|
||||||
|
,,glhf-pledge/1,123171:10-21,127550308,b182592194b2e3a041c4d0a3ceec34e5,,1595816126541,0,179916073,0-4:A.3,0,nefderek,0,a815d176-d0b5-486c-8c2f-dd606ec77bb5,drunk lag CoolStoryBob,#FF69B4
|
||||||
|
,,,,127550308,f1742a95c860173cbed53c898e6ce917,,1595816126586,0,484766599,,0,deepbishop,0,1c14dd76-5384-47b4-bd4e-2f9e5bd010f7,keep chugging,
|
||||||
|
1
twitchchatirc/examples/example.json
Normal file
1
twitchchatirc/examples/example.json
Normal file
File diff suppressed because one or more lines are too long
127
twitchchatirc/examples/example.txt
Normal file
127
twitchchatirc/examples/example.txt
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
[1595816099308] sophiexbc: Kappa
|
||||||
|
[1595816099422] dougie___jones: lag Kappa
|
||||||
|
[1595816099572] coffeeindex: L OMEGALUL G
|
||||||
|
[1595816099785] AmazonESP: Kappa
|
||||||
|
[1595816100038] Deflecti0n: Kappa lag
|
||||||
|
[1595816100393] Dortok: YEP lag
|
||||||
|
[1595816100907] danification9: Sadge
|
||||||
|
[1595816101643] pab1994: yup
|
||||||
|
[1595816101637] aa175: g3 rooooooooooook @BotezLive
|
||||||
|
[1595816102163] vespereq_: It's t h e wine
|
||||||
|
[1595816102288] botezslavic: DRUNK
|
||||||
|
[1595816102306] rowrow_: no johns
|
||||||
|
[1595816102360] slightlyHyPhi: KappaHD
|
||||||
|
[1595816102389] ReadingRailroad: Yep
|
||||||
|
[1595816102598] CrenshawViews: yeah it glitched
|
||||||
|
[1595816102879] Heat_Signature: LAG ON CHESS OMEGALUL
|
||||||
|
[1595816103185] LeetChocolate: Kappa lag
|
||||||
|
[1595816103329] swornheart: that was brain lag
|
||||||
|
[1595816103349] JpForrReal: OMEGALUL
|
||||||
|
[1595816103439] AleFalnx: atpCap
|
||||||
|
[1595816103488] hericium7: Kappa
|
||||||
|
[1595816103492] ezeroh13: YEP LAG
|
||||||
|
[1595816103535] Nativezk: lag Kappa
|
||||||
|
[1595816103731] dougie___jones: yeah totally Kappa
|
||||||
|
[1595816103979] CoDyPhin: Kappa
|
||||||
|
[1595816104011] Nico3094: lag YEP
|
||||||
|
[1595816104086] RoLoSC: yeah little lage
|
||||||
|
[1595816104351] ArcticPolarbear: Kappa lag
|
||||||
|
[1595816104364] kzp99: LAG
|
||||||
|
[1595816104463] FrankDaTankGaming: does that for me sometimes too
|
||||||
|
[1595816104600] modestmage: wasnt that a trade?
|
||||||
|
[1595816104624] yop01: Yes
|
||||||
|
[1595816104597] sophiexbc: Kappa lag
|
||||||
|
[1595816104756] swift_ocelot: YEP lag
|
||||||
|
[1595816105421] cinasxd: kappa
|
||||||
|
[1595816105609] jrude72: So what r u up to for the next 24 hours?
|
||||||
|
[1595816105662] brettydoes: YEP lag
|
||||||
|
[1595816105720] kavifa6176: Brain lag
|
||||||
|
[1595816106052] drawingdead: mhmmm Kappa
|
||||||
|
[1595816106179] Cajun292: bullet lag
|
||||||
|
[1595816106261] bittersandwich: blaming on lag LULW
|
||||||
|
[1595816106687] loayakram: fake
|
||||||
|
[1595816106783] xivi76: you're drunk
|
||||||
|
[1595816106749] vespereq_: WINE LAG
|
||||||
|
[1595816106826] deepbishop: drunk
|
||||||
|
[1595816106963] HaloMonkey3009: Kappa
|
||||||
|
[1595816107192] kilner111: Kappa
|
||||||
|
[1595816107269] NotReformeduwu: YEP
|
||||||
|
[1595816107375] juanhc24v2: YEP LAG
|
||||||
|
[1595816107440] danification9: CHESS LAG
|
||||||
|
[1595816107521] ItsRua: Kappa
|
||||||
|
[1595816107628] Pong8770: Kappa
|
||||||
|
[1595816107793] magnumd0nger: Kappa
|
||||||
|
[1595816107829] rowrow_: no johns boatz
|
||||||
|
[1595816108173] ArcticPolarbear: brain lag
|
||||||
|
[1595816108223] febog: YEP lag
|
||||||
|
[1595816108353] 1y1e: lag exists between eyes and brain
|
||||||
|
[1595816108409] Nicktown: lag Kappa
|
||||||
|
[1595816108414] Gabber__: monkaHmm
|
||||||
|
[1595816108844] Adenosine_Tri_Phosphate: #blamechesscom
|
||||||
|
[1595816108963] maek_28: nope
|
||||||
|
[1595816109129] einn9: sadchamp
|
||||||
|
[1595816109165] thekid_54: 24 hours?
|
||||||
|
[1595816109286] oceanman_takemebytheh4nd: Kappa
|
||||||
|
[1595816109366] skitlzx: lag Kappa
|
||||||
|
[1595816109437] ImDisManyTwitch: 'Wine Lag'
|
||||||
|
[1595816109656] deepbishop: lag in your brain
|
||||||
|
[1595816109830] gretasdisciples: SURE, A lag
|
||||||
|
[1595816110137] cupofcocoa__: Kappa
|
||||||
|
[1595816110138] BBeatless: brain LAG
|
||||||
|
[1595816110119] sophiexbc: Kappa sure
|
||||||
|
[1595816110594] loayakram: NO
|
||||||
|
[1595816110766] Shroffy7: chess.com PepeLaugh
|
||||||
|
[1595816110976] LudwigVanBeethoven23: lag in chess
|
||||||
|
[1595816111332] aa175: THE FINAL POSITION IS A DRAw
|
||||||
|
[1595816111530] danification9: Brain lag
|
||||||
|
[1595816111656] Whole_Lotta_Lies: Kappa
|
||||||
|
[1595816111645] byebrows2020: LAG BLAME
|
||||||
|
[1595816112076] slinkyshoots: Kapp
|
||||||
|
[1595816112201] botezslavic: Kappa Drunk already
|
||||||
|
[1595816112487] Nativezk: Kappa
|
||||||
|
[1595816112658] ElJavieer: Brain lag
|
||||||
|
[1595816112802] k4rzheka: Kappa
|
||||||
|
[1595816113542] Toorshul891: yes
|
||||||
|
[1595816113595] elsenatto: YEP LAG
|
||||||
|
[1595816113605] iamzul95: there was a lag
|
||||||
|
[1595816113693] SemiPro19: wine lag
|
||||||
|
[1595816114198] Clench1k: YEP lag
|
||||||
|
[1595816114801] Jaaledon: Sure, blame it on your ISP
|
||||||
|
[1595816114958] ocelot022: ok sure Kappa
|
||||||
|
[1595816115272] sepehr91: lag pepeD botez2300 pepeD botez2300 pepeD botez2300 pepeD botez2300 pepeD botez2300 lag
|
||||||
|
[1595816115454] deepbishop: brain lag
|
||||||
|
[1595816115903] augusto_vam: BRAIN LAG
|
||||||
|
[1595816115910] 010293101: Dronk
|
||||||
|
[1595816115909] mrchair1982: how have you already dropped so far down from 2300?
|
||||||
|
[1595816116392] xavierrocket: I've had a ton of messed up moves because of lag, I know the pain Sadge
|
||||||
|
[1595816116471] AznBoy222: yep Kappa
|
||||||
|
[1595816116526] Cnoized: I saw it.
|
||||||
|
[1595816116580] jisos12369: YEP lag
|
||||||
|
[1595816116668] juanber: brain lag
|
||||||
|
[1595816116704] RO0PE: botezCIA
|
||||||
|
[1595816117665] Mossico: perhaps your perception of time is impaired at the moment Kappa
|
||||||
|
[1595816118589] lurker_above: 24 hour stream PogChamp
|
||||||
|
[1595816118924] dildo_fire: that's why i play on lichess
|
||||||
|
[1595816119093] zloriginals: Just u
|
||||||
|
[1595816119284] CoDyPhin: 1345 AndreaPls pepeD blobDance AndreaPls pepeD blobDance AndreaPls pepeD blobDance AndreaPls pepeD blobDance AndreaPls pepeD blobDance AndreaPls pepeD blobDance
|
||||||
|
[1595816119373] Schultzky: drunk
|
||||||
|
[1595816119464] loayakram: KAPPA
|
||||||
|
[1595816119454] itzghost6890: TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati TheIlluminati
|
||||||
|
[1595816119878] EpicTripleAssTap: PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls PUTINWALK AndreaPls
|
||||||
|
[1595816119897] skitzt: so 24hr stream?
|
||||||
|
[1595816120966] NoPantzzz: Laggggggg
|
||||||
|
[1595816121587] zPOUTINEZ: its the wine dude
|
||||||
|
[1595816122054] merlinnimue: spirit of rasputin
|
||||||
|
[1595816122170] botezslavic: wine lag Kappa
|
||||||
|
[1595816122308] midgetpanda96: ya
|
||||||
|
[1595816123077] stepkc: Kappa hmm
|
||||||
|
[1595816123425] maek_28: no lag nope
|
||||||
|
[1595816123499] dogplatformr2: KappaRoss
|
||||||
|
[1595816123729] Whole_Lotta_Lies: 5Head lag
|
||||||
|
[1595816124273] Krappa_1_2_3: nymn1 nymn2
|
||||||
|
[1595816124725] creatovert: MIND LAG DUE TO WINE
|
||||||
|
[1595816124894] kaibennett06: YEP LAG
|
||||||
|
[1595816124950] k33ling33: Omg lag
|
||||||
|
[1595816125071] dslx: Yep lag
|
||||||
|
[1595816126541] nefderek: drunk lag CoolStoryBob
|
||||||
|
[1595816126586] deepbishop: keep chugging
|
||||||
1
twitchchatirc/gonvo.txt
Normal file
1
twitchchatirc/gonvo.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<socket.socket fd=1180, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('192.168.88.240', 62994), raddr=('34.212.92.60', 6667)>
|
||||||
4
twitchchatirc/requirements.txt
Normal file
4
twitchchatirc/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
argparse
|
||||||
|
python-decouple
|
||||||
|
emoji
|
||||||
|
csv
|
||||||
216
twitchchatirc/twitch_chat_irc.py
Normal file
216
twitchchatirc/twitch_chat_irc.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import socket, re, json, argparse, emoji, csv
|
||||||
|
# from decouple import config
|
||||||
|
|
||||||
|
class DefaultUser(Exception):
|
||||||
|
"""Raised when you try send a message with the default user"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CallbackFunction(Exception):
|
||||||
|
"""Raised when the callback function does not have (only) one required positional argument"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TwitchChatIRC():
|
||||||
|
__HOST = 'irc.chat.twitch.tv'
|
||||||
|
__DEFAULT_NICK = 'justinfan67420'
|
||||||
|
__DEFAULT_PASS = 'SCHMOOPIIE'
|
||||||
|
__PORT = 6667
|
||||||
|
|
||||||
|
__PATTERN = re.compile(r'@(.+?(?=\s+:)).*PRIVMSG[^:]*:([^\r\n]*)')
|
||||||
|
|
||||||
|
__CURRENT_CHANNEL = None
|
||||||
|
|
||||||
|
def __init__(self, username = None, password = None):
|
||||||
|
|
||||||
|
self.__NICK = self.__DEFAULT_NICK
|
||||||
|
self.__PASS = self.__DEFAULT_PASS
|
||||||
|
|
||||||
|
# overwrite if specified
|
||||||
|
if(username is not None):
|
||||||
|
self.__NICK = username
|
||||||
|
if(password is not None):
|
||||||
|
self.__PASS = 'oauth:'+str(password).lstrip('oauth:')
|
||||||
|
|
||||||
|
# create new socket
|
||||||
|
self.__SOCKET = socket.socket()
|
||||||
|
|
||||||
|
# start connection
|
||||||
|
self.__SOCKET.connect((self.__HOST, self.__PORT))
|
||||||
|
print('Connected to',self.__HOST,'on port',self.__PORT)
|
||||||
|
|
||||||
|
# log in
|
||||||
|
self.__send_raw('CAP REQ :twitch.tv/tags')
|
||||||
|
self.__send_raw('PASS ' + self.__PASS)
|
||||||
|
self.__send_raw('NICK ' + self.__NICK)
|
||||||
|
|
||||||
|
def __send_raw(self, string):
|
||||||
|
self.__SOCKET.send((string+'\r\n').encode('utf-8'))
|
||||||
|
|
||||||
|
def __print_message(self, message):
|
||||||
|
print('['+message['tmi-sent-ts']+']',message['display-name']+':',emoji.demojize(message['message']).encode('utf-8').decode('utf-8','ignore'))
|
||||||
|
|
||||||
|
def __recvall(self, buffer_size):
|
||||||
|
data = b''
|
||||||
|
while True:
|
||||||
|
part = self.__SOCKET.recv(buffer_size)
|
||||||
|
data += part
|
||||||
|
if len(part) < buffer_size:
|
||||||
|
break
|
||||||
|
return data.decode('utf-8')#,'ignore'
|
||||||
|
|
||||||
|
def __join_channel(self,channel_name):
|
||||||
|
channel_lower = channel_name.lower()
|
||||||
|
|
||||||
|
if(self.__CURRENT_CHANNEL != channel_lower):
|
||||||
|
self.__send_raw('JOIN #{}'.format(channel_lower))
|
||||||
|
self.__CURRENT_CHANNEL = channel_lower
|
||||||
|
|
||||||
|
def is_default_user(self):
|
||||||
|
return self.__NICK == self.__DEFAULT_NICK
|
||||||
|
|
||||||
|
def close_connection(self):
|
||||||
|
self.__SOCKET.close()
|
||||||
|
print('Connection closed')
|
||||||
|
|
||||||
|
def listen(self, channel_name, messages = [], timeout=None, message_timeout=1.0, on_message = None, buffer_size = 4096, message_limit = None, output=None):
|
||||||
|
self.__join_channel(channel_name)
|
||||||
|
self.__SOCKET.settimeout(message_timeout)
|
||||||
|
|
||||||
|
if(on_message is None):
|
||||||
|
on_message = self.__print_message
|
||||||
|
|
||||||
|
print('Begin retrieving messages:')
|
||||||
|
|
||||||
|
time_since_last_message = 0
|
||||||
|
readbuffer = ''
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
new_info = self.__recvall(buffer_size)
|
||||||
|
readbuffer += new_info
|
||||||
|
|
||||||
|
if('PING :tmi.twitch.tv' in readbuffer):
|
||||||
|
self.__send_raw('PONG :tmi.twitch.tv')
|
||||||
|
|
||||||
|
matches = list(self.__PATTERN.finditer(readbuffer))
|
||||||
|
|
||||||
|
if(matches):
|
||||||
|
|
||||||
|
time_since_last_message = 0
|
||||||
|
|
||||||
|
if(len(matches) > 1):
|
||||||
|
matches = matches[:-1] # assume last one is incomplete
|
||||||
|
|
||||||
|
last_index = matches[-1].span()[1]
|
||||||
|
readbuffer = readbuffer[last_index:]
|
||||||
|
|
||||||
|
for match in matches:
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
for item in match.group(1).split(';'):
|
||||||
|
keys = item.split('=',1)
|
||||||
|
data[keys[0]]=keys[1]
|
||||||
|
data['message'] = match.group(2)
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
messages.append(data)
|
||||||
|
|
||||||
|
if(callable(on_message)):
|
||||||
|
try:
|
||||||
|
on_message(data)
|
||||||
|
except TypeError:
|
||||||
|
raise Exception('Incorrect number of parameters for function '+on_message.__name__)
|
||||||
|
|
||||||
|
if(message_limit is not None and len(messages) >= message_limit):
|
||||||
|
return messages
|
||||||
|
|
||||||
|
except socket.timeout:
|
||||||
|
if(timeout != None):
|
||||||
|
time_since_last_message += message_timeout
|
||||||
|
|
||||||
|
if(time_since_last_message >= timeout):
|
||||||
|
print('No data received in',timeout,'seconds. Timing out.')
|
||||||
|
break
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('Interrupted by user.')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print('Unknown Error:',e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def send(self, channel_name, message):
|
||||||
|
self.__join_channel(channel_name)
|
||||||
|
|
||||||
|
# check that is using custom login, not default
|
||||||
|
if(self.is_default_user()):
|
||||||
|
raise DefaultUser
|
||||||
|
else:
|
||||||
|
self.__send_raw('PRIVMSG #{} :{}'.format(channel_name.lower(),message))
|
||||||
|
print('Sent "{}" to {}'.format(message,channel_name))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(description='Send and receive Twitch chat messages over IRC with python web sockets. For more info, go to https://dev.twitch.tv/docs/irc/guide')
|
||||||
|
|
||||||
|
parser.add_argument('channel_name', help='Twitch channel name (username)')
|
||||||
|
parser.add_argument('-timeout','-t', default=None, type=float, help='time in seconds needed to close connection after not receiving any new data (default: None = no timeout)')
|
||||||
|
parser.add_argument('-message_timeout','-mt', default=1.0, type=float, help='time in seconds between checks for new data (default: 1 second)')
|
||||||
|
parser.add_argument('-buffer_size','-b', default=4096, type=int, help='buffer size (default: 4096 bytes = 4 KB)')
|
||||||
|
parser.add_argument('-message_limit','-l', default=None, type=int, help='maximum amount of messages to get (default: None = unlimited)')
|
||||||
|
|
||||||
|
parser.add_argument('-username','-u', default=None, help='username (default: None)')
|
||||||
|
parser.add_argument('-oauth', '-password','-p', default=None, help='oath token (default: None). Get custom one from https://twitchapps.com/tmi/')
|
||||||
|
|
||||||
|
parser.add_argument('--send', action='store_true', help='send mode (default: False)')
|
||||||
|
parser.add_argument('-output','-o', default=None, help='output file (default: None = print to standard output)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
twitch_chat_irc = TwitchChatIRC(username=args.username,password=args.oauth)
|
||||||
|
|
||||||
|
if(args.send):
|
||||||
|
if(twitch_chat_irc.is_default_user()):
|
||||||
|
print('Unable to send messages with default user. Please provide valid authentication.')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
message = input('>>> Enter message (blank to exit): \n')
|
||||||
|
if(not message):
|
||||||
|
break
|
||||||
|
twitch_chat_irc.send(args.channel_name, message)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('\nInterrupted by user.')
|
||||||
|
|
||||||
|
else:
|
||||||
|
messages = twitch_chat_irc.listen(
|
||||||
|
args.channel_name,
|
||||||
|
timeout=args.timeout,
|
||||||
|
message_timeout=args.message_timeout,
|
||||||
|
buffer_size=args.buffer_size,
|
||||||
|
message_limit=args.message_limit)
|
||||||
|
|
||||||
|
if(args.output != None):
|
||||||
|
if(args.output.endswith('.json')):
|
||||||
|
with open(args.output, 'w') as fp:
|
||||||
|
json.dump(messages, fp)
|
||||||
|
elif(args.output.endswith('.csv')):
|
||||||
|
with open(args.output, 'w', newline='',encoding='utf-8') as fp:
|
||||||
|
fieldnames = []
|
||||||
|
for message in messages:
|
||||||
|
fieldnames+=message.keys()
|
||||||
|
|
||||||
|
if(len(messages)>0):
|
||||||
|
fc = csv.DictWriter(fp,fieldnames=list(set(fieldnames)))
|
||||||
|
fc.writeheader()
|
||||||
|
fc.writerows(messages)
|
||||||
|
else:
|
||||||
|
f = open(args.output,'w', encoding='utf-8')
|
||||||
|
for message in messages:
|
||||||
|
print('['+message['tmi-sent-ts']+']',message['display-name']+':',message['message'],file=f)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
print('Finished writing',len(messages),'messages to',args.output)
|
||||||
|
|
||||||
|
twitch_chat_irc.close_connection()
|
||||||
Reference in New Issue
Block a user