This commit is contained in:
copyrighttxt
2026-07-06 13:28:00 -04:00
commit 05211f73ef
441 changed files with 94128 additions and 0 deletions
+298
View File
@@ -0,0 +1,298 @@
from flask import Blueprint, render_template, request, redirect, url_for, jsonify, make_response, abort, flash
from app.util import auth, placeinfo
from app.models.asset import Asset
from app.models.placeservers import PlaceServer
from app.models.placeserver_players import PlaceServerPlayer
from app.models.gameservers import GameServer
from app.models.place import Place
from app.models.gamepass_link import GamepassLink
from app.models.userassets import UserAsset
from app.models.groups import Group, GroupRole, GroupRolePermission
from app.models.place_badge import PlaceBadge, UserBadge
from app.models.universe import Universe
from slugify import slugify
from app.pages.develop.develop import ShutdownServer
from app.enums.AssetType import AssetType
from app.models.user import User
from app.util.membership import GetUserMembership
from app.enums.MembershipType import MembershipType
from app.routes.rate import GetAssetLikesAndDislikes, GetUserVoteStatus, GetAssetVotePercentage, GetAssetFavoriteCount, GetUserFavoriteStatus
from app.routes.badgesapi import CalculateBadgeRarity, GetBadgeAwardedPastDay
from app.services import groups
from sqlalchemy import and_, func
from datetime import datetime, timedelta
import logging
GamePagesRoute = Blueprint("games", __name__, template_folder="pages")
def DoesUserownGamepass( gamepass_id, userid ):
GamepassObj : Asset = Asset.query.filter_by(id=gamepass_id, asset_type = AssetType.GamePass).first()
if GamepassObj is None:
return False
UserGamepass : UserAsset = UserAsset.query.filter_by(userid=userid, assetid=GamepassObj.id).first()
if UserGamepass is None:
return False
return True
def DoesUserOwnBadge( badge_id, userid ):
BadgeObj : PlaceBadge = PlaceBadge.query.filter_by(id=badge_id).first()
if BadgeObj is None:
return False
UserBadgeObj : UserBadge = UserBadge.query.filter_by(badge_id=badge_id, user_id=userid).first()
if UserBadgeObj is None:
return False
return True
def GetTotalBadgeAwardedCount( badge_id ) -> int:
return UserBadge.query.filter_by( badge_id = badge_id ).count()
@GamePagesRoute.route("/games/<int:placeid>/", methods=["GET"])
@GamePagesRoute.route("/games/<int:placeid>/<string:slug>", methods=["GET"])
@auth.authenticated_required
def game_page(placeid : int, slug=None):
AssetObject : Asset = Asset.query.filter_by(id=placeid).first()
if AssetObject is None:
return abort(404)
if AssetObject.asset_type != AssetType.Place:
# Let catalog handle the redirect
return redirect(f"/catalog/{str(placeid)}/")
SlugName = slugify(AssetObject.name, lowercase=False)
if SlugName is None or SlugName == "":
SlugName = "unnamed"
if slug is None:
return redirect(f"/games/{str(placeid)}/{SlugName}")
if slug != SlugName:
return redirect(f"/games/{str(placeid)}/{SlugName}")
if AssetObject.creator_type == 0:
CreatorObject : User = User.query.filter_by(id=AssetObject.creator_id).first()
else:
CreatorObject : Group = Group.query.filter_by(id=AssetObject.creator_id).first()
PlaceObj : Place = Place.query.filter_by(placeid=placeid).first()
if PlaceObj is None:
return abort(404)
UniverseObj : Universe = Universe.query.filter_by(id=PlaceObj.parent_universe_id).first()
ActualRootPlace : Asset = Asset.query.filter_by(id=UniverseObj.root_place_id).first()
try:
SplittedDescription = AssetObject.description.split("\n")
LikeCount, DislikeCount = GetAssetLikesAndDislikes(AssetObject.id)
AuthenticatedUser : User = auth.GetCurrentUser()
PlaceServerObjs : list[PlaceServer] = PlaceServer.query.filter_by(serverPlaceId=placeid).all()
ActiveServers = []
for PlaceServerObj in PlaceServerObjs:
PlayersInServer : list[PlaceServerPlayer] = PlaceServerPlayer.query.filter_by(serveruuid=PlaceServerObj.serveruuid).all()
GameServerObj : GameServer = GameServer.query.filter_by(serverId=PlaceServerObj.originServerId).first()
if GameServerObj is None:
continue
ActiveServers.append({
"id": PlaceServerObj.serveruuid,
"playercount": PlaceServerObj.playerCount,
"maxplayercount": PlaceServerObj.maxPlayerCount,
"players": PlayersInServer,
"host": GameServerObj.serverName,
"is_reserved_server": PlaceServerObj.reservedServerAccessCode is not None
})
Gamepasses : list[GamepassLink] = GamepassLink.query.filter_by(universe_id = PlaceObj.parent_universe_id).all()
UserDoesNotHaveBC : bool = UniverseObj.bc_required and GetUserMembership(AuthenticatedUser) == MembershipType.NonBuildersClub
CanShutdownServer = False if AuthenticatedUser.id not in (1, 49) else True
if AssetObject.creator_type == 0:
if AuthenticatedUser.id == AssetObject.creator_id:
CanShutdownServer = True
else:
GroupObj : Group = groups.GetGroupFromId( AssetObject.creator_id )
UserGroupRole : GroupRole = groups.GetUserRolesetInGroup( AuthenticatedUser, GroupObj )
if UserGroupRole is None:
CanShutdownServer = False
UserGroupRolePermission : GroupRolePermission = groups.GetRolesetPermission( UserGroupRole )
if UserGroupRolePermission.manage_group_games:
CanShutdownServer = True
GameBadges : list[PlaceBadge] = PlaceBadge.query.filter_by( universe_id = PlaceObj.parent_universe_id ).all()
return render_template("games/view.html", PlaceAssetObj=AssetObject,
CreatorObj=CreatorObject,
PlaceObj=PlaceObj,
SplittedDescription=SplittedDescription,
PlayerCount=placeinfo.GetPlayingCount(PlaceObj),
LikeCount=LikeCount,
DislikeCount=DislikeCount,
UserVoteStatus=GetUserVoteStatus(AssetObject.id, AuthenticatedUser.id),
UserFavoriteStatus=GetUserFavoriteStatus(AssetObject.id, AuthenticatedUser.id),
FavoriteCount=GetAssetFavoriteCount(AssetObject.id),
ActiveServers=ActiveServers,
ActiveServerCount=len(ActiveServers),
Gamepasses=Gamepasses,
DoesUserownGamepass=DoesUserownGamepass,
UserDoesNotHaveBC=UserDoesNotHaveBC,
IsTooYoung = (datetime.utcnow() - AuthenticatedUser.created).days < UniverseObj.minimum_account_age,
MinAccountAge = UniverseObj.minimum_account_age,
CanShutdownServer = CanShutdownServer,
GameBadges = GameBadges,
CalculateBadgeRarity = CalculateBadgeRarity,
DoesUserOwnBadge = DoesUserOwnBadge,
GetTotalBadgeAwardedCount = GetTotalBadgeAwardedCount,
GetBadgeAwardedPastDay = GetBadgeAwardedPastDay,
UniverseObj = UniverseObj,
UniverseRootPlace = ActualRootPlace
)
except Exception as e:
logging.error(f"Error during rendering game page [{placeid}]: {str(e)}")
return redirect(f"/games/{str(placeid)}/{SlugName}")
@GamePagesRoute.route("/games/<int:placeid>/shutdown-server/<jobid>", methods=["POST"])
@auth.authenticated_required
def game_shutdown_server(placeid : int, jobid : str):
AuthenticatedUser : User = auth.GetCurrentUser()
AssetObject : Asset = Asset.query.filter_by(id=placeid).first()
if AssetObject is None:
return abort(404)
if AssetObject.asset_type != AssetType.Place:
return abort(404)
TargetPlaceServer : PlaceServer = PlaceServer.query.filter_by(serveruuid=jobid).first()
if TargetPlaceServer is None:
return abort(404)
if TargetPlaceServer.serverPlaceId != placeid:
return abort(403)
if (AssetObject.creator_id != AuthenticatedUser.id and AssetObject.creator_type != 1) and AuthenticatedUser.id != 1:
return abort(403)
if AssetObject.creator_type == 1:
GroupObj : Group = groups.GetGroupFromId( AssetObject.creator_id )
UserGroupRole : GroupRole = groups.GetUserRolesetInGroup( AuthenticatedUser, GroupObj )
if UserGroupRole is None:
return abort(403)
UserGroupRolePermission : GroupRolePermission = groups.GetRolesetPermission( UserGroupRole )
if not UserGroupRolePermission.manage_group_games:
return abort(403)
ShutdownServer(TargetPlaceServer.serveruuid)
flash("Requested server shutdown", "success")
return redirect(f"/games/{str(placeid)}/")
@GamePagesRoute.route("/games", methods=["GET"])
@auth.authenticated_required
def games_page():
TopPlacesInfo = []
PageNumber = max( request.args.get('page', default = 1, type = int ), 1 )
SearchQuery = request.args.get('q', default = None, type = str)
GameLookupResultsBackendResult = None
GameLookupResults = []
if SearchQuery:
SearchQuery = SearchQuery.strip().replace('%', '')
if len(SearchQuery) > 0 and len(SearchQuery) <= 32:
GameLookupResultsBackendResult = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).join( Asset, Asset.id == Place.placeid ).filter( Asset.name.ilike(f"%{SearchQuery}%") ).filter_by( moderation_status = 0 ).order_by( Universe.visit_count.desc() ).paginate(page=PageNumber, per_page=36, error_out=False)
else:
flash('Invalid search query.', 'danger')
if GameLookupResultsBackendResult:
for UniverseObj in GameLookupResultsBackendResult:
UniverseObj : Universe
AssetObj : Asset = Asset.query.filter_by(id=UniverseObj.root_place_id).first()
if AssetObj is None:
continue
PlaceObjData = {
"id": UniverseObj.root_place_id,
"name": AssetObj.name,
"playingcount": placeinfo.GetUniversePlayingCount(UniverseObj),
"slug": slugify(AssetObj.name, lowercase=False) if AssetObj.name is not None else "",
"likePercentage": GetAssetVotePercentage(AssetObj.id),
"placeyear": UniverseObj.place_year
}
GameLookupResults.append(PlaceObjData)
PopularGames = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).join( Asset, Asset.id == Place.placeid ).filter_by( moderation_status = 0 ).outerjoin( PlaceServer, PlaceServer.serverPlaceId == Place.placeid ).group_by( Universe.id )
PopularGames = PopularGames.order_by( func.coalesce( func.sum( PlaceServer.playerCount ), 0 ).desc() ).order_by( Universe.visit_count.desc() )
UniverseObjsList = PopularGames.limit(24).all()
for UniverseObj in UniverseObjsList:
UniverseObj : Universe
AssetObj : Asset = Asset.query.filter_by(id=UniverseObj.root_place_id).first()
if AssetObj is None:
continue
PlaceObjData = {
"id": UniverseObj.root_place_id,
"name": AssetObj.name,
"playingcount": placeinfo.GetUniversePlayingCount( UniverseObj ),
"slug": slugify(AssetObj.name, lowercase=False) if AssetObj.name is not None else "",
"likePercentage": GetAssetVotePercentage(UniverseObj.root_place_id),
"placeyear": UniverseObj.place_year
}
TopPlacesInfo.append(PlaceObjData)
RecentlyUpdatedUniverses : list[Universe] = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0) ).join( Asset, Asset.id == Universe.root_place_id ).filter_by( moderation_status = 0 ).order_by(Universe.updated_at.desc()).limit(24).all()
RecentlyUpdatedUniversesInfo = []
for UniverseObj in RecentlyUpdatedUniverses:
PlaceObj : Place = Place.query.filter_by(placeid=UniverseObj.root_place_id).first()
if PlaceObj is None:
continue
AssetObj : Asset = Asset.query.filter_by(id=PlaceObj.placeid).first()
if len(RecentlyUpdatedUniversesInfo) >= 24:
break
PlaceObjData = {
"id": AssetObj.id,
"name": AssetObj.name,
"playingcount": placeinfo.GetUniversePlayingCount( UniverseObj ),
"slug": slugify(AssetObj.name, lowercase=False) if AssetObj.name is not None else "",
"likePercentage": GetAssetVotePercentage(AssetObj.id),
"placeyear": UniverseObj.place_year
}
RecentlyUpdatedUniversesInfo.append(PlaceObjData)
FeaturedGames = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0, Universe.is_featured == True ) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).join( Asset, Asset.id == Place.placeid ).filter_by( moderation_status = 0 ).outerjoin( PlaceServer, PlaceServer.serverPlaceId == Place.placeid ).group_by( Universe.id )
FeaturedGames = FeaturedGames.order_by( func.coalesce( func.sum( PlaceServer.playerCount ), 0 ).desc() ).order_by( Universe.visit_count.desc() )
FeaturedPlaceObjs = FeaturedGames.limit(24).all()
FeaturedPlacesInfo = []
for UniverseObj in FeaturedPlaceObjs:
UniverseObj : Universe
AssetObj : Asset = Asset.query.filter_by(id=UniverseObj.root_place_id).first()
if AssetObj is None:
continue
PlaceObjData = {
"id": UniverseObj.root_place_id,
"name": AssetObj.name,
"playingcount": placeinfo.GetUniversePlayingCount( UniverseObj ),
"slug": slugify(AssetObj.name, lowercase=False) if AssetObj.name is not None else "",
"likePercentage": GetAssetVotePercentage(AssetObj.id),
"placeyear": UniverseObj.place_year
}
FeaturedPlacesInfo.append(PlaceObjData)
return render_template("games/index.html", PopularPlaces=TopPlacesInfo, RecentlyUpdatedPlaces=RecentlyUpdatedUniversesInfo, FeaturedPlacesInfo=FeaturedPlacesInfo, GameLookupResults = GameLookupResults, PageNumber=PageNumber, SearchQuery = SearchQuery, GameLookupResultsBackendResult = GameLookupResultsBackendResult)
from sqlalchemy import func
@GamePagesRoute.route("/games/<genre>/view", methods=["GET"])
@auth.authenticated_required
def games_genre_page(genre : str):
pageNumber = max( request.args.get("page", default=1, type=int), 1 )
if genre == "popular":
PopularGames = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).join(Asset, Asset.id == Place.placeid).outerjoin(PlaceServer, PlaceServer.serverPlaceId == Place.placeid).group_by(Universe.id)
PopularGames = PopularGames.filter( Asset.moderation_status == 0).order_by(func.coalesce(func.sum(PlaceServer.playerCount), 0).desc()).order_by( Universe.visit_count.desc() )
PopularGames = PopularGames.paginate(page=pageNumber, per_page=30, error_out=False)
ViewerFacingGenre = "Popular Games"
pageResults = PopularGames
elif genre == "updated":
UpdatedGames = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0) ).join( Asset, Asset.id == Universe.root_place_id ).filter( Asset.moderation_status == 0 ).order_by(Universe.updated_at.desc())
UpdatedGames = UpdatedGames.paginate(page=pageNumber, per_page=30, error_out=False)
ViewerFacingGenre = "Recently Updated"
pageResults = UpdatedGames
elif genre == "featured":
FeaturedGames = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0, Universe.is_featured == True ) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).join(Asset, Asset.id == Place.placeid).outerjoin(PlaceServer, PlaceServer.serverPlaceId == Place.placeid).group_by(Universe.id)
FeaturedGames = FeaturedGames.filter( Asset.moderation_status == 0).order_by(func.coalesce(func.sum(PlaceServer.playerCount), 0).desc()).order_by( Universe.visit_count.desc() )
FeaturedGames = FeaturedGames.paginate(page=pageNumber, per_page=30, error_out=False)
ViewerFacingGenre = "Featured Games"
pageResults = FeaturedGames
else:
return abort(404)
if pageResults is None:
return abort(404)
def getRootPlace( UniverseObj : Universe ) -> Place :
try:
return UniverseObj.root_place_obj
except:
UniverseObj.root_place_obj = Place.query.filter_by(placeid=UniverseObj.root_place_id).first()
return UniverseObj.root_place_obj
return render_template("games/genre.html", Games=pageResults, Genre=genre, getPlayerCount = placeinfo.GetUniversePlayingCount, getAssetVotePercentage = GetAssetVotePercentage, ViewerFacingGenre=ViewerFacingGenre, getRootPlace=getRootPlace)
+47
View File
@@ -0,0 +1,47 @@
{% extends '__layout__.html' %}
{% block title %}Games{% endblock %}
{% block head %}
<style>
.text-secondary {
color: rgb(199, 199, 199) !important;
}
.wide-place-card {
aspect-ratio: 16/9;
border-radius: 5px;
box-shadow: 0px 0px 8px 0px rgb(0 0 0 / 50%);
transition: 0.2s;
}
.wide-place-card:hover {
transform: scale(1.05);
transition: 0.2s;
z-index: 999;
}
</style>
{% endblock %}
{% block content %}
<div class="w-100 ps-4 pe-4 ms-auto me-auto" style="margin-top:100px;min-height: 100vh;max-width: 1950px;">
<h1>{{ViewerFacingGenre}}</h1>
<div class="p-2 w-100 row">
{% for universe in Games: %}
<a class="text-decoration-none p-1 col-xxl-2 col-lg-3 col-md-4 col-sm-6" href="/games/{{universe.root_place_id}}/">
<div class="overflow-hidden wide-place-card w-100 position-relative">
<img width="100%" style="aspect-ratio: 16/9;" src="/Thumbs/Asset.ashx?assetId={{universe.root_place_id}}&x=352&y=352" alt="{{getRootPlace(universe).assetObj.name}}">
<div class="position-absolute" style="bottom: 0;padding: 5px;width: 100%;background: linear-gradient(to top, rgba(0,0,0,0.9), rgba(0,0,0,0));">
<h5 style="font-size: 17px;font-weight: 700;color:rgb(230,230,230);" class="text-truncate m-0">{{getRootPlace(universe).assetObj.name}}</h5>
<div class="d-flex">
<p class="text-secondary m-0 ms-1 me-auto" style="font-size: 12px;"><span class="bi bi-person"></span> <span class="text-secondary">{{getPlayerCount(universe)}}</span> Playing</p>
<p class="text-secondary m-0 ms-auto me-auto" style="font-size: 12px;"><span class="bi bi-eye"></span> <span class="text-secondary">{{universe.visit_count}}</span> Visits</p>
<p class="text-secondary m-0 me-1 ms-auto" style="font-size: 12px;"><span class="bi bi-hand-thumbs-up"></span> <span class="text-secondary">{{getAssetVotePercentage(universe.root_place_id)}}%</span> Rating</p>
</div>
</div>
</div>
</a>
{%endfor%}
</div>
<div class="mt-2 mb-2 w-100 d-flex">
<a class="m-0 text-decoration-none ms-auto {% if not Games.has_prev %}text-secondary{%endif%}" {% if Games.has_prev %}href="/games/{{Genre}}/view?page={{Games.prev_num}}"{%endif%}>Previous</a>
<p class="m-0 ms-2 me-2 text-white">Page {{Games.page}} of {{Games.pages}}</p>
<a class="m-0 text-decoration-none me-auto {% if not Games.has_next %}text-secondary{%endif%}" {% if Games.has_next %}href="/games/{{Genre}}/view?page={{Games.next_num}}"{%endif%}>Next</a>
</div>
</div>
{% endblock %}
+203
View File
@@ -0,0 +1,203 @@
{% extends '__layout__.html' %}
{% block title %}Games{% endblock %}
{% block head %}
<style>
.text-secondary {
color: rgb(199, 199, 199) !important;
}
.place-card {
height: 220px !important;
aspect-ratio: 0.640909/1;
}
.place-card-container {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 0px;
}
</style>
{% endblock %}
{% block content %}
<div class="w-100 ps-4 pe-4 ms-auto me-auto" style="margin-top:100px;min-height: 100vh;max-width: 1950px;">
{% if GameLookupResults %}
<form method="get" class="ms-md-2 me-md-2" style="min-width: 250px;max-width: 500px;">
<div class="input-group">
<div class="form-floating" style="width: 52%;">
<input type="text" class="form-control" id="form-control" name="q" value="{% if SearchQuery != None %}{{SearchQuery}}{% endif %}" placeholder="Awesome Game">
<label for="search-input">Search</label>
</div>
<button type="submit" class="btn btn-outline-primary fw-bold" style="min-width: 50px;"><i class="bi bi-search"></i></button>
</div>
</form>
<div class="p-2 w-100 place-card-container">
{% for place in GameLookupResults: %}
<a class="text-decoration-none p-1" href="/games/{{place.id}}/{{place.slug}}">
<div class="overflow-hidden place-card w-100">
<div class="position-relative">
<img width="100%" style="aspect-ratio: 1/1;" src="/Thumbs/PlaceIcon.ashx?assetId={{place.id}}&x=150&y=150" alt="{{place.name}}">
<div class="position-absolute" style="bottom: 0px;left: 0px;">
<div class="fw-bold bg-dark text-white" style="font-size: 12px;padding: 3px;border-top-right-radius: 4px;">{{place.placeyear.value}}</div>
</div>
</div>
<div class="p-1">
<h5 class="mb-0" style="font-size: 17px;text-overflow: ellipsis;">{{place.name}}</h5>
<div class="d-flex align-items-center">
<p style="margin:0;font-size: 14px;" class="text-secondary"><i class="bi bi-people"></i> {{place.playingcount}} <span style="font-size: 13px;">Playing</span></p>
</div>
<div class="w-100 votePercentageBackground">
<div class="votePercentageFill" style="width: {{place.likePercentage}}%;"></div>
<div>
<div class="segment" style="left: 18%;"></div>
<div class="segment" style="left: 38%;"></div>
<div class="segment" style="left: 58%;"></div>
<div class="segment" style="left: 78%;"></div>
</div>
</div>
</div>
</div>
</a>
{%endfor%}
</div>
<div class="d-flex">
{% if GameLookupResultsBackendResult.has_prev: %}
<a href="/games?q={{SearchQuery}}&page={{GameLookupResultsBackendResult.prev_num}}" class="ms-auto text-decoration-none text-primary">Previous</a>
{%else%}
<a class="ms-auto text-decoration-none text-secondary">Previous</a>
{% endif %}
<p class="ms-2 me-2 text-white">Page {{GameLookupResultsBackendResult.page}} of {{GameLookupResultsBackendResult.pages}}</p>
{% if GameLookupResultsBackendResult.has_next: %}
<a href="/games?q={{SearchQuery}}&page={{GameLookupResultsBackendResult.next_num}}" class="me-auto text-decoration-none text-primary">Next</a>
{%else%}
<a class="me-auto text-decoration-none text-secondary">Next</a>
{% endif %}
</div>
{% elif not GameLookupResults and SearchQuery %}
<form method="get" class="ms-md-2 me-md-2" style="min-width: 250px;max-width: 500px;">
<div class="input-group">
<div class="form-floating" style="width: 52%;">
<input type="text" class="form-control" id="form-control" name="q" value="{% if SearchQuery != None %}{{SearchQuery}}{% endif %}" placeholder="Awesome Game">
<label for="search-input">Search</label>
</div>
<button type="submit" class="btn btn-outline-primary fw-bold" style="min-width: 50px;"><i class="bi bi-search"></i></button>
</div>
</form>
<div class="w-100 p-2">
<p class="text-white m-auto">No results found for "{{SearchQuery}}"</p>
</div>
{% else %}
<div class="d-md-flex align-items-center w-100">
<div class="d-flex align-items-center">
<h1>Popular</h1>
<a class="ms-3 text-decoration-none" href="/games/popular/view">View All</a>
</div>
<form method="get" class="ms-md-auto me-md-4" style="min-width: 300px;">
<div class="input-group">
<div class="form-floating" style="width: 52%;">
<input type="text" class="form-control" id="form-control" name="q" value="{% if SearchQuery != None %}{{SearchQuery}}{% endif %}" placeholder="Awesome Game">
<label for="search-input">Search</label>
</div>
<button type="submit" class="btn btn-outline-primary fw-bold" style="min-width: 50px;"><i class="bi bi-search"></i></button>
</div>
</form>
</div>
<div class="p-2 w-100 place-card-container">
{% for place in PopularPlaces: %}
<a class="text-decoration-none p-1" href="/games/{{place.id}}/{{place.slug}}">
<div class="overflow-hidden place-card w-100">
<div class="position-relative">
<img width="100%" style="aspect-ratio: 1/1;" src="/Thumbs/PlaceIcon.ashx?assetId={{place.id}}&x=150&y=150" alt="{{place.name}}">
<div class="position-absolute" style="bottom: 0px;left: 0px;">
<div class="fw-bold bg-dark text-white" style="font-size: 12px;padding: 3px;border-top-right-radius: 4px;">{{place.placeyear.value}}</div>
</div>
</div>
<div class="p-1">
<h5 class="mb-0" style="font-size: 17px;text-overflow: ellipsis;">{{place.name}}</h5>
<div class="d-flex align-items-center">
<p style="margin:0;font-size: 14px;" class="text-secondary"><i class="bi bi-people"></i> {{place.playingcount}} <span style="font-size: 13px;">Playing</span></p>
</div>
<div class="w-100 votePercentageBackground">
<div class="votePercentageFill" style="width: {{place.likePercentage}}%;"></div>
<div>
<div class="segment" style="left: 18%;"></div>
<div class="segment" style="left: 38%;"></div>
<div class="segment" style="left: 58%;"></div>
<div class="segment" style="left: 78%;"></div>
</div>
</div>
</div>
</div>
</a>
{%endfor%}
</div>
<div class="d-flex align-items-center">
<h1>Featured</h1>
<a class="ms-3 text-decoration-none" href="/games/featured/view">View All</a>
</div>
<div class="p-2 w-100 place-card-container">
{% for place in FeaturedPlacesInfo: %}
<a class="text-decoration-none p-1" href="/games/{{place.id}}/{{place.slug}}">
<div class="overflow-hidden place-card w-100">
<div class="position-relative">
<img width="100%" style="aspect-ratio: 1/1;" src="/Thumbs/PlaceIcon.ashx?assetId={{place.id}}&x=150&y=150" alt="{{place.name}}">
<div class="position-absolute" style="bottom: 0px;left: 0px;">
<div class="fw-bold bg-dark text-white" style="font-size: 12px;padding: 3px;border-top-right-radius: 4px;">{{place.placeyear.value}}</div>
</div>
</div>
<div class="p-1">
<h5 class="mb-0" style="font-size: 17px;text-overflow: ellipsis;">{{place.name}}</h5>
<div class="d-flex align-items-center">
<p style="margin:0;font-size: 14px;" class="text-secondary"><i class="bi bi-people"></i> {{place.playingcount}} <span style="font-size: 13px;">Playing</span></p>
</div>
<div class="w-100 votePercentageBackground">
<div class="votePercentageFill" style="width: {{place.likePercentage}}%;"></div>
<div>
<div class="segment" style="left: 18%;"></div>
<div class="segment" style="left: 38%;"></div>
<div class="segment" style="left: 58%;"></div>
<div class="segment" style="left: 78%;"></div>
</div>
</div>
</div>
</div>
</a>
{%endfor%}
</div>
<div class="d-flex align-items-center">
<h1>Recently Updated</h1>
<a class="ms-3 text-decoration-none" href="/games/updated/view">View All</a>
</div>
<div class="p-2 w-100 place-card-container">
{% for place in RecentlyUpdatedPlaces: %}
<a class="text-decoration-none p-1" href="/games/{{place.id}}/{{place.slug}}">
<div class="overflow-hidden place-card w-100">
<div class="position-relative">
<img width="100%" style="aspect-ratio: 1/1;" src="/Thumbs/PlaceIcon.ashx?assetId={{place.id}}&x=150&y=150" alt="{{place.name}}">
<div class="position-absolute" style="bottom: 0px;left: 0px;">
<div class="fw-bold bg-dark text-white" style="font-size: 12px;padding: 3px;border-top-right-radius: 4px;">{{place.placeyear.value}}</div>
</div>
</div>
<div class="p-1">
<h5 class="mb-0" style="font-size: 17px;text-overflow: ellipsis;">{{place.name}}</h5>
<div class="d-flex align-items-center">
<p style="margin:0;font-size: 14px;" class="text-secondary"><i class="bi bi-people"></i> {{place.playingcount}} <span style="font-size: 13px;">Playing</span></p>
</div>
<div class="w-100 votePercentageBackground">
<div class="votePercentageFill" style="width: {{place.likePercentage}}%;"></div>
<div>
<div class="segment" style="left: 18%;"></div>
<div class="segment" style="left: 38%;"></div>
<div class="segment" style="left: 58%;"></div>
<div class="segment" style="left: 78%;"></div>
</div>
</div>
</div>
</div>
</a>
{%endfor%}
</div>
{% endif %}
</div>
{% endblock %}
+380
View File
@@ -0,0 +1,380 @@
{% extends '__layout__.html' %}
{% block title %}{{PlaceAssetObj.name}}{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/gameview.css">
<link rel="stylesheet" href="/static/css/icons.css">
<script src="/static/js/assetrate.js"></script>
<script src="/static/js/catalog.js?ver=4"></script>
<div id="X-CSRF-TOKEN" data-xcsrf="{{csrf_token()}}"></div>
<div id="csrf_token" data-csrf-token="{{ csrf_token() }}"></div>
<style>
.text-robux {
color: rgb(26, 212, 103) !important;
font-size: 13px;
font-weight: 600;
}
</style>
{% endblock %}
{% block content %}
<div class="container" style="min-height: 100vh;background-color: rgb(30,30,30);max-width: 1100px;">
<div class="row ps-md-4 pe-md-4" style="padding-top: 120px;">
{% if UniverseObj.root_place_id != PlaceObj.placeid %}
<div class="alert alert-info ">
<p class="m-0">This game is part of <a href="/games/{{UniverseObj.root_place_id}}/--" class="text-decoration-none">{{UniverseRootPlace.name}}</a>'s universe</p>
</div>
{% endif %}
<div class="col-md-8">
<img width="100%" class="rounded" style="aspect-ratio: 16/9;" src="/Thumbs/Asset.ashx?assetId={{PlaceAssetObj.id}}&x=1280&y=720" alt="{{PlaceAssetObj.name}} Thumbnail">
</div>
<div class="col-md-4 position-relative" style="min-height: 200px;">
<h1 class="m-0">{{PlaceAssetObj.name}}</h1>
{% if PlaceAssetObj.creator_type == 0 %}
<p class="m-0">By <a href="/users/{{PlaceAssetObj.creator_id}}/profile" class="text-decoration-none">{{CreatorObj.username}}</a></p>
{% else %}
<p class="m-0">By <a href="/groups/{{PlaceAssetObj.creator_id}}/--" class="text-decoration-none">{{CreatorObj.name}}</a></p>
{% endif %}
<div class="position-absolute w-100 p-2 border-top border-bottom mb-3" style="bottom:0px;">
{% if UniverseObj.is_public: %}
{% if UserDoesNotHaveBC == False %}
{% if PlaceAssetObj.moderation_status != 2 or UniverseObj.moderation_status != 2 %}
{% if IsTooYoung: %}
<button class="btn-lg btn btn-success w-100 disabled">PLAY <i class="bi bi-play-fill"></i></button>
<p class="m-0 w-100 text-center text-secondary" style="font-size: 12px;">Your account must be at least {{MinAccountAge}} days old to play this game</p>
{% else %}
<button class="btn-lg btn btn-success w-100 gamelaunch-btn" data-placeid="{{PlaceAssetObj.id}}">PLAY <i class="bi bi-play"></i></button>
{% endif %}
{% else %}
<button class="btn-lg btn btn-success w-100 disabled">PLAY <i class="bi bi-play-fill"></i></button>
<p class="m-0 w-100 text-center text-secondary" style="font-size: 12px;">This place is currently under review</p>
{% endif %}
{% else %}
<button class="btn-lg btn btn-success w-100 disabled">PLAY <i class="bi bi-play-fill"></i></button>
<p class="m-0 w-100 text-center text-secondary" style="font-size: 12px;">A Builders Club membership is required to join this game</p>
{% endif %}
{%else%}
<button class="btn-lg btn btn-success w-100 fw-bold disabled">Play</button>
<p class="m-0 w-100 text-center text-secondary" style="font-size: 12px;">This place has been set to private by its creator</p>
{%endif%}
<div class="d-flex align-items-center" style="margin: 0;height: 36px;display: block;">
<div class="favorite-button-container" data-favorite-count="{{FavoriteCount}}" data-assetid="{{PlaceAssetObj.id}}" data-userfavorite-status="{{UserFavoriteStatus}}">
<div class="icon-favorite"></div>
<span title="{{FavoriteCount}}" class="text-favorite">{{FavoriteCount}}</span>
</div>
<div class="usersVote" data-assetid="{{PlaceAssetObj.id}}" data-likes="{{LikeCount}}" data-dislikes="{{DislikeCount}}" data-uservote-status="{{UserVoteStatus}}">
<div class="upvote">
<span class="icon-like cursor-pointer"></span>
</div>
<div class="votedetails">
<div class="votecontainer">
<div class="votebackground"></div>
<div class="votepercentage" style="width: 50%;"></div>
<div>
<div class="segment" style="left: 18%;"></div>
<div class="segment" style="left: 38%;"></div>
<div class="segment" style="left: 58%;"></div>
<div class="segment" style="left: 78%;"></div>
</div>
</div>
<div class="votenumbers">
<div class="countleft">
<span title="5" class="vote-up-text">5</span>
</div>
<div class="countright">
<span title="5" class="vote-down-text">5</span>
</div>
</div>
</div>
<div class="downvote">
<span class="icon-dislike cursor-pointer"></span>
</div>
<p id="vote-feedback-text text-danger" style="display: none;font-size: 12px;"></p>
</div>
</div>
</div>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div>
{% for category, message in messages %}
{% if category == 'error': %}
<div class="alert border p-2 text-center alert-danger border-danger">
{{ message }}
</div>
{% endif %}
{% if category == 'success': %}
<div class="alert border p-2 text-center alert-success border-success">
{{ message }}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endwith %}
<nav class="mt-4 m-2 rounded-top" style="background-color: rgb(40, 40, 40) !important;overflow: hidden;">
<div class="nav nav-underline nav-fill page-tabs" id="nav-tab" role="tablist">
<a href="#about" class="nav-link active" id="nav-about-tab" data-bs-toggle="tab" data-bs-target="#nav-about" type="button" role="tab" aria-controls="nav-about" aria-selected="true">About</a>
<a href="#store" class="nav-link" id="nav-store-tab" data-bs-toggle="tab" data-bs-target="#nav-store" type="button" role="tab" aria-controls="nav-store" aria-selected="false">Store</a>
<a href="#servers" class="nav-link" id="nav-servers-tab" data-bs-toggle="tab" data-bs-target="#nav-servers" type="button" role="tab" aria-controls="nav-servers" aria-selected="false">Servers</a>
</div>
</nav>
<div class="tab-content ps-2 pe-2 pt-1 pb-3" id="nav-tabContent">
<div class="tab-pane show active" id="nav-about" role="tabpanel" aria-labelledby="nav-about-tab" tabindex="0">
<h2>Description</h2>
<div class="ms-2" style="min-height: 200px;">
{% for line in SplittedDescription %}
<p class="text-secondary m-0" style="font-size: large;">{{line}}</p>
{% endfor %}
</div>
<div class="border-top border-bottom ms-2 me-2 pt-2 pb-2 row text-center details-container">
<div class="col">
<p class="m-0">Active</p>
<h2>{{PlayerCount}}</h2>
</div>
<div class="col">
<p class="m-0">Visits</p>
<h2>{{PlaceObj.visitcount}}</h2>
</div>
<div class="col">
<p class="m-0">Created</p>
<h2>{{ConvertDatetimeToDayMonthYear(PlaceAssetObj.created_at)}}</h2>
</div>
<div class="col">
<p class="m-0">Updated</p>
<h2>{{ConvertDatetimeToDayMonthYear(PlaceAssetObj.updated_at)}}</h2>
</div>
<div class="col">
<p class="m-0">Server Size</p>
<h2>{{PlaceObj.maxplayers}}</h2>
</div>
</div>
{% if len(GameBadges) != 0: %}
<div class="mt-4">
<h3 class="m-0">Badges</h3>
<div class="p-1" style="max-height: 400px; overflow-y: auto;">
{% for badgeObj in GameBadges: %}
<a href="/badges/{{badgeObj.id}}/--" class="text-decoration-none">
<div class="mb-1 p-2 d-flex" style="background-color: rgb(40,40,40);">
<img style="background-color: rgb(30,30,30);border-radius: 100%;" src="/Thumbs/Asset.ashx?assetId={{badgeObj.icon_image_id}}&x=100&y=100" width="100px" height="100px" alt="{{badgeObj.name}}">
<div>
<h4 class="m-0 ms-2">{{badgeObj.name}}</h4>
<p class="m-0 ms-2 text-secondary" style="font-size: 14px;height: 46px;">{{badgeObj.description}}</p>
{% if DoesUserOwnBadge(badgeObj.id, currentuser.id) %}
<p class="m-0 ms-2 text-white" style="font-size: 13px;">You own this badge</p>
{% endif %}
</div>
<div class="ms-auto me-2 mt-auto mb-auto">
<ul style="display: grid;min-width: 200px;grid: repeat(3, 33.333%) / max-content auto;font-size: 15px;">
<li style="display: contents;">
<p class="m-0 text-secondary text-end">Rarity</p>
<p class="m-0 text-white fw-bold text-center style=">{{ round(CalculateBadgeRarity( badgeObj.id ) * 100, 1) }}%</p>
</li>
<li style="display: contents;">
<p class="m-0 text-secondary text-end">Won Yesterday</p>
<p class="m-0 text-white fw-bold text-center style=">{{ GetBadgeAwardedPastDay( badgeObj.id ) }}</p>
</li>
<li style="display: contents;">
<p class="m-0 text-secondary text-end">Won Ever</p>
<p class="m-0 text-white fw-bold text-center style=">{{ GetTotalBadgeAwardedCount( badgeObj.id ) }}</p>
</li>
</ul>
</div>
</div>
</a>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<div class="tab-pane" id="nav-store" role="tabpanel" aria-labelledby="nav-store-tab" tabindex="0">
<h4 class="m-0 mb-2">Gamepasses</h4>
<div class="d-flex" style="flex-wrap: wrap;justify-content: flex-start;gap: 10px;">
{% for gamepass in Gamepasses: %}
{% if gamepass.gamepass.is_for_sale: %}
<div class="p-1" style="width: 150px; min-height: 200px;background-color: rgb(44, 44, 44);box-shadow: 0 0 5px rgb(0,0,0);">
<a href="/library/{{gamepass.gamepass_id}}/" class="text-decoration-none">
<img style="aspect-ratio: 1/1;width: 100%;background-color: rgb(22, 22, 22);border-radius: 100%;" src="/Thumbs/Asset.ashx?assetId={{gamepass.gamepass_id}}&x=180&y=180">
<h5 class="m-0 mt-1 w-100 text-truncate">{{gamepass.gamepass.name}}</h5>
</a>
<p class="text-robux m-0">R$ {{gamepass.gamepass.price_robux}}</p>
{% if DoesUserownGamepass(gamepass.gamepass_id, currentuser.id) %}
<button class="btn btn-sm btn-light w-100 purchase-button mt-2" disabled>Owned</button>
{% else %}
<button class="btn btn-sm btn-light w-100 purchase-button mt-2" data-asset-id="{{gamepass.gamepass_id}}" data-asset-name="{{gamepass.gamepass.name}}" data-expected-price="{{gamepass.gamepass.price_robux}}" data-currency-type="0">Purchase</button>
{% endif %}
</div>
{% endif %}
{%endfor%}
</div>
{% if len(Gamepasses) == 0: %}
<p class="w-100 text-center mt-5 mb-5">No Gamepasses Found</p>
{% endif %}
</div>
<div class="tab-pane" id="nav-servers" role="tabpanel" aria-labelledby="nav-servers-tab" tabindex="0">
<h2>Servers</h2>
<div class="w-100">
{% for PlaceServer in ActiveServers: %}
<div class="w-100 m-2 p-2" style="background-color: rgb(20, 20, 20);">
<div class="d-flex align-items-center w-100">
<div>
<h5 class="m-0">{{PlaceServer.playercount}} of {{PlaceServer.maxplayercount}} players</h5>
<p class="text-secondary m-0" style="font-size: 10px;">{{PlaceServer.id}} - {{PlaceServer.host}}{% if PlaceServer.is_reserved_server %} - Reserved Server{%endif%}</p>
</div>
<div class="ms-auto">
{% if PlaceServer.is_reserved_server == False %}
<button class="btn btn-outline-light btn-sm gamelaunch-btn" data-placeid="{{PlaceAssetObj.id}}" data-jobid="{{PlaceServer.id}}">Join Server</button>
{% else %}
<button class="btn btn-outline-light btn-sm" disabled>Join Server</button>
{% endif %}
</div>
{% if CanShutdownServer %}
<form action="/games/{{PlaceAssetObj.id}}/shutdown-server/{{PlaceServer.id}}" method="post" class="ms-2">
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
<button class="btn btn-outline-danger btn-sm" type="submit">Shutdown Server</button>
</form>
{%endif%}
</div>
<div class="d-flex p-1 mt-2 d-flex" style="flex-wrap: wrap;justify-content: flex-start;min-height: 60px;gap: 2px;background-color: rgb(15,15,15);border-radius: 3px;">
{% for Player in PlaceServer.players: %}
<a href="/users/{{Player.userid}}/profile" class="bg-dark p-1" style="border-radius: 3px;"><img width="60" height="60" src="/Thumbs/Head.ashx?x=60&y=60&userId={{Player.userid}}" style="border-radius: 3px;" alt="{{Player.user.username}}"></a>
{% endfor%}
{% if PlaceServer.playercount == 0 :%}
<p class="m-0 text-center w-100 mb-auto mt-auto text-white" style="font-size: 12px;">Empty Server</p>
{% endif %}
</div>
<div style="min-height: 8px;overflow:hidden;border-color: rgb(45,45,45) !important;border-radius: 3px;" class="w-100 mt-1 border">
{% if PlaceServer.playercount != 0 %}
<div style="width: {{PlaceServer.playercount / PlaceServer.maxplayercount * 100}}%;background-color: rgb(180, 180, 180);min-height: 8px;"></div>
{% endif %}
</div>
</div>
{%endfor%}
{% if ActiveServerCount == 0: %}
<p class="w-100 text-center">No Active Servers Found</p>
{%endif%}
</div>
</div>
</div>
</div>
<script>
async function GenerateAuthTicket() {
var response = await fetch("/Login/NewAuthTicket", {
method: "POST"
});
return await response.text();
}
document.addEventListener("DOMContentLoaded", async function() {
const TransparentBackgroundTop = document.getElementById("transparent-background-top");
const CloseLauncherBtn = document.getElementById("close-launcher-btn");
const LaunchLoadingBar = document.getElementById("launch-loading-bar");
const LaunchDownloadBtn = document.getElementById("launch-download-btn");
CloseLauncherBtn.addEventListener("click", function() {
TransparentBackgroundTop.style.display = "none";
});
CloseLauncherBtn.style.display = "none";
document.querySelectorAll(".gamelaunch-btn").forEach( async function(element) {
element.addEventListener("click", async function() {
TransparentBackgroundTop.style.display = "flex";
LaunchLoadingBar.style.display = "block";
LaunchDownloadBtn.style.display = "none";
CloseLauncherBtn.style.display = "none";
var placeid = element.getAttribute("data-placeid");
var authTicket = await GenerateAuthTicket();
var authticket2 = await GenerateAuthTicket();
var clientyear = {{UniverseObj.place_year.value}};
if ( clientyear == 2014 ) {
var joinScriptUrl = `http://www.vortexi.cc/game/2014Join.lua?placeId=${placeid}`
} else {
var joinScriptUrl = `https://www.vortexi.cc/Game/placelauncher.ashx?placeId=${placeid}&t=${authticket2}`
}
var jobid = element.getAttribute("data-jobid");
if (jobid) {
joinScriptUrl += `&jobId=${jobid}`;
}
var needsExtraFowardSlash = false;
var isFirefoxBrowser = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (isFirefoxBrowser) {
var FirefoxVersion = parseInt(navigator.userAgent.toLowerCase().split('firefox/')[1]);
if ( FirefoxVersion > 121 ) { // Firefox 122 and above requires an extra forward slash
needsExtraFowardSlash = true;
}
}
window.location.href = `vortexi-player:1+launchmode:play+gameinfo:${authTicket}+placelauncherurl:${joinScriptUrl}+k:l+clientyear:${clientyear}`
setTimeout(function() {
LaunchLoadingBar.style.display = "none";
LaunchDownloadBtn.style.display = "block";
CloseLauncherBtn.style.display = "block";
setTimeout(function() {
TransparentBackgroundTop.style.display = "none";
}, 6000);
}, 4000);
});
});
var WindowHash = window.location.hash;
if (WindowHash) {
var TabElement = document.querySelector(`[href="${WindowHash}"]`);
if (TabElement) {
TabElement.click();
}
}
var Navtabs = document.querySelectorAll(".page-tabs a");
Navtabs.forEach(function(element) {
element.addEventListener("click", function() {
var TabTarget = element.getAttribute("href");
window.location.hash = TabTarget;
});
});
});
</script>
<div class="position-absolute w-100 h-100 justify-content-center align-items-center" id="transparent-background-top" style="top: 0;left: 0;background-color: rgba(0,0,0,0.5);display: none;">
<div class="m-auto rounded-2 p-2 position-relative" style="background-color: rgb(30,30,30); box-shadow: 0 0 10px #333333; min-width: 400px; min-height: 255px; ">
<div class="d-flex w-100">
<img src="/static/img/VortexiLogo.png" width="90px" height="90px" class="ms-auto me-auto" style="margin-top: 46px;">
</div>
<div id="launch-loading-bar" >
<p class="m-0 w-100 text-center text-white">Vortexi is now loading. Get ready to play!</p>
<div class="ps-2 pe-2">
<div class="progress mt-2" style="min-height: 20px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" style="width: 100%;"></div>
</div></div>
</div>
<div id="launch-download-btn" style="display: none;">
<p class="m-0 w-100 text-center text-white">You're moments away from getting into the game!</p>
<div class="ps-3 pe-3 mt-2">
<a class="w-100 btn btn-success text-decoration-none" href="/download">Download and Install Vortexi</a>
</div>
</div>
<div class="position-absolute" style="top: 10px; right: 10px;">
<button class="btn btn-close" id="close-launcher-btn"></button>
</div>
</div>
</div>
<div class="modal fade" id="purchaseModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="purchase-modal-title">Confirm purchase</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="purchase-modal-content">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="purchase-modal-close" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" id="purchase-modal-btn">Purchase</button>
</div>
</div>
</div>
</div>
{% endblock %}